diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 538b1cb5..00000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f86326c0..79e6e0f1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,8 +72,8 @@ android { dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar")))) implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") implementation("androidx.activity:activity-compose:1.4.0") 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-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.fragment:fragment-ktx:1.4.0") 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-viewmodel-compose:2.4.0") - implementation("androidx.room:room-runtime:2.4.0") - annotationProcessor("androidx.room:room-compiler:2.4.0") - kapt("androidx.room:room-compiler:2.4.0") - implementation("androidx.room:room-ktx:2.4.0") + implementation("androidx.room:room-runtime:2.4.1") + annotationProcessor("androidx.room:room-compiler:2.4.1") + kapt("androidx.room:room-compiler:2.4.1") + implementation("androidx.room:room-ktx:2.4.1") implementation("androidx.datastore:datastore:1.0.0") implementation("androidx.datastore:datastore-preferences:1.0.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("com.google.firebase:firebase-analytics-ktx") @@ -135,9 +135,9 @@ dependencies { 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("com.google.guava:guava:31.0.1-jre") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c643728e..7a3c0bbc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,14 +3,11 @@ xmlns:tools="http://schemas.android.com/tools" package="xyz.quaver.pupil"> - - - @@ -38,17 +35,8 @@ - - - - - - - - - - - - diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt index 4a048efa..ca909135 100644 --- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -23,41 +23,31 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context -import android.content.Intent -import android.net.Uri 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.GooglePlayServicesRepairableException 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.engine.okhttp.* import io.ktor.client.features.* import io.ktor.client.features.cache.* import io.ktor.client.features.json.* import io.ktor.client.features.json.serializer.* +import io.ktor.http.* import okhttp3.Protocol import org.kodein.di.* 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.sourceModule -import xyz.quaver.pupil.util.* -import java.util.* +import xyz.quaver.pupil.sources.core.settingsDataStore +import xyz.quaver.pupil.util.ApkDownloadManager class Pupil : Application(), DIAware { override val di: DI by DI.lazy { 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 } @@ -70,6 +60,7 @@ class Pupil : Application(), DIAware { } install(JsonFeature) { serializer = KotlinxSerializer() + accept(ContentType("text", "plain")) } install(HttpTimeout) { requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS @@ -82,43 +73,8 @@ class Pupil : Application(), DIAware { } } } - private lateinit var firebaseAnalytics: FirebaseAnalytics - override fun onCreate() { - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) - - 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("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) + super.onCreate() try { ProviderInstaller.installIfNeeded(this) @@ -151,21 +107,7 @@ class Pupil : Application(), DIAware { enableVibration(true) 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("dark_mode")) { - true -> AppCompatDelegate.MODE_NIGHT_YES - false -> AppCompatDelegate.MODE_NIGHT_NO - }) - - super.onCreate() } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/proto/Settings.kt b/app/src/main/java/xyz/quaver/pupil/proto/Settings.kt deleted file mode 100644 index 301437db..00000000 --- a/app/src/main/java/xyz/quaver/pupil/proto/Settings.kt +++ /dev/null @@ -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 . - */ - -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 { - 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 by dataStore( - fileName = "settings.proto", - serializer = SettingsSerializer -) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/receiver/UpdateBroadcastReceiver.kt b/app/src/main/java/xyz/quaver/pupil/receiver/UpdateBroadcastReceiver.kt deleted file mode 100644 index badff65e..00000000 --- a/app/src/main/java/xyz/quaver/pupil/receiver/UpdateBroadcastReceiver.kt +++ /dev/null @@ -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 . - */ - -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) - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt b/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt index 973bff16..c9d1d718 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt @@ -22,69 +22,105 @@ import android.app.Application import android.content.pm.PackageInfo import android.content.pm.PackageManager 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 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 java.util.* +import java.util.concurrent.ConcurrentHashMap private const val SOURCES_FEATURE = "pupil.sources" private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources" private const val SOURCES_PATH = "pupil.sources.path" data class SourceEntry( - val name: String, - val source: Source, - val icon: Drawable + val packageName: String, + val packagePath: String, + val sourceName: String, + val sourcePath: String, + val sourceDir: String, + val icon: Drawable, + val version: String ) typealias SourceEntries = Map -private val sources = mutableMapOf() - val PackageInfo.isSourceFeatureEnabled get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE } -fun loadSource(app: Application, packageInfo: PackageInfo) { +fun loadSource(app: Application, packageInfo: PackageInfo): List { val packageManager = app.packageManager + val applicationInfo = packageInfo.applicationInfo - val classLoader = PathClassLoader(applicationInfo.sourceDir, null, app.classLoader) - val packageName = packageInfo.packageName - - val sourceName = packageManager.getApplicationLabel(applicationInfo).toString().substringAfter("[Pupil] ") + val packageName = packageManager.getApplicationLabel(applicationInfo).toString().substringAfter("[Pupil] ") + val packagePath = packageInfo.packageName val icon = packageManager.getApplicationIcon(applicationInfo) - packageInfo + val version = packageInfo.versionName + + return packageInfo .applicationInfo .metaData - .getString(SOURCES_PATH) + ?.getString(SOURCES_PATH) ?.split(';') - .orEmpty() - .forEach { sourcePath -> - sources[sourceName] = SourceEntry( + ?.map { source -> + val (sourceName, sourcePath) = source.split(':', limit = 2) + SourceEntry( + packageName, + packagePath, sourceName, - Class.forName("$packageName$sourcePath", false, classLoader) - .getConstructor(Application::class.java) - .newInstance(app) as Source, - icon + sourcePath, + applicationInfo.sourceDir, + 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 { val packageManager = app.packageManager val packages = packageManager.getInstalledPackages( - PackageManager.GET_CONFIGURATIONS or - PackageManager.GET_META_DATA + PackageManager.GET_CONFIGURATIONS or PackageManager.GET_META_DATA ) - val sources = packages.filter { it.isSourceFeatureEnabled } - - sources.forEach { loadSource(app, it) } + return packages.flatMap { packageInfo -> + if (packageInfo.isSourceFeatureEnabled) + loadSource(app, packageInfo) + else + emptyList() + } } -fun sourceModule(app: Application) = DI.Module(name = "source") { - loadSources(app) - bindInstance { Collections.unmodifiableMap(sources) } +@Composable +fun rememberSources(): State> { + val app: Application by rememberInstance() + val sources = remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(Unit) { + while (true) { + sources.value = updateSources(app) + delay(1000) + } + } + + return sources } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/sources/SourceSelectDialog.kt b/app/src/main/java/xyz/quaver/pupil/sources/SourceSelectDialog.kt deleted file mode 100644 index 3d04c33c..00000000 --- a/app/src/main/java/xyz/quaver/pupil/sources/SourceSelectDialog.kt +++ /dev/null @@ -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 . - */ - -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) } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt index 5068117b..c085626f 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -20,39 +20,36 @@ package xyz.quaver.pupil.ui import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.graphics.Color import androidx.core.view.WindowCompat -import androidx.datastore.core.DataStore import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.createGraph import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.systemuicontroller.rememberSystemUiController -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import org.kodein.di.DIAware import org.kodein.di.android.closestDI -import org.kodein.di.compose.rememberInstance import org.kodein.di.instance import org.kodein.log.LoggerFactory import org.kodein.log.newLogger -import xyz.quaver.pupil.proto.Settings 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 @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class) class MainActivity : ComponentActivity(), DIAware { override val di by closestDI() - private val sources: SourceEntries by instance() - private val logger = newLogger(LoggerFactory.default) override fun onCreate(savedInstanceState: Bundle?) { @@ -68,6 +65,8 @@ class MainActivity : ComponentActivity(), DIAware { val systemUiController = rememberSystemUiController() val useDarkIcons = MaterialTheme.colors.isLight + val coroutineScope = rememberCoroutineScope() + SideEffect { systemUiController.setSystemBarsColor( color = Color.Transparent, @@ -75,42 +74,29 @@ class MainActivity : ComponentActivity(), DIAware { ) } - NavHost(navController, startDestination = "main") { - composable("main") { - var launched by rememberSaveable { mutableStateOf(false) } - val settingsDataStore: DataStore by rememberInstance() + NavHost(navController, "source") { + composable("source") { + var source by remember { mutableStateOf(null) } - var sourceSelectDialog by remember { mutableStateOf(false) } + BackHandler( + enabled = source != null + ) { + source = null + } - if (sourceSelectDialog) - SourceSelectDialog(navController, null) - - LaunchedEffect(Unit) { - 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() + if (source == null) + SourceSelector { + coroutineScope.launch { + source = loadSource(application, it) } } + else { + source!!.Entry() } } composable("settings") { } - sources.values.forEach { - it.source.run { - navGraph(navController) - } - } } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt b/app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt new file mode 100644 index 00000000..0260aa06 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt @@ -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 . + */ + +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() } + + val context = LocalContext.current + + val coroutineScope = rememberCoroutineScope() + + val remoteSources by produceState?>(null) { + while (true) { + delay(1000) + value = withContext(Dispatchers.IO) { + client.get>("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() } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/OpenWithItemIDDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/OpenWithItemIDDialog.kt deleted file mode 100644 index 351810a1..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/OpenWithItemIDDialog.kt +++ /dev/null @@ -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 . - */ - -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)) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/ApkDownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/ApkDownloadManager.kt new file mode 100644 index 00000000..87f9d794 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/util/ApkDownloadManager.kt @@ -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 . + */ + +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(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) +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt b/app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt deleted file mode 100644 index fe00822d..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt +++ /dev/null @@ -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 . - */ - -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(android.R.id.content) - keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {} - keyboardDismissListener?.let { - rootView.viewTreeObserver.addOnGlobalLayoutListener(it) - } - } - - fun release() { - val rootView = activity.findViewById(android.R.id.content) - keyboardDismissListener?.let { - rootView.viewTreeObserver.removeOnGlobalLayoutListener(it) - } - keyboardDismissListener = null - } -} diff --git a/app/src/main/java/xyz/quaver/pupil/util/Preferences.kt b/app/src/main/java/xyz/quaver/pupil/util/Preferences.kt deleted file mode 100644 index 4df4cfb7..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/Preferences.kt +++ /dev/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 . - */ - -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() - ) - - 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) = edit().putStringSet(key, value).apply() - - @Suppress("UNCHECKED_CAST") - inline operator fun get(key: String, defaultVal: T = defMap[T::class] as T): T = (all[key] as? T) ?: defaultVal - - fun remove(key: String) { - edit().remove(key).apply() - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/file.kt b/app/src/main/java/xyz/quaver/pupil/util/file.kt deleted file mode 100644 index c7dcf2be..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/file.kt +++ /dev/null @@ -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 . - */ - -package xyz.quaver.pupil.util - -import java.io.File - -fun File.size(): Long = - this.walk().fold(0L) { size, file -> size + file.length() } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/lock.kt b/app/src/main/java/xyz/quaver/pupil/util/lock.kt deleted file mode 100644 index b4afebe7..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/lock.kt +++ /dev/null @@ -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 . - */ - -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 { - 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? = 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 - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt deleted file mode 100644 index 7450876f..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt +++ /dev/null @@ -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 . - */ - -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()[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 - } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt deleted file mode 100644 index c158ad32..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt +++ /dev/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 . - */ - -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))]) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/translation.kt b/app/src/main/java/xyz/quaver/pupil/util/translation.kt deleted file mode 100644 index 2d307e30..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/translation.kt +++ /dev/null @@ -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 . - */ - -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 = emptyMap() - -fun updateTranslations(client: HttpClient) = CoroutineScope(Dispatchers.IO).launch { - translations = emptyMap() - kotlin.runCatching { - translations = client.get>("$contentURL${Preferences["hitomi.tag_translation", Locale.getDefault().language]}.json").filterValues { it.isNotEmpty() } - } -} - -fun getAvailableLanguages(client: HttpClient): List { - 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()) -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/update.kt b/app/src/main/java/xyz/quaver/pupil/util/update.kt deleted file mode 100644 index 1819821d..00000000 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ /dev/null @@ -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 . - */ - -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() - } - } -} \ No newline at end of file diff --git a/app/src/main/proto/settings.proto b/app/src/main/proto/settings.proto deleted file mode 100644 index 3e844a34..00000000 --- a/app/src/main/proto/settings.proto +++ /dev/null @@ -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]; -} \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 98eaeec2..99ad1e58 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -19,4 +19,5 @@ + \ No newline at end of file diff --git a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt index 8da394bc..51190cb2 100644 --- a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt +++ b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt @@ -36,40 +36,6 @@ class ExampleUnitTest { @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 - ) - } - ) - } } } diff --git a/build.gradle.kts b/build.gradle.kts index f76c3139..964ca240 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,8 +25,8 @@ allprojects { google() mavenCentral() mavenLocal() - maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } - maven { url = uri("https://jitpack.io") } + maven { setUrl("https://oss.sonatype.org/content/repositories/snapshots") } + maven { setUrl("https://jitpack.io") } } } diff --git a/q b/q new file mode 100644 index 00000000..9360a290 --- /dev/null +++ b/q @@ -0,0 +1,5 @@ +stash@{0}: WIP on source: eb809c7 Dependency update +stash@{1}: WIP on source: 143c61f WIP +stash@{2}: WIP on source: 27229db Simplify Source definition +stash@{3}: WIP on source: a0e47f8 LF to CRLF +stash@{4}: WIP on dev: d049bf8 Dependency update