Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19450f66a0 | ||
|
|
5b36fd9257 | ||
|
|
a3158d320b | ||
|
|
38494c9fbc | ||
|
|
114158cf73 | ||
|
|
6d108dd7ff | ||
|
|
f36b7f1dbe | ||
|
|
0a22ebd8e9 | ||
|
|
3682eeaf94 | ||
|
|
7df2ae4ba7 | ||
|
|
c9519ec681 | ||
|
|
b146ed684d | ||
|
|
d2787c36d7 | ||
|
|
3ff663114a |
@@ -2,7 +2,7 @@
|
|||||||
*Pupil, Hitomi.la viewer for Android*
|
*Pupil, Hitomi.la viewer for Android*
|
||||||
|
|
||||||

|

|
||||||
[](https://github.com/tom5079/Pupil/releases/download/5.3.6/Pupil-v5.3.6.apk)
|
[](https://github.com/tom5079/Pupil/releases/download/5.3.13/Pupil-v5.3.13.apk)
|
||||||
[](https://discord.gg/Stj4b5v)
|
[](https://discord.gg/Stj4b5v)
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ configurations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 32
|
compileSdk 34
|
||||||
|
targetSdkVersion 34
|
||||||
versionCode 69
|
versionCode 69
|
||||||
versionName "5.3.7"
|
versionName "5.3.13"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
|
||||||
|
|
||||||
@@ -95,15 +95,15 @@ dependencies {
|
|||||||
|
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.5.0"
|
implementation "com.google.android.material:material:1.11.0"
|
||||||
|
|
||||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
implementation platform('com.google.firebase:firebase-bom:32.7.0')
|
||||||
implementation "com.google.firebase:firebase-analytics-ktx"
|
implementation "com.google.firebase:firebase-analytics-ktx"
|
||||||
implementation "com.google.firebase:firebase-crashlytics-ktx"
|
implementation "com.google.firebase:firebase-crashlytics-ktx"
|
||||||
implementation "com.google.firebase:firebase-perf-ktx"
|
implementation "com.google.firebase:firebase-perf-ktx"
|
||||||
|
|
||||||
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
|
implementation "com.google.android.gms:play-services-oss-licenses:17.0.1"
|
||||||
implementation "com.google.android.gms:play-services-mlkit-face-detection:17.0.1"
|
implementation "com.google.android.gms:play-services-mlkit-face-detection:17.1.0"
|
||||||
|
|
||||||
implementation "com.github.clans:fab:1.6.4"
|
implementation "com.github.clans:fab:1.6.4"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"filters": [],
|
"filters": [],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 69,
|
"versionCode": 69,
|
||||||
"versionName": "5.3.5",
|
"versionName": "5.3.12",
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||||
@@ -45,7 +47,8 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service android:name=".services.DownloadService"
|
<service android:name=".services.DownloadService"
|
||||||
android:exported="false"/>
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receiver.UpdateBroadcastReceiver"
|
android:name=".receiver.UpdateBroadcastReceiver"
|
||||||
@@ -61,165 +64,107 @@
|
|||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:parentActivityName=".ui.MainActivity"
|
android:parentActivityName=".ui.MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:scheme="https" />
|
||||||
android:pathPrefix="/galleries"
|
<data android:host="*.hasha.in"/>
|
||||||
android:scheme="http" />
|
<data android:pathPrefix="/reader"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:scheme="https" />
|
||||||
android:pathPrefix="/manga"
|
<data android:host="hitomi.la"/>
|
||||||
android:scheme="http" />
|
<data android:pathPrefix="/galleries"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:scheme="https" />
|
||||||
android:pathPrefix="/doujinshi"
|
<data android:host="hitomi.la" />
|
||||||
android:scheme="http" />
|
<data android:pathPrefix="/manga" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:scheme="https" />
|
||||||
android:pathPrefix="/cg"
|
<data android:host="hitomi.la" />
|
||||||
android:scheme="http" />
|
<data android:pathPrefix="/doujinshi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:scheme="https" />
|
||||||
android:pathPrefix="/reader"
|
<data android:host="hitomi.la" />
|
||||||
android:scheme="http" />
|
<data android:pathPrefix="/cg" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:scheme="https" />
|
||||||
android:pathPrefix="/galleries"
|
<data android:host="hitomi.la" />
|
||||||
android:scheme="https" />
|
<data android:pathPrefix="/imageset" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:scheme="https" />
|
||||||
android:pathPrefix="/manga"
|
<data android:host="hitomi.la" />
|
||||||
android:scheme="https" />
|
<data android:pathPrefix="/reader" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hitomi.la"
|
<data android:host="e-hentai.org" />
|
||||||
android:pathPrefix="/doujinshi"
|
<data android:pathPrefix="/g" />
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter android:autoVerify="true">
|
||||||
<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.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:scheme="https" />
|
||||||
android:host="hitomi.la"
|
<data android:host="e-hentai.org" />
|
||||||
android:pathPrefix="/cg"
|
<data android:pathPrefix="/g" />
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPrefix="/reader"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="hiyobi.me"
|
|
||||||
android:scheme="http"
|
|
||||||
android:pathPrefix="/reader" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="hiyobi.me"
|
|
||||||
android:pathPrefix="/reader"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="e-hentai.org"
|
|
||||||
android:pathPrefix="/g"
|
|
||||||
android:scheme="http" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="e-hentai.org"
|
|
||||||
android:pathPrefix="/g"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SettingsActivity"
|
android:name=".ui.SettingsActivity"
|
||||||
android:label="@string/settings_title">
|
android:label="@string/settings_title">
|
||||||
<tools:validation testUrl="http://ix.io/eer" />
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
@@ -232,17 +177,6 @@
|
|||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:scheme="http"
|
|
||||||
android:host="ix.io"
|
|
||||||
android:pathPattern="/..*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -50,9 +50,15 @@ import xyz.quaver.pupil.types.Tag
|
|||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
typealias PupilInterceptor = (Interceptor.Chain) -> Response
|
typealias PupilInterceptor = (Interceptor.Chain) -> Response
|
||||||
@@ -76,6 +82,36 @@ val client: OkHttpClient
|
|||||||
clientHolder = it
|
clientHolder = it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSSLContext(context: Context): SSLContext {
|
||||||
|
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||||
|
keyStore.load(null, null)
|
||||||
|
|
||||||
|
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||||
|
|
||||||
|
val certificate = context.resources.openRawResource(R.raw.isrgrootx1).use {
|
||||||
|
certificateFactory.generateCertificate(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyStore.setCertificateEntry("isrgrootx1", certificate)
|
||||||
|
|
||||||
|
val defaultTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||||
|
defaultTrustManagerFactory.init(null as KeyStore?)
|
||||||
|
|
||||||
|
defaultTrustManagerFactory.trustManagers.filterIsInstance(X509TrustManager::class.java).forEach { trustManager ->
|
||||||
|
trustManager.acceptedIssuers.forEach { acceptedIssuer ->
|
||||||
|
keyStore.setCertificateEntry(acceptedIssuer.subjectDN.name, acceptedIssuer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||||
|
trustManagerFactory.init(keyStore)
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(null, trustManagerFactory.trustManagers, SecureRandom())
|
||||||
|
|
||||||
|
return sslContext
|
||||||
|
}
|
||||||
|
|
||||||
class Pupil : Application() {
|
class Pupil : Application() {
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: Pupil
|
lateinit var instance: Pupil
|
||||||
@@ -100,7 +136,8 @@ class Pupil : Application() {
|
|||||||
val proxyInfo = getProxyInfo()
|
val proxyInfo = getProxyInfo()
|
||||||
|
|
||||||
clientBuilder = OkHttpClient.Builder()
|
clientBuilder = OkHttpClient.Builder()
|
||||||
.connectTimeout(0, TimeUnit.SECONDS)
|
// .connectTimeout(0, TimeUnit.SECONDS)
|
||||||
|
.sslSocketFactory(getSSLContext(this).socketFactory)
|
||||||
.readTimeout(0, TimeUnit.SECONDS)
|
.readTimeout(0, TimeUnit.SECONDS)
|
||||||
.proxyInfo(proxyInfo)
|
.proxyInfo(proxyInfo)
|
||||||
.addInterceptor { chain ->
|
.addInterceptor { chain ->
|
||||||
|
|||||||
@@ -166,11 +166,7 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<
|
|||||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||||
}
|
}
|
||||||
|
|
||||||
val bytes = try {
|
val bytes = URL(nozomiAddress).readBytes()
|
||||||
URL(nozomiAddress).readBytes()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return emptySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nozomi = mutableSetOf<Int>()
|
val nozomi = mutableSetOf<Int>()
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.app.PendingIntent
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -100,13 +101,15 @@ class DownloadService : Service() {
|
|||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi", "MissingPermission")
|
||||||
private fun notify(galleryID: Int) {
|
private fun notify(galleryID: Int) {
|
||||||
val max = progress[galleryID]?.size ?: 0
|
val max = progress[galleryID]?.size ?: 0
|
||||||
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
|
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
|
||||||
|
|
||||||
val notification = notification[galleryID] ?: return
|
val notification = notification[galleryID] ?: return
|
||||||
|
|
||||||
|
if (!checkNotificationEnabled(this)) return
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
if (isCompleted(galleryID)) {
|
||||||
notification
|
notification
|
||||||
.setContentText(getString(R.string.reader_notification_complete))
|
.setContentText(getString(R.string.reader_notification_complete))
|
||||||
@@ -168,19 +171,26 @@ class DownloadService : Service() {
|
|||||||
private val interceptor: PupilInterceptor = { chain ->
|
private val interceptor: PupilInterceptor = { chain ->
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
|
|
||||||
var response = chain.proceed(request)
|
var response = kotlin.runCatching {
|
||||||
var limit = 5
|
chain.proceed(request)
|
||||||
|
}.getOrNull()
|
||||||
|
var limit = 10
|
||||||
|
|
||||||
while (!response.isSuccessful) {
|
while (response?.isSuccessful != true) {
|
||||||
if (response.code() == 503) {
|
if (response?.code() == 503) {
|
||||||
Thread.sleep(200)
|
Thread.sleep(200)
|
||||||
} else if (--limit > 0)
|
} else if (--limit < 0)
|
||||||
break
|
break
|
||||||
|
|
||||||
response = chain.proceed(request)
|
response = kotlin.runCatching {
|
||||||
|
chain.proceed(request)
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
response.newBuilder()
|
if (response == null)
|
||||||
|
response = chain.proceed(request)
|
||||||
|
|
||||||
|
response!!.newBuilder()
|
||||||
.body(response.body()?.let {
|
.body(response.body()?.let {
|
||||||
ProgressResponseBody(request.tag(), it, progressListener)
|
ProgressResponseBody(request.tag(), it, progressListener)
|
||||||
}).build()
|
}).build()
|
||||||
@@ -207,6 +217,7 @@ class DownloadService : Service() {
|
|||||||
private val callback = object: Callback {
|
private val callback = object: Callback {
|
||||||
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
Log.d("PUPILD", "ONFAILURE ${call.request().tag()}, ${e}")
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
|
||||||
if (e.message?.contains("cancel", true) == false) {
|
if (e.message?.contains("cancel", true) == false) {
|
||||||
@@ -215,6 +226,7 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}")
|
||||||
val (galleryID, index, startId) = call.request().tag() as Tag
|
val (galleryID, index, startId) = call.request().tag() as Tag
|
||||||
val ext = call.request().url().encodedPath().split('.').last()
|
val ext = call.request().url().encodedPath().split('.').last()
|
||||||
|
|
||||||
@@ -394,7 +406,11 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
|
||||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
||||||
|
} else {
|
||||||
|
startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
}
|
||||||
|
|
||||||
when (intent?.getStringExtra(KEY_COMMAND)) {
|
when (intent?.getStringExtra(KEY_COMMAND)) {
|
||||||
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
|
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
|
||||||
@@ -415,7 +431,11 @@ class DownloadService : Service() {
|
|||||||
override fun onBind(p0: Intent?) = binder
|
override fun onBind(p0: Intent?) = binder
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
|
||||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
||||||
|
} else {
|
||||||
|
startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
}
|
||||||
interceptors[Tag::class] = interceptor
|
interceptors[Tag::class] = interceptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,13 @@ import android.view.View
|
|||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -61,7 +64,9 @@ import xyz.quaver.pupil.util.Preferences
|
|||||||
import xyz.quaver.pupil.util.checkUpdate
|
import xyz.quaver.pupil.util.checkUpdate
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
|
import xyz.quaver.pupil.util.requestNotificationPermission
|
||||||
import xyz.quaver.pupil.util.restore
|
import xyz.quaver.pupil.util.restore
|
||||||
|
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -107,6 +112,12 @@ class MainActivity :
|
|||||||
|
|
||||||
private lateinit var binding: MainActivityBinding
|
private lateinit var binding: MainActivityBinding
|
||||||
|
|
||||||
|
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (!isGranted) {
|
||||||
|
showNotificationPermissionExplanationDialog(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = MainActivityBinding.inflate(layoutInflater)
|
binding = MainActivityBinding.inflate(layoutInflater)
|
||||||
@@ -124,6 +135,8 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {}
|
||||||
|
|
||||||
if (Preferences["download_folder", ""].isEmpty())
|
if (Preferences["download_folder", ""].isEmpty())
|
||||||
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
||||||
|
|
||||||
@@ -392,6 +405,10 @@ class MainActivity :
|
|||||||
onDownloadClickedHandler = { position ->
|
onDownloadClickedHandler = { position ->
|
||||||
val galleryID = galleries[position]
|
val galleryID = galleries[position]
|
||||||
|
|
||||||
|
requestNotificationPermission(
|
||||||
|
this@MainActivity,
|
||||||
|
requestNotificationPermssionLauncher
|
||||||
|
) {
|
||||||
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
||||||
DownloadService.cancel(this@MainActivity, galleryID)
|
DownloadService.cancel(this@MainActivity, galleryID)
|
||||||
}
|
}
|
||||||
@@ -399,6 +416,7 @@ class MainActivity :
|
|||||||
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
||||||
DownloadService.download(this@MainActivity, galleryID)
|
DownloadService.download(this@MainActivity, galleryID)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
}
|
}
|
||||||
@@ -477,6 +495,20 @@ class MainActivity :
|
|||||||
private var suggestionJob : Job? = null
|
private var suggestionJob : Job? = null
|
||||||
private fun setupSearchBar() {
|
private fun setupSearchBar() {
|
||||||
with(binding.contents.searchview) {
|
with(binding.contents.searchview) {
|
||||||
|
val scrollSuggestionToTop = {
|
||||||
|
with(binding.suggestionSection.suggestionsList) {
|
||||||
|
MainScope().launch {
|
||||||
|
withTimeout(1000) {
|
||||||
|
val layoutManager = layoutManager as LinearLayoutManager
|
||||||
|
while (layoutManager.findLastVisibleItemPosition() != adapter?.itemCount?.minus(1)) {
|
||||||
|
layoutManager.scrollToPosition(adapter?.itemCount?.minus(1) ?: 0)
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
||||||
override fun onMenuOpened() {
|
override fun onMenuOpened() {
|
||||||
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
||||||
@@ -502,6 +534,7 @@ class MainActivity :
|
|||||||
onFavoriteHistorySwitchClickListener = {
|
onFavoriteHistorySwitchClickListener = {
|
||||||
isFavorite = !isFavorite
|
isFavorite = !isFavorite
|
||||||
swapSuggestions(defaultSuggestions)
|
swapSuggestions(defaultSuggestions)
|
||||||
|
scrollSuggestionToTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMenuItemClickListener = {
|
onMenuItemClickListener = {
|
||||||
@@ -515,6 +548,7 @@ class MainActivity :
|
|||||||
|
|
||||||
if (query.isEmpty() or query.endsWith(' ')) {
|
if (query.isEmpty() or query.endsWith(' ')) {
|
||||||
swapSuggestions(defaultSuggestions)
|
swapSuggestions(defaultSuggestions)
|
||||||
|
scrollSuggestionToTop()
|
||||||
|
|
||||||
return@lambda
|
return@lambda
|
||||||
}
|
}
|
||||||
@@ -546,8 +580,10 @@ class MainActivity :
|
|||||||
|
|
||||||
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
|
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
|
||||||
override fun onFocus() {
|
override fun onFocus() {
|
||||||
if (query.isEmpty() or query.endsWith(' '))
|
if (query.isEmpty() or query.endsWith(' ')) {
|
||||||
swapSuggestions(defaultSuggestions)
|
swapSuggestions(defaultSuggestions)
|
||||||
|
scrollSuggestionToTop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFocusCleared() {
|
override fun onFocusCleared() {
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ import xyz.quaver.pupil.favorites
|
|||||||
import xyz.quaver.pupil.services.DownloadService
|
import xyz.quaver.pupil.services.DownloadService
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.camera
|
import xyz.quaver.pupil.util.camera
|
||||||
|
import xyz.quaver.pupil.util.checkNotificationEnabled
|
||||||
import xyz.quaver.pupil.util.closeCamera
|
import xyz.quaver.pupil.util.closeCamera
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
|
import xyz.quaver.pupil.util.requestNotificationPermission
|
||||||
|
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
|
||||||
import xyz.quaver.pupil.util.startCamera
|
import xyz.quaver.pupil.util.startCamera
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
class ReaderActivity : BaseActivity() {
|
||||||
@@ -117,6 +120,12 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
private lateinit var binding: ReaderActivityBinding
|
private lateinit var binding: ReaderActivityBinding
|
||||||
|
|
||||||
|
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (!isGranted) {
|
||||||
|
showNotificationPermissionExplanationDialog(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||||
@@ -148,10 +157,11 @@ class ReaderActivity : BaseActivity() {
|
|||||||
val uri = intent.data
|
val uri = intent.data
|
||||||
val lastPathSegment = uri?.lastPathSegment
|
val lastPathSegment = uri?.lastPathSegment
|
||||||
if (uri != null && lastPathSegment != null) {
|
if (uri != null && lastPathSegment != null) {
|
||||||
galleryID = when (uri.host) {
|
galleryID = if (uri.host?.endsWith("hasha.in") == true) {
|
||||||
|
lastPathSegment?.toInt() ?: 0
|
||||||
|
} else when (uri.host) {
|
||||||
"hitomi.la" ->
|
"hitomi.la" ->
|
||||||
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||||
"hiyobi.me" -> lastPathSegment.toInt()
|
|
||||||
"e-hentai.org" -> uri.pathSegments[1].toInt()
|
"e-hentai.org" -> uri.pathSegments[1].toInt()
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
@@ -360,6 +370,10 @@ class ReaderActivity : BaseActivity() {
|
|||||||
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
|
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
requestNotificationPermission(
|
||||||
|
this@ReaderActivity,
|
||||||
|
requestNotificationPermssionLauncher
|
||||||
|
) {
|
||||||
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
||||||
|
|
||||||
if (downloadManager.isDownloading(galleryID)) {
|
if (downloadManager.isDownloading(galleryID)) {
|
||||||
@@ -372,6 +386,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
with(binding.retryFab) {
|
with(binding.retryFab) {
|
||||||
setImageResource(R.drawable.refresh)
|
setImageResource(R.drawable.refresh)
|
||||||
|
|||||||
@@ -18,15 +18,20 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
package xyz.quaver.pupil.ui.fragment
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
@@ -34,11 +39,19 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
|
import xyz.quaver.io.FileX
|
||||||
|
import xyz.quaver.io.util.readText
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.client
|
import xyz.quaver.pupil.client
|
||||||
|
import xyz.quaver.pupil.favoriteTags
|
||||||
|
import xyz.quaver.pupil.favorites
|
||||||
|
import xyz.quaver.pupil.types.Tag
|
||||||
|
import xyz.quaver.pupil.util.get
|
||||||
import xyz.quaver.pupil.util.restore
|
import xyz.quaver.pupil.util.restore
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -46,6 +59,31 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
private val requestBackupFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = result.data?.data ?: return@registerForActivityResult
|
||||||
|
val context = context ?: return@registerForActivityResult
|
||||||
|
val view = view ?: return@registerForActivityResult
|
||||||
|
|
||||||
|
val backupData = runCatching {
|
||||||
|
FileX(context, uri).readText()?.let { Json.parseToJsonElement(it) }
|
||||||
|
}.getOrNull() ?: run{
|
||||||
|
Snackbar.make(view, context.getString(R.string.error), Toast.LENGTH_LONG).show()
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val newFavorites = backupData["favorites"]?.let { Json.decodeFromJsonElement<List<Int>>(it) }.orEmpty()
|
||||||
|
val newFavoriteTags = backupData["favorite_tags"]?.let { Json.decodeFromJsonElement<List<Tag>>(it) }.orEmpty()
|
||||||
|
|
||||||
|
favorites.addAll(newFavorites)
|
||||||
|
favoriteTags.addAll(newFavoriteTags)
|
||||||
|
|
||||||
|
Snackbar.make(view, context.getString(R.string.settings_restore_success, newFavorites.size + newFavoriteTags.size), Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
|
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
|
||||||
|
|
||||||
@@ -56,25 +94,6 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
|||||||
val context = context ?: return
|
val context = context ?: return
|
||||||
|
|
||||||
findPreference<Preference>("backup")?.setOnPreferenceClickListener {
|
findPreference<Preference>("backup")?.setOnPreferenceClickListener {
|
||||||
val iconSize = (24 * Resources.getSystem().displayMetrics.density).roundToInt()
|
|
||||||
val strokeWidth = (3 * Resources.getSystem().displayMetrics.density)
|
|
||||||
|
|
||||||
val icon = object: CircularProgressDrawable(context) {
|
|
||||||
override fun getIntrinsicHeight(): Int {
|
|
||||||
return iconSize
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIntrinsicWidth(): Int {
|
|
||||||
return iconSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
icon.strokeWidth = strokeWidth
|
|
||||||
icon.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
|
|
||||||
DrawableCompat.setTint(icon, ContextCompat.getColor(context, R.color.colorAccent))
|
|
||||||
icon.start()
|
|
||||||
it.icon = icon
|
|
||||||
|
|
||||||
val favorites = runCatching {
|
val favorites = runCatching {
|
||||||
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites.json").readText())
|
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites.json").readText())
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
@@ -82,72 +101,37 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
|||||||
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites_tags.json").readText())
|
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites_tags.json").readText())
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
val request = Request.Builder()
|
val favoriteJson = buildJsonObject {
|
||||||
.url(context.getString(R.string.backup_url))
|
|
||||||
.post(
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("f:1", buildJsonObject {
|
|
||||||
favorites?.let {
|
favorites?.let {
|
||||||
put("favorites", it)
|
put("favorites", it)
|
||||||
}
|
}
|
||||||
favoriteTags?.let {
|
favoriteTags?.let {
|
||||||
put("favorite_tags", it)
|
put("favorite_tags", it)
|
||||||
}
|
}
|
||||||
}.toString())
|
|
||||||
.build()
|
|
||||||
).build()
|
|
||||||
|
|
||||||
client.newCall(request).enqueue(object: Callback {
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
val view = view ?: return
|
|
||||||
|
|
||||||
MainScope().launch {
|
|
||||||
it.icon = null
|
|
||||||
}
|
|
||||||
Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
val backupFile = File(context.filesDir, "pupil-backup.json").also {
|
||||||
MainScope().launch {
|
it.writeText(favoriteJson.toString())
|
||||||
it.icon = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code() != 200) {
|
|
||||||
response.close()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
Intent(Intent.ACTION_SEND).apply {
|
||||||
type = "text/plain"
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", backupFile)
|
||||||
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", ""))
|
setDataAndType(uri, "application/json")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
}.let {
|
}.let {
|
||||||
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
context.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference<Preference>("restore")?.setOnPreferenceClickListener {
|
findPreference<Preference>("restore")?.setOnPreferenceClickListener {
|
||||||
val editText = EditText(context).apply {
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog.Builder(context)
|
requestBackupFileLauncher.launch(intent)
|
||||||
.setTitle(R.string.settings_restore_title)
|
|
||||||
.setView(editText)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
restore(editText.text.toString(),
|
|
||||||
onFailure = onFailure@{
|
|
||||||
val view = view ?: return@onFailure
|
|
||||||
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
|
|
||||||
}, onSuccess = onSuccess@{
|
|
||||||
val view = view ?: return@onSuccess
|
|
||||||
Snackbar.make(view, context.getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show()
|
|
||||||
})
|
|
||||||
}.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
// Do Nothing
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,9 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
|||||||
if (!metadataFile.exists()) return@forEach
|
if (!metadataFile.exists()) return@forEach
|
||||||
|
|
||||||
val metadata = metadataFile.readText()?.let {
|
val metadata = metadataFile.readText()?.let {
|
||||||
|
runCatching {
|
||||||
json.decodeFromString<Metadata>(it)
|
json.decodeFromString<Metadata>(it)
|
||||||
|
}.getOrNull()
|
||||||
} ?: return@forEach
|
} ?: return@forEach
|
||||||
|
|
||||||
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach
|
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach
|
||||||
|
|||||||
@@ -18,11 +18,21 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.hitomi.GalleryBlock
|
import xyz.quaver.pupil.hitomi.GalleryBlock
|
||||||
import xyz.quaver.pupil.hitomi.GalleryInfo
|
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||||
import xyz.quaver.pupil.hitomi.imageUrlFromImage
|
import xyz.quaver.pupil.hitomi.imageUrlFromImage
|
||||||
@@ -133,3 +143,30 @@ fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
|
|||||||
|
|
||||||
val JsonElement.content
|
val JsonElement.content
|
||||||
get() = this.jsonPrimitive.contentOrNull
|
get() = this.jsonPrimitive.contentOrNull
|
||||||
|
|
||||||
|
fun checkNotificationEnabled(context: Context) =
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
fun showNotificationPermissionExplanationDialog(context: Context) {
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setTitle(R.string.warning)
|
||||||
|
.setMessage(R.string.notification_denied)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestNotificationPermission(
|
||||||
|
activity: Activity,
|
||||||
|
requestPermissionLauncher: ActivityResultLauncher<String>,
|
||||||
|
showRationale: Boolean = true,
|
||||||
|
ifGranted: () -> Unit,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
checkNotificationEnabled(activity) -> ifGranted()
|
||||||
|
showRationale && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.POST_NOTIFICATIONS) ->
|
||||||
|
showNotificationPermissionExplanationDialog(activity)
|
||||||
|
else ->
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,11 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
val msg = extractReleaseNote(update, Locale.getDefault())
|
val msg = extractReleaseNote(update, Locale.getDefault())
|
||||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
setMessage(Markwon.create(context).toMarkdown(msg))
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
if (!checkNotificationEnabled(context)) {
|
||||||
|
showNotificationPermissionExplanationDialog(context)
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
//Cancel any download queued before
|
//Cancel any download queued before
|
||||||
|
|||||||
31
app/src/main/res/raw/isrgrootx1.pem
Normal file
31
app/src/main/res/raw/isrgrootx1.pem
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||||
|
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||||
|
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||||
|
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||||
|
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||||
|
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||||
|
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||||
|
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||||
|
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||||
|
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||||
|
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||||
|
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||||
|
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||||
|
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||||
|
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||||
|
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||||
|
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||||
|
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||||
|
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||||
|
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||||
|
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||||
|
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||||
|
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||||
|
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||||
|
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||||
|
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
||||||
<string name="settings_clear_history_summary">履歴数: %1$d</string>
|
<string name="settings_clear_history_summary">履歴数: %1$d</string>
|
||||||
<string name="main_drawer_history">履歴</string>
|
<string name="main_drawer_history">履歴</string>
|
||||||
|
<string name="notification_denied">通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。</string>
|
||||||
<string name="main_drawer_home">トップ</string>
|
<string name="main_drawer_home">トップ</string>
|
||||||
<string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
|
<string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
|
||||||
<string name="settings_security_mode_title">セキュリティーモード</string>
|
<string name="settings_security_mode_title">セキュリティーモード</string>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||||
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
||||||
<string name="main_drawer_history">기록</string>
|
<string name="main_drawer_history">기록</string>
|
||||||
|
<string name="notification_denied">백그라운드 다운로드를 위해서는 알림을 활성화할 필요가 있습니다. 알림을 비활성화하면 백그라운드 다운로드와 앱 업데이트 기능을 사용할 수 없습니다.</string>
|
||||||
<string name="main_drawer_home">홈</string>
|
<string name="main_drawer_home">홈</string>
|
||||||
<string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
|
<string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
|
||||||
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>
|
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
|
|
||||||
<string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string>
|
<string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string>
|
||||||
|
|
||||||
|
<string name="notification_denied">Notification permission is required for background downloads. If you deny notifications from this app, in-app update and background download will be disabled.</string>
|
||||||
|
|
||||||
<string name="main_drawer_home">Home</string>
|
<string name="main_drawer_home">Home</string>
|
||||||
<string name="main_drawer_history">History</string>
|
<string name="main_drawer_history">History</string>
|
||||||
<string name="main_drawer_downloads">Downloads</string>
|
<string name="main_drawer_downloads">Downloads</string>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<paths>
|
<paths>
|
||||||
<external-path name="external" path="/"/>
|
<external-path name="external" path="."/>
|
||||||
<external-files-path name="files" path="/"/>
|
<external-files-path name="files" path="."/>
|
||||||
|
<files-path name="files" path="." />
|
||||||
</paths>
|
</paths>
|
||||||
10
build.gradle
10
build.gradle
@@ -6,16 +6,16 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
classpath "com.google.gms:google-services:4.3.10"
|
classpath "com.google.gms:google-services:4.3.15"
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1"
|
classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9"
|
||||||
classpath "com.google.firebase:perf-plugin:1.4.1"
|
classpath "com.google.firebase:perf-plugin:1.4.2"
|
||||||
classpath "com.google.android.gms:oss-licenses-plugin:0.10.5"
|
classpath "com.google.android.gms:oss-licenses-plugin:0.10.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ kotlin.code.style=official
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
kotlin_version=1.6.10
|
kotlin_version=1.9.0
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||||
|
|||||||
Reference in New Issue
Block a user