Compare commits

...

24 Commits
5.3.15 ... dev

Author SHA1 Message Date
tom5079
9e61af7ed9 hello world 2025-03-08 10:30:41 -08:00
tom5079
d4009e8be9 revert mainactivity 2025-03-08 09:03:24 -08:00
tom5079
697eeb61d2 base component activity 2025-03-08 08:59:05 -08:00
tom5079
f6d29ac083 add compose 2025-03-08 08:53:23 -08:00
tom5079
0eff941d48 update README.md 2025-02-26 23:42:41 -08:00
tom5079
be4aca1f6b fix layout in android 35 2025-02-26 23:39:11 -08:00
tom5079
038b8e0ac5 fix language search 2025-02-25 00:19:51 -08:00
tom5079
79a4917897 build apk 2025-02-24 23:55:55 -08:00
tom5079
8c8ead5830 improve japanese 2025-02-24 23:47:44 -08:00
tom5079
a3b6b010be build apk 2025-02-24 23:38:14 -08:00
tom5079
8bf936ee20 build apk 2025-02-24 23:38:14 -08:00
tom5079
c69972f289 update README.md 2025-02-24 23:38:14 -08:00
tom5079
05f555bb91 minsdk 2025-02-24 23:38:14 -08:00
tom5079
83d6058f2b sort mode 2025-02-24 23:38:12 -08:00
tom5079
f888535389 migrate to kts 2025-02-24 23:36:02 -08:00
tom5079
0f2336eccf migrate to kts 2025-02-24 23:36:02 -08:00
tom5079
e8443664dc cleanup 2025-02-24 23:36:02 -08:00
tom5079
0f9b6963a6 dependency update 2025-02-24 23:36:02 -08:00
tom5079
6727ac1014 upgrade agp 2025-02-24 23:36:02 -08:00
tom5079
a15e2c30cb upgrade gcp 2025-02-24 23:36:02 -08:00
tom5079
6c603b2bf3 Merge pull request #171 from maboroshin/master
Update strings.xml improve Japanese translation
2025-02-09 11:24:24 -08:00
maboroshin
8146fc8473 Update strings.xml improve Japanese 2025-02-03 09:17:38 +09:00
maboroshin
3eff34e585 Update strings.xml improve Japanese 2025-02-03 08:28:10 +09:00
maboroshin
b24f4b5306 Update strings.xml reorder Japanese 2025-02-03 07:25:29 +09:00
39 changed files with 1773 additions and 951 deletions

View File

@@ -1,26 +1,29 @@
![Banner](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/pupil-banner.png?raw=true)
*Pupil, Hitomi.la viewer for Android*
*Pupil, Hitomi.la viewer for Android*
![](https://img.shields.io/github/downloads/tom5079/Pupil/total)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.3.13/Pupil-v5.3.13.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.13/Pupil-v5.3.13.apk)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.3.18/Pupil-v5.3.18.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.18/Pupil-v5.3.18.apk)
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
# Features
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.jpg?raw=true)
# Installation
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
or Build app yourself
or Build app yourself
# Manual
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean.
Consider using translator.
# Contribution
Any kind of contribution is appreciated. Feel free to leave PR!
## Tag Translation
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)

View File

@@ -1,145 +0,0 @@
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "kotlin-kapt"
apply plugin: "kotlin-parcelize"
apply plugin: "kotlinx-serialization"
apply plugin: "com.google.android.gms.oss-licenses-plugin"
if (file("google-services.json").exists()) {
logger.lifecycle("Firebase Enabled")
apply plugin: "com.google.gms.google-services"
apply plugin: "com.google.firebase.crashlytics"
apply plugin: "com.google.firebase.firebase-perf"
} else {
logger.lifecycle("Firebase Disabled")
}
ext {
okhttp_version = "3.12.12"
}
configurations {
all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.group == "com.squareup.okhttp3" && details.requested.name == "okhttp") {
// OkHttp drops support before 5.0 since 3.13.0
details.useVersion okhttp_version
}
}
}
}
}
android {
defaultConfig {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
compileSdk 34
targetSdkVersion 34
versionCode 69
versionName "5.3.15"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
debug {
defaultConfig.minSdkVersion 21
minifyEnabled false
shrinkResources false
debuggable true
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
ext.enableCrashlytics = false
ext.alwaysUpdateBuildId = false
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
buildFeatures {
viewBinding true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
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-datetime:0.3.2"
implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.appcompat:appcompat:1.4.1"
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.11.0"
implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation "com.google.firebase:firebase-analytics-ktx"
implementation "com.google.firebase:firebase-crashlytics-ktx"
implementation "com.google.firebase:firebase-perf-ktx"
implementation "com.google.android.gms:play-services-oss-licenses: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.quiph.ui:recyclerviewfastscroller:0.2.1"
implementation 'com.github.piasy:BigImageViewer:1.8.1'
implementation 'com.github.piasy:FrescoImageLoader:1.8.1'
implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1'
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.6.0'
//noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "io.ktor:ktor-network:2.3.10"
implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2"
implementation "net.rdrei.android.dirchooser:library:3.2@aar"
implementation "com.gu:option:1.3"
implementation "com.andrognito.patternlockview:patternlockview:1.0.0"
//implementation "com.andrognito.pinlockview:pinlockview:2.1.0"
implementation "ru.noties.markwon:core:3.1.0"
implementation "com.skyfishjy.ripplebackground:library:1.0.1"
implementation "org.jsoup:jsoup:1.14.3"
implementation "xyz.quaver:documentfilex:0.7.2"
implementation "xyz.quaver:floatingsearchview:1.1.7"
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:rules:1.4.0"
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
}

144
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,144 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.gms.oss.licenses)
alias(libs.plugins.gms.google.services)
alias(libs.plugins.firebase.crashlytics)
alias(libs.plugins.firebase.perf)
id("kotlin-parcelize")
}
android {
namespace = "xyz.quaver.pupil"
compileSdk = 35
defaultConfig {
applicationId = "xyz.quaver.pupil"
minSdk = 21
targetSdk = 35
versionCode = 70
versionName = "5.3.18"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
buildFeatures {
buildConfig = true
compose = true
}
buildTypes {
debug {
isMinifyEnabled = false
isShrinkResources = false
isDebuggable = true
applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG"
extra.apply {
set("enableCrashlytics", false)
set("alwaysUpdateBuildId", false)
}
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
viewBinding = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.datetime)
implementation(platform(libs.compose.bom))
androidTestImplementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)
androidTestImplementation(libs.compose.ui.test)
debugImplementation(libs.compose.ui.test.manifest)
implementation(libs.compose.icons)
implementation(libs.compose.adaptive)
implementation(libs.compose.activity)
implementation(libs.compose.lifecycle.viewmodel)
implementation(libs.compose.livedata)
implementation(libs.compose.navigation)
implementation(libs.accompanist.adaptive)
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.activity.ktx)
implementation(libs.fragment.ktx)
implementation(libs.preference.ktx)
implementation(libs.recyclerview)
implementation(libs.constraintlayout)
implementation(libs.gridlayout)
implementation(libs.biometric)
implementation(libs.work.runtime.ktx)
implementation(libs.library)
implementation(libs.material)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics.ktx)
implementation(libs.firebase.crashlytics.ktx)
implementation(libs.firebase.perf.ktx)
implementation(libs.play.services.oss.licenses)
implementation(libs.play.services.mlkit.face.detection)
implementation(libs.fab)
implementation(libs.bigimageviewer)
implementation(libs.frescoimageloader)
implementation(libs.frescoimageviewfactory)
implementation(libs.imagepipeline.okhttp3)
//noinspection GradleDependency
implementation(libs.okhttp)
implementation(libs.ktor.network)
implementation(libs.dotsindicator)
implementation(libs.pinlockview)
implementation(libs.patternlockview)
implementation(libs.core)
implementation(libs.ripplebackground.library)
implementation(libs.recyclerview.fastscroller)
implementation(libs.jsoup)
implementation(libs.documentfilex)
implementation(libs.floatingsearchview)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.rules)
androidTestImplementation(libs.runner)
androidTestImplementation(libs.espresso.core)
}

Binary file not shown.

Binary file not shown.

View File

@@ -11,10 +11,27 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 69,
"versionName": "5.3.14",
"versionCode": 70,
"versionName": "5.3.18",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 21
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
@@ -17,6 +16,4 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
</resources>
<resources></resources>

View File

@@ -1,27 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="xyz.quaver.pupil">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="23" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="23" />
<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" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation"
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="32"
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32"
tools:ignore="CoarseFineLocation" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<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"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<application
android:name=".Pupil"
@@ -29,20 +40,20 @@
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:theme"
tools:ignore="UnusedAttribute">
tools:ignore="UnusedAttribute"
tools:replace="android:theme">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="face" />
<provider
android:authorities="${applicationId}.provider"
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
@@ -52,15 +63,18 @@
</provider>
<service android:name=".services.DownloadService"
<service
android:name=".services.DownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".services.TransferClientService"
<service
android:name=".services.TransferClientService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".services.TransferServerService"
<service
android:name=".services.TransferServerService"
android:exported="false"
android:foregroundServiceType="dataSync" />
@@ -76,8 +90,8 @@
<activity
android:name=".ui.ReaderActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:parentActivityName=".ui.MainActivity"
android:exported="true">
android:exported="true"
android:parentActivityName=".ui.MainComposeActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
@@ -86,8 +100,8 @@
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="*.hasha.in"/>
<data android:pathPrefix="/reader"/>
<data android:host="*.hasha.in" />
<data android:pathPrefix="/reader" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
@@ -97,8 +111,8 @@
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="hitomi.la"/>
<data android:pathPrefix="/galleries"/>
<data android:host="hitomi.la" />
<data android:pathPrefix="/galleries" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
@@ -178,13 +192,13 @@
</activity>
<activity
android:name=".ui.SettingsActivity"
android:label="@string/settings_title">
</activity>
android:label="@string/settings_title" />
<activity
android:name=".ui.MainActivity"
android:name=".ui.MainComposeActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/NoActionBarAppTheme"
android:exported="true">
android:exported="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
@@ -192,7 +206,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
<activity android:name=".ui.TransferActivity" />
</application>

View File

@@ -26,7 +26,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
@@ -38,22 +37,22 @@ import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import okhttp3.Dispatcher
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import xyz.quaver.io.FileX
import xyz.quaver.pupil.hitomi.evaluationContext
import xyz.quaver.pupil.hitomi.readText
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.SavedSet
import xyz.quaver.pupil.util.getProxyInfo
import xyz.quaver.pupil.util.preferences
import xyz.quaver.pupil.util.proxyInfo
import java.io.File
import java.net.URL
import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.CertificateFactory
import java.util.*
import java.util.UUID
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
@@ -94,16 +93,19 @@ fun getSSLContext(context: Context): SSLContext {
keyStore.setCertificateEntry("isrgrootx1", certificate)
val defaultTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
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)
defaultTrustManagerFactory.trustManagers.filterIsInstance<X509TrustManager>()
.forEach { trustManager ->
trustManager.acceptedIssuers.forEach { acceptedIssuer ->
keyStore.setCertificateEntry(acceptedIssuer.subjectDN.name, acceptedIssuer)
}
}
}
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
val trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
val sslContext = SSLContext.getInstance("TLS")
@@ -125,7 +127,7 @@ class Pupil : Application() {
preferences = PreferenceManager.getDefaultSharedPreferences(this)
val userID = Preferences["user_id", ""].let { userID ->
val userID = Preferences["user_id", ""].let { userID ->
if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
else userID
}
@@ -142,7 +144,10 @@ class Pupil : Application() {
.proxyInfo(proxyInfo)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
)
.header("Referer", "https://hitomi.la/")
.build()
@@ -178,7 +183,8 @@ class Pupil : Application() {
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
favoriteTags =
SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
favoriteTags.filter { it.tag.contains('_') }.forEach {
@@ -209,46 +215,73 @@ class Pupil : Application() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
description = getString(R.string.channel_download_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(
NotificationChannel(
"download",
getString(R.string.channel_download),
NotificationManager.IMPORTANCE_LOW
).apply {
description = getString(R.string.channel_download_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(NotificationChannel("downloader", getString(R.string.channel_downloader), NotificationManager.IMPORTANCE_LOW).apply {
description = getString(R.string.channel_downloader_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(
NotificationChannel(
"downloader",
getString(R.string.channel_downloader),
NotificationManager.IMPORTANCE_LOW
).apply {
description = getString(R.string.channel_downloader_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
description = getString(R.string.channel_update_description)
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(
NotificationChannel(
"update",
getString(R.string.channel_update),
NotificationManager.IMPORTANCE_HIGH
).apply {
description = getString(R.string.channel_update_description)
enableLights(true)
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
})
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
})
manager.createNotificationChannel(NotificationChannel("transfer", getString(R.string.channel_transfer), NotificationManager.IMPORTANCE_LOW).apply {
description = getString(R.string.channel_transfer_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(
NotificationChannel(
"transfer",
getString(R.string.channel_transfer),
NotificationManager.IMPORTANCE_LOW
).apply {
description = getString(R.string.channel_transfer_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
}
AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
})
AppCompatDelegate.setDefaultNightMode(
when (Preferences.get<Boolean>("dark_mode")) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
}
)
super.onCreate()
}

View File

@@ -18,9 +18,9 @@ package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.util.*
import java.util.LinkedList
suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope {
suspend fun doSearch(query: String, sortMode: SortMode): List<Int> = coroutineScope {
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
@@ -34,8 +34,8 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
val negativeTerms = LinkedList<String>()
for (term in terms) {
if (term.matches(Regex("^-.+")))
negativeTerms.push(term.replace(Regex("^-"), ""))
if (term.startsWith("-"))
negativeTerms.push(term.substring(1))
else if (term.isNotBlank())
positiveTerms.push(term)
}
@@ -43,22 +43,25 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
val positiveResults = positiveTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it)
getGalleryIDsForQuery(it, sortMode)
}.getOrElse { emptySet() }
}
}
val negativeResults = negativeTerms.mapIndexed { index, it ->
val negativeResults = negativeTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it)
getGalleryIDsForQuery(it, sortMode)
}.getOrElse { emptySet() }
}
}
val results = when {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(
SearchArgs("all", "index", "all"),
sortMode
)
else -> emptySet()
}.toMutableSet()
@@ -79,9 +82,13 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
}
//negative results
negativeResults.forEachIndexed { index, it ->
negativeResults.forEach {
filterNegative(it.await())
}
results
return@coroutineScope if (sortMode != SortMode.RANDOM) {
results.toList()
} else {
results.shuffled()
}
}

View File

@@ -26,6 +26,54 @@ import java.nio.ByteOrder
import java.security.MessageDigest
import kotlin.math.min
data class SearchArgs(
val area: String?,
val tag: String,
val language: String,
) {
companion object {
fun fromQuery(query: String): SearchArgs? {
if (!query.contains(':')) {
return null
}
val (left, right) = query.split(':')
return when (left) {
"male", "female" -> SearchArgs("tag", query, "all")
"language" -> SearchArgs("all", "index", right)
else -> SearchArgs(left, right, "all")
}
}
}
}
enum class SortMode {
DATE_ADDED,
DATE_PUBLISHED,
POPULAR_TODAY,
POPULAR_WEEK,
POPULAR_MONTH,
POPULAR_YEAR,
RANDOM;
val orderBy: String
get() = when (this) {
DATE_ADDED, DATE_PUBLISHED, RANDOM -> "date"
POPULAR_TODAY, POPULAR_WEEK, POPULAR_MONTH, POPULAR_YEAR -> "popular"
}
val orderByKey: String
get() = when (this) {
DATE_ADDED, RANDOM -> "added"
DATE_PUBLISHED -> "published"
POPULAR_TODAY -> "today"
POPULAR_WEEK -> "week"
POPULAR_MONTH -> "month"
POPULAR_YEAR -> "year"
}
}
//searchlib.js
const val separator = "-"
const val extension = ".html"
@@ -39,51 +87,35 @@ val tag_index_version: String by lazy { getIndexVersion("tagindex") }
val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") }
val tagIndexDomain = "tagindex.hitomi.la"
fun sha256(data: ByteArray) : ByteArray {
fun sha256(data: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data)
}
@OptIn(ExperimentalUnsignedTypes::class)
fun hashTerm(term: String) : UByteArray {
fun hashTerm(term: String): UByteArray {
return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
}
fun sanitize(input: String) : String {
fun sanitize(input: String): String {
return input.replace(Regex("[/#]"), "")
}
fun getIndexVersion(name: String) =
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText()
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText()
//search.js
fun getGalleryIDsForQuery(query: String) : Set<Int> {
query.replace("_", " ").let {
if (it.indexOf(':') > -1) {
val sides = it.split(":")
val ns = sides[0]
var tag = sides[1]
fun getGalleryIDsForQuery(query: String, sortMode: SortMode): Set<Int> {
val sanitizedQuery = query.replace("_", " ")
var area : String? = ns
var language = "all"
when (ns) {
"female", "male" -> {
area = "tag"
tag = it
}
"language" -> {
area = null
language = tag
tag = "index"
}
}
val args = SearchArgs.fromQuery(sanitizedQuery)
return getGalleryIDsFromNozomi(area, tag, language)
}
val key = hashTerm(it)
return if (args != null) {
getGalleryIDsFromNozomi(args, sortMode)
} else {
val key = hashTerm(sanitizedQuery)
val field = "galleries"
val node = getNodeAtAddress(field, 0) ?: return emptySet()
val node = getNodeAtAddress(field, 0)
val data = bSearch(field, key, node)
@@ -95,14 +127,14 @@ fun getGalleryIDsForQuery(query: String) : Set<Int> {
}
fun encodeSearchQueryForUrl(s: Char) =
when(s) {
when (s) {
' ' -> "_"
'/' -> "slash"
'.' -> "dot"
else -> s.toString()
}
fun getSuggestionsForQuery(query: String) : List<Suggestion> {
fun getSuggestionsForQuery(query: String): List<Suggestion> {
query.replace('_', ' ').let {
var field = "global"
var term = it
@@ -114,13 +146,16 @@ fun getSuggestionsForQuery(query: String) : List<Suggestion> {
}
val chars = term.map(::encodeSearchQueryForUrl)
val url = "https://$tagIndexDomain/$field${if (chars.isNotEmpty()) "/${chars.joinToString("/")}" else ""}.json"
val url =
"https://$tagIndexDomain/$field${if (chars.isNotEmpty()) "/${chars.joinToString("/")}" else ""}.json"
val request = Request.Builder()
.url(url)
.build()
val suggestions = json.parseToJsonElement(client.newCall(request).execute().body()?.use { body -> body.string() } ?: return emptyList())
val suggestions = json.parseToJsonElement(
client.newCall(request).execute().body()?.use { body -> body.string() }
?: return emptyList())
return buildList {
suggestions.jsonArray.forEach { suggestionRaw ->
@@ -131,26 +166,34 @@ fun getSuggestionsForQuery(query: String) : List<Suggestion> {
val ns = suggestion[2].content ?: ""
val tagname = sanitize(suggestion[0].content ?: return@forEach)
val url = when(ns) {
val url = when (ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension"
}
add(Suggestion(suggestion[0].content ?: "", suggestion[1].content?.toIntOrNull() ?: 0, url, ns))
add(
Suggestion(
suggestion[0].content ?: "",
suggestion[1].content?.toIntOrNull() ?: 0,
url,
ns
)
)
}
}
}
}
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>): List<Suggestion> {
val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data"
val (offset, length) = data
if (length > 10000 || length <= 0)
throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset+length))
val inbuf = getURLAtRange(url, offset.until(offset + length))
val suggestions = ArrayList<Suggestion>()
@@ -165,23 +208,25 @@ fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggesti
for (i in 0.until(numberOfSuggestions)) {
var top = buffer.int
val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
buffer.position(buffer.position()+top)
val ns = inbuf.sliceArray(buffer.position().until(buffer.position() + top))
.toString(charset("UTF-8"))
buffer.position(buffer.position() + top)
top = buffer.int
val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
buffer.position(buffer.position()+top)
val tag = inbuf.sliceArray(buffer.position().until(buffer.position() + top))
.toString(charset("UTF-8"))
buffer.position(buffer.position() + top)
val count = buffer.int
val tagname = sanitize(tag)
val u =
when(ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension"
}
when (ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension"
}
suggestions.add(Suggestion(tag, count, u, ns))
}
@@ -189,12 +234,17 @@ fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggesti
return suggestions
}
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
val nozomiAddress =
when(area) {
null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
}
fun nozomiAddressFromArgs(args: SearchArgs, sortMode: SortMode) = when {
sortMode != SortMode.DATE_ADDED && sortMode != SortMode.RANDOM ->
if (args.area == "all") "$protocol//$domain/$compressed_nozomi_prefix/${sortMode.orderBy}/${sortMode.orderByKey}-${args.language}$nozomiextension"
else "$protocol//$domain/$compressed_nozomi_prefix/${args.area}/${sortMode.orderBy}/${sortMode.orderByKey}/${args.tag}-${args.language}$nozomiextension"
args.area == "all" -> "$protocol//$domain/$compressed_nozomi_prefix/${args.tag}-${args.language}$nozomiextension"
else -> "$protocol//$domain/$compressed_nozomi_prefix/${args.area}/${args.tag}-${args.language}$nozomiextension"
}
fun getGalleryIDsFromNozomi(args: SearchArgs, sortMode: SortMode): Set<Int> {
val nozomiAddress = nozomiAddressFromArgs(args, sortMode)
val bytes = URL(nozomiAddress).readBytes()
@@ -210,13 +260,13 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<
return nozomi
}
fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
fun getGalleryIDsFromData(data: Pair<Long, Int>): Set<Int> {
val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data"
val (offset, length) = data
if (length > 100000000 || length <= 0)
throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset+length))
val inbuf = getURLAtRange(url, offset.until(offset + length))
val galleryIDs = mutableSetOf<Int>()
@@ -226,7 +276,7 @@ fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
val numberOfGalleryIDs = buffer.int
val expectedLength = numberOfGalleryIDs*4+4
val expectedLength = numberOfGalleryIDs * 4 + 4
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
@@ -239,33 +289,38 @@ fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
return galleryIDs
}
fun getNodeAtAddress(field: String, address: Long) : Node? {
fun getNodeAtAddress(field: String, address: Long): Node {
val url =
when(field) {
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
"languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index"
"nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index"
}
when (field) {
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
"languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index"
"nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index"
}
val nodedata = getURLAtRange(url, address.until(address+ max_node_size))
val nodedata = getURLAtRange(url, address.until(address + max_node_size))
return decodeNode(nodedata)
}
fun getURLAtRange(url: String, range: LongRange) : ByteArray {
fun getURLAtRange(url: String, range: LongRange): ByteArray {
val request = Request.Builder()
.url(url)
.header("Range", "bytes=${range.first}-${range.last}")
.build()
return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf()
}
@OptIn(ExperimentalUnsignedTypes::class)
data class Node(val keys: List<UByteArray>, val datas: List<Pair<Long, Int>>, val subNodeAddresses: List<Long>)
data class Node(
val keys: List<UByteArray>,
val datas: List<Pair<Long, Int>>,
val subNodeAddresses: List<Long>
)
@OptIn(ExperimentalUnsignedTypes::class)
fun decodeNode(data: ByteArray) : Node {
fun decodeNode(data: ByteArray): Node {
val buffer = ByteBuffer
.wrap(data)
.order(ByteOrder.BIG_ENDIAN)
@@ -281,8 +336,8 @@ fun decodeNode(data: ByteArray) : Node {
if (keySize == 0 || keySize > 32)
throw Exception("fatal: !keySize || keySize > 32")
keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize)))
buffer.position(buffer.position()+keySize)
keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize)))
buffer.position(buffer.position() + keySize)
}
val numberOfDatas = buffer.int
@@ -295,7 +350,7 @@ fun decodeNode(data: ByteArray) : Node {
datas.add(Pair(offset, length))
}
val numberOfSubNodeAddresses = B +1
val numberOfSubNodeAddresses = B + 1
val subNodeAddresses = ArrayList<Long>()
for (i in 0.until(numberOfSubNodeAddresses)) {
@@ -307,8 +362,8 @@ fun decodeNode(data: ByteArray) : Node {
}
@OptIn(ExperimentalUnsignedTypes::class)
fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int {
fun bSearch(field: String, key: UByteArray, node: Node): Pair<Long, Int>? {
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray): Int {
val top = min(dv1.size, dv2.size)
for (i in 0.until(top)) {
@@ -321,18 +376,18 @@ fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
return 0
}
fun locateKey(key: UByteArray, node: Node) : Pair<Boolean, Int> {
fun locateKey(key: UByteArray, node: Node): Pair<Boolean, Int> {
for (i in node.keys.indices) {
val cmpResult = compareArrayBuffers(key, node.keys[i])
if (cmpResult <= 0)
return Pair(cmpResult==0, i)
return Pair(cmpResult == 0, i)
}
return Pair(false, node.keys.size)
}
fun isLeaf(node: Node) : Boolean {
fun isLeaf(node: Node): Boolean {
for (subnode in node.subNodeAddresses)
if (subnode != 0L)
return false
@@ -349,6 +404,6 @@ fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
else if (isLeaf(node))
return null
val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null
val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where])
return bSearch(field, key, nextNode)
}

View File

@@ -18,30 +18,68 @@
package xyz.quaver.pupil.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import xyz.quaver.pupil.R
import androidx.core.view.WindowCompat
import xyz.quaver.pupil.util.LockManager
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.normalizeID
open class BaseActivity : AppCompatActivity() {
open class BaseComponentActivity : ComponentActivity() {
private var locked: Boolean = true
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK)
locked = false
else
finish()
private val lockLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK)
locked = false
else
finish()
}
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
locked = !LockManager(this).locks.isNullOrEmpty()
}
@CallSuper
override fun onResume() {
super.onResume()
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (locked)
lockLauncher.launch(Intent(this, LockActivity::class.java))
}
}
open class BaseActivity : AppCompatActivity() {
private var locked: Boolean = true
private val lockLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK)
locked = false
else
finish()
}
@CallSuper
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
@@ -56,12 +94,12 @@ open class BaseActivity : AppCompatActivity() {
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
WindowManager.LayoutParams.FLAG_SECURE
)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (locked)
lockLauncher.launch(Intent(this, LockActivity::class.java))
}
}

View File

@@ -33,7 +33,6 @@ import android.widget.EditText
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
@@ -42,19 +41,40 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.hitomi.SortMode
import xyz.quaver.pupil.hitomi.doSearch
import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.pupil.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.searchHistory
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.types.FavoriteHistorySwitch
import xyz.quaver.pupil.types.LoadingSuggestion
import xyz.quaver.pupil.types.NoResultSuggestion
import xyz.quaver.pupil.types.Suggestion
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.ui.view.MainView
@@ -73,10 +93,19 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
val sortModeLookup = mapOf(
R.id.main_menu_sort_date_added to SortMode.DATE_ADDED,
R.id.main_menu_sort_date_published to SortMode.DATE_PUBLISHED,
R.id.main_menu_sort_popular_today to SortMode.POPULAR_TODAY,
R.id.main_menu_sort_popular_week to SortMode.POPULAR_WEEK,
R.id.main_menu_sort_popular_month to SortMode.POPULAR_MONTH,
R.id.main_menu_sort_popular_year to SortMode.POPULAR_YEAR,
R.id.main_menu_sort_random to SortMode.RANDOM
)
class MainActivity :
BaseActivity(),
NavigationView.OnNavigationItemSelectedListener
{
NavigationView.OnNavigationItemSelectedListener {
enum class Mode {
SEARCH,
@@ -85,25 +114,21 @@ class MainActivity :
FAVORITE
}
enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<Int>()
private var query = ""
set(value) {
field = value
with(findViewById<SearchInputView>(R.id.search_bar_text)) {
if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE)
set(value) {
field = value
with(findViewById<SearchInputView>(R.id.search_bar_text)) {
if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE)
}
}
}
private var queryStack = mutableListOf<String>()
private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST
private var sortMode = SortMode.DATE_ADDED
private var galleryIDs: Deferred<List<Int>>? = null
private var totalItems = 0
@@ -112,11 +137,12 @@ class MainActivity :
private lateinit var binding: MainActivityBinding
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
showNotificationPermissionExplanationDialog(this)
private val requestNotificationPermssionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
showNotificationPermissionExplanationDialog(this)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -127,9 +153,17 @@ class MainActivity :
intent.dataString?.let { url ->
restore(url,
onFailure = {
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
Snackbar.make(
binding.contents.recyclerview,
R.string.settings_backup_failed,
Snackbar.LENGTH_LONG
).show()
}, onSuccess = {
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show()
Snackbar.make(
binding.contents.recyclerview,
getString(R.string.settings_restore_success, it),
Snackbar.LENGTH_LONG
).show()
}
)
}
@@ -138,17 +172,24 @@ class MainActivity :
requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {}
if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
DownloadLocationDialogFragment().show(
supportFragmentManager,
"Download Location Dialog"
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] &&
ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() }
ContextCompat.getExternalFilesDirs(this, null).filterNotNull()
.map { Uri.fromFile(it).toString() }
.contains(Preferences["download_folder", ""])
) {
AlertDialog.Builder(this)
.setTitle(R.string.warning)
.setMessage(R.string.unaccessible_download_folder)
.setPositiveButton(android.R.string.ok) { _, _ ->
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
DownloadLocationDialogFragment().show(
supportFragmentManager,
"Download Location Dialog"
)
}.setNegativeButton(R.string.ignore) { _, _ ->
Preferences["download_folder_ignore_warning"] = true
}.show()
@@ -163,10 +204,12 @@ class MainActivity :
checkUpdate(this)
}
@OptIn(ExperimentalStdlibApi::class)
override fun onBackPressed() {
when {
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START)
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(
GravityCompat.START
)
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = queryStack.last()
@@ -175,6 +218,7 @@ class MainActivity :
fetchGalleries(query, sortMode)
loadBlocks()
}
else -> super.onBackPressed()
}
}
@@ -189,7 +233,7 @@ class MainActivity :
val perPage = Preferences["per_page", "25"].toInt()
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) {
return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 0) {
runOnUiThread {
@@ -204,6 +248,7 @@ class MainActivity :
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) {
runOnUiThread {
@@ -218,20 +263,22 @@ class MainActivity :
true
}
else -> super.onKeyDown(keyCode, event)
}
}
private fun initView() {
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
binding.contents.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// -height of the search view < translationY < 0
binding.contents.searchview.translationY =
min(
max(
binding.contents.searchview.translationY - dy,
-binding.contents.searchview.binding.querySection.root.height.toFloat()
), 0F)
binding.contents.searchview.translationY - dy,
-binding.contents.searchview.binding.querySection.root.height.toFloat()
), 0F
)
if (dy > 0)
binding.contents.fab.hideMenuButton(true)
@@ -240,7 +287,12 @@ class MainActivity :
}
})
Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
Linkify.addLinks(
binding.contents.noresult,
Pattern.compile(getString(R.string.https_text)),
null,
null,
{ _, _ -> getString(R.string.https) })
//NavigationView
binding.navView.setNavigationItemSelectedListener(this)
@@ -261,14 +313,17 @@ class MainActivity :
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_jump_title)
setMessage(getString(
R.string.main_jump_message,
currentPage+1,
ceil(totalItems / perPage.toDouble()).roundToInt()
))
setMessage(
getString(
R.string.main_jump_message,
currentPage + 1,
ceil(totalItems / perPage.toDouble()).roundToInt()
)
)
setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
currentPage =
(editText.text.toString().toIntOrNull() ?: return@setPositiveButton) - 1
runOnUiThread {
cancelFetch()
@@ -322,7 +377,8 @@ class MainActivity :
setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
val galleryID =
editText.text.toString().toIntOrNull() ?: return@setPositiveButton
GalleryDialog(this@MainActivity, galleryID).apply {
onChipClickedHandler.add {
@@ -344,7 +400,7 @@ class MainActivity :
}
with(binding.contents.view) {
setOnPageTurnListener(object: MainView.OnPageTurnListener {
setOnPageTurnListener(object : MainView.OnPageTurnListener {
override fun onPrev(page: Int) {
currentPage--
@@ -409,10 +465,11 @@ class MainActivity :
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)
}
else {
} else {
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
DownloadService.download(this@MainActivity, galleryID)
}
@@ -485,6 +542,7 @@ class MainActivity :
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
} + FavoriteHistorySwitch(getString(R.string.search_show_histories))
}
else -> {
searchHistory.map {
Suggestion(it)
@@ -492,7 +550,7 @@ class MainActivity :
}
}.reversed()
private var suggestionJob : Job? = null
private var suggestionJob: Job? = null
private fun setupSearchBar() {
with(binding.contents.searchview) {
val scrollSuggestionToTop = {
@@ -500,7 +558,10 @@ class MainActivity :
MainScope().launch {
withTimeout(1000) {
val layoutManager = layoutManager as LinearLayoutManager
while (layoutManager.findLastVisibleItemPosition() != adapter?.itemCount?.minus(1)) {
while (layoutManager.findLastVisibleItemPosition() != adapter?.itemCount?.minus(
1
)
) {
layoutManager.scrollToPosition(adapter?.itemCount?.minus(1) ?: 0)
delay(100)
}
@@ -509,7 +570,7 @@ class MainActivity :
}
}
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
onMenuStatusChangeListener = object : FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() {
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
}
@@ -561,7 +622,8 @@ class MainActivity :
suggestionJob = CoroutineScope(Dispatchers.IO).launch {
val suggestions = kotlin.runCatching {
getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList()
getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }
.toMutableList()
}.getOrElse { mutableListOf() }
suggestions.filter {
@@ -573,12 +635,16 @@ class MainActivity :
}
withContext(Dispatchers.Main) {
swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString())))
swapSuggestions(
if (suggestions.isNotEmpty()) suggestions else listOf(
NoResultSuggestion(getText(R.string.main_no_result).toString())
)
)
}
}
}
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
onFocusChangeListener = object : FloatingSearchView.OnFocusChangeListener {
override fun onFocus() {
if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(defaultSuggestions)
@@ -604,8 +670,14 @@ class MainActivity :
}
fun onActionMenuItemSelected(item: MenuItem?) {
when(item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
when (item?.itemId) {
R.id.main_menu_settings -> startActivity(
Intent(
this@MainActivity,
SettingsActivity::class.java
)
)
R.id.main_menu_thin -> {
val thin = !item.isChecked
@@ -620,21 +692,15 @@ class MainActivity :
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
item.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
R.id.main_menu_sort_date_added,
R.id.main_menu_sort_date_published,
R.id.main_menu_sort_popular_today,
R.id.main_menu_sort_popular_week,
R.id.main_menu_sort_popular_month,
R.id.main_menu_sort_popular_year,
R.id.main_menu_sort_random -> {
sortMode = sortModeLookup[item.itemId]!!
item.isChecked = true
runOnUiThread {
@@ -653,7 +719,7 @@ class MainActivity :
runOnUiThread {
binding.drawer.closeDrawers()
when(item.itemId) {
when (item.itemId) {
R.id.main_drawer_home -> {
cancelFetch()
clearGalleries()
@@ -664,6 +730,7 @@ class MainActivity :
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_history -> {
cancelFetch()
clearGalleries()
@@ -674,6 +741,7 @@ class MainActivity :
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_downloads -> {
cancelFetch()
clearGalleries()
@@ -684,6 +752,7 @@ class MainActivity :
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_favorite -> {
cancelFetch()
clearGalleries()
@@ -694,20 +763,35 @@ class MainActivity :
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
}
R.id.main_drawer_github -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
}
R.id.main_drawer_homepage -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.home_page))
)
)
}
R.id.main_drawer_email -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
}
R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.discord))
)
)
}
}
}
@@ -745,17 +829,18 @@ class MainActivity :
}
if (query.isNotEmpty() && mode != Mode.SEARCH) {
Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
setAction(android.R.string.ok) {
cancelFetch()
clearGalleries()
currentPage = 0
mode = Mode.SEARCH
queryStack.clear()
fetchGalleries(query, sortMode)
loadBlocks()
}
}.show()
Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT)
.apply {
setAction(android.R.string.ok) {
cancelFetch()
clearGalleries()
currentPage = 0
mode = Mode.SEARCH
queryStack.clear()
fetchGalleries(query, sortMode)
loadBlocks()
}
}.show()
}
galleryIDs = null
@@ -764,22 +849,16 @@ class MainActivity :
return
galleryIDs = CoroutineScope(Dispatchers.IO).async {
when(mode) {
when (mode) {
Mode.SEARCH -> {
when {
query.isEmpty() and defaultQuery.isEmpty() -> {
when(sortMode) {
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
else -> getGalleryIDsFromNozomi(null, "index", "all")
}.also {
totalItems = it.size
}
}
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = it.size
}
doSearch(
"$defaultQuery $query",
sortMode
).also {
totalItems = it.size
}
}
Mode.HISTORY -> {
when {
query.isEmpty() -> {
@@ -787,36 +866,42 @@ class MainActivity :
totalItems = it.size
}
}
else -> {
val result = doSearch(query).sorted()
val result = doSearch(query, SortMode.DATE_ADDED).sorted()
histories.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.DOWNLOAD -> {
val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
val downloads =
DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
when {
query.isEmpty() -> downloads.reversed().also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
val result = doSearch(query, SortMode.DATE_ADDED).sorted()
downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.FAVORITE -> {
when {
query.isEmpty() -> favorites.reversed().also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
val result = doSearch(query, SortMode.DATE_ADDED).sorted()
favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
@@ -849,10 +934,18 @@ class MainActivity :
}
launch(Dispatchers.Main) {
binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage)
binding.contents.view.setCurrentPage(
currentPage + 1,
galleryIDs.size > (currentPage + 1) * perPage
)
}
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
galleryIDs.slice(
currentPage * perPage until min(
currentPage * perPage + perPage,
galleryIDs.size
)
).chunked(5).let { chunks ->
for (chunk in chunks)
chunk.map { galleryID ->
async {

View File

@@ -0,0 +1,44 @@
package xyz.quaver.pupil.ui
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.adaptive.calculateDisplayFeatures
import xyz.quaver.pupil.ui.compose.MainApp
import xyz.quaver.pupil.ui.theme.AppTheme
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.requestNotificationPermission
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
class MainComposeActivity : BaseComponentActivity() {
private val requestNotificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
showNotificationPermissionExplanationDialog(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestNotificationPermission(this, requestNotificationPermissionLauncher, false)
val viewModel: MainViewModel by viewModels()
setContent {
val displayFeatures = calculateDisplayFeatures(this)
val uiState by viewModel.searchState.collectAsStateWithLifecycle()
AppTheme {
MainApp(
uiState = uiState,
displayFeatures = displayFeatures
)
}
}
}
}

View File

@@ -0,0 +1,20 @@
package xyz.quaver.pupil.ui.compose
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import androidx.window.core.layout.WindowSizeClass
import androidx.window.layout.DisplayFeature
import xyz.quaver.pupil.ui.viewmodel.SearchState
@Composable
fun MainApp(
uiState: SearchState,
displayFeatures: List<DisplayFeature>,
navController: NavController = rememberNavController(),
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,
) {
Text("Hello, World!")
}

View File

@@ -122,7 +122,7 @@ class DefaultQueryDialog : DialogFragment() {
s.replace(
0,
s.length,
s.toString().toLowerCase(java.util.Locale.getDefault())
s.toString().lowercase()
)
}
})

View File

@@ -21,7 +21,6 @@ package xyz.quaver.pupil.ui.dialog
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
@@ -29,8 +28,6 @@ import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.io.FileX
import xyz.quaver.io.util.toFile
import xyz.quaver.pupil.R
@@ -56,8 +53,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
it.data?.data?.also { uri ->
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) {
entries[null]?.locationAvailable?.text = uri.toFile(context)?.canonicalPath
@@ -87,32 +83,6 @@ class DownloadLocationDialogFragment : DialogFragment() {
}
}
private val requestDownloadFolderOldLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val context = context ?: return@registerForActivityResult
val dialog = dialog ?: return@registerForActivityResult
if (it.resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = it.data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite()) {
Snackbar.make(
dialog.window!!.decorView.rootView,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
}
else {
entries[null]?.locationAvailable?.text = directory
Preferences["download_folder"] = File(directory).toURI().toString()
}
}
}
private fun initView() {
val externalFilesDirs = ContextCompat.getExternalFilesDirs(requireContext(), null)
@@ -147,24 +117,11 @@ class DownloadLocationDialogFragment : DialogFragment() {
}
button.performClick()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
requestDownloadFolderLauncher.launch(intent)
} else { // Can't use SAF on old Androids!
val config = DirectoryChooserConfig.builder()
.newDirectoryName("Pupil")
.allowNewDirectoryNameModification(true)
.build()
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
}
requestDownloadFolderOldLauncher.launch(intent)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
requestDownloadFolderLauncher.launch(intent)
}
entries[null] = this
}

View File

@@ -0,0 +1,149 @@
package xyz.quaver.pupil.ui.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF006688)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFC2E8FF)
val md_theme_light_onPrimaryContainer = Color(0xFF001E2B)
val md_theme_light_secondary = Color(0xFF4E616D)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFD1E5F3)
val md_theme_light_onSecondaryContainer = Color(0xFF091E28)
val md_theme_light_tertiary = Color(0xFF5F5A7D)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFE5DEFF)
val md_theme_light_onTertiaryContainer = Color(0xFF1C1736)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFBFCFE)
val md_theme_light_onBackground = Color(0xFF191C1E)
val md_theme_light_surface = Color(0xFFFBFCFE)
val md_theme_light_onSurface = Color(0xFF191C1E)
val md_theme_light_surfaceVariant = Color(0xFFDCE3E9)
val md_theme_light_onSurfaceVariant = Color(0xFF40484D)
val md_theme_light_outline = Color(0xFF71787D)
val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3)
val md_theme_light_inverseSurface = Color(0xFF2E3133)
val md_theme_light_inversePrimary = Color(0xFF75D1FF)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF006688)
val md_theme_light_outlineVariant = Color(0xFFC0C7CD)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFF75D1FF)
val md_theme_dark_onPrimary = Color(0xFF003548)
val md_theme_dark_primaryContainer = Color(0xFF004D67)
val md_theme_dark_onPrimaryContainer = Color(0xFFC2E8FF)
val md_theme_dark_secondary = Color(0xFFB5C9D7)
val md_theme_dark_onSecondary = Color(0xFF20333D)
val md_theme_dark_secondaryContainer = Color(0xFF364954)
val md_theme_dark_onSecondaryContainer = Color(0xFFD1E5F3)
val md_theme_dark_tertiary = Color(0xFFC9C2EA)
val md_theme_dark_onTertiary = Color(0xFF312C4C)
val md_theme_dark_tertiaryContainer = Color(0xFF474364)
val md_theme_dark_onTertiaryContainer = Color(0xFFE5DEFF)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF191C1E)
val md_theme_dark_onBackground = Color(0xFFE1E2E5)
val md_theme_dark_surface = Color(0xFF191C1E)
val md_theme_dark_onSurface = Color(0xFFE1E2E5)
val md_theme_dark_surfaceVariant = Color(0xFF40484D)
val md_theme_dark_onSurfaceVariant = Color(0xFFC0C7CD)
val md_theme_dark_outline = Color(0xFF8A9297)
val md_theme_dark_inverseOnSurface = Color(0xFF191C1E)
val md_theme_dark_inverseSurface = Color(0xFFE1E2E5)
val md_theme_dark_inversePrimary = Color(0xFF006688)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFF75D1FF)
val md_theme_dark_outlineVariant = Color(0xFF40484D)
val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFF4FC3F7)
val Gray50 = Color(0xFFF9FAFB)
val Gray100 = Color(0xFFF3F4F6)
val Gray200 = Color(0xFFE5E7EB)
val Gray300 = Color(0xFFD1D5DB)
val Gray400 = Color(0xFF9CA3AF)
val Gray500 = Color(0xFF6B7280)
val Gray600 = Color(0xFF4B5563)
val Gray700 = Color(0xFF374151)
val Gray800 = Color(0xFF1F2937)
val Gray900 = Color(0xFF111827)
val Red50 = Color(0xFFFEF2F2)
val Red100 = Color(0xFFFEE2E2)
val Red200 = Color(0xFFFECACA)
val Red300 = Color(0xFFFCA5A5)
val Red400 = Color(0xFFF87171)
val Red500 = Color(0xFFEF4444)
val Red600 = Color(0xFFDC2626)
val Red700 = Color(0xFFB91C1C)
val Red800 = Color(0xFF991B1B)
val Red900 = Color(0xFF7F1D1D)
val Yellow50 = Color(0xFFFFFBEB)
val Yellow100 = Color(0xFFFEF3C7)
val Yellow200 = Color(0xFFFDE68A)
val Yellow300 = Color(0xFFFCD34D)
val Yellow400 = Color(0xFFFBBF24)
val Yellow500 = Color(0xFFF59E0B)
val Yellow600 = Color(0xFFD97706)
val Yellow700 = Color(0xFFB45309)
val Yellow800 = Color(0xFF92400E)
val Yellow900 = Color(0xFF78350F)
val Green50 = Color(0xFFECFDF5)
val Green100 = Color(0xFFD1FAE5)
val Green200 = Color(0xFFA7F3D0)
val Green300 = Color(0xFF6EE7B7)
val Green400 = Color(0xFF34D399)
val Green500 = Color(0xFF10B981)
val Green600 = Color(0xFF059669)
val Green700 = Color(0xFF047857)
val Green800 = Color(0xFF065F46)
val Green900 = Color(0xFF064E3B)
val Blue50 = Color(0xFFEFF6FF)
val Blue100 = Color(0xFFDBEAFE)
val Blue200 = Color(0xFFBFDBFE)
val Blue300 = Color(0xFF93C5FD)
val Blue400 = Color(0xFF60A5FA)
val Blue500 = Color(0xFF3B82F6)
val Blue600 = Color(0xFF2563EB)
val Blue700 = Color(0xFF1D4ED8)
val Blue800 = Color(0xFF1E40AF)
val Blue900 = Color(0xFF1E3A8A)
val Indigo50 = Color(0xFFEEF2FF)
val Indigo100 = Color(0xFFE0E7FF)
val Indigo200 = Color(0xFFC7D2FE)
val Indigo300 = Color(0xFFA5B4FC)
val Indigo400 = Color(0xFF818CF8)
val Indigo500 = Color(0xFF6366F1)
val Indigo600 = Color(0xFF4F46E5)
val Indigo700 = Color(0xFF4338CA)
val Indigo800 = Color(0xFF3730A3)
val Indigo900 = Color(0xFF312E81)
val Purple50 = Color(0xFFF5F3FF)
val Purple100 = Color(0xFFEDE9FE)
val Purple200 = Color(0xFFDDD6FE)
val Purple300 = Color(0xFFC4B5FD)
val Purple400 = Color(0xFFA78BFA)
val Purple500 = Color(0xFF8B5CF6)
val Purple600 = Color(0xFF7C3AED)
val Purple700 = Color(0xFF6D28D9)
val Purple800 = Color(0xFF5B21B6)
val Purple900 = Color(0xFF4C1D95)
val Pink50 = Color(0xFFFDF2F8)
val Pink100 = Color(0xFFFCE7F3)
val Pink200 = Color(0xFFFBCFE8)
val Pink300 = Color(0xFFF9A8D4)
val Pink400 = Color(0xFFF472B6)
val Pink500 = Color(0xFFEC4899)
val Pink600 = Color(0xFFDB2777)
val Pink700 = Color(0xFFBE185D)
val Pink800 = Color(0xFF9D174D)
val Pink900 = Color(0xFF831843)

View File

@@ -0,0 +1,97 @@
package xyz.quaver.pupil.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColors = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val DarkColors = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit,
) {
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colors = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
darkTheme -> DarkColors
else -> LightColors
}
MaterialTheme(
colorScheme = colors,
content = content
)
}

View File

@@ -76,7 +76,7 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
s ?: return
if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
s.replace(0, s.length, s.toString().lowercase())
}
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {

View File

@@ -0,0 +1,12 @@
package xyz.quaver.pupil.ui.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SearchState())
val searchState: StateFlow<SearchState> = _uiState
}
data class SearchState(val stub: String = "")

View File

@@ -24,24 +24,26 @@ import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
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 kotlinx.serialization.json.*
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import okhttp3.Request
import xyz.quaver.pupil.R
import xyz.quaver.pupil.hitomi.GalleryBlock
import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.imageUrlFromImage
import java.util.*
import kotlin.collections.ArrayList
import java.util.Locale
@OptIn(ExperimentalStdlibApi::class)
fun String.wordCapitalize() : String {
fun String.wordCapitalize(): String {
val result = ArrayList<String>()
@SuppressLint("DefaultLocale")
@@ -59,8 +61,9 @@ private val suffix = listOf(
"TB" //really?
)
fun byteToString(byte: Long, precision : Int = 1) : String {
var size = byte.toDouble(); var suffixIndex = 0
fun byteToString(byte: Long, precision: Int = 1): String {
var size = byte.toDouble()
var suffixIndex = 0
while (size >= 1024) {
size /= 1024
@@ -92,6 +95,7 @@ val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
"-group-" to { if (groups.isNotEmpty()) groups.joinToString() else "N/A" }
// TODO
)
/**
* Formats download folder name with given Metadata
*/
@@ -117,20 +121,20 @@ suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
runCatching {
imageUrlFromImage(galleryID, it, false)
}
.onFailure {
FirebaseCrashlytics.getInstance().recordException(it)
}
.getOrDefault("https://a/")
.onFailure {
FirebaseCrashlytics.getInstance().recordException(it)
}
.getOrDefault("https://a/")
)
.header("Referer", "https://hitomi.la/")
}
}
fun byteCount(codePoint: Int): Int = when (codePoint) {
in 0 ..< 0x80 -> 1
in 0x80 ..< 0x800 -> 2
in 0x800 ..< 0x10000 -> 3
in 0x10000 ..< 0x110000 -> 4
in 0..<0x80 -> 1
in 0x80..<0x800 -> 2
in 0x800..<0x10000 -> 3
in 0x10000..<0x110000 -> 4
else -> 0
}
@@ -168,7 +172,10 @@ val JsonElement.content
fun checkNotificationEnabled(context: Context) =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
fun showNotificationPermissionExplanationDialog(context: Context) {
AlertDialog.Builder(context)
@@ -182,12 +189,16 @@ fun requestNotificationPermission(
activity: Activity,
requestPermissionLauncher: ActivityResultLauncher<String>,
showRationale: Boolean = true,
ifGranted: () -> Unit,
ifGranted: () -> Unit = { },
) {
when {
checkNotificationEnabled(activity) -> ifGranted()
showRationale && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.POST_NOTIFICATIONS) ->
showRationale && ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.POST_NOTIFICATIONS
) ->
showNotificationPermissionExplanationDialog(activity)
else ->
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
@@ -18,8 +17,8 @@
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
@@ -32,44 +31,43 @@
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:layout_width="match_parent"
android:layout_height="match_parent"
app:addLastItemPadding="true"
app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true"
app:handleHeight="72dp"
app:handleVisibilityDuration="1000"
app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:trackMarginStart="64dp"
app:addLastItemPadding="true"
app:popupDrawable="@android:color/transparent">
app:popupDrawable="@android:color/transparent"
app:trackMarginStart="64dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="64dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
android:paddingTop="64dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</xyz.quaver.pupil.ui.view.MainView>
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle"
android:id="@+id/progressbar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
android:indeterminate="true" />
<TextView
android:id="@+id/noresult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/main_no_result"
android:linksClickable="true"
android:visibility="invisible"/>
android:text="@string/main_no_result"
android:visibility="invisible" />
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/fab"
@@ -84,28 +82,28 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
app:fab_size="mini" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/jump_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
app:fab_size="mini" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/random_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_random"
app:fab_size="mini"/>
app:fab_size="mini" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/id_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
app:fab_size="mini" />
</com.github.clans.fab.FloatingActionMenu>
@@ -113,15 +111,15 @@
android:id="@+id/searchview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:close_search_on_keyboard_dismiss="false"
app:dismissOnOutsideTouch="true"
app:leftActionMode="showHamburger"
app:menu="@menu/main"
app:searchBarMarginLeft="6dp"
app:searchBarMarginRight="6dp"
app:searchBarMarginTop="6dp"
app:searchHint="@string/search_hint"
app:suggestionAnimDuration="250"
app:showSearchKey="true"
app:leftActionMode="showHamburger"
app:menu="@menu/main"
app:dismissOnOutsideTouch="true"
app:close_search_on_keyboard_dismiss="false" />
app:suggestionAnimDuration="250" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
@@ -29,37 +28,37 @@
android:id="@+id/scroller"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:handleDrawable="@drawable/thumb"
app:handleHeight="72dp"
app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:handleHasFixedSize="true"
app:addLastItemPadding="true"
app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true"
app:handleHeight="72dp"
app:handleVisibilityDuration="1000"
app:handleWidth="24dp"
app:popupDrawable="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<include layout="@layout/reader_eye_card"
<include
android:id="@+id/eye_card"
android:visibility="gone"
android:layout_height="wrap_content"
layout="@layout/reader_eye_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"/>
android:layout_margin="8dp"
android:visibility="gone" />
<ProgressBar
android:id="@+id/download_progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="4dp"/>
android:layout_height="4dp" />
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/fab"
android:layout_width="wrap_content"
@@ -72,33 +71,33 @@
android:id="@+id/download_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_download"
app:fab_label="@string/reader_fab_download"
app:fab_size="mini"/>
app:fab_size="mini"
app:srcCompat="@drawable/ic_download" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/retry_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/refresh"
app:fab_label="@string/reader_fab_retry"
app:fab_size="mini"/>
app:fab_size="mini"
app:srcCompat="@drawable/refresh" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/auto_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/eye_white"
app:fab_label="@string/reader_fab_auto"
app:fab_size="mini"/>
app:fab_size="mini"
app:srcCompat="@drawable/eye_white" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fullscreen_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_fullscreen"
app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini"/>
app:fab_size="mini"
app:srcCompat="@drawable/ic_fullscreen" />
</com.github.clans.fab.FloatingActionMenu>

View File

@@ -26,11 +26,21 @@
app:showAsAction="ifRoom">
<menu>
<group android:checkableBehavior="single">
<item android:id="@+id/main_menu_sort_newest"
android:title="@string/main_menu_sort_newest"
<item android:id="@+id/main_menu_sort_date_added"
android:title="@string/main_menu_sort_date_added"
android:checked="true"/>
<item android:id="@+id/main_menu_sort_popular"
android:title="@string/main_menu_sort_popular"/>
<item android:id="@+id/main_menu_sort_date_published"
android:title="@string/main_menu_sort_date_published"/>
<item android:id="@+id/main_menu_sort_popular_today"
android:title="@string/main_menu_sort_popular_today"/>
<item android:id="@+id/main_menu_sort_popular_week"
android:title="@string/main_menu_sort_popular_week"/>
<item android:id="@+id/main_menu_sort_popular_month"
android:title="@string/main_menu_sort_popular_month"/>
<item android:id="@+id/main_menu_sort_popular_year"
android:title="@string/main_menu_sort_popular_year"/>
<item android:id="@+id/main_menu_sort_random"
android:title="@string/main_menu_sort_random"/>
</group>
</menu>
</item>

View File

@@ -1,165 +1,202 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="galleryblock_language">言語: %1$s</string>
<string name="galleryblock_series">シリーズ: %1$s</string>
<string name="galleryblock_type">タイプ: %1$s</string>
<string name="main_no_result">結果なし</string>
<string name="search_hint">ギャラリー検索</string>
<string name="settings_clear_cache">キャッシュクリア</string>
<string name="settings_clear_cache_alert_message">キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか?</string>
<string name="settings_storage_usage">%s使用中</string>
<string name="settings_storage_usage_loading">ストレージ使用量読み込み中…</string>
<string name="settings_default_query">デフォルトキーワード</string>
<string name="settings_galleries_per_page">一回にロードするギャラリー数</string>
<string name="settings_search_title">検索設定</string>
<string name="settings_title">設定</string>
<string name="update_notification_description">アップデートダウンロード中</string>
<string name="update_title">新しいアップデートがあります</string>
<string name="warning">注意</string>
<string name="settings_miscellaneous_title">その他</string>
<string name="settings_mirror_title">ミラーサーバー</string>
<string name="settings_clear_history">履歴を削除</string>
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
<string name="settings_clear_history_summary">履歴数: %1$d</string>
<string name="main_drawer_history">履歴</string>
<string name="notification_denied">通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。</string>
<string name="main_drawer_home">トップ</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_summary">アプリ履歴でアプリの画面を表示しない</string>
<string name="reader_go_to_page">移動</string>
<string name="default_query_dialog_language_selector_none">非選択</string>
<string name="default_query_dialog_filter_BL">BLフィルター</string>
<string name="default_query_dialog_filter_guro">グロフィルター</string>
<string name="default_query_dialog_language">"言語: "</string>
<string name="default_query_dialog_title">デフォルトキーワード設定</string>
<string name="main_drawer_group_contact_title">お問い合わせ先</string>
<string name="main_drawer_group_contact_homepage">ホームページ</string>
<string name="main_drawer_group_contact_help">ヘルプ</string>
<string name="main_drawer_group_contact_github">Github</string>
<string name="main_drawer_group_contact_email">メールを送る</string>
<string name="reader_fab_fullscreen">フルスクリーン</string>
<string name="error">エラー</string>
<string name="ignore">無視</string>
<string name="unlimited">制限なし</string>
<string name="copied_to_clipboard">クリップボードにコピーしました</string>
<string name="channel_download">ダウンロード</string>
<string name="channel_download_description">ダウンロードの進行を通知</string>
<string name="reader_fab_download">バックグラウンドダウンロー</string>
<string name="reader_notification_text">ダウンロード中…</string>
<string name="reader_notification_complete">ダウンロード完了</string>
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
<string name="channel_downloader">ダウンロー</string>
<string name="channel_downloader_description">ダウンローダの状態を表示</string>
<string name="channel_update">アップデート</string>
<string name="channel_update_description">アップデートの進行状況を表示</string>
<string name="channel_transfer">転送</string>
<string name="channel_transfer_description">他の機器へのデータ転送の進行状況を表示</string>
<string name="unable_to_connect">hitomi.laに接続できません</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再インストールしてください。</string>
<string name="main_no_result">結果なし</string>
<string name="unaccessible_download_folder">アンドロイド11以上では、現在のダウンロードフォルダに外部アプリからアクセスできません。ダウンロードフォルダを変更しますか</string>
<string name="notification_denied">通知を無効にすると、バックグラウンドでのダウンロードとアプリのアップデート機能が使用不可になります。</string>
<string name="main_drawer_home">トップ</string>
<string name="main_drawer_history">履歴</string>
<string name="main_drawer_downloads">ダウンロード</string>
<string name="main_drawer_favorite">ブックマーク</string>
<string name="main_drawer_group_contact_title">お問い合わせ先</string>
<string name="main_drawer_group_contact_help">ヘルプ</string>
<string name="main_drawer_group_contact_homepage">ホームページ</string>
<string name="main_drawer_group_contact_github">Github</string>
<string name="main_drawer_group_contact_email">メールを送る</string>
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="main_menu_thin">簡単モード</string>
<string name="main_menu_sort">並び替え</string>
<string name="main_menu_sort_date_added">新しい順</string>
<string name="main_menu_sort_date_published">新しい順(発売日)</string>
<string name="main_menu_sort_popular_today">人気順(日間)</string>
<string name="main_menu_sort_popular_week">人気順(週間)</string>
<string name="main_menu_sort_popular_month">人気順(月間)</string>
<string name="main_menu_sort_popular_year">人気順(年間)</string>
<string name="main_menu_sort_random">ランダム</string>
<string name="main_jump_title">ページ移動</string>
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
<string name="channel_transfer">転送</string>
<string name="unable_to_connect">hitomi.laに接続できません</string>
<string name="main_move_to_page">%1$dページへ移動</string>
<string name="settings_clear_downloads">ダウンロード削除</string>
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string>
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
<string name="main_drawer_favorite">ブックマーク</string>
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
<string name="main_open_gallery_by_id">IDで作品を開く</string>
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
<string name="settings_storage">ストレージ</string>
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="settings_app_lock">アプリロック</string>
<string name="settings_app_lock_type">アップロックの種類</string>
<string name="settings_app_version_title">バージョン(アップデート確認)</string>
<string name="settings_lock_biometrics">生体認識</string>
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
<string name="settings_lock_enabled">有効</string>
<string name="settings_lock_fingerprint">指紋</string>
<string name="settings_lock_password">パスワード</string>
<string name="settings_lock_pattern">パターン</string>
<string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string>
<string name="settings_lock_none">なし</string>
<string name="settings_lock_remove_message">ロックを無効にしますか?</string>
<string name="reader_loading">ロード中</string>
<string name="main_menu_sort">ソート</string>
<string name="main_menu_sort_newest">投稿日時順</string>
<string name="main_menu_sort_popular">人気順</string>
<string name="ignore">無視</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
<string name="settings_dark_mode_title">ダークモード</string>
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
<string name="gallery_details">ギャラリー情報</string>
<string name="gallery_artists">アーティスト</string>
<string name="gallery_characters">キャラクター</string>
<string name="main_fab_random">ランダムに作品を開く</string>
<string name="main_fab_cancel">すべてのダウンロードをキャンセル</string>
<string name="main_move_to_page">%1$dページへ移動</string>
<string name="main_download">ダウンロード</string>
<string name="main_delete">削除</string>
<string name="update_title">最新版あり</string>
<string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックして更新</string>
<string name="update_notification_description">最新版をダウンロード中&#8230;</string>
<string name="update_release_note"># 更新履歴(v%1$s)\n%2$s</string>
<string name="search_hint">作品を検索</string>
<string name="search_all">すべての作品を対象に検索</string>
<string name="search_show_histories">履歴を見る</string>
<string name="search_show_tags">お気に入りのタグを見る</string>
<string name="gallery_details">作品情報</string>
<string name="gallery_thumbnails">サムネイル</string>
<string name="gallery_related">おすすめ</string>
<string name="gallery_artists">作者</string>
<string name="gallery_groups">グループ</string>
<string name="gallery_language">言語</string>
<string name="gallery_series">シリーズ</string>
<string name="gallery_characters">キャラクター</string>
<string name="gallery_tags">タグ</string>
<string name="gallery_thumbnails">サムネイル</string>
<string name="gallery_related">おすすめ</string>
<string name="settings_nomedia_title">イメージを隠す</string>
<string name="main_delete">削除</string>
<string name="main_download">ダウンロード</string>
<string name="settings_backup_title">ブックマークバックアップ</string>
<string name="settings_restore_title">ブックマーク復元</string>
<string name="settings_backup_file_created">バックアップファイルを作成しました</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_success">%1$d項目を復元しました</string>
<string name="settings_download_folder">ダウンロード場所</string>
<string name="settings_download_folder_internal">内部ストレージ</string>
<string name="settings_download_folder_removable">外部SDカード</string>
<string name="settings_download_folder_available">%s 使用可能</string>
<string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
<string name="settings_beta">ベータチャンネルでアップデートを受信</string>
<string name="galleryblock_series">シリーズ: %1$s</string>
<string name="galleryblock_type">タイプ: %1$s</string>
<string name="galleryblock_language">言語: %1$s</string>
<!-- READER -->
<string name="reader_loading">読込中</string>
<string name="reader_go_to_page">移動</string>
<string name="reader_fab_fullscreen">全画面</string>
<string name="reader_fab_retry">再試行</string>
<string name="reader_fab_auto">まばたき検知スクロール</string>
<string name="reader_fab_auto_cancel">まばたき検知を中止</string>
<string name="reader_fab_download">バックグラウンドでダウンロード</string>
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
<string name="reader_notification_text">ダウンロード中…</string>
<string name="reader_notification_complete">ダウンロード完了</string>
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
<string name="no_camera">この機器には前面カメラが装着されていません</string>
<string name="downloader_running">ダウンローダー起動中</string>
<string name="settings_title">設定</string>
<string name="settings_app_version_title">バージョン(クリックで更新確認)</string>
<string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">低解像度イメージ</string>
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_beta">ベータ版チャンネルでアップデート</string>
<string name="settings_search_title">検索設定</string>
<string name="settings_galleries_per_page">一度に読み込む作品数</string>
<string name="settings_default_query">検索語句の初期値</string>
<string name="settings_storage">保存領域</string>
<string name="settings_manage_storage">保存領域の管理</string>
<string name="settings_storage_usage">%s使用中</string>
<string name="settings_storage_usage_loading">保存領域の使用量を算出中…</string>
<string name="settings_clear_cache">キャッシュを削除</string>
<string name="settings_clear_cache_alert_message">キャッシュを削除すると画像の読込に時間がかかります。実行しますか?</string>
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
<string name="settings_clear_downloads">ダウンロード済みを削除</string>
<string name="settings_clear_downloads_alert_message">ダウンロードした作品をすべて削除します。\n実行しますか</string>
<string name="settings_clear_history">履歴を削除</string>
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
<string name="settings_clear_history_summary">履歴数: %1$d</string>
<string name="settings_download_folder_name">フォルダ名パターン</string>
<string name="settings_invalid_download_folder_name">フォルダ名に使用できない文字が含まれています</string>
<string name="settings_download_folder_name_message">変数 %s は対応する値に置換されます\n\n%s</string>
<string name="settings_download_folder">ダウンロード場所</string>
<string name="settings_download_folder_removable">取り外し可能メディア</string>
<string name="settings_download_folder_internal">内部の保存領域</string>
<string name="settings_download_folder_available">%s 使用可能</string>
<string name="settings_download_folder_custom">手動で設定</string>
<string name="settings_download_folder_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_cache_limit">キャッシュサイズ制限</string>
<string name="settings_nomedia_title">画像を隠す</string>
<string name="settings_low_quality">低解像度の画像</string>
<string name="settings_low_quality_summary">読込速度とデータ使用料を改善するため低解像度の画像を読み込む</string>
<string name="settings_transfer_data">他の機器にデータを転送</string>
<string name="settings_app_lock">アプリをロック</string>
<string name="settings_app_lock_type">アップをロックする方法</string>
<string name="settings_networking">ネットワーク</string>
<string name="settings_mirror_summary">ミラーサーバから画像を読み込む</string>
<string name="settings_proxy_title">プロクシ</string>
<string name="settings_max_concurrent_download">並列ダウンロード</string>
<string name="settings_miscellaneous_title">その他</string>
<string name="settings_tag_translation">タグの言語</string>
<string name="settings_tag_translation_message">Githubにて翻訳に参加できます</string>
<string name="settings_rtl">綴じ方向を左にする</string>
<string name="settings_security_mode_title">セキュリティーモード</string>
<string name="settings_security_mode_summary">アプリ履歴でアプリの画面を表示しない</string>
<string name="settings_dark_mode_title">ダークモード</string>
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
<string name="settings_import_old_galleries">旧ギャラリーインポート</string>
<string name="settings_user_id">ユーザーID</string>
<string name="settings_oss">オープンソースライセンス</string>
<string name="settings_manage_favorites">ブックマーク管理</string>
<string name="settings_backup_title">ブックマークをバックアップ</string>
<string name="settings_backup_failed">エラーが発生しました</string>
<string name="settings_backup_share">バックアップ共有</string>
<string name="settings_backup_file_created">バックアップファイルを作成しました</string>
<string name="settings_restore_title">ブックマーク復元</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_success">%1$d項目を復元しました</string>
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
<string name="settings_lock_enabled">有効</string>
<string name="settings_lock_none">なし</string>
<string name="settings_lock_pattern">パターン</string>
<string name="settings_lock_password">パスワード</string>
<string name="settings_lock_biometrics">生体認証</string>
<string name="settings_lock_fingerprint">指紋</string>
<string name="settings_lock_fingerprint_without_lock">予備のロックが設定されていないと指紋ロックは使用できません</string>
<string name="settings_lock_fingerprint_prompt">Pupil 指紋ロック™</string>
<string name="settings_lock_remove_message">ロックを無効にしますか?</string>
<string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string>
<string name="default_query_dialog_title">検索語句の初期値を設定</string>
<string name="default_query_dialog_language">"言語: "</string>
<string name="default_query_dialog_filter_BL">BLフィルター</string>
<string name="default_query_dialog_filter_guro">グロフィルター</string>
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string>
<string name="default_query_dialog_language_selector_none">非選択</string>
<string name="settings_mirror_title">ミラーサーバー</string>
<string name="proxy_dialog_type">プロクシの種類</string>
<string name="proxy_dialog_addr_hint">サーバーアドレス</string>
<string name="proxy_dialog_port_hint">ポート番号</string>
<string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">プロクシタイプ</string>
<string name="proxy_dialog_port_hint">ポート</string>
<string name="proxy_dialog_password_hint">パスワード</string>
<string name="proxy_dialog_error">エラー</string>
<string name="proxy_dialog_addr_hint">サーバーアドレス</string>
<string name="proxy_dialog_server">サーバー</string>
<string name="main_menu_thin">簡単モード</string>
<string name="main_fab_cancel">すべてのダウンロードキャンセル</string>
<string name="channel_update">アップデート</string>
<string name="channel_update_description">アップデートの進行状態を表示</string>
<string name="settings_import_old_galleries">旧ギャラリーインポート</string>
<string name="import_old_galleries_folder_not_readable">フォルダを読めません</string>
<string name="import_old_galleries_notification">旧ギャラリーインポート中…</string>
<string name="import_old_galleries_notification_done">インポート完了</string>
<string name="main_fab_random">ランダムギャラリーを開く</string>
<string name="settings_lock_fingerprint_without_lock">予備のロックが設定されていないと指紋ロックは使用できません</string>
<string name="settings_lock_fingerprint_prompt">Pupil指紋ロック™</string>
<string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string>
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string>
<string name="settings_user_id">ユーザーID</string>
<string name="copied_to_clipboard">クリップボードにコピーしました</string>
<string name="reader_fab_retry">リトライ</string>
<string name="reader_fab_auto">まばたき検知スクロール</string>
<string name="search_all">全てのギャラリーを対象に検索</string>
<string name="settings_rtl">綴じ方向を左にする</string>
<string name="settings_manage_favorites">ブックマーク管理</string>
<string name="settings_backup_failed">エラーが発生しました</string>
<string name="settings_backup_share">バックアップ共有</string>
<string name="channel_downloader">ダウンローダ</string>
<string name="channel_downloader_description">ダウンローダの状態を表示</string>
<string name="downloader_running">ダウンローダー起動中</string>
<string name="settings_download_folder_name">フォルダ名パターン</string>
<string name="settings_invalid_download_folder_name">フォルダ名に使用できない文字が含まれています</string>
<string name="settings_download_folder_name_message">%sに含まれている文字列を対応する変数に置換します\n\n%s</string>
<string name="settings_manage_storage">ストレージ管理</string>
<string name="settings_oss">オープンソースライセンス</string>
<string name="search_show_tags">お気に入りのタグを見る</string>
<string name="search_show_histories">履歴を見る</string>
<string name="reader_fab_auto_cancel">まばたき検知を中止</string>
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
<string name="no_camera">この機器には前面カメラが装着されていません</string>
<string name="error">エラー</string>
<string name="settings_cache_limit">キャッシュサイズ制限</string>
<string name="unlimited">制限なし</string>
<string name="settings_tag_translation">タグ言語</string>
<string name="settings_tag_translation_message">Githubにて翻訳に参加できます</string>
<string name="settings_max_concurrent_download">並列ダウンロード</string>
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか</string>
<string name="settings_networking">ネットワーク</string>
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
<string name="settings_transfer_data">他の機器にデータを転送</string>
<string name="channel_transfer_description">他の機器へのデータ転送の進捗度を表示</string>
</resources>
</resources>

View File

@@ -45,6 +45,7 @@
<string name="reader_notification_complete">다운로드 완료</string>
<string name="reader_fab_download_cancel">백그라운드 다운로드 취소</string>
<string name="main_drawer_downloads">다운로드</string>
<string name="main_menu_sort_random">무작위</string>
<string name="main_jump_title">페이지 이동</string>
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
<string name="channel_transfer">전송</string>
@@ -71,8 +72,6 @@
<string name="settings_lock_remove_message">잠금을 해제할까요?</string>
<string name="reader_loading">로딩중</string>
<string name="main_menu_sort">정렬</string>
<string name="main_menu_sort_popular">인기순</string>
<string name="main_menu_sort_newest">시간순</string>
<string name="ignore">무시</string>
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
<string name="settings_dark_mode_title">다크 모드</string>
@@ -162,4 +161,10 @@
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
<string name="settings_transfer_data">다른 기기에 데이터 전송</string>
<string name="channel_transfer_description">다른 기기에 데이터 전송 시 상태 표시</string>
<string name="main_menu_sort_date_added">추가일</string>
<string name="main_menu_sort_date_published">발매일</string>
<string name="main_menu_sort_popular_today">인기순 (오늘)</string>
<string name="main_menu_sort_popular_week">인기순 (이번 주)</string>
<string name="main_menu_sort_popular_month">인기순 (이번 달)</string>
<string name="main_menu_sort_popular_year">인기순 (이번 해)</string>
</resources>

View File

@@ -0,0 +1,20 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
<style name="NoActionBarAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>

View File

@@ -70,8 +70,13 @@
<string name="main_menu_thin">Thin Mode</string>
<string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string>
<string name="main_menu_sort_popular">Popular</string>
<string name="main_menu_sort_date_added">Date Added</string>
<string name="main_menu_sort_date_published">Date Published</string>
<string name="main_menu_sort_popular_today">Popular: Today</string>
<string name="main_menu_sort_popular_week">Popular: Week</string>
<string name="main_menu_sort_popular_month">Popular: Month</string>
<string name="main_menu_sort_popular_year">Popular: Year</string>
<string name="main_menu_sort_random">Random</string>
<string name="main_jump_title">Jump to page</string>
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>

View File

@@ -1,35 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.gms:google-services:4.3.15"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9"
classpath "com.google.firebase:perf-plugin:1.4.2"
classpath "com.google.android.gms:oss-licenses-plugin:0.10.6"
}
}
allprojects {
repositories {
google()
mavenCentral()
jcenter()
maven { url "https://jitpack.io" }
maven { url "https://guardian.github.io/maven/repo-releases/" }
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

11
build.gradle.kts Normal file
View File

@@ -0,0 +1,11 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlinx.serialization) apply false
alias(libs.plugins.gms.oss.licenses) apply false
alias(libs.plugins.gms.google.services) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false
}

View File

@@ -20,4 +20,6 @@ kotlin.code.style=official
android.enableJetifier=true
android.useAndroidX=true
kotlin_version=1.9.0
kotlin_version=2.1.10
android.nonTransitiveRClass=false
android.nonFinalResIds=false

130
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,130 @@
[versions]
agp = "8.8.1"
kotlin = "2.0.0"
activityKtx = "1.10.0"
appcompat = "1.7.0"
bigimageviewer = "1.8.1"
biometric = "1.1.0"
constraintlayout = "2.2.0"
core = "3.1.0"
coreKtx = "1.15.0"
documentfilex = "0.7.2"
espressoCore = "3.6.1"
fab = "1.6.4"
firebaseBom = "33.9.0"
firebaseCrashlyticsGradle = "3.0.3"
floatingsearchview = "1.1.7"
fragmentKtx = "1.8.5"
googleServices = "4.4.2"
gradle = "8.8.0"
gridlayout = "1.0.0"
imagepipelineOkhttp3 = "2.6.0"
jsoup = "1.18.3"
junit = "4.13.2"
junitVersion = "1.2.1"
kotlinAndroidExtensions = "2.1.10"
kotlinGradlePlugin = "2.1.10"
kotlinSerialization = "2.1.10"
kotlinxCoroutinesAndroid = "1.8.0"
kotlinxCoroutinesTest = "1.6.1"
kotlinxDatetime = "0.4.0"
kotlinxSerializationJson = "1.5.1"
ktorNetwork = "2.3.10"
library = "1.2.0"
libraryVersion = "3.2"
material = "1.12.0"
okhttp = "3.12.12"
ossLicensesPlugin = "0.10.6"
patternlockview = "1.0.0"
perfPlugin = "1.4.2"
playServicesMlkitFaceDetection = "17.1.0"
playServicesOssLicenses = "17.1.0"
preferenceKtx = "1.2.1"
recyclerview = "1.4.0"
rules = "1.6.1"
runner = "1.6.2"
skyfishjyLibrary = "1.0.1"
dotsindicator = "5.1.0"
workRuntimeKtx = "2.10.0"
material3WindowSizeClassAndroid = "1.3.1"
[libraries]
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
bigimageviewer = { module = "com.github.piasy:BigImageViewer", version.ref = "bigimageviewer" }
biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
core = { module = "ru.noties.markwon:core", version.ref = "core" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
dirchooser-library = { module = "net.rdrei.android.dirchooser:library", version.ref = "libraryVersion" }
documentfilex = { module = "xyz.quaver:documentfilex", version.ref = "documentfilex" }
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
fab = { module = "com.github.clans:fab", version.ref = "fab" }
firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsGradle" }
firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx" }
firebase-perf-ktx = { module = "com.google.firebase:firebase-perf-ktx" }
floatingsearchview = { module = "xyz.quaver:floatingsearchview", version.ref = "floatingsearchview" }
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
frescoimageloader = { module = "com.github.piasy:FrescoImageLoader", version.ref = "bigimageviewer" }
frescoimageviewfactory = { module = "com.github.piasy:FrescoImageViewFactory", version.ref = "bigimageviewer" }
google-services = { module = "com.google.gms:google-services", version.ref = "googleServices" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
gridlayout = { module = "androidx.gridlayout:gridlayout", version.ref = "gridlayout" }
imagepipeline-okhttp3 = { module = "com.facebook.fresco:imagepipeline-okhttp3", version.ref = "imagepipelineOkhttp3" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { module = "junit:junit", version.ref = "junit" }
kotlin-android-extensions = { module = "org.jetbrains.kotlin:kotlin-android-extensions", version.ref = "kotlinAndroidExtensions" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinGradlePlugin" }
kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinSerialization" }
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktorNetwork" }
library = { module = "com.daimajia.swipelayout:library", version.ref = "library" }
material = { module = "com.google.android.material:material", version.ref = "material" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
oss-licenses-plugin = { module = "com.google.android.gms:oss-licenses-plugin", version.ref = "ossLicensesPlugin" }
patternlockview = { module = "com.github.aritraroy:PatternLockView", version = "master-SNAPSHOT" }
perf-plugin = { module = "com.google.firebase:perf-plugin", version.ref = "perfPlugin" }
play-services-mlkit-face-detection = { module = "com.google.android.gms:play-services-mlkit-face-detection", version.ref = "playServicesMlkitFaceDetection" }
play-services-oss-licenses = { module = "com.google.android.gms:play-services-oss-licenses", version.ref = "playServicesOssLicenses" }
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
ripplebackground-library = { module = "com.skyfishjy.ripplebackground:library", version.ref = "skyfishjyLibrary" }
rules = { module = "androidx.test:rules", version.ref = "rules" }
runner = { module = "androidx.test:runner", version.ref = "runner" }
dotsindicator = { module = "com.tbuonomo:dotsindicator", version.ref = "dotsindicator" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
recyclerview-fastscroller = { module = "com.quiph.ui:recyclerviewfastscroller", version = "1.0.0" }
pinlockview = { module = "com.github.aritraroy:pinlockview", version = "2.1.0" }
compose-bom = { module = "androidx.compose:compose-bom", version = "2025.02.00" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-ui = { module = "androidx.compose.ui:ui" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
compose-icons = { module = "androidx.compose.material:material-icons-extended" }
compose-adaptive = { module = "androidx.compose.material3.adaptive:adaptive" }
compose-activity = { module = "androidx.activity:activity-compose", version = "1.10.1" }
compose-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version = "2.8.7" }
compose-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
compose-navigation = { module = "androidx.navigation:navigation-compose", version = "2.8.8" }
accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version = "0.37.2" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
gms-oss-licenses = { id = "com.google.android.gms.oss-licenses-plugin", version.ref = "ossLicensesPlugin" }
gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsGradle" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "perfPlugin" }

Binary file not shown.

View File

@@ -1,6 +1,7 @@
#Tue Oct 13 22:37:11 KST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

297
gradlew vendored Normal file → Executable file
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,80 +15,116 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -97,87 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

194
gradlew.bat vendored
View File

@@ -1,100 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1 +0,0 @@
include ':app'

32
settings.gradle.kts Normal file
View File

@@ -0,0 +1,32 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.google.android.gms.oss-licenses-plugin") {
useModule("com.google.android.gms:oss-licenses-plugin:${requested.version}")
}
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
rootProject.name = "Pupil"
include(":app")