Implemented source launching
This commit is contained in:
17
.idea/deploymentTargetDropDown.xml
generated
17
.idea/deploymentTargetDropDown.xml
generated
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="deploymentTargetDropDown">
|
|
||||||
<targetSelectedWithDropDown>
|
|
||||||
<Target>
|
|
||||||
<type value="QUICK_BOOT_TARGET" />
|
|
||||||
<deviceKey>
|
|
||||||
<Key>
|
|
||||||
<type value="VIRTUAL_DEVICE_PATH" />
|
|
||||||
<value value="$USER_HOME$/.android/avd/Pixel_2_API_30.avd" />
|
|
||||||
</Key>
|
|
||||||
</deviceKey>
|
|
||||||
</Target>
|
|
||||||
</targetSelectedWithDropDown>
|
|
||||||
<timeTargetWasSelectedWithDropDown value="2021-12-25T06:38:27.847239Z" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -72,8 +72,8 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
|
||||||
|
|
||||||
implementation("androidx.activity:activity-compose:1.4.0")
|
implementation("androidx.activity:activity-compose:1.4.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
|
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
|
||||||
@@ -100,7 +100,7 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-client-okhttp:1.6.7")
|
implementation("io.ktor:ktor-client-okhttp:1.6.7")
|
||||||
implementation("io.ktor:ktor-client-serialization:1.6.7")
|
implementation("io.ktor:ktor-client-serialization:1.6.7")
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0")
|
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||||
implementation("androidx.activity:activity-ktx:1.4.0")
|
implementation("androidx.activity:activity-ktx:1.4.0")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.0")
|
implementation("androidx.fragment:fragment-ktx:1.4.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
@@ -110,17 +110,17 @@ dependencies {
|
|||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
|
||||||
|
|
||||||
implementation("androidx.room:room-runtime:2.4.0")
|
implementation("androidx.room:room-runtime:2.4.1")
|
||||||
annotationProcessor("androidx.room:room-compiler:2.4.0")
|
annotationProcessor("androidx.room:room-compiler:2.4.1")
|
||||||
kapt("androidx.room:room-compiler:2.4.0")
|
kapt("androidx.room:room-compiler:2.4.1")
|
||||||
implementation("androidx.room:room-ktx:2.4.0")
|
implementation("androidx.room:room-ktx:2.4.1")
|
||||||
|
|
||||||
implementation("androidx.datastore:datastore:1.0.0")
|
implementation("androidx.datastore:datastore:1.0.0")
|
||||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||||
|
|
||||||
implementation("org.kodein.di:kodein-di-framework-compose:7.10.0")
|
implementation("org.kodein.di:kodein-di-framework-compose:7.10.0")
|
||||||
|
|
||||||
implementation("com.google.android.material:material:1.4.0")
|
implementation("com.google.android.material:material:1.5.0")
|
||||||
|
|
||||||
implementation(platform("com.google.firebase:firebase-bom:29.0.3"))
|
implementation(platform("com.google.firebase:firebase-bom:29.0.3"))
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||||
@@ -135,9 +135,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation("ru.noties.markwon:core:3.1.0")
|
implementation("ru.noties.markwon:core:3.1.0")
|
||||||
|
|
||||||
implementation("xyz.quaver.pupil.sources:core:0.0.1-alpha01-DEV10")
|
implementation("xyz.quaver.pupil.sources:core:0.0.1-alpha01-DEV16")
|
||||||
|
|
||||||
implementation("xyz.quaver:documentfilex:0.7.1")
|
implementation("xyz.quaver:documentfilex:0.7.2")
|
||||||
implementation("xyz.quaver:subsampledimage:0.0.1-alpha19-SNAPSHOT")
|
implementation("xyz.quaver:subsampledimage:0.0.1-alpha19-SNAPSHOT")
|
||||||
|
|
||||||
implementation("com.google.guava:guava:31.0.1-jre")
|
implementation("com.google.guava:guava:31.0.1-jre")
|
||||||
|
|||||||
@@ -3,14 +3,11 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="xyz.quaver.pupil">
|
package="xyz.quaver.pupil">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
@@ -38,17 +35,8 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
|
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".receiver.UpdateBroadcastReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
@@ -62,14 +50,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:scheme="http"
|
|
||||||
android:host="ix.io"
|
|
||||||
android:pathPattern="/..*" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -23,41 +23,31 @@ import android.app.Notification
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
|
||||||
import com.google.firebase.analytics.ktx.analytics
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import com.google.firebase.ktx.Firebase
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.okhttp.*
|
import io.ktor.client.engine.okhttp.*
|
||||||
import io.ktor.client.features.*
|
import io.ktor.client.features.*
|
||||||
import io.ktor.client.features.cache.*
|
import io.ktor.client.features.cache.*
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.features.json.*
|
||||||
import io.ktor.client.features.json.serializer.*
|
import io.ktor.client.features.json.serializer.*
|
||||||
|
import io.ktor.http.*
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import org.kodein.di.*
|
import org.kodein.di.*
|
||||||
import org.kodein.di.android.x.androidXModule
|
import org.kodein.di.android.x.androidXModule
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.pupil.proto.settingsDataStore
|
|
||||||
import xyz.quaver.pupil.sources.core.NetworkCache
|
import xyz.quaver.pupil.sources.core.NetworkCache
|
||||||
import xyz.quaver.pupil.sources.sourceModule
|
import xyz.quaver.pupil.sources.core.settingsDataStore
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.ApkDownloadManager
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Pupil : Application(), DIAware {
|
class Pupil : Application(), DIAware {
|
||||||
|
|
||||||
override val di: DI by DI.lazy {
|
override val di: DI by DI.lazy {
|
||||||
import(androidXModule(this@Pupil))
|
import(androidXModule(this@Pupil))
|
||||||
import(sourceModule(this@Pupil))
|
|
||||||
|
|
||||||
bind { singleton { NetworkCache(applicationContext) } }
|
bind { singleton { NetworkCache(this@Pupil, instance()) } }
|
||||||
|
bindSingleton { ApkDownloadManager(this@Pupil, instance()) }
|
||||||
|
|
||||||
bindSingleton { settingsDataStore }
|
bindSingleton { settingsDataStore }
|
||||||
|
|
||||||
@@ -70,6 +60,7 @@ class Pupil : Application(), DIAware {
|
|||||||
}
|
}
|
||||||
install(JsonFeature) {
|
install(JsonFeature) {
|
||||||
serializer = KotlinxSerializer()
|
serializer = KotlinxSerializer()
|
||||||
|
accept(ContentType("text", "plain"))
|
||||||
}
|
}
|
||||||
install(HttpTimeout) {
|
install(HttpTimeout) {
|
||||||
requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||||
@@ -82,43 +73,8 @@ class Pupil : Application(), DIAware {
|
|||||||
} }
|
} }
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
super.onCreate()
|
||||||
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
|
|
||||||
val userID = Preferences["user_id", ""].let { userID ->
|
|
||||||
if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
|
|
||||||
else userID
|
|
||||||
}
|
|
||||||
|
|
||||||
firebaseAnalytics = Firebase.analytics
|
|
||||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
|
||||||
|
|
||||||
try {
|
|
||||||
Preferences.get<String>("download_folder").also {
|
|
||||||
if (it.startsWith("content"))
|
|
||||||
contentResolver.takePersistableUriPermission(
|
|
||||||
Uri.parse(it),
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!FileX(this, it).canWrite())
|
|
||||||
throw Exception()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Preferences.remove("download_folder")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Preferences["reset_secure", false]) {
|
|
||||||
Preferences["security_mode"] = false
|
|
||||||
Preferences["reset_secure"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
@@ -151,21 +107,7 @@ class Pupil : Application(), DIAware {
|
|||||||
enableVibration(true)
|
enableVibration(true)
|
||||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_LOW).apply {
|
|
||||||
description = getString(R.string.channel_update_description)
|
|
||||||
enableLights(false)
|
|
||||||
enableVibration(false)
|
|
||||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
|
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
})
|
|
||||||
|
|
||||||
super.onCreate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2021 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.proto
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.core.CorruptionException
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.datastore.core.Serializer
|
|
||||||
import androidx.datastore.dataStore
|
|
||||||
import com.google.protobuf.InvalidProtocolBufferException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
object SettingsSerializer : Serializer<Settings> {
|
|
||||||
override val defaultValue: Settings = Settings.getDefaultInstance()
|
|
||||||
|
|
||||||
override suspend fun readFrom(input: InputStream): Settings {
|
|
||||||
try {
|
|
||||||
return Settings.parseFrom(input)
|
|
||||||
} catch (exception: InvalidProtocolBufferException) {
|
|
||||||
throw CorruptionException("Cannot read proto.", exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
val Application.settingsDataStore: DataStore<Settings> by dataStore(
|
|
||||||
fileName = "settings.proto",
|
|
||||||
serializer = SettingsSerializer
|
|
||||||
)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.receiver
|
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UpdateBroadcastReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
context ?: return
|
|
||||||
|
|
||||||
when (intent?.action) {
|
|
||||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
|
||||||
|
|
||||||
// Validate download
|
|
||||||
val downloadID: Long = Preferences["update_download_id"]
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
|
|
||||||
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2) != downloadID)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Get target uri
|
|
||||||
|
|
||||||
val query = DownloadManager.Query()
|
|
||||||
.setFilterById(downloadID)
|
|
||||||
|
|
||||||
val uri = downloadManager.query(query).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
|
|
||||||
val uri = Uri.parse(it)
|
|
||||||
|
|
||||||
when (uri.scheme) {
|
|
||||||
"file" ->
|
|
||||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)
|
|
||||||
)
|
|
||||||
"content" -> uri
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
null
|
|
||||||
} ?: return
|
|
||||||
|
|
||||||
// Build Notification
|
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(context, "update")
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
.setContentTitle(context.getText(R.string.update_download_completed))
|
|
||||||
.setContentText(context.getText(R.string.update_download_completed_description))
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
notificationManager.notify(R.id.notification_id_update, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -22,69 +22,105 @@ import android.app.Application
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import dalvik.system.PathClassLoader
|
import dalvik.system.PathClassLoader
|
||||||
import org.kodein.di.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.bindFactory
|
||||||
|
import org.kodein.di.bindInstance
|
||||||
|
import org.kodein.di.bindProvider
|
||||||
|
import org.kodein.di.compose.rememberInstance
|
||||||
import xyz.quaver.pupil.sources.core.Source
|
import xyz.quaver.pupil.sources.core.Source
|
||||||
import java.util.*
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
private const val SOURCES_FEATURE = "pupil.sources"
|
private const val SOURCES_FEATURE = "pupil.sources"
|
||||||
private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources"
|
private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources"
|
||||||
private const val SOURCES_PATH = "pupil.sources.path"
|
private const val SOURCES_PATH = "pupil.sources.path"
|
||||||
|
|
||||||
data class SourceEntry(
|
data class SourceEntry(
|
||||||
val name: String,
|
val packageName: String,
|
||||||
val source: Source,
|
val packagePath: String,
|
||||||
val icon: Drawable
|
val sourceName: String,
|
||||||
|
val sourcePath: String,
|
||||||
|
val sourceDir: String,
|
||||||
|
val icon: Drawable,
|
||||||
|
val version: String
|
||||||
)
|
)
|
||||||
typealias SourceEntries = Map<String, SourceEntry>
|
typealias SourceEntries = Map<String, SourceEntry>
|
||||||
|
|
||||||
private val sources = mutableMapOf<String, SourceEntry>()
|
|
||||||
|
|
||||||
val PackageInfo.isSourceFeatureEnabled
|
val PackageInfo.isSourceFeatureEnabled
|
||||||
get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE }
|
get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE }
|
||||||
|
|
||||||
fun loadSource(app: Application, packageInfo: PackageInfo) {
|
fun loadSource(app: Application, packageInfo: PackageInfo): List<SourceEntry> {
|
||||||
val packageManager = app.packageManager
|
val packageManager = app.packageManager
|
||||||
|
|
||||||
val applicationInfo = packageInfo.applicationInfo
|
val applicationInfo = packageInfo.applicationInfo
|
||||||
|
|
||||||
val classLoader = PathClassLoader(applicationInfo.sourceDir, null, app.classLoader)
|
val packageName = packageManager.getApplicationLabel(applicationInfo).toString().substringAfter("[Pupil] ")
|
||||||
val packageName = packageInfo.packageName
|
val packagePath = packageInfo.packageName
|
||||||
|
|
||||||
val sourceName = packageManager.getApplicationLabel(applicationInfo).toString().substringAfter("[Pupil] ")
|
|
||||||
|
|
||||||
val icon = packageManager.getApplicationIcon(applicationInfo)
|
val icon = packageManager.getApplicationIcon(applicationInfo)
|
||||||
|
|
||||||
packageInfo
|
val version = packageInfo.versionName
|
||||||
|
|
||||||
|
return packageInfo
|
||||||
.applicationInfo
|
.applicationInfo
|
||||||
.metaData
|
.metaData
|
||||||
.getString(SOURCES_PATH)
|
?.getString(SOURCES_PATH)
|
||||||
?.split(';')
|
?.split(';')
|
||||||
.orEmpty()
|
?.map { source ->
|
||||||
.forEach { sourcePath ->
|
val (sourceName, sourcePath) = source.split(':', limit = 2)
|
||||||
sources[sourceName] = SourceEntry(
|
SourceEntry(
|
||||||
|
packageName,
|
||||||
|
packagePath,
|
||||||
sourceName,
|
sourceName,
|
||||||
Class.forName("$packageName$sourcePath", false, classLoader)
|
sourcePath,
|
||||||
.getConstructor(Application::class.java)
|
applicationInfo.sourceDir,
|
||||||
.newInstance(app) as Source,
|
icon,
|
||||||
icon
|
version
|
||||||
)
|
)
|
||||||
}
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadSources(app: Application) {
|
fun loadSource(app: Application, sourceEntry: SourceEntry): Source {
|
||||||
|
val classLoader = PathClassLoader(sourceEntry.sourceDir, null, app.classLoader)
|
||||||
|
|
||||||
|
return Class.forName("${sourceEntry.packagePath}${sourceEntry.sourcePath}", false, classLoader)
|
||||||
|
.getConstructor(Application::class.java)
|
||||||
|
.newInstance(app) as Source
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSources(app: Application): List<SourceEntry> {
|
||||||
val packageManager = app.packageManager
|
val packageManager = app.packageManager
|
||||||
|
|
||||||
val packages = packageManager.getInstalledPackages(
|
val packages = packageManager.getInstalledPackages(
|
||||||
PackageManager.GET_CONFIGURATIONS or
|
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_META_DATA
|
||||||
PackageManager.GET_META_DATA
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val sources = packages.filter { it.isSourceFeatureEnabled }
|
return packages.flatMap { packageInfo ->
|
||||||
|
if (packageInfo.isSourceFeatureEnabled)
|
||||||
sources.forEach { loadSource(app, it) }
|
loadSource(app, packageInfo)
|
||||||
|
else
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sourceModule(app: Application) = DI.Module(name = "source") {
|
@Composable
|
||||||
loadSources(app)
|
fun rememberSources(): State<List<SourceEntry>> {
|
||||||
bindInstance { Collections.unmodifiableMap(sources) }
|
val app: Application by rememberInstance()
|
||||||
|
val sources = remember { mutableStateOf<List<SourceEntry>>(emptyList()) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
sources.value = updateSources(app)
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources
|
||||||
}
|
}
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2021 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.sources
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
|
||||||
import org.kodein.di.compose.rememberInstance
|
|
||||||
import xyz.quaver.pupil.sources.core.Source
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourceSelectDialog(navController: NavController, currentSource: String? = null, onDismissRequest: () -> Unit = { }) {
|
|
||||||
SourceSelectDialog(currentSource = currentSource, onDismissRequest = onDismissRequest) {
|
|
||||||
onDismissRequest()
|
|
||||||
navController.navigate(it.name) {
|
|
||||||
currentSource?.let { popUpTo("main") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourceSelectDialogItem(sourceEntry: SourceEntry, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = rememberDrawablePainter(sourceEntry.icon),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
sourceEntry.name,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Settings,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Button(
|
|
||||||
enabled = !isSelected,
|
|
||||||
onClick = {
|
|
||||||
onSelected(sourceEntry.source)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("GO")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourceSelectDialog(currentSource: String? = null, onDismissRequest: () -> Unit = { }, onSelected: (Source) -> Unit = { }) {
|
|
||||||
val sourceEntries: SourceEntries by rememberInstance()
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismissRequest) {
|
|
||||||
Card(
|
|
||||||
elevation = 8.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Column() {
|
|
||||||
sourceEntries.values.forEach { SourceSelectDialogItem(it, it.name == currentSource, onSelected) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,39 +20,36 @@ package xyz.quaver.pupil.ui
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.createGraph
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import org.kodein.di.compose.rememberInstance
|
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import org.kodein.log.LoggerFactory
|
import org.kodein.log.LoggerFactory
|
||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
import xyz.quaver.pupil.proto.Settings
|
|
||||||
import xyz.quaver.pupil.sources.SourceEntries
|
import xyz.quaver.pupil.sources.SourceEntries
|
||||||
import xyz.quaver.pupil.sources.SourceSelectDialog
|
import xyz.quaver.pupil.sources.SourceEntry
|
||||||
|
import xyz.quaver.pupil.sources.core.Source
|
||||||
|
import xyz.quaver.pupil.sources.loadSource
|
||||||
import xyz.quaver.pupil.ui.theme.PupilTheme
|
import xyz.quaver.pupil.ui.theme.PupilTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
|
||||||
class MainActivity : ComponentActivity(), DIAware {
|
class MainActivity : ComponentActivity(), DIAware {
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
|
|
||||||
private val sources: SourceEntries by instance()
|
|
||||||
|
|
||||||
private val logger = newLogger(LoggerFactory.default)
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -68,6 +65,8 @@ class MainActivity : ComponentActivity(), DIAware {
|
|||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val useDarkIcons = MaterialTheme.colors.isLight
|
val useDarkIcons = MaterialTheme.colors.isLight
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
SideEffect {
|
SideEffect {
|
||||||
systemUiController.setSystemBarsColor(
|
systemUiController.setSystemBarsColor(
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
@@ -75,42 +74,29 @@ class MainActivity : ComponentActivity(), DIAware {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(navController, startDestination = "main") {
|
NavHost(navController, "source") {
|
||||||
composable("main") {
|
composable("source") {
|
||||||
var launched by rememberSaveable { mutableStateOf(false) }
|
var source by remember { mutableStateOf<Source?>(null) }
|
||||||
val settingsDataStore: DataStore<Settings> by rememberInstance()
|
|
||||||
|
|
||||||
var sourceSelectDialog by remember { mutableStateOf(false) }
|
BackHandler(
|
||||||
|
enabled = source != null
|
||||||
|
) {
|
||||||
|
source = null
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceSelectDialog)
|
if (source == null)
|
||||||
SourceSelectDialog(navController, null)
|
SourceSelector {
|
||||||
|
coroutineScope.launch {
|
||||||
LaunchedEffect(Unit) {
|
source = loadSource(application, it)
|
||||||
val recentSource =
|
|
||||||
settingsDataStore.data.map { it.recentSource }
|
|
||||||
.first()
|
|
||||||
|
|
||||||
if (recentSource.isEmpty()) {
|
|
||||||
sourceSelectDialog = true
|
|
||||||
launched = true
|
|
||||||
} else {
|
|
||||||
if (!launched) {
|
|
||||||
navController.navigate(recentSource)
|
|
||||||
launched = true
|
|
||||||
} else {
|
|
||||||
onBackPressed()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
source!!.Entry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
|
|
||||||
}
|
}
|
||||||
sources.values.forEach {
|
|
||||||
it.source.run {
|
|
||||||
navGraph(navController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
295
app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt
Normal file
295
app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.DownloadDone
|
||||||
|
import androidx.compose.material.icons.filled.Explore
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.capitalize
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
|
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||||
|
import com.google.accompanist.insets.ui.BottomNavigation
|
||||||
|
import com.google.accompanist.insets.ui.Scaffold
|
||||||
|
import com.google.accompanist.insets.ui.TopAppBar
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.kodein.di.compose.rememberInstance
|
||||||
|
import xyz.quaver.pupil.sources.SourceEntries
|
||||||
|
import xyz.quaver.pupil.sources.SourceEntry
|
||||||
|
import xyz.quaver.pupil.sources.rememberSources
|
||||||
|
import xyz.quaver.pupil.util.ApkDownloadManager
|
||||||
|
|
||||||
|
private sealed class SourceSelectorScreen(val route: String, val icon: ImageVector) {
|
||||||
|
object Local: SourceSelectorScreen("local", Icons.Default.DownloadDone)
|
||||||
|
object Explore: SourceSelectorScreen("explore", Icons.Default.Explore)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sourceSelectorScreens = listOf(
|
||||||
|
SourceSelectorScreen.Local,
|
||||||
|
SourceSelectorScreen.Explore
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceListItem(icon: Painter, name: String, version: String, actions: @Composable () -> Unit = { }) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
elevation = 4.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
icon,
|
||||||
|
contentDescription = "source icon",
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
name.capitalize(Locale.current)
|
||||||
|
)
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
||||||
|
Text(
|
||||||
|
"v$version",
|
||||||
|
style = MaterialTheme.typography.caption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Local(onSource: (SourceEntry) -> Unit) {
|
||||||
|
val sources by rememberSources()
|
||||||
|
|
||||||
|
if (sources.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Column(
|
||||||
|
Modifier.align(Alignment.Center),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
||||||
|
Text("(´∇`)", style = MaterialTheme.typography.h2)
|
||||||
|
}
|
||||||
|
Text("No sources found!\nLet's go download one.", textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn {
|
||||||
|
items(sources) { source ->
|
||||||
|
SourceListItem(
|
||||||
|
rememberDrawablePainter(source.icon),
|
||||||
|
source.sourceName,
|
||||||
|
source.version
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onSource(source) }
|
||||||
|
) {
|
||||||
|
Text("GO")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class RemoteSourceInfo(
|
||||||
|
val projectName: String,
|
||||||
|
val name: String,
|
||||||
|
val version: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Explore() {
|
||||||
|
val sources by rememberSources()
|
||||||
|
val localSources by derivedStateOf {
|
||||||
|
sources.map {
|
||||||
|
it.packageName to it
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
val client: HttpClient by rememberInstance()
|
||||||
|
|
||||||
|
val downloadManager: ApkDownloadManager by rememberInstance()
|
||||||
|
val progresses = remember { mutableStateMapOf<String, Float>() }
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val remoteSources by produceState<Map<String, RemoteSourceInfo>?>(null) {
|
||||||
|
while (true) {
|
||||||
|
delay(1000)
|
||||||
|
value = withContext(Dispatchers.IO) {
|
||||||
|
client.get<Map<String, RemoteSourceInfo>>("https://raw.githubusercontent.com/tom5079/PupilSources/master/versions.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (remoteSources == null)
|
||||||
|
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||||
|
else
|
||||||
|
LazyColumn {
|
||||||
|
items(remoteSources?.values?.toList() ?: emptyList()) { source ->
|
||||||
|
SourceListItem(
|
||||||
|
rememberImagePainter("https://raw.githubusercontent.com/tom5079/PupilSources/master/${source.projectName}/src/main/res/mipmap-xxxhdpi/ic_launcher.png"),
|
||||||
|
source.name,
|
||||||
|
source.version
|
||||||
|
) {
|
||||||
|
if (source.name !in progresses)
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (source.name in localSources) {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", localSources[source.name]!!.packagePath, null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else coroutineScope.launch {
|
||||||
|
progresses[source.name] = 0f
|
||||||
|
downloadManager.download(source.projectName, source.name, source.version)
|
||||||
|
.onCompletion {
|
||||||
|
progresses.remove(source.name)
|
||||||
|
}.collectLatest {
|
||||||
|
progresses[source.name] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
if (source.name !in localSources) Icons.Default.Download
|
||||||
|
else Icons.Outlined.Info,
|
||||||
|
contentDescription = "download"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val progress = progresses[source.name]
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier.padding(12.dp, 0.dp)
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
progress?.isFinite() == true ->
|
||||||
|
CircularProgressIndicator(progress, modifier = Modifier.size(24.dp))
|
||||||
|
else ->
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceSelector(onSource: (SourceEntry) -> Unit) {
|
||||||
|
val bottomNavController = rememberNavController()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text("Sources")
|
||||||
|
},
|
||||||
|
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.statusBars)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
BottomNavigation(
|
||||||
|
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
||||||
|
) {
|
||||||
|
val navBackStackEntry by bottomNavController.currentBackStackEntryAsState()
|
||||||
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
|
||||||
|
sourceSelectorScreens.forEach { screen ->
|
||||||
|
BottomNavigationItem(
|
||||||
|
icon = { Icon(screen.icon, contentDescription = screen.route) },
|
||||||
|
label = { Text(screen.route) },
|
||||||
|
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||||
|
onClick = {
|
||||||
|
bottomNavController.navigate(screen.route) {
|
||||||
|
popUpTo(bottomNavController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { contentPadding ->
|
||||||
|
NavHost(bottomNavController, startDestination = "local", modifier = Modifier.padding(contentPadding)) {
|
||||||
|
composable(SourceSelectorScreen.Local.route) { Local(onSource) }
|
||||||
|
composable(SourceSelectorScreen.Explore.route) { Explore() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2021 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OpenWithItemIDDialog(onDismissRequest: (String?) -> Unit = { }) {
|
|
||||||
var itemID by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = { onDismissRequest(null) }) {
|
|
||||||
Card(
|
|
||||||
elevation = 8.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.main_open_gallery_by_id),
|
|
||||||
style = MaterialTheme.typography.h6
|
|
||||||
)
|
|
||||||
TextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = itemID,
|
|
||||||
onValueChange = {
|
|
||||||
itemID = it
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Go),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onGo = { onDismissRequest(itemID) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
onClick = { onDismissRequest(itemID) }
|
|
||||||
) {
|
|
||||||
Text(stringResource(android.R.string.ok))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.features.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.util.cio.*
|
||||||
|
import io.ktor.utils.io.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import io.ktor.utils.io.jvm.javaio.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.io.use
|
||||||
|
|
||||||
|
class ApkDownloadManager(private val context: Context, private val client: HttpClient) {
|
||||||
|
fun download(projectName: String, sourceName: String, version: String) = flow {
|
||||||
|
val url = "https://github.com/tom5079/PupilSources/releases/download/$sourceName-$version/$projectName-release.apk"
|
||||||
|
|
||||||
|
val file = File(context.externalCacheDir, "apks/$sourceName-$version.apk").also {
|
||||||
|
it.parentFile?.mkdir()
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.get<HttpStatement>(url).execute { response ->
|
||||||
|
val channel: ByteReadChannel = response.receive()
|
||||||
|
val contentLength = response.contentLength() ?: -1
|
||||||
|
var readBytes = 0f
|
||||||
|
|
||||||
|
file.outputStream().use { outputStream ->
|
||||||
|
while (!channel.isClosedForRead) {
|
||||||
|
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
|
while (!packet.isEmpty) {
|
||||||
|
val bytes = packet.readBytes()
|
||||||
|
outputStream.write(bytes)
|
||||||
|
|
||||||
|
readBytes += bytes.size
|
||||||
|
emit(readBytes / contentLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(Float.POSITIVE_INFINITY)
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2021 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewTreeObserver
|
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/68389802/how-to-clear-textfield-focus-when-closing-the-keyboard-and-prevent-two-back-pres
|
|
||||||
class KeyboardManager(context: Context) {
|
|
||||||
private val activity = context as Activity
|
|
||||||
private var keyboardDismissListener: KeyboardDismissListener? = null
|
|
||||||
|
|
||||||
private abstract class KeyboardDismissListener(
|
|
||||||
private val rootView: View,
|
|
||||||
private val onKeyboardDismiss: () -> Unit
|
|
||||||
) : ViewTreeObserver.OnGlobalLayoutListener {
|
|
||||||
private var isKeyboardClosed: Boolean = false
|
|
||||||
override fun onGlobalLayout() {
|
|
||||||
val r = Rect()
|
|
||||||
rootView.getWindowVisibleDisplayFrame(r)
|
|
||||||
val screenHeight = rootView.rootView.height
|
|
||||||
val keypadHeight = screenHeight - r.bottom
|
|
||||||
if (keypadHeight > screenHeight * 0.15) {
|
|
||||||
// 0.15 ratio is right enough to determine keypad height.
|
|
||||||
isKeyboardClosed = false
|
|
||||||
} else if (!isKeyboardClosed) {
|
|
||||||
isKeyboardClosed = true
|
|
||||||
onKeyboardDismiss.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun attachKeyboardDismissListener(onKeyboardDismiss: () -> Unit) {
|
|
||||||
val rootView = activity.findViewById<View>(android.R.id.content)
|
|
||||||
keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {}
|
|
||||||
keyboardDismissListener?.let {
|
|
||||||
rootView.viewTreeObserver.addOnGlobalLayoutListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
val rootView = activity.findViewById<View>(android.R.id.content)
|
|
||||||
keyboardDismissListener?.let {
|
|
||||||
rootView.viewTreeObserver.removeOnGlobalLayoutListener(it)
|
|
||||||
}
|
|
||||||
keyboardDismissListener = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
lateinit var preferences: SharedPreferences
|
|
||||||
|
|
||||||
@Deprecated("Use DataStore")
|
|
||||||
object Preferences: SharedPreferences by preferences {
|
|
||||||
|
|
||||||
val defMap = mapOf(
|
|
||||||
String::class to "",
|
|
||||||
Int::class to -1,
|
|
||||||
Long::class to -1L,
|
|
||||||
Boolean::class to false,
|
|
||||||
Set::class to emptySet<Any>()
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun set(key: String, value: String) = edit().putString(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Int) = edit().putInt(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Long) = edit().putLong(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Boolean) = edit().putBoolean(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Set<String>) = edit().putStringSet(key, value).apply()
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
inline operator fun <reified T: Any> get(key: String, defaultVal: T = defMap[T::class] as T): T = (all[key] as? T) ?: defaultVal
|
|
||||||
|
|
||||||
fun remove(key: String) {
|
|
||||||
edit().remove(key).apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
fun File.size(): Long =
|
|
||||||
this.walk().fold(0L) { size, file -> size + file.length() }
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.File
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
fun hash(password: String): String {
|
|
||||||
val bytes = password.toByteArray()
|
|
||||||
val md = MessageDigest.getInstance("SHA-256")
|
|
||||||
|
|
||||||
return md.digest(bytes).fold("") { str, it -> str + "%02x".format(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ret1: SHA-256 Hash
|
|
||||||
// Ret2: Hash salt
|
|
||||||
fun hashWithSalt(password: String): Pair<String, String> {
|
|
||||||
val salt = (0 until 12).map { source.random() }.joinToString()
|
|
||||||
|
|
||||||
return Pair(hash(password+salt), salt)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
|
||||||
|
|
||||||
enum class Type {
|
|
||||||
PATTERN,
|
|
||||||
PIN,
|
|
||||||
PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun generate(type: Type, password: String): Lock {
|
|
||||||
val (hash, salt) = hashWithSalt(password)
|
|
||||||
return Lock(type, hash, salt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun match(password: String): Boolean {
|
|
||||||
return hash(password+salt) == hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LockManager(base: Context): ContextWrapper(base) {
|
|
||||||
|
|
||||||
var locks: ArrayList<Lock>? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun load() {
|
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
|
||||||
|
|
||||||
if (!lock.exists()) {
|
|
||||||
lock.createNewFile()
|
|
||||||
lock.writeText("[]")
|
|
||||||
}
|
|
||||||
|
|
||||||
locks = Json.decodeFromString(lock.readText())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save() {
|
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
|
||||||
|
|
||||||
if (!lock.exists())
|
|
||||||
lock.createNewFile()
|
|
||||||
|
|
||||||
lock.writeText(Json.encodeToString(locks?.toList() ?: listOf()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun add(lock: Lock) {
|
|
||||||
remove(lock.type)
|
|
||||||
locks?.add(lock)
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun remove(type: Lock.Type) {
|
|
||||||
locks?.removeAll { it.type == type }
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun check(password: String): Boolean? {
|
|
||||||
return locks?.any {
|
|
||||||
it.match(password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmpty(): Boolean {
|
|
||||||
return locks.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isNotEmpty(): Boolean = !isEmpty()
|
|
||||||
|
|
||||||
fun contains(type: Lock.Type): Boolean {
|
|
||||||
return locks?.any { it.type == type } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.view.View
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.geometry.Rect
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.toAndroidRect
|
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
import org.kodein.di.DIAware
|
|
||||||
import org.kodein.di.DirectDIAware
|
|
||||||
import org.kodein.di.direct
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import xyz.quaver.graphics.subsampledimage.ImageSource
|
|
||||||
import xyz.quaver.graphics.subsampledimage.newBitmapRegionDecoder
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.inputStream
|
|
||||||
import xyz.quaver.pupil.sources.SourceEntries
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
operator fun JsonElement.get(index: Int) =
|
|
||||||
this.jsonArray[index]
|
|
||||||
|
|
||||||
operator fun JsonElement.get(tag: String) =
|
|
||||||
this.jsonObject[tag]
|
|
||||||
|
|
||||||
val JsonElement.content
|
|
||||||
get() = this.jsonPrimitive.contentOrNull
|
|
||||||
|
|
||||||
fun DIAware.source(source: String) = lazy { direct.source(source) }
|
|
||||||
fun DirectDIAware.source(source: String) = instance<SourceEntries>()[source]!!
|
|
||||||
|
|
||||||
class FileXImageSource(val file: FileX): ImageSource {
|
|
||||||
private val decoder by lazy {
|
|
||||||
file.inputStream()!!.use {
|
|
||||||
newBitmapRegionDecoder(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val imageSize by lazy { Size(decoder.width.toFloat(), decoder.height.toFloat()) }
|
|
||||||
|
|
||||||
override fun decodeRegion(region: Rect, sampleSize: Int): ImageBitmap =
|
|
||||||
decoder.decodeRegion(region.toAndroidRect(), BitmapFactory.Options().apply {
|
|
||||||
inSampleSize = sampleSize
|
|
||||||
}).asImageBitmap()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberFileXImageSource(file: FileX) = remember {
|
|
||||||
FileXImageSource(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sha256(data: ByteArray) : ByteArray {
|
|
||||||
return MessageDigest.getInstance("SHA-256").digest(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
val Context.activity: Activity?
|
|
||||||
get() {
|
|
||||||
var currentContext = this
|
|
||||||
while (currentContext is ContextWrapper) {
|
|
||||||
if (currentContext is Activity)
|
|
||||||
return currentContext
|
|
||||||
currentContext = currentContext.baseContext
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Proxy
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ProxyInfo(
|
|
||||||
val type: Proxy.Type,
|
|
||||||
val host: String? = null,
|
|
||||||
val port: Int? = null,
|
|
||||||
val username: String? = null,
|
|
||||||
val password: String? = null
|
|
||||||
) {
|
|
||||||
fun proxy() : Proxy {
|
|
||||||
return if (host.isNullOrBlank() || port == null)
|
|
||||||
return Proxy.NO_PROXY
|
|
||||||
else
|
|
||||||
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Migrate to ktor-client and implement proxy authentication
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProxyInfo(): ProxyInfo =
|
|
||||||
Json.decodeFromString(Preferences["proxy", Json.encodeToString(ProxyInfo(Proxy.Type.DIRECT))])
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.bind
|
|
||||||
import org.kodein.di.bindInstance
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private val filesURL = "https://api.github.com/repos/tom5079/Pupil/git/trees/tags"
|
|
||||||
private val contentURL = "https://raw.githubusercontent.com/tom5079/Pupil/tags/"
|
|
||||||
|
|
||||||
private var translations: Map<String, String> = emptyMap()
|
|
||||||
|
|
||||||
fun updateTranslations(client: HttpClient) = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
translations = emptyMap()
|
|
||||||
kotlin.runCatching {
|
|
||||||
translations = client.get<Map<String, String>>("$contentURL${Preferences["hitomi.tag_translation", Locale.getDefault().language]}.json").filterValues { it.isNotEmpty() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAvailableLanguages(client: HttpClient): List<String> {
|
|
||||||
val languages = Locale.getISOLanguages()
|
|
||||||
|
|
||||||
val json = runCatching { runBlocking { Json.parseToJsonElement(client.get(filesURL)) } }.getOrNull()
|
|
||||||
|
|
||||||
return listOf("en") + (json?.get("tree")?.jsonArray?.mapNotNull {
|
|
||||||
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
|
|
||||||
|
|
||||||
languages.firstOrNull { code -> code.equals(name, ignoreCase = true) }
|
|
||||||
} ?: emptyList())
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
import ru.noties.markwon.Markwon
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun getReleases(url: String) : JsonArray {
|
|
||||||
return try {
|
|
||||||
URL(url).readText().let {
|
|
||||||
Json.parseToJsonElement(it).jsonArray
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
JsonArray(emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkUpdate(url: String) : JsonObject? {
|
|
||||||
val releases = getReleases(url)
|
|
||||||
|
|
||||||
if (releases.isEmpty())
|
|
||||||
return null
|
|
||||||
|
|
||||||
return releases.firstOrNull {
|
|
||||||
Preferences["beta"] || it.jsonObject["prerelease"]?.jsonPrimitive?.booleanOrNull == false
|
|
||||||
}?.let {
|
|
||||||
if (it.jsonObject["tag_name"]?.jsonPrimitive?.contentOrNull == BuildConfig.VERSION_NAME)
|
|
||||||
null
|
|
||||||
else
|
|
||||||
it.jsonObject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getApkUrl(releases: JsonObject) : String? {
|
|
||||||
return releases["assets"]?.jsonArray?.firstOrNull {
|
|
||||||
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: "")
|
|
||||||
}.let {
|
|
||||||
it?.jsonObject?.get("browser_download_url")?.jsonPrimitive?.contentOrNull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkUpdate(context: Context, force: Boolean = false) {
|
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
|
|
||||||
|
|
||||||
if (!force && ignoreUpdateUntil > System.currentTimeMillis())
|
|
||||||
return
|
|
||||||
|
|
||||||
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
|
|
||||||
val markdown = update["body"]!!.jsonPrimitive.content
|
|
||||||
|
|
||||||
val target = when(locale.language) {
|
|
||||||
"ko" -> "한국어"
|
|
||||||
"ja" -> "日本語"
|
|
||||||
else -> "English"
|
|
||||||
}
|
|
||||||
|
|
||||||
val releaseNote = Regex("^# Release Note.+$")
|
|
||||||
val language = Regex("^## $target$")
|
|
||||||
val end = Regex("^#.+$")
|
|
||||||
|
|
||||||
var releaseNoteFlag = false
|
|
||||||
var languageFlag = false
|
|
||||||
|
|
||||||
val result = StringBuilder()
|
|
||||||
|
|
||||||
for(line in markdown.lines()) {
|
|
||||||
if (releaseNote.matches(line)) {
|
|
||||||
releaseNoteFlag = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (releaseNoteFlag) {
|
|
||||||
if (language.matches(line)) {
|
|
||||||
languageFlag = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (languageFlag) {
|
|
||||||
if (end.matches(line))
|
|
||||||
break
|
|
||||||
|
|
||||||
result.append(line+"\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.getString(R.string.update_release_note, update["tag_name"]?.jsonPrimitive?.contentOrNull, result.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
val update =
|
|
||||||
checkUpdate(context.getString(R.string.release_url)) ?: return@launch
|
|
||||||
|
|
||||||
val url = getApkUrl(update) ?: return@launch
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.update_title)
|
|
||||||
val msg = extractReleaseNote(update, Locale.getDefault())
|
|
||||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
|
|
||||||
//Cancel any download queued before
|
|
||||||
|
|
||||||
val id: Long = Preferences["update_download_id"]
|
|
||||||
|
|
||||||
if (id != -1L)
|
|
||||||
downloadManager.remove(id)
|
|
||||||
|
|
||||||
val target = File(context.getExternalFilesDir(null), "Pupil.apk").also {
|
|
||||||
it.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
|
||||||
.setTitle(context.getText(R.string.update_notification_description))
|
|
||||||
.setDestinationUri(Uri.fromFile(target))
|
|
||||||
|
|
||||||
downloadManager.enqueue(request).also {
|
|
||||||
Preferences["update_download_id"] = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ ->
|
|
||||||
if (!force)
|
|
||||||
preferences.edit()
|
|
||||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
syntax = "proto2";
|
|
||||||
|
|
||||||
option java_package = "xyz.quaver.pupil.proto";
|
|
||||||
option java_multiple_files = true;
|
|
||||||
|
|
||||||
message Settings {
|
|
||||||
optional string recent_source = 1;
|
|
||||||
optional ReaderOptions mainReaderOption = 2;
|
|
||||||
optional ReaderOptions fullscreenReaderOption = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ReaderOptions {
|
|
||||||
enum Layout {
|
|
||||||
AUTO = 0;
|
|
||||||
SINGLE_PAGE = 1;
|
|
||||||
DOUBLE_PAGE = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Orientation {
|
|
||||||
VERTICAL_DOWN = 0;
|
|
||||||
VERTICAL_UP = 1;
|
|
||||||
HORIZONTAL_RIGHT = 2;
|
|
||||||
HORIZONTAL_LEFT = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
optional Layout layout = 1;
|
|
||||||
optional Orientation orientation = 2;
|
|
||||||
optional bool snap = 3 [default = true];
|
|
||||||
optional bool padding = 4 [default = true];
|
|
||||||
}
|
|
||||||
@@ -19,4 +19,5 @@
|
|||||||
|
|
||||||
<paths>
|
<paths>
|
||||||
<cache-path name="cached_image" path="networkcache/" />
|
<cache-path name="cached_image" path="networkcache/" />
|
||||||
|
<external-cache-path name="apks" path="apks/" />
|
||||||
</paths>
|
</paths>
|
||||||
@@ -36,40 +36,6 @@ class ExampleUnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() {
|
||||||
val itemID = 145360
|
|
||||||
|
|
||||||
val client = HttpClient()
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
client.getItem(
|
|
||||||
itemID.toString(),
|
|
||||||
onReader = {
|
|
||||||
assertEquals(
|
|
||||||
listOf(
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_tTXMCveG_fb60ec5a563ca8ab9fd5297aee678f0753001844",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_gkhHpOSi_49d9003f22e05d7a70b9b877d105a9496cb382b6",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_1IUNPaSk_fe889333f982b2f85ffdcc8eaf7d1ec435cae4d8",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_DdnS7iux_a72d128adb7334bd2aa45fa0ebc888405d2d6158",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_vLQun9me_76a6914ff06c6b9df3ed69d43524e5b7eb7d063c",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_hRnmqsBz_47695027ed3c1e039bd61a4bd059bd977218dceb",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_aQB3cyXP_bbc55416bed60989c57150ce8d1dd7476e7e4573",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_4ulxFtgy_50d903e92bc4e4e610c65d2f67d46ec679abb3b1",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_Jf8cvnm6_9fe96de962feaffe72bc96be1e1255bb50d1ec6a",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_C7rUVlhL_f968ff4e44e850bc38e9e18f0ef25b5cc3de376f",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_pEJB2Fl7_095351ba621239059891f3f41476de529014b17e",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_6PN0QgvO_ed11c8baeb3565d86b3563d7760beee10d302364",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_s73REfCc_c373538bf02c14dc7d9f195b2893c50380cfd74f",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_UxiyBoPO_4cb9f733c9bab6552f380ce49f7ec9fc5a35e0a7",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_VHa1wSsp_94be272f1947be8731b474aaf966d2395ed32a4e",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_hXrkCWlN_ed908b9d1febbb875ce79d04fd52bdb9132ff980",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_voBeugVP_a491b8fdf4dc033a7cb0393fbafd8d505eb776b9",
|
|
||||||
"https://newtoki15.org/data/editor/1901/1077963982_ZVCMAjoJ_29eca6af62de41b228590e61748208572c78db61"
|
|
||||||
),
|
|
||||||
it.urls
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ allprojects {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
|
maven { setUrl("https://oss.sonatype.org/content/repositories/snapshots") }
|
||||||
maven { url = uri("https://jitpack.io") }
|
maven { setUrl("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user