mirror of
https://git.sr.ht/~oppen/ariane
synced 2024-12-21 23:44:30 +00:00
history rewrite, plus tls client cert wip
This commit is contained in:
parent
244459b3c3
commit
3f6b78ed08
29 changed files with 401 additions and 237 deletions
|
@ -1 +1 @@
|
|||
Två
|
||||
a.kt
|
|
@ -7,7 +7,7 @@
|
|||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
<option name="gradleJvm" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
|
|
@ -32,7 +32,10 @@ class GeminiDatasourceTests {
|
|||
val capsule = capsules.random()
|
||||
println("Using $capsule for Gemini tests")
|
||||
capsuleIndex = capsules.indexOf(capsule)
|
||||
gemini = Datasource.factory(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
gemini = Datasource.factory(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
db.history()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package oppen.ariane.io.bookmarks
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(entities = [BookmarkEntity::class], version = 2)
|
||||
abstract class BookmarksDB: RoomDatabase() {
|
||||
abstract fun bookmarksDao(): BookmarksDao
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package oppen.ariane.io.bookmarks
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface BookmarksDatasource {
|
||||
|
||||
fun get(onBookmarks: (List<Bookmark>) -> Unit)
|
||||
fun add(bookmark: Bookmark, onAdded: () -> Unit)
|
||||
fun delete(bookmark: Bookmark, onDelete: () -> Unit)
|
||||
|
||||
fun moveUp(bookmark: Bookmark, onMoved: () -> Unit)
|
||||
fun moveDown(bookmark: Bookmark, onMoved: () -> Unit)
|
||||
fun update(bookmark: Bookmark, label: String?, uri: String?, onUpdate: () -> Unit)
|
||||
|
||||
companion object{
|
||||
fun getDefault(applicationContext: Context): BookmarksDatasource{
|
||||
return RoomBookmarks(applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package oppen.ariane.io.bookmarks
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
class RoomBookmarks(applicationContext: Context): BookmarksDatasource {
|
||||
|
||||
private val db: BookmarksDB = Room.databaseBuilder(applicationContext, BookmarksDB::class.java, "tva_bookmarks").build()
|
||||
|
||||
override fun get(onBookmarks: (List<Bookmark>) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val dbBookmarks = db.bookmarksDao().getAll()
|
||||
val bookmarks = mutableListOf<Bookmark>()
|
||||
|
||||
dbBookmarks.forEach { bookmarkEntity ->
|
||||
bookmarks.add(
|
||||
Bookmark(
|
||||
uid = bookmarkEntity.uid,
|
||||
label = bookmarkEntity.label ?: "Unknown",
|
||||
uri = URI.create(bookmarkEntity.uri),
|
||||
index = bookmarkEntity.uiIndex ?: 0)
|
||||
)
|
||||
}
|
||||
onBookmarks(bookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(bookmark: Bookmark, onAdded: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val bookmarkEntity = BookmarkEntity(
|
||||
label = bookmark.label,
|
||||
uri = bookmark.uri.toString(),
|
||||
uiIndex = bookmark.index,
|
||||
folder = "~/")
|
||||
|
||||
db.bookmarksDao().insertAll(bookmarkEntity)
|
||||
onAdded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveUp(bookmark: Bookmark, onMoved: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
|
||||
//todo - this method is broken:
|
||||
val prev = db.bookmarksDao().getBookmark(bookmark.index -1)
|
||||
val target = db.bookmarksDao().getBookmark(bookmark.index)
|
||||
|
||||
db.bookmarksDao().updateUIIndex(prev.uid, bookmark.index)
|
||||
db.bookmarksDao().updateUIIndex(target.uid, bookmark.index - 1)
|
||||
onMoved()
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveDown(bookmark: Bookmark, onMoved: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val next = db.bookmarksDao().getBookmark(bookmark.index + 1)
|
||||
val target = db.bookmarksDao().getBookmark(bookmark.index)
|
||||
|
||||
db.bookmarksDao().updateUIIndex(next.uid, bookmark.index)
|
||||
db.bookmarksDao().updateUIIndex(target.uid, bookmark.index + 1)
|
||||
onMoved()
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(bookmark: Bookmark, label: String?, uri: String?, onUpdate: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
db.bookmarksDao().updateContent(bookmark.uid, label ?: "", uri ?: "")
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(bookmark: Bookmark, onDelete: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val entity = db.bookmarksDao().getBookmark(bookmark.index)
|
||||
db.bookmarksDao().delete(entity)
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package oppen.ariane.io.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import oppen.ariane.io.database.bookmarks.BookmarkEntity
|
||||
import oppen.ariane.io.database.bookmarks.BookmarksDao
|
||||
import oppen.ariane.io.database.history.HistoryDao
|
||||
import oppen.ariane.io.database.history.HistoryEntity
|
||||
|
||||
@Database(entities = [BookmarkEntity::class, HistoryEntity::class], version = 3)
|
||||
abstract class ArianeAbstractDatabase: RoomDatabase() {
|
||||
abstract fun bookmarks(): BookmarksDao
|
||||
abstract fun history(): HistoryDao
|
||||
}
|
14
app/src/main/java/oppen/ariane/io/database/ArianeDatabase.kt
Normal file
14
app/src/main/java/oppen/ariane/io/database/ArianeDatabase.kt
Normal file
|
@ -0,0 +1,14 @@
|
|||
package oppen.ariane.io.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import oppen.ariane.io.database.bookmarks.ArianeBookmarks
|
||||
import oppen.ariane.io.database.history.ArianeHistory
|
||||
|
||||
class ArianeDatabase(context: Context) {
|
||||
|
||||
private val db: ArianeAbstractDatabase = Room.databaseBuilder(context, ArianeAbstractDatabase::class.java, "ariane_database_v1").build()
|
||||
|
||||
fun bookmarks(): ArianeBookmarks = ArianeBookmarks(db)
|
||||
fun history(): ArianeHistory = ArianeHistory(db)
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package oppen.ariane.io.database.bookmarks
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import oppen.ariane.io.database.ArianeAbstractDatabase
|
||||
import java.net.URI
|
||||
|
||||
class ArianeBookmarks(private val db: ArianeAbstractDatabase): 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(bookmarkEntity)
|
||||
onAdded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveUp(bookmarkEntry: BookmarkEntry, onMoved: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
|
||||
//todo - this method is broken:
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package oppen.ariane.io.bookmarks
|
||||
package oppen.ariane.io.database.bookmarks
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
|
@ -1,8 +1,8 @@
|
|||
package oppen.ariane.io.bookmarks
|
||||
package oppen.ariane.io.database.bookmarks
|
||||
|
||||
import java.net.URI
|
||||
|
||||
class Bookmark(
|
||||
class BookmarkEntry(
|
||||
val uid: Int,
|
||||
val label: String,
|
||||
val uri: URI,
|
|
@ -1,4 +1,4 @@
|
|||
package oppen.ariane.io.bookmarks
|
||||
package oppen.ariane.io.database.bookmarks
|
||||
|
||||
import androidx.room.*
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package oppen.ariane.io.database.bookmarks
|
||||
|
||||
interface BookmarksDatasource {
|
||||
|
||||
fun get(onBookmarks: (List<BookmarkEntry>) -> Unit)
|
||||
fun add(bookmarkEntry: 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,70 @@
|
|||
package oppen.ariane.io.database.history
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import oppen.ariane.io.database.ArianeAbstractDatabase
|
||||
|
||||
class ArianeHistory(private val db: ArianeAbstractDatabase): 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) {
|
||||
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) {
|
||||
//todo
|
||||
}
|
||||
|
||||
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,24 @@
|
|||
package oppen.ariane.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)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package oppen.ariane.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 oppen.ariane.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 oppen.ariane.io.database.history
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
class HistoryEntry(
|
||||
val uid: Int,
|
||||
val timestamp: Long,
|
||||
val uri: Uri
|
||||
)
|
|
@ -2,6 +2,7 @@ package oppen.ariane.io.gemini
|
|||
|
||||
import android.content.Context
|
||||
import oppen.ariane.io.GemState
|
||||
import oppen.ariane.io.database.history.ArianeHistory
|
||||
import java.net.URI
|
||||
|
||||
interface Datasource {
|
||||
|
@ -11,8 +12,8 @@ interface Datasource {
|
|||
fun goBack(onUpdate: (state: GemState) -> Unit)
|
||||
|
||||
companion object{
|
||||
fun factory(context: Context): Datasource {
|
||||
return GeminiDatasource(context)
|
||||
fun factory(context: Context, history: ArianeHistory): Datasource {
|
||||
return GeminiDatasource(context, history)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ import androidx.preference.PreferenceManager
|
|||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import oppen.ariane.io.GemState
|
||||
import oppen.ariane.io.database.history.ArianeHistory
|
||||
import oppen.ariane.io.database.history.HistoryEntry
|
||||
import oppen.ariane.io.keymanager.ArianeKeyManager
|
||||
import oppen.isGemini
|
||||
import oppen.toURI
|
||||
|
@ -18,7 +20,7 @@ import javax.net.ssl.*
|
|||
|
||||
const val GEMINI_SCHEME = "gemini"
|
||||
|
||||
class GeminiDatasource(private val context: Context): Datasource {
|
||||
class GeminiDatasource(private val context: Context, val history: ArianeHistory): Datasource {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val addressBuilder = AddressBuilder()
|
||||
|
@ -88,7 +90,7 @@ class GeminiDatasource(private val context: Context): Datasource {
|
|||
else -> SSLContext.getInstance(protocol)
|
||||
}
|
||||
|
||||
sslContext.init(arianeKeyManager.getFactory()?.keyManagers, DummyTrustManager.get(), null)
|
||||
sslContext.init(arianeKeyManager.getFactory(context)?.keyManagers, DummyTrustManager.get(), null)
|
||||
val factory: SSLSocketFactory = sslContext.socketFactory
|
||||
|
||||
//todo to here ----------------------------------------------------------------------------
|
||||
|
@ -140,6 +142,11 @@ class GeminiDatasource(private val context: Context): Datasource {
|
|||
|
||||
println("Ariane: response header: $headerLine")
|
||||
|
||||
if(headerLine == null){
|
||||
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, "Server did not respond with a Gemini header")))
|
||||
return
|
||||
}
|
||||
|
||||
val header = GeminiResponse.parseHeader(headerLine)
|
||||
|
||||
when {
|
||||
|
@ -197,6 +204,8 @@ class GeminiDatasource(private val context: Context): Datasource {
|
|||
runtimeHistory.add(uri)
|
||||
println("Ariane 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){
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package oppen.ariane.io.keymanager
|
||||
|
||||
import android.R
|
||||
import android.content.Context
|
||||
import oppen.ariane.R
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.KeyStore
|
||||
|
@ -17,7 +17,7 @@ class ArianeKeyManager {
|
|||
val hasLoadedKey = false
|
||||
return when {
|
||||
hasLoadedKey -> {
|
||||
val keyStore: KeyStore = KeyStore.getInstance("BKS")
|
||||
val keyStore: KeyStore = KeyStore.getInstance("BKS")//or "pkcs12"v?
|
||||
val inputStream: InputStream = ByteArrayInputStream("dummy".toByteArray())
|
||||
keyStore.load(inputStream, "yourKeyStorePassword".toCharArray())
|
||||
inputStream.close()
|
||||
|
@ -30,4 +30,14 @@ class ArianeKeyManager {
|
|||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun getFactoryDemo(context: Context): KeyManagerFactory? {
|
||||
val keyStore: KeyStore = KeyStore.getInstance("pkcs12")//or "pkcs12"v?
|
||||
keyStore.load(context.resources.openRawResource(R.raw.cert), "PASSWORD".toCharArray())
|
||||
|
||||
val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("X509")
|
||||
keyManagerFactory.init(keyStore, "PASSWORD".toCharArray())
|
||||
|
||||
return keyManagerFactory
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package oppen.ariane.ui
|
|||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
|
@ -19,11 +18,11 @@ import oppen.ariane.Ariane
|
|||
import oppen.ariane.R
|
||||
import oppen.ariane.databinding.ActivityGemBinding
|
||||
import oppen.ariane.io.GemState
|
||||
import oppen.ariane.io.bookmarks.BookmarksDatasource
|
||||
import oppen.ariane.io.database.ArianeDatabase
|
||||
import oppen.ariane.io.database.bookmarks.BookmarksDatasource
|
||||
import oppen.ariane.io.gemini.Datasource
|
||||
import oppen.ariane.io.gemini.GeminiResponse
|
||||
import oppen.ariane.io.gemini.RuntimeCache
|
||||
import oppen.ariane.io.history.uris.HistoryInterface
|
||||
import oppen.ariane.ui.audio_player.AudioPlayer
|
||||
import oppen.ariane.ui.bookmarks.BookmarkDialog
|
||||
import oppen.ariane.ui.bookmarks.BookmarksDialog
|
||||
|
@ -36,7 +35,6 @@ import oppen.ariane.ui.modals_menus.input.InputDialog
|
|||
import oppen.ariane.ui.modals_menus.overflow.OverflowPopup
|
||||
import oppen.ariane.ui.settings.SettingsActivity
|
||||
import oppen.hideKeyboard
|
||||
import oppen.toURI
|
||||
import oppen.visibleRetainingSpace
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
@ -50,14 +48,12 @@ const val CREATE_BINARY_FILE_REQ = 630
|
|||
class GemActivity : AppCompatActivity() {
|
||||
|
||||
private var inSearch = false
|
||||
|
||||
private val mediaPlayer = MediaPlayer()
|
||||
|
||||
private lateinit var bookmarkDatasource: BookmarksDatasource
|
||||
|
||||
private val model by viewModels<GemViewModel>()
|
||||
private lateinit var binding: ActivityGemBinding
|
||||
private lateinit var history: HistoryInterface
|
||||
|
||||
private val adapter = GemtextAdapter { adapter, uri, longTap, position: Int, view ->
|
||||
if(longTap){
|
||||
LinkPopup.show(view, uri){ menuId ->
|
||||
|
@ -89,6 +85,7 @@ class GemActivity : AppCompatActivity() {
|
|||
binding.addressEdit.hint = getString(R.string.main_input_hint)
|
||||
inSearch = false
|
||||
}
|
||||
|
||||
model.request(uri)
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +93,8 @@ class GemActivity : AppCompatActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bookmarkDatasource = BookmarksDatasource.getDefault(applicationContext)
|
||||
val db = ArianeDatabase(applicationContext)
|
||||
bookmarkDatasource = db.bookmarks()
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_gem)
|
||||
binding.viewmodel = model
|
||||
|
@ -105,52 +103,11 @@ class GemActivity : AppCompatActivity() {
|
|||
binding.gemtextRecycler.layoutManager = LinearLayoutManager(this)
|
||||
binding.gemtextRecycler.adapter = adapter
|
||||
|
||||
history = HistoryInterface.default(this)
|
||||
|
||||
model.initialise(
|
||||
home = PreferenceManager.getDefaultSharedPreferences(this).getString("home_capsule", Ariane.DEFAULT_HOME_CAPSULE) ?: Ariane.DEFAULT_HOME_CAPSULE,
|
||||
gemini = Datasource.factory(this),
|
||||
bookmarks = BookmarksDatasource.getDefault(applicationContext)
|
||||
){ state ->
|
||||
|
||||
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 -> showAlert("${GeminiResponse.getCodeString(state.header.code)}: ${state.header.meta}")
|
||||
is GemState.ResponseGemtext -> renderGemtext(state)
|
||||
is GemState.ResponseText -> renderText(state)
|
||||
is GemState.ResponseImage -> renderImage(state)
|
||||
is GemState.ResponseAudio -> renderAudio(state)
|
||||
is GemState.ResponseBinary -> renderBinary(state)
|
||||
is GemState.Blank -> {
|
||||
binding.addressEdit.setText("")
|
||||
adapter.render(arrayListOf())
|
||||
}
|
||||
is GemState.ResponseUnknownMime -> {
|
||||
runOnUiThread {
|
||||
loadingView(false)
|
||||
AlertDialog.Builder(this, R.style.AppDialogTheme)
|
||||
.setTitle(R.string.unknown_mime_dialog_title)
|
||||
.setMessage("Address: ${state.uri}\nMeta: ${state.header.meta}")
|
||||
.setPositiveButton("Download") { _, _ ->
|
||||
loadingView(true)
|
||||
model.requestBinaryDownload(state.uri)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gemini = Datasource.factory(this, db.history()),
|
||||
db = db,
|
||||
onState = this::handleState)
|
||||
|
||||
binding.addressEdit.setOnEditorActionListener { _, actionId, _ ->
|
||||
when (actionId) {
|
||||
|
@ -210,7 +167,7 @@ class GemActivity : AppCompatActivity() {
|
|||
startActivity(Intent.createChooser(this, null))
|
||||
}
|
||||
}
|
||||
R.id.overflow_menu_history -> HistoryDialog.show(this) { historyAddress ->
|
||||
R.id.overflow_menu_history -> HistoryDialog.show(this, db.history()) { historyAddress ->
|
||||
model.request(historyAddress)
|
||||
}
|
||||
R.id.overflow_menu_about -> AboutDialog.show(this)
|
||||
|
@ -235,6 +192,46 @@ class GemActivity : AppCompatActivity() {
|
|||
checkIntentExtras(intent)
|
||||
}
|
||||
|
||||
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 -> showAlert("${GeminiResponse.getCodeString(state.header.code)}: ${state.header.meta}")
|
||||
is GemState.ResponseGemtext -> renderGemtext(state)
|
||||
is GemState.ResponseText -> renderText(state)
|
||||
is GemState.ResponseImage -> renderImage(state)
|
||||
is GemState.ResponseAudio -> renderAudio(state)
|
||||
is GemState.ResponseBinary -> renderBinary(state)
|
||||
is GemState.Blank -> {
|
||||
binding.addressEdit.setText("")
|
||||
adapter.render(arrayListOf())
|
||||
}
|
||||
is GemState.ResponseUnknownMime -> {
|
||||
runOnUiThread {
|
||||
loadingView(false)
|
||||
AlertDialog.Builder(this, R.style.AppDialogTheme)
|
||||
.setTitle(R.string.unknown_mime_dialog_title)
|
||||
.setMessage("Address: ${state.uri}\nMeta: ${state.header.meta}")
|
||||
.setPositiveButton("Download") { _, _ ->
|
||||
loadingView(true)
|
||||
model.requestBinaryDownload(state.uri)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
|
@ -305,8 +302,6 @@ class GemActivity : AppCompatActivity() {
|
|||
binding.gemtextRecycler.post {
|
||||
binding.gemtextRecycler.scrollToPosition(0)
|
||||
}
|
||||
|
||||
history.add(state.uri.toString())
|
||||
}
|
||||
|
||||
private fun renderText(state: GemState.ResponseText) = runOnUiThread {
|
||||
|
@ -401,10 +396,14 @@ class GemActivity : AppCompatActivity() {
|
|||
if(visible) binding.appBar.setExpanded(true)
|
||||
}
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
override fun onBackPressed() {
|
||||
if(model.canGoBack()){
|
||||
model.goBack()
|
||||
model.goBack{ state ->
|
||||
/*
|
||||
Passing the callback here so we can eventually add a mechanism to restore scroll position
|
||||
*/
|
||||
handleState(state)
|
||||
}
|
||||
}else{
|
||||
println("Ariane history is empty - exiting")
|
||||
super.onBackPressed()
|
||||
|
|
|
@ -2,21 +2,21 @@ package oppen.ariane.ui
|
|||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import oppen.ariane.Ariane
|
||||
import oppen.ariane.io.gemini.Datasource
|
||||
import oppen.ariane.io.GemState
|
||||
import oppen.ariane.io.bookmarks.BookmarksDatasource
|
||||
import oppen.ariane.io.database.ArianeDatabase
|
||||
import oppen.ariane.io.database.bookmarks.BookmarksDatasource
|
||||
import java.net.URI
|
||||
|
||||
class GemViewModel: ViewModel() {
|
||||
|
||||
private lateinit var gemini: Datasource
|
||||
private lateinit var bookmarks: BookmarksDatasource
|
||||
private lateinit var db: ArianeDatabase
|
||||
private var onState: (state: GemState) -> Unit = {}
|
||||
|
||||
fun initialise(home: String, gemini: Datasource, bookmarks: BookmarksDatasource, onState: (state: GemState) -> Unit){
|
||||
fun initialise(home: String, gemini: Datasource, db: ArianeDatabase, onState: (state: GemState) -> Unit){
|
||||
this.gemini = gemini
|
||||
this.bookmarks = bookmarks
|
||||
this.db = db
|
||||
this.onState = onState
|
||||
|
||||
request(home)
|
||||
|
@ -49,5 +49,5 @@ class GemViewModel: ViewModel() {
|
|||
}
|
||||
|
||||
fun canGoBack(): Boolean = gemini.canGoBack()
|
||||
fun goBack() = gemini.goBack(onState)
|
||||
fun goBack(onGoBack: (state: GemState) -> Unit) = gemini.goBack(onGoBack)
|
||||
}
|
|
@ -7,8 +7,8 @@ import android.view.View
|
|||
import androidx.appcompat.app.AppCompatDialog
|
||||
import kotlinx.android.synthetic.main.fragment_bookmark_dialog.view.*
|
||||
import oppen.ariane.R
|
||||
import oppen.ariane.io.bookmarks.Bookmark
|
||||
import oppen.ariane.io.bookmarks.BookmarksDatasource
|
||||
import oppen.ariane.io.database.bookmarks.BookmarkEntry
|
||||
import oppen.ariane.io.database.bookmarks.BookmarksDatasource
|
||||
import java.net.URI
|
||||
|
||||
|
||||
|
@ -55,7 +55,7 @@ class BookmarkDialog(
|
|||
|
||||
bookmarkDatasource.add(
|
||||
|
||||
Bookmark(
|
||||
BookmarkEntry(
|
||||
uid = -1,
|
||||
label = view.bookmark_name.text.toString(),
|
||||
uri = URI.create(view.bookmark_uri.text.toString()),
|
||||
|
|
|
@ -6,18 +6,18 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.bookmark.view.*
|
||||
import oppen.ariane.R
|
||||
import oppen.ariane.io.bookmarks.Bookmark
|
||||
import oppen.ariane.io.database.bookmarks.BookmarkEntry
|
||||
import oppen.visible
|
||||
|
||||
class BookmarksAdapter(val onBookmark: (bookmark: Bookmark) -> Unit, val onOverflow: (view: View, bookmark: Bookmark, isFirst: Boolean, isLast: Boolean) -> Unit): RecyclerView.Adapter<BookmarksAdapter.ViewHolder>() {
|
||||
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<Bookmark>()
|
||||
val bookmarks = mutableListOf<BookmarkEntry>()
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
fun update(bookmarks: List<Bookmark>){
|
||||
fun update(bookmarkEntries: List<BookmarkEntry>){
|
||||
this.bookmarks.clear()
|
||||
this.bookmarks.addAll(bookmarks)
|
||||
this.bookmarks.addAll(bookmarkEntries)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
@ -51,19 +51,19 @@ class BookmarksAdapter(val onBookmark: (bookmark: Bookmark) -> Unit, val onOverf
|
|||
|
||||
override fun getItemCount(): Int = bookmarks.size
|
||||
|
||||
fun hide(bookmark: Bookmark) {
|
||||
bookmark.visible = false
|
||||
notifyItemChanged(bookmarks.indexOf(bookmark))
|
||||
fun hide(bookmarkEntry: BookmarkEntry) {
|
||||
bookmarkEntry.visible = false
|
||||
notifyItemChanged(bookmarks.indexOf(bookmarkEntry))
|
||||
}
|
||||
|
||||
fun show(bookmark: Bookmark) {
|
||||
bookmark.visible = true
|
||||
notifyItemChanged(bookmarks.indexOf(bookmark))
|
||||
fun show(bookmarkEntry: BookmarkEntry) {
|
||||
bookmarkEntry.visible = true
|
||||
notifyItemChanged(bookmarks.indexOf(bookmarkEntry))
|
||||
}
|
||||
|
||||
fun remove(bookmark: Bookmark){
|
||||
val index = bookmarks.indexOf(bookmark)
|
||||
bookmarks.remove(bookmark)
|
||||
fun remove(bookmarkEntry: BookmarkEntry){
|
||||
val index = bookmarks.indexOf(bookmarkEntry)
|
||||
bookmarks.remove(bookmarkEntry)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
|
@ -11,15 +11,15 @@ import com.google.android.material.snackbar.BaseTransientBottomBar
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_bookmarks.view.*
|
||||
import oppen.ariane.R
|
||||
import oppen.ariane.io.bookmarks.Bookmark
|
||||
import oppen.ariane.io.bookmarks.BookmarksDatasource
|
||||
import oppen.ariane.io.database.bookmarks.BookmarkEntry
|
||||
import oppen.ariane.io.database.bookmarks.BookmarksDatasource
|
||||
import oppen.visible
|
||||
|
||||
|
||||
class BookmarksDialog(
|
||||
context: Context,
|
||||
private val bookmarkDatasource: BookmarksDatasource,
|
||||
onBookmark: (bookmark: Bookmark) -> Unit): AppCompatDialog(context, R.style.FSDialog) {
|
||||
onBookmark: (bookmarkEntry: BookmarkEntry) -> Unit): AppCompatDialog(context, R.style.FSDialog) {
|
||||
|
||||
var bookmarksAdapter: BookmarksAdapter
|
||||
|
||||
|
@ -82,9 +82,9 @@ class BookmarksDialog(
|
|||
}
|
||||
}
|
||||
|
||||
private fun edit(bookmark: Bookmark){
|
||||
BookmarkDialog(context, BookmarkDialog.mode_edit, null, bookmark.uri.toString(), bookmark.label){ label, uri ->
|
||||
bookmarkDatasource.update(bookmark, label, uri){
|
||||
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)
|
||||
|
@ -99,15 +99,15 @@ class BookmarksDialog(
|
|||
* Bookmark isn't actually deleted from the DB until the Snackbar disappears. Which is nice.
|
||||
*
|
||||
*/
|
||||
private fun delete(bookmark: Bookmark){
|
||||
private fun delete(bookmarkEntry: BookmarkEntry){
|
||||
//OnDelete
|
||||
bookmarksAdapter.hide(bookmark)
|
||||
Snackbar.make(view, "Deleted ${bookmark.label}", Snackbar.LENGTH_SHORT).addCallback(object: Snackbar.Callback() {
|
||||
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(bookmark)
|
||||
else -> bookmarkDatasource.delete(bookmark){
|
||||
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_ACTION -> bookmarksAdapter.show(bookmarkEntry)
|
||||
else -> bookmarkDatasource.delete(bookmarkEntry){
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
bookmarksAdapter.remove(bookmark)
|
||||
bookmarksAdapter.remove(bookmarkEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,8 +116,8 @@ class BookmarksDialog(
|
|||
}.show()
|
||||
}
|
||||
|
||||
private fun moveUp(bookmark: Bookmark){
|
||||
bookmarkDatasource.moveUp(bookmark){
|
||||
private fun moveUp(bookmarkEntry: BookmarkEntry){
|
||||
bookmarkDatasource.moveUp(bookmarkEntry){
|
||||
bookmarkDatasource.get { bookmarks ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
bookmarksAdapter.update(bookmarks)
|
||||
|
@ -126,8 +126,8 @@ class BookmarksDialog(
|
|||
}
|
||||
}
|
||||
|
||||
private fun moveDown(bookmark: Bookmark){
|
||||
bookmarkDatasource.moveDown(bookmark){
|
||||
private fun moveDown(bookmarkEntry: BookmarkEntry){
|
||||
bookmarkDatasource.moveDown(bookmarkEntry){
|
||||
bookmarkDatasource.get { bookmarks ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
bookmarksAdapter.update(bookmarks)
|
||||
|
|
|
@ -7,8 +7,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import kotlinx.android.synthetic.main.row_history.view.*
|
||||
import oppen.delay
|
||||
import oppen.ariane.R
|
||||
import oppen.ariane.io.database.history.HistoryEntry
|
||||
|
||||
class HistoryAdapter(val history: List<String>, val onClick:(address: String) -> Unit): RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
|
||||
class HistoryAdapter(val history: List<HistoryEntry>, val onClick:(entry: HistoryEntry) -> Unit): RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(view: View): RecyclerView.ViewHolder(view)
|
||||
|
||||
|
@ -19,7 +20,7 @@ class HistoryAdapter(val history: List<String>, val onClick:(address: String) ->
|
|||
override fun getItemCount(): Int = history.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.itemView.history_address.text = history[position]
|
||||
holder.itemView.history_address.text = history[position].uri.toString()
|
||||
holder.itemView.history_row.setOnClickListener {
|
||||
delay(500){
|
||||
onClick(history[holder.adapterPosition])
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package oppen.ariane.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
|
||||
|
@ -11,14 +13,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import kotlinx.android.synthetic.main.dialog_about.view.close_tab_dialog
|
||||
import kotlinx.android.synthetic.main.dialog_history.view.*
|
||||
import oppen.ariane.R
|
||||
import oppen.ariane.io.database.history.ArianeHistory
|
||||
import oppen.ariane.io.gemini.RuntimeCache
|
||||
import oppen.ariane.io.history.uris.HistoryInterface
|
||||
|
||||
object HistoryDialog {
|
||||
fun show(context: Context, onHistoryItem: (address: String) -> Unit){
|
||||
val historyCache = HistoryInterface.default(context)
|
||||
val history = historyCache.get()
|
||||
|
||||
fun show(context: Context, history: ArianeHistory, onHistoryItem: (address: String) -> Unit){
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
val view = View.inflate(context, R.layout.dialog_history, null)
|
||||
|
@ -34,9 +33,10 @@ object HistoryDialog {
|
|||
inflater.inflate(R.menu.history_overflow_menu, popup.menu)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
if(menuItem.itemId == R.id.history_overflow_clear_history){
|
||||
historyCache.clear()
|
||||
dialog.dismiss()
|
||||
Toast.makeText(context, "History cleared", Toast.LENGTH_SHORT).show()
|
||||
history.clear {
|
||||
dialog.dismiss()
|
||||
Toast.makeText(context, "History cleared", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}else if(menuItem.itemId == R.id.history_overflow_clear_runtime_cache){
|
||||
RuntimeCache.clear()
|
||||
dialog.dismiss()
|
||||
|
@ -49,12 +49,16 @@ object HistoryDialog {
|
|||
}
|
||||
|
||||
view.history_recycler.layoutManager = LinearLayoutManager(context)
|
||||
view.history_recycler.adapter = HistoryAdapter(history.asReversed()){ address ->
|
||||
onHistoryItem(address)
|
||||
dialog.dismiss()
|
||||
|
||||
history.get { history ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
view.history_recycler.adapter = HistoryAdapter(history) { entry ->
|
||||
onHistoryItem(entry.uri.toString())
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
}
|
||||
}
|
BIN
app/src/main/res/raw/cert.pfx
Normal file
BIN
app/src/main/res/raw/cert.pfx
Normal file
Binary file not shown.
Loading…
Reference in a new issue