mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-21 23:13:56 +00:00
Merge branch 'dev' into balista
This commit is contained in:
37
.github/workflows/beta.yml
vendored
37
.github/workflows/beta.yml
vendored
@@ -75,18 +75,21 @@ jobs:
|
||||
|
||||
- name: List files in the directory
|
||||
run: ls -l
|
||||
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
- name: Upload Build Artifacts
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: Dantotsu
|
||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||
name: APKs
|
||||
path: |
|
||||
app/build/outputs/apk/google/alpha/*/*.apk
|
||||
app/build/outputs/apk/google/alpha/*/*/*.apk
|
||||
app/build/outputs/apk/google/alpha/*/*/*/*.apk
|
||||
|
||||
- name: Upload APK to Discord and Telegram
|
||||
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
|
||||
@@ -99,14 +102,34 @@ jobs:
|
||||
if [ ${#commit_messages} -gt $max_length ]; then
|
||||
commit_messages="${commit_messages:0:$max_length}... (truncated)"
|
||||
fi
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
#Telegram
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \
|
||||
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk" \
|
||||
-F "caption=armeabi-v7a" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk" \
|
||||
-F "caption=arm64-v8a" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk" \
|
||||
-F "caption=x86" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk" \
|
||||
-F "caption=x86_64" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
|
||||
env:
|
||||
COMMIT_LOG: ${{ env.COMMIT_LOG }}
|
||||
|
||||
@@ -6,6 +6,10 @@ plugins {
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
def gitCommitHash = providers.exec {
|
||||
commandLine("git", "rev-parse", "--verify", "--short", "HEAD")
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
|
||||
@@ -17,6 +21,14 @@ android {
|
||||
versionName "3.0.0"
|
||||
versionCode 300000000
|
||||
signingConfig signingConfigs.debug
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "store"
|
||||
@@ -38,7 +50,7 @@ android {
|
||||
buildTypes {
|
||||
alpha {
|
||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||
versionNameSuffix "-alpha01"
|
||||
versionNameSuffix "-alpha01-" + gitCommitHash
|
||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
|
||||
debuggable System.getenv("CI") == null
|
||||
@@ -95,6 +107,7 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.10.0'
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
@@ -145,6 +158,9 @@ dependencies {
|
||||
// String Matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
||||
implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS'
|
||||
//implementation 'com.github.yausername.youtubedl-android:library:0.15.0'
|
||||
|
||||
// Aniyomi
|
||||
implementation 'io.reactivex:rxjava:1.3.8'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
|
||||
@@ -73,12 +73,24 @@
|
||||
android:resource="@xml/upcoming_widget_info" />
|
||||
</receiver>
|
||||
<activity
|
||||
android:name=".widgets.upcoming.UpcomingWidgetConfigureActivity"
|
||||
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.statistics.ProfileStatsWidget"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/statistics_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
|
||||
|
||||
<activity
|
||||
@@ -115,6 +127,14 @@
|
||||
android:name=".settings.ExtensionsActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity
|
||||
android:name=".widgets.statistics.ProfileStatsConfigure"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
@@ -350,16 +370,6 @@
|
||||
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".download.video.ExoplayerDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -92,6 +92,7 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||
import ani.dantotsu.connections.anilist.Genre
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.bakaupdates.MangaUpdates
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||
import ani.dantotsu.media.Media
|
||||
@@ -102,6 +103,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.util.CountUpTimer
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
@@ -134,9 +136,12 @@ import io.noties.markwon.html.TagHandlerNoOp
|
||||
import io.noties.markwon.image.AsyncDrawable
|
||||
import io.noties.markwon.image.glide.GlideImagesPlugin
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -214,7 +219,8 @@ fun initActivity(a: Activity) {
|
||||
window,
|
||||
window.decorView
|
||||
).hide(WindowInsetsCompat.Type.statusBars())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
|
||||
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
window.decorView.rootWindowInsets?.displayCutout?.apply {
|
||||
if (boundingRects.size > 0) {
|
||||
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
|
||||
@@ -619,9 +625,14 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
if (file.url.startsWith("content://")) {
|
||||
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
||||
.override(size).into(this)
|
||||
} else {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,31 +887,6 @@ fun savePrefs(
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val requiredPermissions = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||
}.toTypedArray()
|
||||
|
||||
return if (requiredPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
requiredPermissions,
|
||||
DOWNLOADS_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
|
||||
|
||||
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
@@ -1004,6 +990,54 @@ fun countDown(media: Media, view: ViewGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
fun sinceWhen(media: Media, view: ViewGroup) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
MangaUpdates().search(media.name ?: media.nameRomaji, media.startDate)?.let {
|
||||
val latestChapter = it.metadata.series.latestChapter ?: it.record.chapter?.let { chapter ->
|
||||
if (chapter.contains("-"))
|
||||
chapter.split("-")[1].trim()
|
||||
else
|
||||
chapter
|
||||
}?.toInt()
|
||||
val timeSince = (System.currentTimeMillis() -
|
||||
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val v =
|
||||
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
|
||||
|
||||
object : CountUpTimer(86400000) {
|
||||
override fun onTick(second: Int) {
|
||||
val a = second + timeSince
|
||||
v.mediaCountdown.text = currActivity()?.getString(
|
||||
R.string.time_format,
|
||||
a / 86400,
|
||||
a % 86400 / 3600,
|
||||
a % 86400 % 3600 / 60,
|
||||
a % 86400 % 3600 % 60
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
// The legend will never die.
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun displayTimer(media: Media, view: ViewGroup) {
|
||||
when {
|
||||
media.anime != null -> countDown(media, view)
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
|
||||
else -> { } // No timer yet
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
|
||||
this.forEach {
|
||||
if (it.value.id == id) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package ani.dantotsu
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Animatable
|
||||
@@ -18,8 +19,8 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -54,6 +55,7 @@ import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.FeedActivity
|
||||
import ani.dantotsu.profile.activity.NotificationActivity
|
||||
import ani.dantotsu.settings.ExtensionsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
@@ -228,17 +230,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val preferences: SourcePreferences = Injekt.get()
|
||||
if (preferences.animeExtensionUpdatesCount()
|
||||
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
||||
) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"You have extension updates available!",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
binding.root.isMotionEventSplittingEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -282,6 +273,16 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
val preferences: SourcePreferences = Injekt.get()
|
||||
if (preferences.animeExtensionUpdatesCount()
|
||||
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
||||
) {
|
||||
snackString(R.string.extension_updates_available)
|
||||
?.setDuration(Snackbar.LENGTH_LONG)
|
||||
?.setAction(R.string.review) {
|
||||
startActivity(Intent(this, ExtensionsActivity::class.java))
|
||||
}
|
||||
}
|
||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
||||
selectedOption = if (fragment != null) {
|
||||
when (fragment) {
|
||||
@@ -448,7 +449,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
|
||||
/*lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
@@ -457,7 +458,7 @@ class MainActivity : AppCompatActivity() {
|
||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/ //TODO: remove this
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
@@ -482,7 +483,7 @@ class MainActivity : AppCompatActivity() {
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
||||
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.nicehttp.addGenericDns
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -40,7 +41,7 @@ fun initializeNetwork() {
|
||||
|
||||
defaultHeaders = mapOf(
|
||||
"User-Agent" to
|
||||
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||
defaultUserAgentProvider()
|
||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.util.Log
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -40,20 +41,54 @@ object Anilist {
|
||||
"SCORE_DESC",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING_DESC",
|
||||
"START_DATE_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"SCORE"
|
||||
)
|
||||
|
||||
val source = listOf(
|
||||
"ORIGINAL",
|
||||
"MANGA",
|
||||
"LIGHT NOVEL",
|
||||
"VISUAL NOVEL",
|
||||
"VIDEO GAME",
|
||||
"OTHER",
|
||||
"NOVEL",
|
||||
"DOUJINSHI",
|
||||
"ANIME",
|
||||
"WEB NOVEL",
|
||||
"LIVE ACTION",
|
||||
"GAME",
|
||||
"COMIC",
|
||||
"MULTIMEDIA PROJECT",
|
||||
"PICTURE BOOK"
|
||||
)
|
||||
|
||||
val animeStatus = listOf(
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT YET RELEASED",
|
||||
"CANCELLED"
|
||||
)
|
||||
|
||||
val mangaStatus = listOf(
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT YET RELEASED",
|
||||
"HIATUS",
|
||||
"CANCELLED"
|
||||
)
|
||||
|
||||
val seasons = listOf(
|
||||
"WINTER", "SPRING", "SUMMER", "FALL"
|
||||
)
|
||||
|
||||
val anime_formats = listOf(
|
||||
val animeFormats = listOf(
|
||||
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
||||
)
|
||||
|
||||
val manga_formats = listOf(
|
||||
val mangaFormats = listOf(
|
||||
"MANGA", "NOVEL", "ONE SHOT"
|
||||
)
|
||||
|
||||
@@ -117,6 +152,9 @@ object Anilist {
|
||||
episodesWatched = null
|
||||
chapterRead = null
|
||||
PrefManager.removeVal(PrefName.AnilistToken)
|
||||
//logout from comments api
|
||||
CommentsAPI.logout()
|
||||
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> executeQuery(
|
||||
|
||||
@@ -72,7 +72,7 @@ class AnilistQueries {
|
||||
media.cameFromContinue = false
|
||||
|
||||
val query =
|
||||
"""{Media(id:${media.id}){id favourites popularity mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
|
||||
"""{Media(id:${media.id}){id favourites popularity mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
|
||||
runBlocking {
|
||||
val anilist = async {
|
||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
||||
@@ -139,7 +139,15 @@ class AnilistQueries {
|
||||
?: "SUPPORTING"
|
||||
|
||||
else -> i.role.toString()
|
||||
}
|
||||
},
|
||||
voiceActor = i.voiceActors?.map {
|
||||
Author(
|
||||
id = it.id,
|
||||
name = it.name?.userPreferred,
|
||||
image = it.image?.medium,
|
||||
role = it.languageV2
|
||||
)
|
||||
} as ArrayList<Author>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -885,18 +893,23 @@ class AnilistQueries {
|
||||
sort: String? = null,
|
||||
genres: MutableList<String>? = null,
|
||||
tags: MutableList<String>? = null,
|
||||
status: String? = null,
|
||||
source: String? = null,
|
||||
format: String? = null,
|
||||
countryOfOrigin: String? = null,
|
||||
isAdult: Boolean = false,
|
||||
onList: Boolean? = null,
|
||||
excludedGenres: MutableList<String>? = null,
|
||||
excludedTags: MutableList<String>? = null,
|
||||
startYear: Int? = null,
|
||||
seasonYear: Int? = null,
|
||||
season: String? = null,
|
||||
id: Int? = null,
|
||||
hd: Boolean = false,
|
||||
adultOnly: Boolean = false
|
||||
): SearchResults? {
|
||||
val query = """
|
||||
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
|
||||
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
|
||||
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
||||
pageInfo {
|
||||
total
|
||||
@@ -941,14 +954,19 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
}
|
||||
""".replace("\n", " ").replace(""" """, "")
|
||||
val variables = """{"type":"$type","isAdult":$isAdult
|
||||
${if (adultOnly) ""","isAdult":true""" else ""}
|
||||
${if (onList != null) ""","onList":$onList""" else ""}
|
||||
${if (page != null) ""","page":"$page"""" else ""}
|
||||
${if (id != null) ""","id":"$id"""" else ""}
|
||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||
${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
|
||||
${if (season != null) ""","season":"$season"""" else ""}
|
||||
${if (search != null) ""","search":"$search"""" else ""}
|
||||
${if (source != null) ""","source":"$source"""" else ""}
|
||||
${if (sort != null) ""","sort":"$sort"""" else ""}
|
||||
${if (status != null) ""","status":"$status"""" else ""}
|
||||
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
||||
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
|
||||
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedGenres?.isNotEmpty() == true)
|
||||
@@ -980,7 +998,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
else ""
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
|
||||
if (response?.media != null) {
|
||||
val responseArray = arrayListOf<Media>()
|
||||
@@ -1012,7 +1029,11 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
excludedGenres = excludedGenres,
|
||||
tags = tags,
|
||||
excludedTags = excludedTags,
|
||||
status = status,
|
||||
source = source,
|
||||
format = format,
|
||||
countryOfOrigin = countryOfOrigin,
|
||||
startYear = startYear,
|
||||
seasonYear = seasonYear,
|
||||
season = season,
|
||||
results = responseArray,
|
||||
@@ -1022,9 +1043,60 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
}
|
||||
return null
|
||||
}
|
||||
private val onListAnime = (if(PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace("\"", "")
|
||||
private val isAdult = (if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
|
||||
private fun recentAnimeUpdates(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
|
||||
}
|
||||
private fun trendingMovies(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
private fun topRatedAnime(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
private fun mostFavAnime(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
suspend fun loadAnimeList(): Query.AnimeList?{
|
||||
return executeQuery<Query.AnimeList>(
|
||||
"""{
|
||||
recentUpdates:${recentAnimeUpdates()}
|
||||
trendingMovies:${trendingMovies()}
|
||||
topRated:${topRatedAnime()}
|
||||
mostFav:${mostFavAnime()}
|
||||
}""".trimIndent(), force = true
|
||||
)
|
||||
|
||||
}
|
||||
private val onListManga = (if(PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace("\"", "")
|
||||
private fun trendingManga(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
private fun trendingManhwa(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
private fun trendingNovel(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
private fun topRatedManga(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
private fun mostFavManga(): String{
|
||||
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
suspend fun loadMangaList(): Query.MangaList?{
|
||||
return executeQuery<Query.MangaList>(
|
||||
"""{
|
||||
trendingManga:${trendingManga()}
|
||||
trendingManhwa:${trendingManhwa()}
|
||||
trendingNovel:${trendingNovel()}
|
||||
topRated:${topRatedManga()}
|
||||
mostFav:${mostFavManga()}
|
||||
}""".trimIndent(), force = true
|
||||
)
|
||||
|
||||
}
|
||||
suspend fun recentlyUpdated(
|
||||
smaller: Boolean = true,
|
||||
greater: Long = 0,
|
||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||
): MutableList<Media>? {
|
||||
@@ -1074,21 +1146,6 @@ Page(page:$page,perPage:50) {
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||
}
|
||||
if (smaller) {
|
||||
val response = execute()?.airingSchedules ?: return null
|
||||
val idArr = mutableListOf<Int>()
|
||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||
return response.mapNotNull { i ->
|
||||
i.media?.let {
|
||||
if (!idArr.contains(it.id))
|
||||
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
} else null
|
||||
else null
|
||||
}
|
||||
}.toMutableList()
|
||||
} else {
|
||||
var i = 1
|
||||
val list = mutableListOf<Media>()
|
||||
var res: Page? = null
|
||||
@@ -1108,7 +1165,6 @@ Page(page:$page,perPage:50) {
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCharacterDetails(character: Character): Character {
|
||||
@@ -1331,7 +1387,52 @@ Page(page:$page,perPage:50) {
|
||||
author.yearMedia = yearMedia
|
||||
return author
|
||||
}
|
||||
suspend fun getVoiceActorsDetails(author: Author): Author {
|
||||
fun query(page: Int = 0) = """ {
|
||||
Staff(id:${author.id}) {
|
||||
id
|
||||
characters(page: $page,sort:FAVOURITES_DESC) {
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
}
|
||||
nodes{
|
||||
id
|
||||
name {
|
||||
first
|
||||
middle
|
||||
last
|
||||
full
|
||||
native
|
||||
userPreferred
|
||||
}
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
val characters = arrayListOf<Character>()
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
hasNextPage = executeQuery<Query.Author>(
|
||||
query(page),
|
||||
force = true
|
||||
)?.data?.author?.characters?.let {
|
||||
it.nodes?.forEach { i ->
|
||||
characters.add(Character(i.id, i.name?.userPreferred, i.image?.large, i.image?.medium, "", false))
|
||||
}
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: false
|
||||
}
|
||||
author.character = characters
|
||||
return author
|
||||
}
|
||||
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
|
||||
return executeQuery<Query.ToggleFollow>(
|
||||
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
|
||||
@@ -1394,15 +1495,10 @@ Page(page:$page,perPage:50) {
|
||||
"""{
|
||||
favoriteAnime:${userFavMediaQuery(true, 1, id)}
|
||||
favoriteManga:${userFavMediaQuery(false, 1, id)}
|
||||
animeMediaList:${bannerImageQuery("ANIME", id)}
|
||||
mangaMediaList:${bannerImageQuery("MANGA", id)}
|
||||
}""".trimIndent(), force = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun bannerImageQuery(type: String, id: Int?): String {
|
||||
return """MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } }"""
|
||||
}
|
||||
|
||||
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
|
||||
val reset = if (resetNotification) "true" else "false"
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.webkit.internal.ApiFeature.P
|
||||
import androidx.webkit.internal.StartupApiFeature
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
@@ -58,45 +60,36 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
||||
|
||||
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||
suspend fun setAnimePlanned() =
|
||||
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||
|
||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
||||
|
||||
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||
suspend fun setMangaPlanned() =
|
||||
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||
|
||||
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
@@ -144,18 +137,15 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
sort = Anilist.sortBy[2],
|
||||
season = season,
|
||||
seasonYear = year,
|
||||
hd = true
|
||||
hd = true,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)?.results
|
||||
)
|
||||
}
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
||||
|
||||
private val animePopular = MutableLiveData<SearchResults?>(null)
|
||||
|
||||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
@@ -170,7 +160,8 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
genres = genres,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -185,13 +176,60 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList
|
||||
r.onList,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
|
||||
)
|
||||
)
|
||||
|
||||
var loaded: Boolean = false
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||
|
||||
private val popularMovies: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
|
||||
|
||||
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
|
||||
|
||||
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
|
||||
suspend fun loadAll() {
|
||||
val res = Anilist.query.loadAnimeList()?.data
|
||||
|
||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
|
||||
res?.apply{
|
||||
val idArr = mutableListOf<Int>()
|
||||
updated.postValue(recentUpdates?.airingSchedules?.mapNotNull {i ->
|
||||
i.media?.let {
|
||||
if (!idArr.contains(it.id))
|
||||
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
}else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)){
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
}else if ((listOnly && it.mediaListEntry != null)) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
}else null
|
||||
else null
|
||||
}
|
||||
}?.toMutableList() ?: arrayListOf())
|
||||
popularMovies.postValue(trendingMovies?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
topRatedAnime.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
mostFavAnime.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnilistMangaViewModel : ViewModel() {
|
||||
@@ -209,23 +247,11 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
hd = true
|
||||
hd = true,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)?.results
|
||||
)
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadTrendingNovel() =
|
||||
updated.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
format = "NOVEL"
|
||||
)?.results
|
||||
)
|
||||
|
||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
@@ -242,7 +268,8 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
genres = genres,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -257,17 +284,53 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
r.season,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
|
||||
var loaded: Boolean = false
|
||||
|
||||
private val popularManga: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
|
||||
|
||||
private val popularManhwa: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
|
||||
|
||||
private val popularNovel: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
|
||||
|
||||
private val topRatedManga: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
|
||||
|
||||
private val mostFavManga: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
|
||||
suspend fun loadAll() {
|
||||
val response = Anilist.query.loadMangaList()?.data
|
||||
|
||||
response?.apply {
|
||||
popularManga.postValue(trendingManga?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
popularManhwa.postValue(trendingManhwa?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
popularNovel.postValue(trendingNovel?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
topRatedManga.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
mostFavManga.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnilistSearch : ViewModel() {
|
||||
@@ -286,13 +349,17 @@ class AnilistSearch : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
r.season,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -305,11 +372,15 @@ class AnilistSearch : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
)
|
||||
@@ -347,11 +418,6 @@ class ProfileViewModel : ViewModel() {
|
||||
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
|
||||
private val listImages: MutableLiveData<ArrayList<String?>> =
|
||||
MutableLiveData<ArrayList<String?>>(arrayListOf())
|
||||
|
||||
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
||||
|
||||
suspend fun setData(id: Int) {
|
||||
val res = Anilist.query.initProfilePage(id)
|
||||
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
|
||||
@@ -367,30 +433,11 @@ class ProfileViewModel : ViewModel() {
|
||||
}
|
||||
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
|
||||
|
||||
val bannerImages = arrayListOf<String?>(null, null)
|
||||
val animeRandom = res?.data?.animeMediaList?.lists?.mapNotNull {
|
||||
it.entries?.mapNotNull { entry ->
|
||||
val imageUrl = entry.media?.bannerImage
|
||||
if (imageUrl != null && imageUrl != "null") imageUrl
|
||||
else null
|
||||
}
|
||||
}?.flatten()?.randomOrNull()
|
||||
bannerImages[0] = animeRandom
|
||||
val mangaRandom = res?.data?.mangaMediaList?.lists?.mapNotNull {
|
||||
it.entries?.mapNotNull { entry ->
|
||||
val imageUrl = entry.media?.bannerImage
|
||||
if (imageUrl != null && imageUrl != "null") imageUrl
|
||||
else null
|
||||
}
|
||||
}?.flatten()?.randomOrNull()
|
||||
bannerImages[1] = mangaRandom
|
||||
listImages.postValue(bannerImages)
|
||||
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
mangaFav.postValue(mangaFav.value)
|
||||
animeFav.postValue(animeFav.value)
|
||||
listImages.postValue(listImages.value)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,17 @@ data class SearchResults(
|
||||
var onList: Boolean? = null,
|
||||
var perPage: Int? = null,
|
||||
var search: String? = null,
|
||||
var countryOfOrigin :String? = null,
|
||||
var sort: String? = null,
|
||||
var genres: MutableList<String>? = null,
|
||||
var excludedGenres: MutableList<String>? = null,
|
||||
var tags: MutableList<String>? = null,
|
||||
var excludedTags: MutableList<String>? = null,
|
||||
var status: String? = null,
|
||||
var source: String? = null,
|
||||
var format: String? = null,
|
||||
var seasonYear: Int? = null,
|
||||
var startYear: Int? = null,
|
||||
var season: String? = null,
|
||||
var page: Int = 1,
|
||||
var results: MutableList<Media>,
|
||||
@@ -37,12 +41,24 @@ data class SearchResults(
|
||||
)
|
||||
)
|
||||
}
|
||||
status?.let {
|
||||
list.add(SearchChip("STATUS", currContext()!!.getString(R.string.filter_status, it)))
|
||||
}
|
||||
source?.let {
|
||||
list.add(SearchChip("SOURCE", currContext()!!.getString(R.string.filter_source, it)))
|
||||
}
|
||||
format?.let {
|
||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||
}
|
||||
countryOfOrigin?.let {
|
||||
list.add(SearchChip("COUNTRY", currContext()!!.getString(R.string.filter_country, it)))
|
||||
}
|
||||
season?.let {
|
||||
list.add(SearchChip("SEASON", it))
|
||||
}
|
||||
startYear?.let {
|
||||
list.add(SearchChip("START_YEAR", it.toString()))
|
||||
}
|
||||
seasonYear?.let {
|
||||
list.add(SearchChip("SEASON_YEAR", it.toString()))
|
||||
}
|
||||
@@ -74,8 +90,12 @@ data class SearchResults(
|
||||
fun removeChip(chip: SearchChip) {
|
||||
when (chip.type) {
|
||||
"SORT" -> sort = null
|
||||
"STATUS" -> status = null
|
||||
"SOURCE" -> source = null
|
||||
"FORMAT" -> format = null
|
||||
"COUNTRY" -> countryOfOrigin = null
|
||||
"SEASON" -> season = null
|
||||
"START_YEAR" -> startYear = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||
|
||||
@@ -55,7 +55,7 @@ data class CharacterConnection(
|
||||
@SerialName("nodes") var nodes: List<Character>?,
|
||||
|
||||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -72,7 +72,7 @@ data class CharacterEdge(
|
||||
@SerialName("name") var name: String?,
|
||||
|
||||
// The voice actors of the character
|
||||
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
@SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
|
||||
// The voice actors of the character with role date
|
||||
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
||||
|
||||
@@ -147,12 +147,35 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("animeMediaList") val animeMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("mangaMediaList") val mangaMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?
|
||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?)
|
||||
}
|
||||
@Serializable
|
||||
data class AnimeList(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||
)
|
||||
}
|
||||
@Serializable
|
||||
data class MangaList(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ToggleFollow(
|
||||
@SerialName("data")
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package ani.dantotsu.connections.bakaupdates
|
||||
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okio.ByteString.Companion.encode
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
class MangaUpdates {
|
||||
|
||||
private val Int?.dateFormat get() = String.format("%02d", this)
|
||||
|
||||
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
|
||||
|
||||
suspend fun search(title: String, startDate: FuzzyDate?) : MangaUpdatesResponse.Results? {
|
||||
return tryWithSuspend {
|
||||
val query = JSONObject().apply {
|
||||
try {
|
||||
put("search", title.encode(Charset.forName("UTF-8")))
|
||||
startDate?.let {
|
||||
put(
|
||||
"start_date",
|
||||
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
|
||||
)
|
||||
}
|
||||
put("include_metadata", true)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
|
||||
res.results?.forEach{ println("MangaUpdates: $it") }
|
||||
res.results?.first { it.metadata.series.lastUpdated?.timestamp != null }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MangaUpdatesResponse(
|
||||
@SerialName("total_hits")
|
||||
val totalHits: Int?,
|
||||
@SerialName("page")
|
||||
val page: Int?,
|
||||
@SerialName("per_page")
|
||||
val perPage: Int?,
|
||||
val results: List<Results>? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Results(
|
||||
val record: Record,
|
||||
val metadata: MetaData
|
||||
) {
|
||||
@Serializable
|
||||
data class Record(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("title")
|
||||
val title: String,
|
||||
@SerialName("volume")
|
||||
val volume: String?,
|
||||
@SerialName("chapter")
|
||||
val chapter: String?,
|
||||
@SerialName("release_date")
|
||||
val releaseDate: String
|
||||
)
|
||||
@Serializable
|
||||
data class MetaData(
|
||||
val series: Series
|
||||
) {
|
||||
@Serializable
|
||||
data class Series(
|
||||
@SerialName("series_id")
|
||||
val seriesId: Long?,
|
||||
@SerialName("title")
|
||||
val title: String?,
|
||||
@SerialName("latest_chapter")
|
||||
val latestChapter: Int?,
|
||||
@SerialName("last_updated")
|
||||
val lastUpdated: LastUpdated?
|
||||
) {
|
||||
@Serializable
|
||||
data class LastUpdated(
|
||||
@SerialName("timestamp")
|
||||
val timestamp: Long,
|
||||
@SerialName("as_rfc3339")
|
||||
val asRfc3339: String,
|
||||
@SerialName("as_string")
|
||||
val asString: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object CommentsAPI {
|
||||
val address: String = "https://1224665.xyz:443"
|
||||
private const val ADDRESS: String = "https://1224665.xyz:443"
|
||||
var authToken: String? = null
|
||||
var userId: String? = null
|
||||
var isBanned: Boolean = false
|
||||
@@ -33,7 +33,7 @@ object CommentsAPI {
|
||||
var totalVotes: Int = 0
|
||||
|
||||
suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? {
|
||||
var url = "$address/comments/$id/$page"
|
||||
var url = "$ADDRESS/comments/$id/$page"
|
||||
val request = requestBuilder()
|
||||
tag?.let {
|
||||
url += "?tag=$it"
|
||||
@@ -61,7 +61,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
|
||||
val url = "$address/comments/parent/$id/$page"
|
||||
val url = "$ADDRESS/comments/parent/$id/$page"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -83,7 +83,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun getSingleComment(id: Int): Comment? {
|
||||
val url = "$address/comments/$id"
|
||||
val url = "$ADDRESS/comments/$id"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -105,7 +105,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun vote(commentId: Int, voteType: Int): Boolean {
|
||||
val url = "$address/comments/vote/$commentId/$voteType"
|
||||
val url = "$ADDRESS/comments/vote/$commentId/$voteType"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url)
|
||||
@@ -121,7 +121,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
|
||||
val url = "$address/comments"
|
||||
val url = "$ADDRESS/comments"
|
||||
val body = FormBody.Builder()
|
||||
.add("user_id", userId ?: return null)
|
||||
.add("media_id", mediaId.toString())
|
||||
@@ -169,7 +169,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun deleteComment(commentId: Int): Boolean {
|
||||
val url = "$address/comments/$commentId"
|
||||
val url = "$ADDRESS/comments/$commentId"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.delete(url)
|
||||
@@ -185,7 +185,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun editComment(commentId: Int, content: String): Boolean {
|
||||
val url = "$address/comments/$commentId"
|
||||
val url = "$ADDRESS/comments/$commentId"
|
||||
val body = FormBody.Builder()
|
||||
.add("content", content)
|
||||
.build()
|
||||
@@ -204,7 +204,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun banUser(userId: String): Boolean {
|
||||
val url = "$address/ban/$userId"
|
||||
val url = "$ADDRESS/ban/$userId"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url)
|
||||
@@ -225,7 +225,7 @@ object CommentsAPI {
|
||||
mediaTitle: String,
|
||||
reportedId: String
|
||||
): Boolean {
|
||||
val url = "$address/report/$commentId"
|
||||
val url = "$ADDRESS/report/$commentId"
|
||||
val body = FormBody.Builder()
|
||||
.add("username", username)
|
||||
.add("mediaName", mediaTitle)
|
||||
@@ -247,7 +247,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
|
||||
val url = "$address/notification/reply"
|
||||
val url = "$ADDRESS/notification/reply"
|
||||
val request = requestBuilder(client)
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -268,7 +268,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
|
||||
val url = "$address/user"
|
||||
val url = "$ADDRESS/user"
|
||||
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -310,7 +310,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
}
|
||||
val url = "$address/authenticate"
|
||||
val url = "$ADDRESS/authenticate"
|
||||
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
|
||||
repeat(MAX_RETRIES) {
|
||||
try {
|
||||
@@ -348,6 +348,17 @@ object CommentsAPI {
|
||||
snackString("Failed to login after multiple attempts")
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
PrefManager.removeVal(PrefName.CommentAuthResponse)
|
||||
PrefManager.removeVal(PrefName.CommentTokenExpiry)
|
||||
authToken = null
|
||||
userId = null
|
||||
isBanned = false
|
||||
isAdmin = false
|
||||
isMod = false
|
||||
totalVotes = 0
|
||||
}
|
||||
|
||||
private suspend fun authRequest(
|
||||
token: String,
|
||||
url: String,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package ani.dantotsu.connections.github
|
||||
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.settings.Developer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
|
||||
class Contributors {
|
||||
|
||||
fun getContributors(): Array<Developer> {
|
||||
var developers = arrayOf<Developer>()
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val repo = getAppString(R.string.repo)
|
||||
val res = client.get("https://api.github.com/repos/$repo/contributors")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.find { it.login == "rebelonion"}?.let { first ->
|
||||
developers = developers.plus(
|
||||
Developer(
|
||||
first.login,
|
||||
first.avatarUrl,
|
||||
"Owner and Maintainer",
|
||||
first.htmlUrl
|
||||
)
|
||||
).plus(arrayOf(
|
||||
Developer(
|
||||
"Wai What",
|
||||
"https://avatars.githubusercontent.com/u/149729762?v=4",
|
||||
"Icon Designer",
|
||||
"https://github.com/WaiWhat"
|
||||
),
|
||||
Developer(
|
||||
"MarshMeadow",
|
||||
"https://avatars.githubusercontent.com/u/88599122?v=4",
|
||||
"Beta Icon Designer",
|
||||
"https://github.com/MarshMeadow?tab=repositories"
|
||||
),
|
||||
Developer(
|
||||
"Zaxx69",
|
||||
"https://avatars.githubusercontent.com/u/138523882?v=4",
|
||||
"Telegram Admin",
|
||||
"https://github.com/Zaxx69"
|
||||
),
|
||||
Developer(
|
||||
"Arif Alam",
|
||||
"https://avatars.githubusercontent.com/u/70383209?v=4",
|
||||
"Head Discord Moderator",
|
||||
"https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
)
|
||||
))
|
||||
}
|
||||
res.filter {it.login != "rebelonion"}.forEach {
|
||||
developers = developers.plus(
|
||||
Developer(
|
||||
it.login,
|
||||
it.avatarUrl,
|
||||
"Contributor",
|
||||
it.htmlUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return developers
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GithubResponse(
|
||||
@SerialName("login")
|
||||
val login: String,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String,
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String
|
||||
)
|
||||
}
|
||||
55
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
55
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package ani.dantotsu.connections.github
|
||||
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.settings.Developer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
|
||||
class Forks {
|
||||
|
||||
fun getForks(): Array<Developer> {
|
||||
var forks = arrayOf<Developer>()
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val res = client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.forEach {
|
||||
forks = forks.plus(
|
||||
Developer(
|
||||
it.name,
|
||||
it.owner.avatarUrl,
|
||||
it.owner.login,
|
||||
it.htmlUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return forks
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GithubResponse(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
val owner: Owner,
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class Owner(
|
||||
@SerialName("login")
|
||||
val login: String,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.callback.FolderCallback
|
||||
import com.anggrayudi.storage.file.deleteRecursively
|
||||
import com.anggrayudi.storage.file.findFolder
|
||||
import com.anggrayudi.storage.file.moveFileTo
|
||||
import com.anggrayudi.storage.file.moveFolderTo
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Serializable
|
||||
|
||||
class DownloadsManager(private val context: Context) {
|
||||
@@ -42,27 +53,29 @@ class DownloadsManager(private val context: Context) {
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(downloadedType: DownloadedType) {
|
||||
fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
|
||||
downloadsList.remove(downloadedType)
|
||||
removeDirectory(downloadedType)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
removeDirectory(downloadedType)
|
||||
withContext(Dispatchers.Main) {
|
||||
onFinished()
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: MediaType) {
|
||||
val subDirectory = type.asText()
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
val baseDirectory = getBaseDirectory(context, type)
|
||||
val directory = baseDirectory?.findFolder(title)
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
cleanDownloads()
|
||||
}
|
||||
when (type) {
|
||||
@@ -89,23 +102,17 @@ class DownloadsManager(private val context: Context) {
|
||||
|
||||
private fun cleanDownload(type: MediaType) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = type.asText()
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory"
|
||||
)
|
||||
val directory = getBaseDirectory(context, type)
|
||||
val downloadsSubLists = when (type) {
|
||||
MediaType.MANGA -> mangaDownloadedTypes
|
||||
MediaType.ANIME -> animeDownloadedTypes
|
||||
else -> novelDownloadedTypes
|
||||
}
|
||||
if (directory.exists()) {
|
||||
if (directory?.exists() == true && directory.isDirectory) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
file.deleteRecursively()
|
||||
}
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
file.deleteRecursively(context, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,27 +120,57 @@ class DownloadsManager(private val context: Context) {
|
||||
val iterator = downloadsList.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val download = iterator.next()
|
||||
val downloadDir = File(directory, download.title)
|
||||
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||
val downloadDir = directory?.findFolder(download.title)
|
||||
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
file.parentFile?.mkdirs()
|
||||
fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
|
||||
try {
|
||||
if (oldUri == newUri) {
|
||||
finished(false, "Source and destination are the same")
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
val oldBase =
|
||||
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
|
||||
val newBase =
|
||||
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
|
||||
val folder =
|
||||
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
|
||||
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object:
|
||||
FolderCallback() {
|
||||
override fun onFailed(errorCode: ErrorCode) {
|
||||
when (errorCode) {
|
||||
ErrorCode.CANCELED -> finished(false, "Move canceled")
|
||||
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(false, "Cannot create file in target")
|
||||
ErrorCode.INVALID_TARGET_FOLDER -> finished(true, "Invalid target folder") // seems to still work
|
||||
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(false, "No space left on target path")
|
||||
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
|
||||
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(false, "Source folder not found")
|
||||
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(false, "Storage permission denied")
|
||||
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(false, "Target folder cannot have same path with source folder")
|
||||
else -> finished(false, "Failed to move downloads: $errorCode")
|
||||
}
|
||||
Logger.log("Failed to move downloads: $errorCode")
|
||||
super.onFailed(errorCode)
|
||||
}
|
||||
|
||||
override fun onCompleted(result: Result) {
|
||||
finished(true, "Successfully moved downloads")
|
||||
super.onCompleted(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
snackString("Error: ${e.message}")
|
||||
finished(false, "Failed to move downloads: ${e.message}")
|
||||
return
|
||||
}
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||
@@ -149,98 +186,35 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||
val directory = when (downloadedType.type) {
|
||||
MediaType.MANGA -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
MediaType.ANIME -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
}
|
||||
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||
val directory =
|
||||
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
snackString("Successfully deleted")
|
||||
|
||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||
val directory = when (downloadedType.type) {
|
||||
MediaType.MANGA -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
MediaType.ANIME -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
}
|
||||
val destination = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
if (copied) {
|
||||
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: MediaType) {
|
||||
val directory = when (type) {
|
||||
MediaType.MANGA -> {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
}
|
||||
MediaType.ANIME -> {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
}
|
||||
else -> {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
}
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
val directory = getBaseDirectory(context, type)
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
}
|
||||
|
||||
downloadsList.removeAll { it.type == type }
|
||||
@@ -248,59 +222,95 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val novelLocation = "Dantotsu/Novel"
|
||||
const val mangaLocation = "Dantotsu/Manga"
|
||||
const val animeLocation = "Dantotsu/Anime"
|
||||
private const val BASE_LOCATION = "Dantotsu"
|
||||
private const val MANGA_SUB_LOCATION = "Manga"
|
||||
private const val ANIME_SUB_LOCATION = "Anime"
|
||||
private const val NOVEL_SUB_LOCATION = "Novel"
|
||||
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||
|
||||
fun getDirectory(
|
||||
context: Context,
|
||||
type: MediaType,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): File {
|
||||
fun String?.findValidName(): String {
|
||||
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and create a base directory for the given type
|
||||
* @param context the context
|
||||
* @param type the type of media
|
||||
* @return the base directory
|
||||
*/
|
||||
|
||||
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
|
||||
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
||||
if (baseDirectory == Uri.EMPTY) return null
|
||||
var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
|
||||
base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
|
||||
return when (type) {
|
||||
MediaType.MANGA -> {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title"
|
||||
)
|
||||
}
|
||||
base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
|
||||
}
|
||||
|
||||
MediaType.ANIME -> {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title"
|
||||
)
|
||||
}
|
||||
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title"
|
||||
)
|
||||
}
|
||||
base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and create a subdirectory for the given type
|
||||
* @param context the context
|
||||
* @param type the type of media
|
||||
* @param title the title of the media
|
||||
* @param chapter the chapter of the media
|
||||
* @return the subdirectory
|
||||
*/
|
||||
fun getSubDirectory(
|
||||
context: Context,
|
||||
type: MediaType,
|
||||
overwrite: Boolean,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): DocumentFile? {
|
||||
val baseDirectory = getBaseDirectory(context, type) ?: return null
|
||||
return if (chapter != null) {
|
||||
baseDirectory.findOrCreateFolder(title, false)
|
||||
?.findOrCreateFolder(chapter, overwrite)
|
||||
} else {
|
||||
baseDirectory.findOrCreateFolder(title, overwrite)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDirSize(context: Context, type: MediaType, title: String, chapter: String? = null): Long {
|
||||
val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0
|
||||
var size = 0L
|
||||
directory.listFiles().forEach {
|
||||
size += it.length()
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
private fun DocumentFile.findOrCreateFolder(
|
||||
name: String, overwrite: Boolean
|
||||
): DocumentFile? {
|
||||
return if (overwrite) {
|
||||
findFolder(name.findValidName())?.delete()
|
||||
createDirectory(name.findValidName())
|
||||
} else {
|
||||
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: MediaType) : Serializable
|
||||
data class DownloadedType(
|
||||
val pTitle: String, val pChapter: String, val type: MediaType
|
||||
) : Serializable {
|
||||
val title: String
|
||||
get() = pTitle.findValidName()
|
||||
val chapter: String
|
||||
get() = pChapter.findValidName()
|
||||
}
|
||||
|
||||
@@ -9,24 +9,21 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
@@ -36,6 +33,12 @@ import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import com.arthenica.ffmpegkit.SessionState
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
@@ -46,7 +49,6 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -56,13 +58,12 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
||||
class AnimeDownloaderService : Service() {
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
@@ -88,6 +89,7 @@ class AnimeDownloaderService : Service() {
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(100, 0, false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
@@ -156,27 +158,14 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
@UnstableApi
|
||||
fun cancelDownload(taskName: String) {
|
||||
val url =
|
||||
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
||||
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
||||
if (url.isEmpty()) {
|
||||
snackString("Failed to cancel download")
|
||||
return
|
||||
val sessionIds =
|
||||
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
|
||||
.map { it.sessionId }.toMutableList()
|
||||
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||
sessionIds.forEach {
|
||||
FFmpegKit.cancel(it)
|
||||
}
|
||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||
DownloadService.sendSetStopReason(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
|
||||
false
|
||||
)
|
||||
DownloadService.sendRemoveDownload(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
false
|
||||
)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
mutex.withLock {
|
||||
downloadJobs[taskName]?.cancel()
|
||||
@@ -209,7 +198,7 @@ class AnimeDownloaderService : Service() {
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun download(task: AnimeDownloadTask) {
|
||||
try {
|
||||
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||
//val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||
withContext(Dispatchers.Main) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
@@ -220,18 +209,80 @@ class AnimeDownloaderService : Service() {
|
||||
true
|
||||
}
|
||||
|
||||
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
||||
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
currActivity()?.let {
|
||||
Helper.downloadVideo(
|
||||
it,
|
||||
task.video,
|
||||
task.subtitle
|
||||
)
|
||||
val outputDir = getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
task.title,
|
||||
task.episode
|
||||
) ?: throw Exception("Failed to create output directory")
|
||||
|
||||
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
|
||||
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
|
||||
?: throw Exception("Failed to create output file")
|
||||
|
||||
var percent = 0
|
||||
var totalLength = 0.0
|
||||
val path = FFmpegKitConfig.getSafParameterForWrite(
|
||||
this@AnimeDownloaderService,
|
||||
outputFile.uri
|
||||
)
|
||||
val headersStringBuilder = StringBuilder().append(" ")
|
||||
task.video.file.headers.forEach {
|
||||
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
|
||||
}
|
||||
headersStringBuilder.append(" ")
|
||||
FFprobeKit.executeAsync(
|
||||
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"",
|
||||
{
|
||||
Logger.log("FFprobeKit: $it")
|
||||
}, {
|
||||
if (it.message.toDoubleOrNull() != null) {
|
||||
totalLength = it.message.toDouble()
|
||||
}
|
||||
})
|
||||
|
||||
var request = "-headers"
|
||||
val headers = headersStringBuilder.toString()
|
||||
if (task.video.file.headers.isNotEmpty()) {
|
||||
request += headers
|
||||
}
|
||||
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
|
||||
println("Request: $request")
|
||||
val ffTask =
|
||||
FFmpegKit.executeAsync(request,
|
||||
{ session ->
|
||||
val state: SessionState = session.state
|
||||
val returnCode = session.returnCode
|
||||
// CALLED WHEN SESSION IS EXECUTED
|
||||
Logger.log(
|
||||
java.lang.String.format(
|
||||
"FFmpeg process exited with state %s and rc %s.%s",
|
||||
state,
|
||||
returnCode,
|
||||
session.failStackTrace
|
||||
)
|
||||
)
|
||||
|
||||
}, {
|
||||
// CALLED WHEN SESSION PRINTS LOGS
|
||||
Logger.log(it.message)
|
||||
}) {
|
||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||
val timeInMilliseconds = it.time
|
||||
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||
percent = ((it.time / 1000) / totalLength * 100).toInt()
|
||||
}
|
||||
Logger.log("Statistics: $it")
|
||||
}
|
||||
task.sessionId = ffTask.sessionId
|
||||
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||
ffTask.sessionId
|
||||
|
||||
saveMediaInfo(task)
|
||||
task.subtitle?.let {
|
||||
@@ -245,86 +296,115 @@ class AnimeDownloaderService : Service() {
|
||||
)
|
||||
)
|
||||
}
|
||||
val downloadStarted =
|
||||
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
||||
|
||||
if (!downloadStarted) {
|
||||
Logger.log("Download failed to start")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed to start")
|
||||
broadcastDownloadFailed(task.episode)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed")
|
||||
Logger.log("Download failed: ${download.failureReason}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
while (ffTask.state != SessionState.COMPLETED) {
|
||||
if (ffTask.state == SessionState.FAILED) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
task.episode
|
||||
)
|
||||
)
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${download.failureReason}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
||||
Logger.log("Download completed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download completed")
|
||||
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFinished(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
|
||||
Logger.log("Download stopped")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download stopped")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download stopped")
|
||||
break
|
||||
}
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
download.percentDownloaded.toInt()
|
||||
} Download failed"
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffTask.failStackTrace}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
) {}
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${getTaskName(task.title, task.episode)}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
builder.setProgress(
|
||||
100, percent.coerceAtMost(99),
|
||||
false
|
||||
)
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
percent.coerceAtMost(99)
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (ffTask.state == SessionState.COMPLETED) {
|
||||
if (ffTask.returnCode.isValueError) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
} Download failed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
) {}
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${getTaskName(task.title, task.episode)}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
return@withContext
|
||||
}
|
||||
Logger.log("Download completed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
} Download completed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${getTaskName(task.title, task.episode)} Download completed")
|
||||
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFinished(task.episode)
|
||||
} else throw Exception("Download failed")
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||
@@ -337,35 +417,24 @@ class AnimeDownloaderService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun hasDownloadStarted(
|
||||
downloadManager: DownloadManager,
|
||||
task: AnimeDownloadTask,
|
||||
timeout: Long
|
||||
): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < timeout) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
return true
|
||||
}
|
||||
// Delay between each poll
|
||||
kotlinx.coroutines.delay(500)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.animeLocation}/${task.title}"
|
||||
)
|
||||
val episodeDirectory = File(directory, task.episode)
|
||||
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
||||
val directory =
|
||||
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val episodeDirectory =
|
||||
getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
?: throw Exception("Directory not found")
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -399,14 +468,25 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@AnimeDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
@@ -417,13 +497,16 @@ class AnimeDownloaderService : Service() {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -490,14 +573,15 @@ class AnimeDownloaderService : Service() {
|
||||
val episodeImage: String? = null,
|
||||
val retries: Int = 2,
|
||||
val simultaneousDownloads: Int = 2,
|
||||
var sessionId: Long = -1
|
||||
) {
|
||||
fun getTaskName(): String {
|
||||
return "$title - $episode"
|
||||
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getTaskName(title: String, episode: String): String {
|
||||
return "$title - $episode"
|
||||
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,7 +595,6 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
object AnimeServiceDataSingleton {
|
||||
var video: Video? = null
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
|
||||
@@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
@@ -25,6 +24,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
@@ -33,6 +33,7 @@ import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -44,6 +45,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -55,9 +57,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
@@ -66,6 +72,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineAnimeAdapter
|
||||
private lateinit var total: TextView
|
||||
private var downloadsJob: Job = Job()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -112,10 +119,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
})
|
||||
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
0 -> layoutList
|
||||
1 -> layoutcompact
|
||||
1 -> layoutCompact
|
||||
else -> layoutList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
@@ -136,7 +143,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
grid()
|
||||
}
|
||||
|
||||
layoutcompact.setOnClickListener {
|
||||
layoutCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
@@ -156,11 +163,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
||||
getDownloads()
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
@@ -168,20 +175,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val media =
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title.findValidName() }
|
||||
media?.let {
|
||||
val mediaModel = getMedia(it)
|
||||
if (mediaModel == null) {
|
||||
snackString("Error loading media.json")
|
||||
return@let
|
||||
lifecycleScope.launch {
|
||||
val mediaModel = getMedia(it)
|
||||
if (mediaModel == null) {
|
||||
snackString("Error loading media.json")
|
||||
return@launch
|
||||
}
|
||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
}
|
||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
@@ -204,13 +213,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
if (mediaIds.isEmpty()) {
|
||||
snackString("No media found") // if this happens, terrible things have happened
|
||||
}
|
||||
for (mediaId in mediaIds) {
|
||||
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
|
||||
.removeDownload(mediaId.toString())
|
||||
}
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
@@ -238,7 +241,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
@@ -261,7 +263,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -281,25 +282,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
/**
|
||||
* Load media.json file from the directory and convert it to Media class
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return Media object
|
||||
*/
|
||||
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
return try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -311,8 +326,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
}
|
||||
?: return null
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
@@ -322,22 +342,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
/**
|
||||
* Load OfflineAnimeModel from the directory
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return OfflineAnimeModel object
|
||||
*/
|
||||
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||
cover.uri
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
val banner = directory?.findFile("banner.jpg")
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
|
||||
@@ -10,17 +10,18 @@ import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
@@ -31,6 +32,9 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.deleteRecursively
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
@@ -51,8 +55,6 @@ import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
@@ -189,13 +191,20 @@ class MangaDownloaderService : Service() {
|
||||
true
|
||||
}
|
||||
|
||||
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
getSubDirectory(
|
||||
this@MangaDownloaderService,
|
||||
MediaType.MANGA,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
)?.deleteRecursively(this@MangaDownloaderService)
|
||||
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
@@ -263,24 +272,18 @@ class MangaDownloaderService : Service() {
|
||||
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||
try {
|
||||
// Define the directory within the private external storage space
|
||||
val directory = File(
|
||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$title/$chapter"
|
||||
)
|
||||
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Create a file reference within that directory for your image
|
||||
val file = File(directory, fileName)
|
||||
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile(fileName)?.forceDelete(this)
|
||||
// Create a file reference within that directory for the image
|
||||
val file =
|
||||
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
|
||||
|
||||
// Use a FileOutputStream to write the bitmap to the file
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
file.openOutputStream(this, false).use { outputStream ->
|
||||
if (outputStream == null) throw Exception("Output stream is null")
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Exception while saving image: ${e.message}")
|
||||
snackString("Exception while saving image: ${e.message}")
|
||||
@@ -291,13 +294,12 @@ class MangaDownloaderService : Service() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val directory =
|
||||
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -312,7 +314,10 @@ class MangaDownloaderService : Service() {
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
file.writeText(jsonString)
|
||||
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
@@ -327,7 +332,7 @@ class MangaDownloaderService : Service() {
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
@@ -337,14 +342,16 @@ class MangaDownloaderService : Service() {
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
@@ -23,6 +22,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
@@ -30,6 +30,7 @@ import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -41,6 +42,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -48,9 +50,13 @@ import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
@@ -59,6 +65,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineMangaAdapter
|
||||
private lateinit var total: TextView
|
||||
private var downloadsJob: Job = Job()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -148,11 +155,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||
getDownloads()
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text =
|
||||
@@ -164,14 +171,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
media?.let {
|
||||
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
@@ -194,9 +202,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
@@ -225,7 +230,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
@@ -248,7 +252,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -268,42 +271,62 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
downloads = listOf()
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.setItems(downloads)
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
/**
|
||||
* Load media.json file from the directory and convert it to Media class
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return Media object
|
||||
*/
|
||||
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
return try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
}
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
@@ -313,22 +336,22 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||
cover.uri
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
val banner = directory?.findFile("banner.jpg")
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
@@ -336,14 +359,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val readChapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val chapters = " Chapters"
|
||||
return OfflineMangaModel(
|
||||
title,
|
||||
score,
|
||||
totalchapter,
|
||||
readchapter,
|
||||
totalChapter,
|
||||
readChapter,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
|
||||
@@ -16,15 +16,19 @@ import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
@@ -250,24 +254,25 @@ class NovelDownloaderService : Service() {
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Failed to download file: ${response.message}")
|
||||
}
|
||||
val directory = getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
) ?: throw Exception("Directory not found")
|
||||
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
|
||||
|
||||
val file = File(
|
||||
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
|
||||
)
|
||||
|
||||
// Create directories if they don't exist
|
||||
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
|
||||
|
||||
// Overwrite existing file
|
||||
if (file.exists()) file.delete()
|
||||
val file = directory.createFile("application/epub+zip", "0.epub")
|
||||
?: throw Exception("File not created")
|
||||
|
||||
//download cover
|
||||
task.coverUrl?.let {
|
||||
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
||||
}
|
||||
val outputStream = this@NovelDownloaderService.contentResolver.openOutputStream(file.uri) ?: throw Exception("Could not open OutputStream")
|
||||
|
||||
val sink = file.sink().buffer()
|
||||
val sink = outputStream.sink().buffer()
|
||||
val responseBody = response.body
|
||||
val totalBytes = responseBody.contentLength()
|
||||
var downloadedBytes = 0L
|
||||
@@ -352,13 +357,16 @@ class NovelDownloaderService : Service() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val directory =
|
||||
DownloadsManager.getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title
|
||||
) ?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -372,33 +380,47 @@ class NovelDownloaderService : Service() {
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@NovelDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(
|
||||
Dispatchers.IO
|
||||
) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
Logger.log("Downloading url $url")
|
||||
try {
|
||||
connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package ani.dantotsu.download.video
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
||||
import androidx.media3.exoplayer.scheduler.Scheduler
|
||||
import ani.dantotsu.R
|
||||
|
||||
@UnstableApi
|
||||
class ExoplayerDownloadService :
|
||||
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||
companion object {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
}
|
||||
|
||||
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
|
||||
|
||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
||||
|
||||
override fun getForegroundNotification(
|
||||
downloads: MutableList<Download>,
|
||||
notMetRequirements: Int
|
||||
): Notification =
|
||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||
this,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
notMetRequirements
|
||||
)
|
||||
}
|
||||
@@ -53,140 +53,6 @@ import java.util.concurrent.Executors
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
object Helper {
|
||||
|
||||
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
video.file.headers.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
val mimeType = when (video.format) {
|
||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
||||
else -> MimeTypes.APPLICATION_MP4
|
||||
}
|
||||
|
||||
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
||||
var sub: MediaItem.SubtitleConfiguration? = null
|
||||
if (subtitle != null) {
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
.Builder(Uri.parse(subtitle.file.url))
|
||||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||
.setMimeType(
|
||||
when (subtitle.type) {
|
||||
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
||||
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
|
||||
val mediaItem = builder.build()
|
||||
val downloadHelper = DownloadHelper.forMediaItem(
|
||||
context,
|
||||
mediaItem,
|
||||
DefaultRenderersFactory(context),
|
||||
dataSourceFactory
|
||||
)
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||
override fun onPrepared(helper: DownloadHelper) {
|
||||
helper.getDownloadRequest(null).let {
|
||||
DownloadService.sendAddDownload(
|
||||
context,
|
||||
ExoplayerDownloadService::class.java,
|
||||
it,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareError(helper: DownloadHelper, e: IOException) {
|
||||
logError(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private var download: DownloadManager? = null
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
|
||||
@Synchronized
|
||||
@UnstableApi
|
||||
fun downloadManager(context: Context): DownloadManager {
|
||||
return download ?: let {
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
database,
|
||||
getSimpleCache(context),
|
||||
dataSourceFactory,
|
||||
executorService
|
||||
).apply {
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
maxParallelDownloads = 3
|
||||
}
|
||||
downloadManager.addListener( //for testing
|
||||
object : DownloadManager.Listener {
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
when (download.state) {
|
||||
Download.STATE_COMPLETED -> Logger.log("Download Completed")
|
||||
Download.STATE_FAILED -> Logger.log("Download Failed")
|
||||
Download.STATE_STOPPED -> Logger.log("Download Stopped")
|
||||
Download.STATE_QUEUED -> Logger.log("Download Queued")
|
||||
Download.STATE_DOWNLOADING -> Logger.log("Download Downloading")
|
||||
Download.STATE_REMOVING -> Logger.log("Download Removing")
|
||||
Download.STATE_RESTARTING -> Logger.log("Download Restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
downloadManager
|
||||
}
|
||||
}
|
||||
|
||||
private var downloadDirectory: File? = null
|
||||
|
||||
@Synchronized
|
||||
private fun getDownloadDirectory(context: Context): File {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(null)
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.filesDir
|
||||
}
|
||||
}
|
||||
return downloadDirectory!!
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun startAnimeDownloadService(
|
||||
context: Context,
|
||||
@@ -225,15 +91,6 @@ object Helper {
|
||||
.setTitle("Download Exists")
|
||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
.setPositiveButton("Yes") { _, _ ->
|
||||
DownloadService.sendRemoveDownload(
|
||||
context,
|
||||
ExoplayerDownloadService::class.java,
|
||||
PrefManager.getAnimeDownloadPreferences().getString(
|
||||
animeDownloadTask.getTaskName(),
|
||||
""
|
||||
) ?: "",
|
||||
false
|
||||
)
|
||||
PrefManager.getAnimeDownloadPreferences().edit()
|
||||
.remove(animeDownloadTask.getTaskName())
|
||||
.apply()
|
||||
@@ -243,12 +100,13 @@ object Helper {
|
||||
episode,
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
) {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton("No") { _, _ -> }
|
||||
@@ -263,18 +121,6 @@ object Helper {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getSimpleCache(context: Context): SimpleCache {
|
||||
return if (simpleCache == null) {
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||
simpleCache!!
|
||||
} else {
|
||||
simpleCache!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
|
||||
@@ -207,6 +207,21 @@ class AnimeFragment : Fragment() {
|
||||
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getMovies().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getTopRated().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getMostFav().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
if (animePageAdapter.trendingViewPager != null) {
|
||||
animePageAdapter.updateHeight()
|
||||
model.getTrending().observe(viewLifecycleOwner) {
|
||||
@@ -263,7 +278,7 @@ class AnimeFragment : Fragment() {
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending(1)
|
||||
model.loadUpdated()
|
||||
model.loadAll()
|
||||
model.loadPopular(
|
||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularAnimeList
|
||||
|
||||
@@ -195,24 +195,67 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||
binding.animeUpdatedRecyclerView.layoutManager =
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeUpdatedRecyclerView,
|
||||
animeUpdatedProgressBar,
|
||||
animeRecently
|
||||
)
|
||||
animePopular.visibility = View.VISIBLE
|
||||
animePopular.startAnimation(setSlideUp())
|
||||
if (adaptor.itemCount == 0) {
|
||||
animeRecentlyContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
fun updateMovies(adaptor: MediaAdaptor) {
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeMoviesRecyclerView,
|
||||
animeMoviesProgressBar,
|
||||
animeMovies
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeTopRatedRecyclerView,
|
||||
animeTopRatedProgressBar,
|
||||
animeTopRated
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeMostFavRecyclerView,
|
||||
animeMostFavProgressBar,
|
||||
animeMostFav
|
||||
)
|
||||
}
|
||||
}
|
||||
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(
|
||||
binding.animeUpdatedRecyclerView.context,
|
||||
recyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeRecently.visibility = View.VISIBLE
|
||||
binding.animeRecently.startAnimation(setSlideUp())
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animePopular.visibility = View.VISIBLE
|
||||
binding.animePopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
if (Anilist.avatar != null && ready.value == true) {
|
||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||
|
||||
@@ -80,8 +80,8 @@ class HomeFragment : Fragment() {
|
||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
|
||||
blurImage(binding.homeUserBg, Anilist.bg)
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
blurImage(if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen, Anilist.bg)
|
||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
@@ -137,6 +137,7 @@ class HomeFragment : Fragment() {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
|
||||
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
var reached = false
|
||||
|
||||
@@ -160,11 +160,31 @@ class MangaFragment : Fragment() {
|
||||
})
|
||||
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
|
||||
if (i == true) {
|
||||
model.getTrendingNovel().observe(viewLifecycleOwner) {
|
||||
model.getPopularNovel().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getPopularManga().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getPopularManhwa().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManhwa(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getTopRated().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getMostFav().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
if (mangaPageAdapter.trendingViewPager != null) {
|
||||
mangaPageAdapter.updateHeight()
|
||||
model.getTrending().observe(viewLifecycleOwner) {
|
||||
@@ -237,7 +257,7 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending()
|
||||
model.loadTrendingNovel()
|
||||
model.loadAll()
|
||||
model.loadPopular(
|
||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularMangaList
|
||||
|
||||
@@ -178,25 +178,76 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
||||
binding.mangaListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
|
||||
}
|
||||
|
||||
fun updateTrendingManga(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTrendingMangaRecyclerView,
|
||||
mangaTrendingMangaProgressBar,
|
||||
mangaTrendingManga
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTrendingManhwaRecyclerView,
|
||||
mangaTrendingManhwaProgressBar,
|
||||
mangaTrendingManhwa
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateNovel(adaptor: MediaAdaptor) {
|
||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||
binding.mangaNovelRecyclerView.layoutManager =
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaNovelRecyclerView,
|
||||
mangaNovelProgressBar,
|
||||
mangaNovel
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTopRatedRecyclerView,
|
||||
mangaTopRatedProgressBar,
|
||||
mangaTopRated
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaMostFavRecyclerView,
|
||||
mangaMostFavProgressBar,
|
||||
mangaMostFav
|
||||
)
|
||||
mangaPopular.visibility = View.VISIBLE
|
||||
mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
}
|
||||
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(
|
||||
binding.mangaNovelRecyclerView.context,
|
||||
recyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.mangaNovel.visibility = View.VISIBLE
|
||||
binding.mangaNovel.startAnimation(setSlideUp())
|
||||
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.mangaPopular.visibility = View.VISIBLE
|
||||
binding.mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
|
||||
@@ -3,9 +3,10 @@ package ani.dantotsu.media
|
||||
import java.io.Serializable
|
||||
|
||||
data class Author(
|
||||
val id: Int,
|
||||
val name: String?,
|
||||
val image: String?,
|
||||
val role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
||||
var id: Int,
|
||||
var name: String?,
|
||||
var image: String?,
|
||||
var role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
|
||||
var character: ArrayList<Character>? = null
|
||||
) : Serializable
|
||||
|
||||
@@ -32,7 +32,7 @@ class AuthorActivity : AppCompatActivity() {
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
private var author: Author? = null
|
||||
private var loaded = false
|
||||
|
||||
private var isVoiceArtist: Boolean = false
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -55,43 +55,59 @@ class AuthorActivity : AppCompatActivity() {
|
||||
binding.studioClose.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
isVoiceArtist = intent.getBooleanExtra("isVoiceArtist", false)
|
||||
if (isVoiceArtist) {
|
||||
model.getVoiceActor().observe(this) {
|
||||
if (it != null) {
|
||||
author = it
|
||||
loaded = true
|
||||
binding.studioProgressBar.visibility = View.GONE
|
||||
binding.studioRecycler.visibility = View.VISIBLE
|
||||
binding.studioRecycler.adapter = CharacterAdapter(author!!.character ?: arrayListOf())
|
||||
binding.studioRecycler.layoutManager = GridLayoutManager(
|
||||
this,
|
||||
(screenWidth / 120f).toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
}else{
|
||||
model.getAuthor().observe(this) {
|
||||
if (it != null) {
|
||||
author = it
|
||||
loaded = true
|
||||
binding.studioProgressBar.visibility = View.GONE
|
||||
binding.studioRecycler.visibility = View.VISIBLE
|
||||
|
||||
model.getAuthor().observe(this) {
|
||||
if (it != null) {
|
||||
author = it
|
||||
loaded = true
|
||||
binding.studioProgressBar.visibility = View.GONE
|
||||
binding.studioRecycler.visibility = View.VISIBLE
|
||||
val titlePosition = arrayListOf<Int>()
|
||||
val concatAdapter = ConcatAdapter()
|
||||
val map = author!!.yearMedia ?: return@observe
|
||||
val keys = map.keys.toTypedArray()
|
||||
var pos = 0
|
||||
|
||||
val titlePosition = arrayListOf<Int>()
|
||||
val concatAdapter = ConcatAdapter()
|
||||
val map = author!!.yearMedia ?: return@observe
|
||||
val keys = map.keys.toTypedArray()
|
||||
var pos = 0
|
||||
|
||||
val gridSize = (screenWidth / 124f).toInt()
|
||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (position in titlePosition) {
|
||||
true -> gridSize
|
||||
else -> 1
|
||||
val gridSize = (screenWidth / 124f).toInt()
|
||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (position in titlePosition) {
|
||||
true -> gridSize
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i in keys.indices) {
|
||||
val medias = map[keys[i]]!!
|
||||
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
|
||||
titlePosition.add(pos)
|
||||
pos += (empty + medias.size + 1)
|
||||
for (i in keys.indices) {
|
||||
val medias = map[keys[i]]!!
|
||||
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
|
||||
titlePosition.add(pos)
|
||||
pos += (empty + medias.size + 1)
|
||||
|
||||
concatAdapter.addAdapter(TitleAdapter("${keys[i]} (${medias.size})"))
|
||||
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
|
||||
concatAdapter.addAdapter(EmptyAdapter(empty))
|
||||
}
|
||||
concatAdapter.addAdapter(TitleAdapter("${keys[i]} (${medias.size})"))
|
||||
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
|
||||
concatAdapter.addAdapter(EmptyAdapter(empty))
|
||||
}
|
||||
|
||||
binding.studioRecycler.adapter = concatAdapter
|
||||
binding.studioRecycler.layoutManager = gridLayoutManager
|
||||
binding.studioRecycler.adapter = concatAdapter
|
||||
binding.studioRecycler.layoutManager = gridLayoutManager
|
||||
}
|
||||
}
|
||||
}
|
||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
||||
@@ -99,7 +115,7 @@ class AuthorActivity : AppCompatActivity() {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
if (author != null)
|
||||
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
|
||||
withContext(Dispatchers.IO) { if (isVoiceArtist) model.loadVoiceActor(author!!) else model.loadAuthor(author!!)}
|
||||
live.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import ani.dantotsu.setAnimation
|
||||
import java.io.Serializable
|
||||
|
||||
class AuthorAdapter(
|
||||
private val authorList: ArrayList<Author>
|
||||
private val authorList: ArrayList<Author>,
|
||||
private val isVA: Boolean = false,
|
||||
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
|
||||
val binding =
|
||||
@@ -43,7 +44,7 @@ class AuthorAdapter(
|
||||
Intent(
|
||||
itemView.context,
|
||||
AuthorActivity::class.java
|
||||
).putExtra("author", author as Serializable),
|
||||
).putExtra("author", author as Serializable).putExtra("isVoiceArtist", isVA),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
itemView.context as Activity,
|
||||
Pair.create(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import org.checkerframework.checker.units.qual.A
|
||||
import java.io.Serializable
|
||||
|
||||
data class Character(
|
||||
@@ -14,5 +16,6 @@ data class Character(
|
||||
var age: String? = null,
|
||||
var gender: String? = null,
|
||||
var dateOfBirth: FuzzyDate? = null,
|
||||
var roles: ArrayList<Media>? = null
|
||||
var roles: ArrayList<Media>? = null,
|
||||
val voiceActor: ArrayList<Author>? = null,
|
||||
) : Serializable
|
||||
@@ -28,6 +28,7 @@ class CharacterAdapter(
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val character = characterList[position]
|
||||
val whitespace = "${character.role} "
|
||||
character.voiceActor
|
||||
binding.itemCompactRelation.text = whitespace
|
||||
binding.itemCompactImage.loadImage(character.image)
|
||||
binding.itemCompactTitle.text = character.name
|
||||
|
||||
@@ -2,7 +2,9 @@ package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
@@ -36,7 +38,13 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SpoilerPlugin()).build()
|
||||
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
|
||||
|
||||
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf(), true)
|
||||
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(
|
||||
activity, LinearLayoutManager.HORIZONTAL, false
|
||||
)
|
||||
if (binding.voiceActorRecycler.adapter!!.itemCount == 0) {
|
||||
binding.voiceActorContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
@@ -173,6 +173,7 @@ class MediaAdaptor(
|
||||
val b = (holder as MediaPageViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (bannerAnimations)
|
||||
@@ -182,7 +183,7 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
@@ -231,7 +232,7 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
@@ -53,6 +54,7 @@ import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.LauncherWrapper
|
||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -66,7 +68,7 @@ import kotlin.math.abs
|
||||
|
||||
|
||||
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
|
||||
lateinit var launcher: LauncherWrapper
|
||||
lateinit var binding: ActivityMediaBinding
|
||||
private val scope = lifecycleScope
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
@@ -92,6 +94,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return
|
||||
}
|
||||
val contract = ActivityResultContracts.OpenDocumentTree()
|
||||
launcher = LauncherWrapper(this, contract)
|
||||
|
||||
mediaSingleton = null
|
||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||
MediaSingleton.bitmap = null
|
||||
@@ -576,4 +581,4 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
companion object {
|
||||
var mediaSingleton: Media? = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.countDown
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.databinding.FragmentMediaInfoBinding
|
||||
@@ -35,8 +34,10 @@ import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemQuelsBinding
|
||||
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
|
||||
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
|
||||
import ani.dantotsu.databinding.ItemTitleSearchBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTextBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTrailerBinding
|
||||
import ani.dantotsu.displayTimer
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
@@ -91,7 +92,6 @@ class MediaInfoFragment : Fragment() {
|
||||
if (media != null && !loaded) {
|
||||
loaded = true
|
||||
|
||||
|
||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||
val infoName = tripleTab + (media.name ?: media.nameRomaji)
|
||||
@@ -225,8 +225,7 @@ class MediaInfoFragment : Fragment() {
|
||||
.setDuration(400).start()
|
||||
}
|
||||
}
|
||||
|
||||
countDown(media, binding.mediaInfoContainer)
|
||||
displayTimer(media, binding.mediaInfoContainer)
|
||||
val parent = _binding?.mediaInfoContainer!!
|
||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
|
||||
@@ -438,113 +437,138 @@ class MediaInfoFragment : Fragment() {
|
||||
|
||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||
if (media.sequel != null || media.prequel != null) {
|
||||
val bind = ItemQuelsBinding.inflate(
|
||||
ItemQuelsBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
|
||||
if (media.sequel != null) {
|
||||
bind.mediaInfoSequel.visibility = View.VISIBLE
|
||||
bind.mediaInfoSequelImage.loadImage(
|
||||
media.sequel!!.banner ?: media.sequel!!.cover
|
||||
)
|
||||
bind.mediaInfoSequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.sequel as Serializable
|
||||
), null
|
||||
if (media.sequel != null) {
|
||||
mediaInfoSequel.visibility = View.VISIBLE
|
||||
mediaInfoSequelImage.loadImage(
|
||||
media.sequel!!.banner ?: media.sequel!!.cover
|
||||
)
|
||||
}
|
||||
}
|
||||
if (media.prequel != null) {
|
||||
bind.mediaInfoPrequel.visibility = View.VISIBLE
|
||||
bind.mediaInfoPrequelImage.loadImage(
|
||||
media.prequel!!.banner ?: media.prequel!!.cover
|
||||
)
|
||||
bind.mediaInfoPrequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
mediaInfoSequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.prequel as Serializable
|
||||
), null
|
||||
)
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.sequel as Serializable
|
||||
), null
|
||||
)
|
||||
}
|
||||
}
|
||||
if (media.prequel != null) {
|
||||
mediaInfoPrequel.visibility = View.VISIBLE
|
||||
mediaInfoPrequelImage.loadImage(
|
||||
media.prequel!!.banner ?: media.prequel!!.cover
|
||||
)
|
||||
mediaInfoPrequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.prequel as Serializable
|
||||
), null
|
||||
)
|
||||
}
|
||||
}
|
||||
parent.addView(root)
|
||||
}
|
||||
|
||||
ItemTitleSearchBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
|
||||
titleSearchImage.loadImage(media.banner ?: media.cover)
|
||||
titleSearchText.text =
|
||||
getString(R.string.search_title, media.mainName())
|
||||
titleSearchCard.setSafeOnClickListener {
|
||||
val query = Intent(requireContext(), SearchActivity::class.java)
|
||||
.putExtra("type", "ANIME")
|
||||
.putExtra("query", media.mainName())
|
||||
.putExtra("search", true)
|
||||
ContextCompat.startActivity(requireContext(), query, null)
|
||||
}
|
||||
|
||||
parent.addView(root)
|
||||
}
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
val bindi = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
|
||||
bindi.itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||
bindi.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bindi.root)
|
||||
itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.characters)
|
||||
bind.itemRecycler.adapter =
|
||||
CharacterAdapter(media.characters!!)
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
).apply {
|
||||
itemTitle.setText(R.string.characters)
|
||||
itemRecycler.adapter =
|
||||
CharacterAdapter(media.characters!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.staff.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.staff)
|
||||
bind.itemRecycler.adapter =
|
||||
AuthorAdapter(media.staff!!)
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
).apply {
|
||||
itemTitle.setText(R.string.staff)
|
||||
itemRecycler.adapter =
|
||||
AuthorAdapter(media.staff!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.recommended)
|
||||
bind.itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
).apply {
|
||||
itemTitle.setText(R.string.recommended)
|
||||
itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -569,6 +593,7 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onViewCreated(view, null)
|
||||
}
|
||||
|
||||
|
||||
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import java.util.Locale
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object MediaNameAdapter {
|
||||
|
||||
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
|
||||
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
|
||||
private const val REGEX_EPISODE =
|
||||
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
|
||||
|
||||
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val soft = subdubMatcher.group(1)
|
||||
val subdub = subdubMatcher.group(2)
|
||||
val bed = subdubMatcher.group(3) ?: ""
|
||||
|
||||
val toggled = when (typeToSetTo) {
|
||||
SubDubType.SUB -> "sub"
|
||||
SubDubType.DUB -> "dub"
|
||||
SubDubType.NULL -> ""
|
||||
}
|
||||
val toggledCasePreserved =
|
||||
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
||||
?.isUpperCase() == true
|
||||
) toggled.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.ROOT
|
||||
) else it.toString()
|
||||
} else toggled
|
||||
|
||||
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubDub(text: String): SubDubType {
|
||||
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
||||
when (subdub) {
|
||||
"sub" -> SubDubType.SUB
|
||||
"dub" -> SubDubType.DUB
|
||||
else -> SubDubType.NULL
|
||||
}
|
||||
} else {
|
||||
SubDubType.NULL
|
||||
}
|
||||
}
|
||||
|
||||
enum class SubDubType {
|
||||
SUB, DUB, NULL
|
||||
}
|
||||
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonPattern: Pattern = Pattern.compile(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun findEpisodeNumber(text: String): Float? {
|
||||
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, Pattern.CASE_INSENSITIVE)
|
||||
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||
|
||||
return if (episodeMatcher.find()) {
|
||||
if (episodeMatcher.group(2) != null) {
|
||||
episodeMatcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedEpisodeNumberPattern: Pattern =
|
||||
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||
val failedEpisodeNumberMatcher: Matcher =
|
||||
failedEpisodeNumberPattern.matcher(text)
|
||||
if (failedEpisodeNumberMatcher.find()) {
|
||||
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEpisodeNumber(text: String): String {
|
||||
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||
text
|
||||
}
|
||||
val letterPattern = Regex("[a-zA-Z]")
|
||||
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||
removedNumber
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeEpisodeNumberCompletely(text: String): String {
|
||||
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "")
|
||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||
val failedEpisodeNumberPattern =
|
||||
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
|
||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||
}
|
||||
} else {
|
||||
removedNumber
|
||||
}
|
||||
}
|
||||
|
||||
fun findChapterNumber(text: String): Float? {
|
||||
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
||||
return if (matcher.find()) {
|
||||
matcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedChapterNumberPattern: Pattern =
|
||||
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||
val failedChapterNumberMatcher: Matcher =
|
||||
failedChapterNumberPattern.matcher(text)
|
||||
if (failedChapterNumberMatcher.find()) {
|
||||
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import org.checkerframework.checker.units.qual.A
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
@@ -25,12 +26,16 @@ class OtherDetailsViewModel : ViewModel() {
|
||||
suspend fun loadAuthor(m: Author) {
|
||||
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
|
||||
}
|
||||
|
||||
private val voiceActor: MutableLiveData<Author> = MutableLiveData(null)
|
||||
fun getVoiceActor(): LiveData<Author> = voiceActor
|
||||
suspend fun loadVoiceActor(m: Author) {
|
||||
if (voiceActor.value == null) voiceActor.postValue(Anilist.query.getVoiceActorsDetails(m))
|
||||
}
|
||||
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
|
||||
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||
suspend fun loadCalendar() {
|
||||
val curr = System.currentTimeMillis() / 1000
|
||||
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
||||
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
|
||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||
val map = mutableMapOf<String, MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||
|
||||
@@ -70,11 +70,16 @@ class SearchActivity : AppCompatActivity() {
|
||||
intent.getStringExtra("type") ?: "ANIME",
|
||||
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
|
||||
onList = listOnly,
|
||||
search = intent.getStringExtra("query"),
|
||||
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
|
||||
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
|
||||
sort = intent.getStringExtra("sortBy"),
|
||||
status = intent.getStringExtra("status"),
|
||||
source = intent.getStringExtra("source"),
|
||||
countryOfOrigin = intent.getStringExtra("country"),
|
||||
season = intent.getStringExtra("season"),
|
||||
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
|
||||
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
|
||||
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
@@ -133,8 +138,12 @@ class SearchActivity : AppCompatActivity() {
|
||||
excludedTags = it.excludedTags
|
||||
tags = it.tags
|
||||
season = it.season
|
||||
startYear = it.startYear
|
||||
seasonYear = it.seasonYear
|
||||
status = it.status
|
||||
source = it.source
|
||||
format = it.format
|
||||
countryOfOrigin = it.countryOfOrigin
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -45,6 +47,19 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||
private lateinit var binding: ItemSearchHeaderBinding
|
||||
|
||||
private fun updateFilterTextViewDrawable() {
|
||||
val filterDrawable = when (activity.result.sort) {
|
||||
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
|
||||
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
|
||||
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
|
||||
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
|
||||
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
|
||||
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
|
||||
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
|
||||
else -> R.drawable.ic_round_filter_alt_24
|
||||
}
|
||||
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||
val binding =
|
||||
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
@@ -93,7 +108,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
binding.searchAdultCheck.isChecked = adult
|
||||
binding.searchList.isChecked = listOnly == true
|
||||
|
||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
|
||||
activity.updateChips = { it.update() }
|
||||
}
|
||||
|
||||
@@ -103,6 +118,59 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
binding.searchFilter.setOnClickListener {
|
||||
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
||||
}
|
||||
binding.searchFilter.setOnLongClickListener {
|
||||
val popupMenu = PopupMenu(activity, binding.searchFilter)
|
||||
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
|
||||
popupMenu.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.sort_by_score -> {
|
||||
activity.result.sort = Anilist.sortBy[0]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
R.id.sort_by_popular -> {
|
||||
activity.result.sort = Anilist.sortBy[1]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
R.id.sort_by_trending -> {
|
||||
activity.result.sort = Anilist.sortBy[2]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
R.id.sort_by_recent -> {
|
||||
activity.result.sort = Anilist.sortBy[3]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
R.id.sort_by_a_z -> {
|
||||
activity.result.sort = Anilist.sortBy[4]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
R.id.sort_by_z_a -> {
|
||||
activity.result.sort = Anilist.sortBy[5]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
R.id.sort_by_pure_pain -> {
|
||||
activity.result.sort = Anilist.sortBy[6]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
popupMenu.show()
|
||||
true
|
||||
}
|
||||
binding.searchByImage.setOnClickListener {
|
||||
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
|
||||
}
|
||||
@@ -257,7 +325,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
}
|
||||
|
||||
|
||||
class SearchChipAdapter(val activity: SearchActivity) :
|
||||
class SearchChipAdapter(val activity: SearchActivity, private val searchAdapter: SearchAdapter) :
|
||||
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||
private var chips = activity.result.toChipList()
|
||||
|
||||
@@ -274,11 +342,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
|
||||
val chip = chips[position]
|
||||
holder.binding.root.apply {
|
||||
text = chip.text
|
||||
text = chip.text.replace("_", " ")
|
||||
setOnClickListener {
|
||||
activity.result.removeChip(chip)
|
||||
update()
|
||||
activity.search()
|
||||
searchAdapter.updateFilterTextViewDrawable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,6 +356,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
fun update() {
|
||||
chips = activity.result.toChipList()
|
||||
notifyDataSetChanged()
|
||||
searchAdapter.updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = chips.size
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.PopupMenu
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -17,6 +21,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
|
||||
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
@@ -38,6 +45,54 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
private var exGenres = mutableListOf<String>()
|
||||
private var selectedTags = mutableListOf<String>()
|
||||
private var exTags = mutableListOf<String>()
|
||||
private fun updateChips() {
|
||||
binding.searchFilterGenres.adapter?.notifyDataSetChanged()
|
||||
binding.searchFilterTags.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun startBounceZoomAnimation(view: View? = null) {
|
||||
val targetView = view ?: binding.sortByFilter
|
||||
val bounceZoomAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
|
||||
targetView.startAnimation(bounceZoomAnimation)
|
||||
}
|
||||
|
||||
private fun setSortByFilterImage() {
|
||||
val filterDrawable = when (activity.result.sort) {
|
||||
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
|
||||
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
|
||||
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
|
||||
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
|
||||
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
|
||||
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
|
||||
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
|
||||
else -> R.drawable.ic_round_filter_alt_24
|
||||
}
|
||||
binding.sortByFilter.setImageResource(filterDrawable)
|
||||
}
|
||||
|
||||
private fun resetSearchFilter() {
|
||||
activity.result.sort = null
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
|
||||
startBounceZoomAnimation(binding.sortByFilter)
|
||||
activity.result.countryOfOrigin = null
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
|
||||
selectedGenres.clear()
|
||||
exGenres.clear()
|
||||
selectedTags.clear()
|
||||
exTags.clear()
|
||||
binding.searchStatus.setText("")
|
||||
binding.searchSource.setText("")
|
||||
binding.searchFormat.setText("")
|
||||
binding.searchSeason.setText("")
|
||||
binding.searchYear.setText("")
|
||||
binding.searchStatus.clearFocus()
|
||||
binding.searchFormat.clearFocus()
|
||||
binding.searchSeason.clearFocus()
|
||||
binding.searchYear.clearFocus()
|
||||
updateChips()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -47,14 +102,149 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
exGenres = activity.result.excludedGenres ?: mutableListOf()
|
||||
selectedTags = activity.result.tags ?: mutableListOf()
|
||||
exTags = activity.result.excludedTags ?: mutableListOf()
|
||||
setSortByFilterImage()
|
||||
|
||||
binding.resetSearchFilter.setOnClickListener {
|
||||
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||
rotateAnimation.duration = 500
|
||||
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
|
||||
rotateAnimation.start()
|
||||
resetSearchFilter()
|
||||
}
|
||||
|
||||
binding.resetSearchFilter.setOnLongClickListener {
|
||||
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||
rotateAnimation.duration = 500
|
||||
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
|
||||
rotateAnimation.start()
|
||||
val bounceAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
|
||||
|
||||
binding.resetSearchFilter.startAnimation(bounceAnimation)
|
||||
binding.resetSearchFilter.postDelayed({
|
||||
resetSearchFilter()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
activity.result.apply {
|
||||
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
|
||||
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
|
||||
format = binding.searchFormat.text.toString().ifBlank { null }
|
||||
season = binding.searchSeason.text.toString().ifBlank { null }
|
||||
startYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
sort = activity.result.sort
|
||||
genres = selectedGenres
|
||||
tags = selectedTags
|
||||
excludedGenres = exGenres
|
||||
excludedTags = exTags
|
||||
}
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
dismiss()
|
||||
}
|
||||
}, 500)
|
||||
true
|
||||
}
|
||||
|
||||
binding.sortByFilter.setOnClickListener {
|
||||
val popupMenu = PopupMenu(requireContext(), it)
|
||||
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.sort_by_score -> {
|
||||
activity.result.sort = Anilist.sortBy[0]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_popular -> {
|
||||
activity.result.sort = Anilist.sortBy[1]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_trending -> {
|
||||
activity.result.sort = Anilist.sortBy[2]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_recent -> {
|
||||
activity.result.sort = Anilist.sortBy[3]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_a_z -> {
|
||||
activity.result.sort = Anilist.sortBy[4]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_z_a -> {
|
||||
activity.result.sort = Anilist.sortBy[5]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_pure_pain -> {
|
||||
activity.result.sort = Anilist.sortBy[6]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
binding.countryFilter.setOnClickListener {
|
||||
val popupMenu = PopupMenu(requireContext(), it)
|
||||
popupMenu.menuInflater.inflate(R.menu.country_filter_menu, popupMenu.menu)
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.country_global -> {
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
R.id.country_china -> {
|
||||
activity.result.countryOfOrigin = "CN"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
R.id.country_south_korea -> {
|
||||
activity.result.countryOfOrigin = "KR"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
R.id.country_japan -> {
|
||||
activity.result.countryOfOrigin = "JP"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
R.id.country_taiwan -> {
|
||||
activity.result.countryOfOrigin = "TW"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
binding.searchFilterApply.setOnClickListener {
|
||||
activity.result.apply {
|
||||
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
|
||||
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
|
||||
format = binding.searchFormat.text.toString().ifBlank { null }
|
||||
sort = binding.searchSortBy.text.toString().ifBlank { null }
|
||||
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
|
||||
season = binding.searchSeason.text.toString().ifBlank { null }
|
||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
if (activity.result.type == "ANIME") {
|
||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
} else {
|
||||
startYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
}
|
||||
sort = activity.result.sort
|
||||
countryOfOrigin = activity.result.countryOfOrigin
|
||||
genres = selectedGenres
|
||||
tags = selectedTags
|
||||
excludedGenres = exGenres
|
||||
@@ -67,15 +257,22 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
binding.searchFilterCancel.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.searchSortBy.setText(activity.result.sort?.let {
|
||||
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
|
||||
})
|
||||
binding.searchSortBy.setAdapter(
|
||||
val format = if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
|
||||
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
|
||||
binding.searchStatus.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
resources.getStringArray(R.array.sort_by)
|
||||
format
|
||||
)
|
||||
)
|
||||
|
||||
binding.searchSource.setText(activity.result.source?.replace("_", " "))
|
||||
binding.searchSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
Anilist.source.toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -84,11 +281,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
|
||||
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
|
||||
if (activity.result.type == "ANIME") {
|
||||
binding.searchYear.setText(activity.result.seasonYear?.toString())
|
||||
} else {
|
||||
binding.searchYear.setText(activity.result.startYear?.toString())
|
||||
}
|
||||
binding.searchYear.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
|
||||
.reversed().toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
|
||||
else {
|
||||
binding.searchSeason.setText(activity.result.season)
|
||||
binding.searchSeason.setAdapter(
|
||||
@@ -98,16 +309,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
Anilist.seasons.toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
binding.searchYear.setText(activity.result.seasonYear?.toString())
|
||||
binding.searchYear.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
|
||||
.reversed().toTypedArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->
|
||||
|
||||
@@ -5,6 +5,7 @@ import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.snackString
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -51,21 +52,17 @@ class SubtitleDownloader {
|
||||
downloadedType: DownloadedType
|
||||
) {
|
||||
try {
|
||||
val directory = DownloadsManager.getDirectory(
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context,
|
||||
downloadedType.type,
|
||||
false,
|
||||
downloadedType.title,
|
||||
downloadedType.chapter
|
||||
)
|
||||
if (!directory.exists()) { //just in case
|
||||
directory.mkdirs()
|
||||
}
|
||||
) ?: throw Exception("Could not create directory")
|
||||
val type = loadSubtitleType(url)
|
||||
val subtiteFile = File(directory, "subtitle.${type}")
|
||||
if (subtiteFile.exists()) {
|
||||
subtiteFile.delete()
|
||||
}
|
||||
subtiteFile.createNewFile()
|
||||
directory.findFile("subtitle.${type}")?.delete()
|
||||
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
|
||||
?: throw Exception("Could not create subtitle file")
|
||||
|
||||
val client = Injekt.get<NetworkHelper>().client
|
||||
val request = Request.Builder().url(url).build()
|
||||
@@ -77,7 +74,8 @@ class SubtitleDownloader {
|
||||
}
|
||||
|
||||
reponse.body.byteStream().use { input ->
|
||||
subtiteFile.outputStream().use { output ->
|
||||
subtitleFile.openOutputStream(context, false).use { output ->
|
||||
if (output == null) throw Exception("Could not open output stream")
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import java.util.Locale
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AnimeNameAdapter {
|
||||
companion object {
|
||||
const val episodeRegex =
|
||||
"(episode|episodio|ep|e)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
const val failedEpisodeNumberRegex =
|
||||
"(?<!part\\s)\\b(\\d+)\\b"
|
||||
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||
|
||||
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val soft = subdubMatcher.group(1)
|
||||
val subdub = subdubMatcher.group(2)
|
||||
val bed = subdubMatcher.group(3) ?: ""
|
||||
|
||||
val toggled = when (typeToSetTo) {
|
||||
SubDubType.SUB -> "sub"
|
||||
SubDubType.DUB -> "dub"
|
||||
SubDubType.NULL -> ""
|
||||
}
|
||||
val toggledCasePreserved =
|
||||
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
||||
?.isUpperCase() == true
|
||||
) toggled.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.ROOT
|
||||
) else it.toString()
|
||||
} else toggled
|
||||
|
||||
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubDub(text: String): SubDubType {
|
||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
||||
when (subdub) {
|
||||
"sub" -> SubDubType.SUB
|
||||
"dub" -> SubDubType.DUB
|
||||
else -> SubDubType.NULL
|
||||
}
|
||||
} else {
|
||||
SubDubType.NULL
|
||||
}
|
||||
}
|
||||
|
||||
enum class SubDubType {
|
||||
SUB, DUB, NULL
|
||||
}
|
||||
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun findEpisodeNumber(text: String): Float? {
|
||||
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
|
||||
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||
|
||||
return if (episodeMatcher.find()) {
|
||||
if (episodeMatcher.group(2) != null) {
|
||||
episodeMatcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedEpisodeNumberPattern: Pattern =
|
||||
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||
val failedEpisodeNumberMatcher: Matcher =
|
||||
failedEpisodeNumberPattern.matcher(text)
|
||||
if (failedEpisodeNumberMatcher.find()) {
|
||||
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEpisodeNumber(text: String): String {
|
||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||
text
|
||||
}
|
||||
val letterPattern = Regex("[a-zA-Z]")
|
||||
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||
removedNumber
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeEpisodeNumberCompletely(text: String): String {
|
||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "")
|
||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||
val failedEpisodeNumberPattern =
|
||||
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||
}
|
||||
} else {
|
||||
removedNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,16 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.countDown
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.displayTimer
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.openSettings
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
@@ -51,7 +52,7 @@ class AnimeWatchAdapter(
|
||||
private val fragment: AnimeWatchFragment,
|
||||
private val watchSources: WatchSources
|
||||
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
|
||||
|
||||
private var autoSelect = true
|
||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||
private var _binding: ItemAnimeWatchBinding? = null
|
||||
|
||||
@@ -403,7 +404,7 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
val ep = media.anime.episodes!![continueEp]!!
|
||||
|
||||
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
||||
|
||||
binding.itemEpisodeImage.loadImage(
|
||||
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||
@@ -436,7 +437,8 @@ class AnimeWatchAdapter(
|
||||
val sourceFound = media.anime.episodes!!.isNotEmpty()
|
||||
binding.animeSourceNotFound.isGone = sourceFound
|
||||
binding.faqbutton.isGone = sourceFound
|
||||
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) {
|
||||
|
||||
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
|
||||
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
|
||||
val nextIndex = media.selected!!.sourceIndex + 1
|
||||
binding.animeSource.setText(binding.animeSource.adapter
|
||||
@@ -452,6 +454,7 @@ class AnimeWatchAdapter(
|
||||
fragment.loadEpisodes(nextIndex, false)
|
||||
}
|
||||
}
|
||||
binding.animeSource.setOnClickListener { autoSelect = false }
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
@@ -497,8 +500,7 @@ class AnimeWatchAdapter(
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
//Timer
|
||||
countDown(media, binding.animeSourceContainer)
|
||||
displayTimer(media, binding.animeSourceContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils
|
||||
@@ -34,13 +35,14 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
||||
@@ -53,6 +55,8 @@ import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
@@ -224,7 +228,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||
episode.desc =
|
||||
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
|
||||
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
|
||||
episode.title ?: ""
|
||||
).isBlank()
|
||||
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||
@@ -421,7 +425,19 @@ class AnimeWatchFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
activity?.let{
|
||||
if (!hasDirAccess(it)) {
|
||||
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||
if (success) {
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
} else {
|
||||
snackString("Permission is required to download")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
||||
@@ -441,8 +457,9 @@ class AnimeWatchFragment : Fragment() {
|
||||
i,
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
episodeAdapter.purgeDownload(i)
|
||||
) {
|
||||
episodeAdapter.purgeDownload(i)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -453,20 +470,15 @@ class AnimeWatchFragment : Fragment() {
|
||||
i,
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
||||
DownloadService.sendRemoveDownload(
|
||||
requireContext(),
|
||||
ExoplayerDownloadService::class.java,
|
||||
id,
|
||||
true
|
||||
)
|
||||
episodeAdapter.deleteDownload(i)
|
||||
) {
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
||||
episodeAdapter.deleteDownload(i)
|
||||
}
|
||||
}
|
||||
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
@@ -530,7 +542,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
for (download in downloadManager.animeDownloadedTypes) {
|
||||
if (download.title == media.mainName()) {
|
||||
if (download.title == media.mainName().findValidName()) {
|
||||
episodeAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.annotation.OptIn
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
@@ -18,9 +17,12 @@ import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import com.bumptech.glide.Glide
|
||||
@@ -55,15 +57,7 @@ class EpisodeAdapter(
|
||||
var arr: List<Episode> = arrayListOf(),
|
||||
var offlineMode: Boolean
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private lateinit var index: DownloadIndex
|
||||
|
||||
|
||||
init {
|
||||
if (offlineMode) {
|
||||
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
|
||||
}
|
||||
}
|
||||
val context = fragment.requireContext()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return (when (viewType) {
|
||||
@@ -102,7 +96,7 @@ class EpisodeAdapter(
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val ep = arr[position]
|
||||
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
|
||||
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||
ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
||||
} else {
|
||||
ep.number
|
||||
} ?: ""
|
||||
@@ -247,17 +241,8 @@ class EpisodeAdapter(
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||
if (position != -1) {
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
|
||||
media.mainName(),
|
||||
episodeNumber
|
||||
)
|
||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
val size = try {
|
||||
val download = index.getDownload(id)
|
||||
bytesToHuman(download?.bytesDownloaded ?: 0)
|
||||
bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ import ani.dantotsu.connections.discord.RPC
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.getCurrentBrightnessValue
|
||||
import ani.dantotsu.hideSystemBars
|
||||
@@ -113,6 +113,8 @@ import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.others.AniSkip
|
||||
@@ -393,7 +395,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
isCastApiAvailable = GoogleApiAvailability.getInstance()
|
||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
|
||||
castContext =
|
||||
CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
|
||||
castPlayer = CastPlayer(castContext!!)
|
||||
castPlayer!!.setSessionAvailabilityListener(this)
|
||||
} catch (e: Exception) {
|
||||
@@ -441,41 +444,43 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN)
|
||||
|
||||
if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) {
|
||||
if (PrefManager.getVal(PrefName.RotationPlayer)) {
|
||||
orientationListener =
|
||||
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
when (orientation) {
|
||||
in 45..135 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
if (PrefManager.getVal(PrefName.RotationPlayer)) {
|
||||
orientationListener =
|
||||
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
when (orientation) {
|
||||
in 45..135 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
}
|
||||
|
||||
in 225..315 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
in 315..360, in 0..45 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
}
|
||||
in 225..315 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
in 315..360, in 0..45 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
}
|
||||
}
|
||||
orientationListener?.enable()
|
||||
}
|
||||
orientationListener?.enable()
|
||||
}
|
||||
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
exoRotate.setOnClickListener {
|
||||
requestedOrientation = rotation
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
exoRotate.setOnClickListener {
|
||||
requestedOrientation = rotation
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
setupSubFormatting(playerView)
|
||||
|
||||
@@ -998,7 +1003,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
episodeTitleArr = arrayListOf()
|
||||
episodes.forEach {
|
||||
val episode = it.value
|
||||
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
|
||||
val cleanedTitle = MediaNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
|
||||
episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotBlank() && cleanedTitle != "null") ": $cleanedTitle" else ""}")
|
||||
}
|
||||
|
||||
@@ -1083,35 +1088,48 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
|
||||
lifecycleScope.launch {
|
||||
val presence = RPC.createPresence(RPC.Companion.RPCData(
|
||||
applicationId = Discord.application_Id,
|
||||
type = RPC.Type.WATCHING,
|
||||
activityName = media.userPreferredName,
|
||||
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
|
||||
R.string.episode_num,
|
||||
ep.number
|
||||
),
|
||||
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
|
||||
largeImage = media.cover?.let {
|
||||
RPC.Link(
|
||||
media.userPreferredName,
|
||||
it
|
||||
)
|
||||
},
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
Discord.small_Image
|
||||
),
|
||||
buttons = mutableListOf(
|
||||
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
|
||||
val buttons = when (discordMode) {
|
||||
"nothing" -> mutableListOf(
|
||||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||
RPC.Link(
|
||||
"Stream on Dantotsu",
|
||||
getString(R.string.github)
|
||||
)
|
||||
|
||||
"dantotsu" -> mutableListOf(
|
||||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
|
||||
)
|
||||
|
||||
"anilist" -> {
|
||||
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
|
||||
val anilistLink = "https://anilist.co/user/$userId/"
|
||||
mutableListOf(
|
||||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||
RPC.Link("View My AniList", anilistLink)
|
||||
)
|
||||
}
|
||||
|
||||
else -> mutableListOf()
|
||||
}
|
||||
val presence = RPC.createPresence(
|
||||
RPC.Companion.RPCData(
|
||||
applicationId = Discord.application_Id,
|
||||
type = RPC.Type.WATCHING,
|
||||
activityName = media.userPreferredName,
|
||||
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
|
||||
R.string.episode_num,
|
||||
ep.number
|
||||
),
|
||||
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
|
||||
largeImage = media.cover?.let {
|
||||
RPC.Link(
|
||||
media.userPreferredName,
|
||||
it
|
||||
)
|
||||
},
|
||||
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
|
||||
buttons = buttons
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val intent = Intent(context, DiscordService::class.java).apply {
|
||||
putExtra("presence", presence)
|
||||
}
|
||||
@@ -1119,7 +1137,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
@@ -1156,7 +1173,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
if (PrefManager.getVal(PrefName.Cast)) {
|
||||
playerView.findViewById<CustomCastButton>(R.id.exo_cast).apply {
|
||||
visibility = View.VISIBLE
|
||||
if(PrefManager.getVal(PrefName.UseInternalCast)) {
|
||||
if (PrefManager.getVal(PrefName.UseInternalCast)) {
|
||||
try {
|
||||
CastButtonFactory.setUpMediaRouteButton(context, this)
|
||||
dialogFactory = CustomCastThemeFactory()
|
||||
@@ -1319,7 +1336,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
|
||||
val list = (PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
List::class.java
|
||||
) as List<Int>).toMutableList()
|
||||
if (list.contains(media.id)) list.remove(media.id)
|
||||
list.add(media.id)
|
||||
PrefManager.setCustomVal("continueAnimeList", list)
|
||||
@@ -1413,7 +1434,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
|
||||
cacheFactory = CacheDataSource.Factory().apply {
|
||||
setCache(Helper.getSimpleCache(this@ExoplayerView))
|
||||
setCache(VideoCache.getInstance(this@ExoplayerView))
|
||||
if (ext.server.offline) {
|
||||
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
|
||||
} else {
|
||||
@@ -1430,15 +1451,28 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
|
||||
val downloadedMediaItem = if (ext.server.offline) {
|
||||
val key = ext.server.name
|
||||
downloadId = PrefManager.getAnimeDownloadPreferences()
|
||||
.getString(key, null)
|
||||
if (downloadId != null) {
|
||||
Helper.downloadManager(this)
|
||||
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
|
||||
val titleName = ext.server.name.split("/").first()
|
||||
val episodeName = ext.server.name.split("/").last()
|
||||
|
||||
val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
|
||||
if (directory != null) {
|
||||
val files = directory.listFiles()
|
||||
println(files)
|
||||
val docFile = directory.listFiles().firstOrNull {
|
||||
it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
|
||||
}
|
||||
if (docFile != null) {
|
||||
val uri = docFile.uri
|
||||
MediaItem.Builder().setUri(uri).setMimeType(mimeType).build()
|
||||
} else {
|
||||
snackString("File not found")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
snackString("Download not found")
|
||||
snackString("Directory not found")
|
||||
null
|
||||
}
|
||||
|
||||
} else null
|
||||
|
||||
mediaItem = if (downloadedMediaItem == null) {
|
||||
@@ -1813,7 +1847,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
|
||||
if (!functionstarted && !disappeared && PrefManager.getVal(PrefName.AutoHideTimeStamps)) {
|
||||
disappearSkip()
|
||||
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)){
|
||||
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)) {
|
||||
skipTimeButton.visibility = View.VISIBLE
|
||||
exoSkip.visibility = View.GONE
|
||||
skipTimeText.text = new.skipType.getType()
|
||||
@@ -2152,11 +2186,16 @@ class CustomCastButton : MediaRouteButton {
|
||||
fun setCastCallback(castCallback: () -> Unit) {
|
||||
this.castCallback = castCallback
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
return if (PrefManager.getVal(PrefName.UseInternalCast)) {
|
||||
|
||||
@@ -30,10 +30,13 @@ import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||
import ani.dantotsu.databinding.ItemStreamBinding
|
||||
import ani.dantotsu.databinding.ItemUrlBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
@@ -376,6 +379,45 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
} else {
|
||||
binding.urlDownload.visibility = View.GONE
|
||||
}
|
||||
val subtitles = extractor.subtitles
|
||||
if (subtitles.isNotEmpty()) {
|
||||
binding.urlSub.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.urlSub.visibility = View.GONE
|
||||
}
|
||||
binding.urlSub.setOnClickListener {
|
||||
if (subtitles.isNotEmpty()) {
|
||||
val subtitleNames = subtitles.map { it.language }
|
||||
var subtitleToDownload: Subtitle? = null
|
||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Subtitle")
|
||||
.setSingleChoiceItems(
|
||||
subtitleNames.toTypedArray(),
|
||||
-1
|
||||
) { _, which ->
|
||||
subtitleToDownload = subtitles[which]
|
||||
}
|
||||
.setPositiveButton("Download") { dialog, _ ->
|
||||
scope.launch {
|
||||
if (subtitleToDownload != null) {
|
||||
SubtitleDownloader.downloadSubtitle(
|
||||
requireContext(),
|
||||
subtitleToDownload!!.file.url,
|
||||
DownloadedType(media!!.mainName(), media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number, MediaType.ANIME)
|
||||
)
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
snackString("No Subtitles Available")
|
||||
}
|
||||
}
|
||||
binding.urlDownload.setSafeOnClickListener {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||
extractor.server.name
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import android.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
@@ -35,13 +36,15 @@ import java.util.TimeZone
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class CommentItem(val comment: Comment,
|
||||
private val markwon: Markwon,
|
||||
val parentSection: Section,
|
||||
private val commentsFragment: CommentsFragment,
|
||||
private val backgroundColor: Int,
|
||||
val commentDepth: Int
|
||||
) : BindableItem<ItemCommentsBinding>() {
|
||||
class CommentItem(
|
||||
val comment: Comment,
|
||||
private val markwon: Markwon,
|
||||
val parentSection: Section,
|
||||
private val commentsFragment: CommentsFragment,
|
||||
private val backgroundColor: Int,
|
||||
val commentDepth: Int
|
||||
) :
|
||||
BindableItem<ItemCommentsBinding>() {
|
||||
lateinit var binding: ItemCommentsBinding
|
||||
val adapter = GroupieAdapter()
|
||||
private var subCommentIds: MutableList<Int> = mutableListOf()
|
||||
@@ -63,9 +66,6 @@ class CommentItem(val comment: Comment,
|
||||
val isUserComment = CommentsAPI.userId == comment.userId
|
||||
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
|
||||
markwon.setMarkdown(viewBinding.commentText, comment.content)
|
||||
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
|
||||
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
|
||||
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
|
||||
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||
if (comment.tag == null) {
|
||||
viewBinding.commentUserTagLayout.visibility = View.GONE
|
||||
@@ -157,41 +157,71 @@ class CommentItem(val comment: Comment,
|
||||
Toast.makeText(context, "Owner", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
viewBinding.commentDelete.setOnClickListener {
|
||||
dialogBuilder(getAppString(R.string.delete_comment), getAppString(R.string.delete_comment_confirm)) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.deleteComment(comment.commentId)
|
||||
if (success) {
|
||||
snackString(R.string.comment_deleted)
|
||||
parentSection.remove(this@CommentItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewBinding.commentBanUser.setOnClickListener {
|
||||
dialogBuilder(getAppString(R.string.ban_user), getAppString(R.string.ban_user_confirm)) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.banUser(comment.userId)
|
||||
if (success) {
|
||||
snackString(R.string.user_banned)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewBinding.commentReport.setOnClickListener {
|
||||
dialogBuilder(getAppString(R.string.report_comment), getAppString(R.string.report_comment_confirm)) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.reportComment(
|
||||
comment.commentId,
|
||||
comment.username,
|
||||
commentsFragment.mediaName,
|
||||
comment.userId
|
||||
)
|
||||
if (success) {
|
||||
snackString(R.string.comment_reported)
|
||||
viewBinding.commentInfo.setOnClickListener {
|
||||
val popup = PopupMenu(commentsFragment.requireContext(), viewBinding.commentInfo)
|
||||
popup.menuInflater.inflate(R.menu.profile_details_menu, popup.menu)
|
||||
popup.menu.findItem(R.id.commentDelete)?.isVisible = isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod
|
||||
popup.menu.findItem(R.id.commentBanUser)?.isVisible = (CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment
|
||||
popup.menu.findItem(R.id.commentReport)?.isVisible = !isUserComment
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.commentReport -> {
|
||||
dialogBuilder(
|
||||
getAppString(R.string.report_comment),
|
||||
getAppString(R.string.report_comment_confirm)
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.reportComment(
|
||||
comment.commentId,
|
||||
comment.username,
|
||||
commentsFragment.mediaName,
|
||||
comment.userId
|
||||
)
|
||||
if (success) {
|
||||
snackString(R.string.comment_reported)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.commentDelete -> {
|
||||
dialogBuilder(
|
||||
getAppString(R.string.delete_comment),
|
||||
getAppString(R.string.delete_comment_confirm)
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.deleteComment(comment.commentId)
|
||||
if (success) {
|
||||
snackString(R.string.comment_deleted)
|
||||
parentSection.remove(this@CommentItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.commentBanUser -> {
|
||||
dialogBuilder(
|
||||
getAppString(R.string.ban_user),
|
||||
getAppString(R.string.ban_user_confirm)
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.banUser(comment.userId)
|
||||
if (success) {
|
||||
snackString(R.string.user_banned)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
//fill the icon if the user has liked the comment
|
||||
setVoteButtons(viewBinding)
|
||||
@@ -227,7 +257,6 @@ class CommentItem(val comment: Comment,
|
||||
comment.upvotes -= 1
|
||||
}
|
||||
comment.downvotes += if (voteType == -1) 1 else -1
|
||||
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class CommentsFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
activity = requireActivity() as MediaDetailsActivity
|
||||
|
||||
binding.commentsList.setBaseline(activity.navBar, activity.binding.commentInputLayout)
|
||||
binding.commentsListContainer.setBaseline(activity.navBar, activity.binding.commentInputLayout)
|
||||
|
||||
//get the media id from the intent
|
||||
val mediaId = arguments?.getInt("mediaId") ?: -1
|
||||
@@ -370,7 +370,6 @@ class CommentsFragment : Fragment() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
tag = null
|
||||
binding.commentsList.setBaseline(activity.navBar, activity.binding.commentInputLayout)
|
||||
section.groups.forEach {
|
||||
if (it is CommentItem && it.containsGif()) {
|
||||
it.notifyChanged()
|
||||
|
||||
@@ -15,6 +15,7 @@ import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemChapterListBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.setAnimation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -267,10 +268,10 @@ class MangaChapterAdapter(
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
val ep = arr[position]
|
||||
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
|
||||
val parsedNumber = MediaNameAdapter.findChapterNumber(ep.number)?.toInt()
|
||||
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
|
||||
if (media.userProgress != null) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number)
|
||||
if ((MediaNameAdapter.findChapterNumber(ep.number)
|
||||
?: 9999f) <= media.userProgress!!.toFloat()
|
||||
)
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
@@ -279,7 +280,7 @@ class MangaChapterAdapter(
|
||||
binding.itemEpisodeCont.setOnLongClickListener {
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
MediaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
)
|
||||
true
|
||||
}
|
||||
@@ -315,7 +316,7 @@ class MangaChapterAdapter(
|
||||
} else binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
|
||||
if (media.userProgress != null) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number)
|
||||
if ((MediaNameAdapter.findChapterNumber(ep.number)
|
||||
?: 9999f) <= media.userProgress!!.toFloat()
|
||||
) {
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
@@ -326,7 +327,7 @@ class MangaChapterAdapter(
|
||||
binding.root.setOnLongClickListener {
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
MediaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class MangaNameAdapter {
|
||||
companion object {
|
||||
private const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
|
||||
private const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
|
||||
fun findChapterNumber(text: String): Float? {
|
||||
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
||||
return if (matcher.find()) {
|
||||
matcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedChapterNumberPattern: Pattern =
|
||||
Pattern.compile(filedChapterNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||
val failedChapterNumberMatcher: Matcher =
|
||||
failedChapterNumberPattern.matcher(text)
|
||||
if (failedChapterNumberMatcher.find()) {
|
||||
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.media.anime.handleProgress
|
||||
import ani.dantotsu.openSettings
|
||||
@@ -385,8 +386,8 @@ class MangaReadAdapter(
|
||||
)
|
||||
}
|
||||
|
||||
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
|
||||
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
|
||||
val startChapter = MediaNameAdapter.findChapterNumber(names[limit * (position)])
|
||||
val endChapter = MediaNameAdapter.findChapterNumber(names[last - 1])
|
||||
val startChapterString = if (startChapter != null) {
|
||||
"Ch.$startChapter"
|
||||
} else {
|
||||
@@ -448,7 +449,7 @@ class MangaReadAdapter(
|
||||
chapter.scanlator !in hiddenScanlators
|
||||
}
|
||||
val formattedChapters = filteredChapters.map {
|
||||
MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
|
||||
MediaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
|
||||
}
|
||||
if (formattedChapters.contains(continueEp)) {
|
||||
continueEp = chapters[formattedChapters.indexOf(continueEp)]
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -34,6 +35,7 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||
import ani.dantotsu.dp
|
||||
@@ -41,6 +43,7 @@ import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
@@ -55,6 +58,8 @@ import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
@@ -189,7 +194,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
)
|
||||
|
||||
for (download in downloadManager.mangaDownloadedTypes) {
|
||||
if (download.title == media.mainName()) {
|
||||
if (download.title == media.mainName().findValidName()) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
}
|
||||
@@ -227,7 +232,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
//filter by selected language
|
||||
val progressChapterIndex = (chapters?.indexOfFirst {
|
||||
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||
} ?: 0) + 1
|
||||
|
||||
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
|
||||
@@ -433,51 +438,65 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
|
||||
fun onMangaChapterDownloadClick(i: String) {
|
||||
if (!isNotificationPermissionGranted()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let { chapter ->
|
||||
val parser =
|
||||
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
|
||||
parser?.let {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val images = parser.imageList(chapter.sChapter)
|
||||
|
||||
// Create a download task
|
||||
val downloadTask = MangaDownloaderService.DownloadTask(
|
||||
title = media.mainName(),
|
||||
chapter = chapter.title!!,
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
retries = 2,
|
||||
simultaneousDownloads = 2
|
||||
activity?.let {
|
||||
if (!isNotificationPermissionGranted()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
it,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
fun continueDownload() {
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let { chapter ->
|
||||
val parser =
|
||||
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
|
||||
parser?.let {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val images = parser.imageList(chapter.sChapter)
|
||||
|
||||
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||
// Create a download task
|
||||
val downloadTask = MangaDownloaderService.DownloadTask(
|
||||
title = media.mainName(),
|
||||
chapter = chapter.title!!,
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
retries = 2,
|
||||
simultaneousDownloads = 2
|
||||
)
|
||||
|
||||
// If the service is not already running, start it
|
||||
if (!MangaServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, MangaDownloaderService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(requireContext(), intent)
|
||||
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||
|
||||
// If the service is not already running, start it
|
||||
if (!MangaServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, MangaDownloaderService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(requireContext(), intent)
|
||||
}
|
||||
MangaServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
|
||||
// Inform the adapter that the download has started
|
||||
withContext(Dispatchers.Main) {
|
||||
chapterAdapter.startDownload(i)
|
||||
}
|
||||
}
|
||||
MangaServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
|
||||
// Inform the adapter that the download has started
|
||||
withContext(Dispatchers.Main) {
|
||||
chapterAdapter.startDownload(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasDirAccess(it)) {
|
||||
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||
if (success) {
|
||||
continueDownload()
|
||||
} else {
|
||||
snackString("Permission is required to download")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continueDownload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,8 +518,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
i,
|
||||
MediaType.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.deleteDownload(i)
|
||||
) {
|
||||
chapterAdapter.deleteDownload(i)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterStopDownloadClick(i: String) {
|
||||
@@ -517,8 +537,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
i,
|
||||
MediaType.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.purgeDownload(i)
|
||||
) {
|
||||
chapterAdapter.purgeDownload(i)
|
||||
}
|
||||
}
|
||||
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.net.Uri
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -36,7 +37,18 @@ abstract class BaseImageAdapter(
|
||||
chapter: MangaChapter
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
val settings = activity.defaultSettings
|
||||
val images = chapter.images()
|
||||
private val chapterImages = chapter.images()
|
||||
var images = chapterImages
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
images = if (settings.layout == CurrentReaderSettings.Layouts.PAGED
|
||||
&& settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) {
|
||||
chapterImages.reversed()
|
||||
} else {
|
||||
chapterImages
|
||||
}
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
@@ -165,6 +177,10 @@ abstract class BaseImageAdapter(
|
||||
it.load(localFile.absoluteFile)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else if (link.url.startsWith("content://")) {
|
||||
it.load(Uri.parse(link.url))
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
mangaCache.get(link.url)?.let { imageData ->
|
||||
val bitmap = imageData.fetchAndProcessImage(
|
||||
@@ -175,6 +191,7 @@ abstract class BaseImageAdapter(
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
?.let {
|
||||
@@ -207,5 +224,4 @@ abstract class BaseImageAdapter(
|
||||
return newBitmap
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,9 +57,9 @@ import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaSingleton
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
@@ -129,6 +129,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
var sliding = false
|
||||
var isAnimating = false
|
||||
|
||||
private val directionRLBT get() = defaultSettings.direction == RIGHT_TO_LEFT
|
||||
|| defaultSettings.direction == BOTTOM_TO_TOP
|
||||
private val directionPagedBT get() = defaultSettings.layout == CurrentReaderSettings.Layouts.PAGED
|
||||
&& defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||
@@ -180,7 +185,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
val chapter = (MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
val chapter = (MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
?.minus(1L) ?: 0).toString()
|
||||
if (chapter == "0.0" && PrefManager.getVal(PrefName.ChapterZeroReader)
|
||||
// Not asking individually or incognito
|
||||
@@ -224,8 +229,13 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
|
||||
?: 1))
|
||||
else
|
||||
binding.mangaReaderPager.currentItem =
|
||||
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
|
||||
if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP ) {
|
||||
binding.mangaReaderPager.currentItem =
|
||||
(maxChapterPage.toInt() - value.toInt()) / (dualPage { 2 } ?: 1)
|
||||
} else {
|
||||
binding.mangaReaderPager.currentItem =
|
||||
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
|
||||
}
|
||||
pageSliderHide()
|
||||
}
|
||||
}
|
||||
@@ -331,7 +341,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderNextChapter.setOnClickListener {
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
if (directionRLBT) {
|
||||
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
|
||||
else snackString(getString(R.string.first_chapter))
|
||||
} else {
|
||||
@@ -344,7 +354,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderPreviousChapter.setOnClickListener {
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
if (directionRLBT) {
|
||||
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
|
||||
else snackString(getString(R.string.next_chapter_not_found))
|
||||
} else {
|
||||
@@ -361,16 +371,12 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
PrefManager.setCustomVal("${media.id}_current_chp", chap.number)
|
||||
currentChapterIndex = chaptersArr.indexOf(chap.number)
|
||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
if (directionRLBT) {
|
||||
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
} else {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
}
|
||||
applySettings()
|
||||
val context = this
|
||||
@@ -378,6 +384,25 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
|
||||
lifecycleScope.launch {
|
||||
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
|
||||
val buttons = when (discordMode) {
|
||||
"nothing" -> mutableListOf(
|
||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
)
|
||||
"dantotsu" -> mutableListOf(
|
||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
RPC.Link("Read on Dantotsu", getString(R.string.dantotsu))
|
||||
)
|
||||
"anilist" -> {
|
||||
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
|
||||
val anilistLink = "https://anilist.co/user/$userId/"
|
||||
mutableListOf(
|
||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
RPC.Link("View My AniList", anilistLink)
|
||||
)
|
||||
}
|
||||
else -> mutableListOf()
|
||||
}
|
||||
val presence = RPC.createPresence(
|
||||
RPC.Companion.RPCData(
|
||||
applicationId = Discord.application_Id,
|
||||
@@ -386,20 +411,9 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
details = chap.title?.takeIf { it.isNotEmpty() }
|
||||
?: getString(R.string.chapter_num, chap.number),
|
||||
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
|
||||
largeImage = media.cover?.let { cover ->
|
||||
RPC.Link(media.userPreferredName, cover)
|
||||
},
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
Discord.small_Image
|
||||
),
|
||||
buttons = mutableListOf(
|
||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
RPC.Link(
|
||||
"Stream on Dantotsu",
|
||||
getString(R.string.github)
|
||||
)
|
||||
)
|
||||
largeImage = media.cover?.let { cover -> RPC.Link(media.userPreferredName, cover) },
|
||||
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
|
||||
buttons = buttons
|
||||
)
|
||||
)
|
||||
val intent = Intent(context, DiscordService::class.java).apply {
|
||||
@@ -455,7 +469,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
currentChapterPage = PrefManager.getCustomVal("${media.id}_${chapter.number}", 1L)
|
||||
|
||||
val chapImages = chapter.images()
|
||||
val chapImages = if (directionPagedBT) {
|
||||
chapter.images().reversed()
|
||||
} else {
|
||||
chapter.images()
|
||||
}
|
||||
|
||||
maxChapterPage = 0
|
||||
if (chapImages.isNotEmpty()) {
|
||||
@@ -479,7 +497,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
}
|
||||
|
||||
val currentPage = currentChapterPage.toInt()
|
||||
val currentPage = if (directionPagedBT) {
|
||||
maxChapterPage - currentChapterPage + 1
|
||||
} else {
|
||||
currentChapterPage
|
||||
}.toInt()
|
||||
|
||||
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)) {
|
||||
binding.mangaReaderSwipy.vertical = true
|
||||
@@ -508,10 +530,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.mangaReaderSwipy.onTopSwiped = {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderSwipy.onBottomSwiped = {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
}
|
||||
binding.mangaReaderSwipy.topBeingSwiped = { value ->
|
||||
@@ -620,7 +642,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
RecyclerView.VERTICAL
|
||||
else
|
||||
RecyclerView.HORIZONTAL,
|
||||
!(defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == LEFT_TO_RIGHT)
|
||||
directionRLBT
|
||||
)
|
||||
manager.preloadItemCount = 5
|
||||
|
||||
@@ -637,6 +659,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
else false
|
||||
}
|
||||
|
||||
manager.setStackFromEnd(defaultSettings.direction == BOTTOM_TO_TOP)
|
||||
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
|
||||
defaultSettings.apply {
|
||||
@@ -691,9 +715,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
visibility = View.VISIBLE
|
||||
adapter = imageAdapter
|
||||
layoutDirection =
|
||||
if (defaultSettings.direction == BOTTOM_TO_TOP || defaultSettings.direction == RIGHT_TO_LEFT)
|
||||
View.LAYOUT_DIRECTION_RTL
|
||||
else View.LAYOUT_DIRECTION_LTR
|
||||
if (directionRLBT) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
|
||||
orientation =
|
||||
if (defaultSettings.direction == LEFT_TO_RIGHT || defaultSettings.direction == RIGHT_TO_LEFT)
|
||||
ViewPager2.ORIENTATION_HORIZONTAL
|
||||
@@ -782,7 +804,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
||||
if (screenWidth / 5 in x + 1..<y) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
|
||||
PressPos.RIGHT
|
||||
} else {
|
||||
PressPos.LEFT
|
||||
@@ -790,7 +812,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
|
||||
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
|
||||
PressPos.LEFT
|
||||
} else {
|
||||
PressPos.RIGHT
|
||||
@@ -886,9 +908,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
binding.mangaReaderSlider.layoutDirection =
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP)
|
||||
if (directionRLBT)
|
||||
View.LAYOUT_DIRECTION_RTL
|
||||
else View.LAYOUT_DIRECTION_LTR
|
||||
else
|
||||
View.LAYOUT_DIRECTION_LTR
|
||||
shouldShow?.apply { isContVisible = !this }
|
||||
if (isContVisible) {
|
||||
isContVisible = false
|
||||
@@ -896,12 +919,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
isAnimating = true
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
|
||||
.setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(
|
||||
binding.mangaReaderBottomLayout,
|
||||
"translationY",
|
||||
0f,
|
||||
128f
|
||||
)
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
@@ -921,7 +939,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private var loading = false
|
||||
fun updatePageNumber(page: Long) {
|
||||
fun updatePageNumber(pageNumber: Long) {
|
||||
var page = pageNumber
|
||||
if (directionPagedBT) {
|
||||
page = maxChapterPage - pageNumber + 1
|
||||
}
|
||||
if (currentChapterPage != page) {
|
||||
currentChapterPage = page
|
||||
PrefManager.setCustomVal("${media.id}_${chapter.number}", page)
|
||||
@@ -969,7 +991,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
PrefManager.setCustomVal("${media.id}_save_progress", true)
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
.toString()
|
||||
)
|
||||
dialog.dismiss()
|
||||
@@ -991,7 +1013,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
)
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
.toString()
|
||||
)
|
||||
runnable.run()
|
||||
@@ -1086,4 +1108,4 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
@@ -94,23 +95,23 @@ class NovelReadFragment : Fragment(),
|
||||
)
|
||||
)
|
||||
) {
|
||||
val file = File(
|
||||
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
|
||||
)
|
||||
if (!file.exists()) return false
|
||||
val fileUri = FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
"${requireContext().packageName}.provider",
|
||||
file
|
||||
)
|
||||
val intent = Intent(context, NovelReaderActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(fileUri, "application/epub+zip")
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
try {
|
||||
val directory =
|
||||
DownloadsManager.getSubDirectory(context?:currContext()!!, MediaType.NOVEL, false, novel.name)
|
||||
val file = directory?.findFile(novel.name)
|
||||
if (file?.exists() == false) return false
|
||||
val fileUri = file?.uri ?: return false
|
||||
val intent = Intent(context, NovelReaderActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(fileUri, "application/epub+zip")
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
startActivity(intent)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
return false
|
||||
}
|
||||
startActivity(intent)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@@ -135,7 +136,7 @@ class NovelReadFragment : Fragment(),
|
||||
novel.name,
|
||||
MediaType.NOVEL
|
||||
)
|
||||
)
|
||||
) {}
|
||||
}
|
||||
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
|
||||
@@ -46,11 +46,11 @@ class CommentNotificationTask : Task {
|
||||
)
|
||||
|
||||
notifications =
|
||||
notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
|
||||
notifications?.filter { !it.type.isGlobal() || it.notificationId > recentGlobal }
|
||||
?.toMutableList()
|
||||
|
||||
val newRecentGlobal =
|
||||
notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
|
||||
notifications?.filter { it.type.isGlobal() }?.maxOfOrNull { it.notificationId }
|
||||
if (newRecentGlobal != null) {
|
||||
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
|
||||
}
|
||||
@@ -313,4 +313,6 @@ class CommentNotificationTask : Task {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int?.isGlobal() = this == 3 || this == 420
|
||||
}
|
||||
@@ -3,13 +3,11 @@ package ani.dantotsu.notifications.subscription
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.Selected
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.Episode
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaChapter
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
@@ -105,7 +103,7 @@ class SubscriptionHelper {
|
||||
}
|
||||
|
||||
return chp?.apply {
|
||||
selected.latest = MangaNameAdapter.findChapterNumber(number) ?: 0f
|
||||
selected.latest = MediaNameAdapter.findChapterNumber(number) ?: 0f
|
||||
saveSelected(id, selected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,16 +36,8 @@ object Download {
|
||||
}
|
||||
|
||||
private fun getDownloadDir(context: Context): File {
|
||||
val direct: File
|
||||
if (PrefManager.getVal(PrefName.SdDl)) {
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
|
||||
val parentDirectory = arrayOfFiles[1].toString()
|
||||
direct = File(parentDirectory)
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
} else {
|
||||
direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
}
|
||||
val direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
return direct
|
||||
}
|
||||
|
||||
@@ -96,52 +88,10 @@ object Download {
|
||||
when (PrefManager.getVal(PrefName.DownloadManager) as Int) {
|
||||
1 -> oneDM(context, file, notif ?: fileName)
|
||||
2 -> adm(context, file, fileName, folder)
|
||||
else -> defaultDownload(context, file, fileName, folder, notif ?: fileName)
|
||||
else -> oneDM(context, file, notif ?: fileName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun defaultDownload(
|
||||
context: Context,
|
||||
file: FileUrl,
|
||||
fileName: String,
|
||||
folder: String,
|
||||
notif: String
|
||||
) {
|
||||
val manager =
|
||||
context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val request: DownloadManager.Request = DownloadManager.Request(Uri.parse(file.url))
|
||||
file.headers.forEach {
|
||||
request.addRequestHeader(it.key, it.value)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
|
||||
if (PrefManager.getVal(PrefName.SdDl) && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
|
||||
val parentDirectory = arrayOfFiles[1].toString() + folder
|
||||
val direct = File(parentDirectory)
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
request.setDestinationUri(Uri.fromFile(File("$parentDirectory$fileName")))
|
||||
} else {
|
||||
val direct = File(Environment.DIRECTORY_DOWNLOADS + "/Dantotsu$folder")
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
request.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
"/Dantotsu$folder$fileName"
|
||||
)
|
||||
}
|
||||
request.setTitle(notif)
|
||||
manager.enqueue(request)
|
||||
toast(currContext()?.getString(R.string.started_downloading, notif))
|
||||
} catch (e: SecurityException) {
|
||||
toast(currContext()?.getString(R.string.permission_required))
|
||||
} catch (e: Exception) {
|
||||
toast(e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun oneDM(context: Context, file: FileUrl, notif: String) {
|
||||
val appName =
|
||||
if (isPackageInstalled("idm.internet.download.manager.plus", context.packageManager)) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.BottomSheetImageBinding
|
||||
import ani.dantotsu.downloadsPermission
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
||||
@@ -22,6 +21,7 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.shareImage
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -12,8 +12,8 @@ import ani.dantotsu.R
|
||||
class Xpandable @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
var expanded: Boolean = false
|
||||
private var listener: OnChangeListener? = null
|
||||
private var expanded: Boolean = false
|
||||
private var listeners: ArrayList<OnChangeListener> = arrayListOf()
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.Xpandable) {
|
||||
@@ -50,7 +50,9 @@ class Xpandable @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
postDelayed({
|
||||
listener?.onRetract()
|
||||
listeners.forEach{
|
||||
it.onRetract()
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@@ -64,13 +66,19 @@ class Xpandable @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
postDelayed({
|
||||
listener?.onExpand()
|
||||
listeners.forEach{
|
||||
it.onExpand()
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setOnChangeListener(listener: OnChangeListener) {
|
||||
this.listener = listener
|
||||
fun addOnChangeListener(listener: OnChangeListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: OnChangeListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
interface OnChangeListener {
|
||||
|
||||
@@ -3,7 +3,7 @@ package ani.dantotsu.parsers
|
||||
import android.content.Context
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.snackString
|
||||
@@ -73,12 +73,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
configurableSource.getPreferenceKey(),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPreferences.all.filterValues { AnimeNameAdapter.getSubDub(it.toString()) != AnimeNameAdapter.Companion.SubDubType.NULL }
|
||||
sharedPreferences.all.filterValues { MediaNameAdapter.getSubDub(it.toString()) != MediaNameAdapter.SubDubType.NULL }
|
||||
.forEach { value ->
|
||||
return when (AnimeNameAdapter.getSubDub(value.value.toString())) {
|
||||
AnimeNameAdapter.Companion.SubDubType.SUB -> false
|
||||
AnimeNameAdapter.Companion.SubDubType.DUB -> true
|
||||
AnimeNameAdapter.Companion.SubDubType.NULL -> false
|
||||
return when (MediaNameAdapter.getSubDub(value.value.toString())) {
|
||||
MediaNameAdapter.SubDubType.SUB -> false
|
||||
MediaNameAdapter.SubDubType.DUB -> true
|
||||
MediaNameAdapter.SubDubType.NULL -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,8 +92,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
val configurableSource = extension.sources[sourceLanguage] as? ConfigurableAnimeSource
|
||||
?: return
|
||||
val type = when (setDub) {
|
||||
true -> AnimeNameAdapter.Companion.SubDubType.DUB
|
||||
false -> AnimeNameAdapter.Companion.SubDubType.SUB
|
||||
true -> MediaNameAdapter.SubDubType.DUB
|
||||
false -> MediaNameAdapter.SubDubType.SUB
|
||||
}
|
||||
currContext()?.let { context ->
|
||||
val sharedPreferences =
|
||||
@@ -101,9 +101,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
configurableSource.getPreferenceKey(),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPreferences.all.filterValues { AnimeNameAdapter.getSubDub(it.toString()) != AnimeNameAdapter.Companion.SubDubType.NULL }
|
||||
sharedPreferences.all.filterValues { MediaNameAdapter.getSubDub(it.toString()) != MediaNameAdapter.SubDubType.NULL }
|
||||
.forEach { value ->
|
||||
val setValue = AnimeNameAdapter.setSubDub(value.value.toString(), type)
|
||||
val setValue = MediaNameAdapter.setSubDub(value.value.toString(), type)
|
||||
if (setValue != null) {
|
||||
sharedPreferences.edit().putString(value.key, setValue).apply()
|
||||
}
|
||||
@@ -122,9 +122,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPreferences.all.filterValues {
|
||||
AnimeNameAdapter.setSubDub(
|
||||
MediaNameAdapter.setSubDub(
|
||||
it.toString(),
|
||||
AnimeNameAdapter.Companion.SubDubType.NULL
|
||||
MediaNameAdapter.SubDubType.NULL
|
||||
) != null
|
||||
}
|
||||
.forEach { _ -> return true }
|
||||
@@ -150,7 +150,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
val sortedEpisodes = if (res[0].episode_number == -1f) {
|
||||
// Find the number in the string and sort by that number
|
||||
val sortedByStringNumber = res.sortedBy {
|
||||
val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
|
||||
val matchResult = MediaNameAdapter.findEpisodeNumber(it.name)
|
||||
val number = matchResult ?: Float.MAX_VALUE
|
||||
it.episode_number = number // Store the found number in episode_number
|
||||
number
|
||||
@@ -171,13 +171,13 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
var episodeCounter = 1f
|
||||
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
|
||||
val seasonGroups =
|
||||
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
||||
res.groupBy { MediaNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
||||
seasonGroups.keys.sortedBy { it }
|
||||
.flatMap { season ->
|
||||
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
||||
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
||||
val potentialNumber =
|
||||
AnimeNameAdapter.findEpisodeNumber(episode.name)
|
||||
MediaNameAdapter.findEpisodeNumber(episode.name)
|
||||
if (potentialNumber != null) {
|
||||
episode.episode_number = potentialNumber
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
@@ -33,9 +33,9 @@ abstract class MangaParser : BaseParser() {
|
||||
): MangaChapter? {
|
||||
val chapter = loadChapters(mangaLink, extra, sManga)
|
||||
val max = chapter
|
||||
.maxByOrNull { MangaNameAdapter.findChapterNumber(it.number) ?: 0f }
|
||||
.maxByOrNull { MediaNameAdapter.findChapterNumber(it.number) ?: 0f }
|
||||
return max
|
||||
?.takeIf { latest < (MangaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
|
||||
?.takeIf { latest < (MediaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
@@ -18,6 +21,7 @@ import java.util.Locale
|
||||
|
||||
class OfflineAnimeParser : AnimeParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private val context = Injekt.get<Application>()
|
||||
|
||||
override val name = "Offline"
|
||||
override val saveName = "Offline"
|
||||
@@ -29,22 +33,19 @@ class OfflineAnimeParser : AnimeParser() {
|
||||
extra: Map<String, String>?,
|
||||
sAnime: SAnime
|
||||
): List<Episode> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.animeLocation}/$animeLink"
|
||||
)
|
||||
val directory = getSubDirectory(context, MediaType.ANIME, false, animeLink)
|
||||
//get all of the folder names and add them to the list
|
||||
val episodes = mutableListOf<Episode>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (directory?.exists() == true) {
|
||||
directory.listFiles().forEach {
|
||||
//put the title and episdode number in the extra data
|
||||
val extraData = mutableMapOf<String, String>()
|
||||
extraData["title"] = animeLink
|
||||
extraData["episode"] = it.name
|
||||
extraData["episode"] = it.name!!
|
||||
if (it.isDirectory) {
|
||||
val episode = Episode(
|
||||
it.name,
|
||||
"$animeLink - ${it.name}",
|
||||
it.name!!,
|
||||
getTaskName(animeLink,it.name!!),
|
||||
it.name,
|
||||
null,
|
||||
null,
|
||||
@@ -54,7 +55,7 @@ class OfflineAnimeParser : AnimeParser() {
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
episodes.sortBy { AnimeNameAdapter.findEpisodeNumber(it.number) }
|
||||
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
|
||||
return episodes
|
||||
}
|
||||
return emptyList()
|
||||
@@ -131,18 +132,19 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
|
||||
|
||||
private fun getSubtitle(title: String, episode: String): List<Subtitle>? {
|
||||
currContext()?.let {
|
||||
DownloadsManager.getDirectory(
|
||||
DownloadsManager.getSubDirectory(
|
||||
it,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
title,
|
||||
episode
|
||||
).listFiles()?.forEach { file ->
|
||||
if (file.name.contains("subtitle")) {
|
||||
)?.listFiles()?.forEach { file ->
|
||||
if (file.name?.contains("subtitle") == true) {
|
||||
return listOf(
|
||||
Subtitle(
|
||||
"Downloaded Subtitle",
|
||||
Uri.fromFile(file).toString(),
|
||||
determineSubtitletype(file.absolutePath)
|
||||
file.uri.toString(),
|
||||
determineSubtitletype(file.name ?: "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -14,6 +17,7 @@ import java.io.File
|
||||
|
||||
class OfflineMangaParser : MangaParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private val context = Injekt.get<Application>()
|
||||
|
||||
override val hostUrl: String = "Offline"
|
||||
override val name: String = "Offline"
|
||||
@@ -23,17 +27,14 @@ class OfflineMangaParser : MangaParser() {
|
||||
extra: Map<String, String>?,
|
||||
sManga: SManga
|
||||
): List<MangaChapter> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$mangaLink"
|
||||
)
|
||||
val directory = getSubDirectory(context, MediaType.MANGA, false, mangaLink)
|
||||
//get all of the folder names and add them to the list
|
||||
val chapters = mutableListOf<MangaChapter>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (directory?.exists() == true) {
|
||||
directory.listFiles().forEach {
|
||||
if (it.isDirectory) {
|
||||
val chapter = MangaChapter(
|
||||
it.name,
|
||||
it.name!!,
|
||||
"$mangaLink/${it.name}",
|
||||
it.name,
|
||||
null,
|
||||
@@ -43,23 +44,22 @@ class OfflineMangaParser : MangaParser() {
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
chapters.sortBy { MangaNameAdapter.findChapterNumber(it.number) }
|
||||
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
|
||||
return chapters
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$chapterLink"
|
||||
)
|
||||
val title = chapterLink.split("/").first()
|
||||
val chapter = chapterLink.split("/").last()
|
||||
val directory = getSubDirectory(context, MediaType.MANGA, false, title, chapter)
|
||||
val images = mutableListOf<MangaImage>()
|
||||
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (directory?.exists() == true) {
|
||||
directory.listFiles().forEach {
|
||||
if (it.isFile) {
|
||||
val image = MangaImage(it.absolutePath, false, null)
|
||||
val image = MangaImage(it.uri.toString(), false, null)
|
||||
images.add(image)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -11,6 +14,7 @@ import java.io.File
|
||||
|
||||
class OfflineNovelParser : NovelParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private val context = Injekt.get<Application>()
|
||||
|
||||
override val hostUrl: String = "Offline"
|
||||
override val name: String = "Offline"
|
||||
@@ -21,24 +25,21 @@ class OfflineNovelParser : NovelParser() {
|
||||
|
||||
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
|
||||
//link should be a directory
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/$link"
|
||||
)
|
||||
val directory = getSubDirectory(context, MediaType.NOVEL, false, link)
|
||||
val chapters = mutableListOf<Book>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (directory?.exists() == true) {
|
||||
directory.listFiles().forEach {
|
||||
if (it.isDirectory) {
|
||||
val chapter = Book(
|
||||
it.name,
|
||||
it.absolutePath + "/cover.jpg",
|
||||
it.name?:"Unknown",
|
||||
it.uri.toString(),
|
||||
null,
|
||||
listOf(it.absolutePath + "/0.epub")
|
||||
listOf(it.uri.toString())
|
||||
)
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
chapters.sortBy { MangaNameAdapter.findChapterNumber(it.name) }
|
||||
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.name) }
|
||||
return chapters.first()
|
||||
}
|
||||
return Book(
|
||||
@@ -60,20 +61,16 @@ class OfflineNovelParser : NovelParser() {
|
||||
val returnList: MutableList<ShowResponse> = mutableListOf()
|
||||
for (title in returnTitles) {
|
||||
//need to search the subdirectories for the ShowResponses
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/$title"
|
||||
)
|
||||
val directory = getSubDirectory(context, MediaType.NOVEL, false, title)
|
||||
val names = mutableListOf<String>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (directory?.exists() == true) {
|
||||
directory.listFiles().forEach {
|
||||
if (it.isDirectory) {
|
||||
names.add(it.name)
|
||||
names.add(it.name?: "Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
val cover =
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
|
||||
val cover = directory?.findFile("cover.jpg")?.uri.toString()
|
||||
names.forEach {
|
||||
returnList.add(ShowResponse(it, it, cover))
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.databinding.ActivityProfileBinding
|
||||
import ani.dantotsu.databinding.ItemProfileAppBarBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.user.ListActivity
|
||||
@@ -45,6 +46,7 @@ import kotlin.math.abs
|
||||
|
||||
class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
lateinit var binding: ActivityProfileBinding
|
||||
private lateinit var bindingProfileAppBar: ItemProfileAppBarBinding
|
||||
private var selected: Int = 0
|
||||
lateinit var navBar: AnimatedBottomBar
|
||||
|
||||
@@ -108,145 +110,165 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
binding.profileViewPager.setCurrentItem(selected, true)
|
||||
}
|
||||
})
|
||||
val userLevel = intent.getStringExtra("userLVL") ?: ""
|
||||
binding.followButton.isGone = user.id == Anilist.userid || Anilist.userid == null
|
||||
binding.followButton.text = getString(
|
||||
when {
|
||||
user.isFollowing -> R.string.unfollow
|
||||
user.isFollower -> R.string.follows_you
|
||||
else -> R.string.follow
|
||||
}
|
||||
)
|
||||
if (user.isFollowing && user.isFollower) binding.followButton.text = getString(R.string.mutual)
|
||||
binding.followButton.setOnClickListener {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val res = Anilist.query.toggleFollow(user.id)
|
||||
if (res?.data?.toggleFollow != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString(R.string.success)
|
||||
user.isFollowing = res.data.toggleFollow.isFollowing
|
||||
binding.followButton.text = getString(
|
||||
when {
|
||||
user.isFollowing -> R.string.unfollow
|
||||
user.isFollower -> R.string.follows_you
|
||||
else -> R.string.follow
|
||||
}
|
||||
)
|
||||
if (user.isFollowing && user.isFollower)
|
||||
binding.followButton.text = getString(R.string.mutual)
|
||||
|
||||
bindingProfileAppBar = ItemProfileAppBarBinding.bind(binding.root).apply {
|
||||
|
||||
val userLevel = intent.getStringExtra("userLVL") ?: ""
|
||||
followButton.isGone =
|
||||
user.id == Anilist.userid || Anilist.userid == null
|
||||
followButton.text = getString(
|
||||
when {
|
||||
user.isFollowing -> R.string.unfollow
|
||||
user.isFollower -> R.string.follows_you
|
||||
else -> R.string.follow
|
||||
}
|
||||
)
|
||||
if (user.isFollowing && user.isFollower) followButton.text =
|
||||
getString(R.string.mutual)
|
||||
followButton.setOnClickListener {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val res = Anilist.query.toggleFollow(user.id)
|
||||
if (res?.data?.toggleFollow != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString(R.string.success)
|
||||
user.isFollowing = res.data.toggleFollow.isFollowing
|
||||
followButton.text = getString(
|
||||
when {
|
||||
user.isFollowing -> R.string.unfollow
|
||||
user.isFollower -> R.string.follows_you
|
||||
else -> R.string.follow
|
||||
}
|
||||
)
|
||||
if (user.isFollowing && user.isFollower)
|
||||
followButton.text = getString(R.string.mutual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.profileProgressBar.visibility = View.GONE
|
||||
binding.profileAppBar.visibility = View.VISIBLE
|
||||
binding.profileMenuButton.setOnClickListener {
|
||||
val popup = PopupMenu(this@ProfileActivity, binding.profileMenuButton)
|
||||
popup.menuInflater.inflate(R.menu.menu_profile, popup.menu)
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_view_following -> {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", "Following")
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
true
|
||||
}
|
||||
binding.profileProgressBar.visibility = View.GONE
|
||||
profileAppBar.visibility = View.VISIBLE
|
||||
profileMenuButton.setOnClickListener {
|
||||
val popup = PopupMenu(this@ProfileActivity, profileMenuButton)
|
||||
popup.menuInflater.inflate(R.menu.menu_profile, popup.menu)
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_view_following -> {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", "Following")
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_view_followers -> {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", "Followers")
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.action_view_followers -> {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", "Followers")
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_view_on_anilist -> {
|
||||
openLinkInBrowser("https://anilist.co/user/${user.name}")
|
||||
true
|
||||
}
|
||||
R.id.action_view_on_anilist -> {
|
||||
openLinkInBrowser("https://anilist.co/user/${user.name}")
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
|
||||
binding.profileUserAvatar.loadImage(user.avatar?.medium)
|
||||
binding.profileUserAvatar.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
this@ProfileActivity,
|
||||
"${user.name}'s [Avatar]",
|
||||
user.avatar?.medium
|
||||
profileUserAvatar.loadImage(user.avatar?.medium)
|
||||
profileUserAvatar.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
this@ProfileActivity,
|
||||
"${user.name}'s [Avatar]",
|
||||
user.avatar?.medium
|
||||
)
|
||||
}
|
||||
|
||||
val userLevelText = "${user.name} $userLevel"
|
||||
profileUserName.text = userLevelText
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
|
||||
blurImage(
|
||||
if (bannerAnimations) profileBannerImage else profileBannerImageNoKen,
|
||||
user.bannerImage ?: user.avatar?.medium
|
||||
)
|
||||
}
|
||||
profileBannerImage.updateLayoutParams { height += statusBarHeight }
|
||||
profileBannerImageNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
profileBannerGradient.updateLayoutParams { height += statusBarHeight }
|
||||
profileCloseButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
profileMenuButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
profileButtonContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
profileBannerImage.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
this@ProfileActivity,
|
||||
user.name + " [Banner]",
|
||||
user.bannerImage
|
||||
)
|
||||
}
|
||||
|
||||
val userLevelText = "${user.name} $userLevel"
|
||||
binding.profileUserName.text = userLevelText
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.profileBannerImage.pause()
|
||||
blurImage(binding.profileBannerImage, user.bannerImage ?: user.avatar?.medium)
|
||||
binding.profileBannerImage.updateLayoutParams { height += statusBarHeight }
|
||||
binding.profileBannerGradient.updateLayoutParams { height += statusBarHeight }
|
||||
binding.profileMenuButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.profileButtonContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.profileBannerImage.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
this@ProfileActivity,
|
||||
user.name + " [Banner]",
|
||||
user.bannerImage
|
||||
)
|
||||
}
|
||||
|
||||
mMaxScrollSize = binding.profileAppBar.totalScrollRange
|
||||
binding.profileAppBar.addOnOffsetChangedListener(this@ProfileActivity)
|
||||
mMaxScrollSize = profileAppBar.totalScrollRange
|
||||
profileAppBar.addOnOffsetChangedListener(this@ProfileActivity)
|
||||
|
||||
|
||||
binding.profileFollowerCount.text = followers.toString()
|
||||
binding.profileFollowerCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", getString(R.string.followers))
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
}
|
||||
profileFollowerCount.text = followers.toString()
|
||||
profileFollowerCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", getString(R.string.followers))
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
binding.profileFollowingCount.text = following.toString()
|
||||
binding.profileFollowingCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", "Following")
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
}
|
||||
profileFollowingCount.text = following.toString()
|
||||
profileFollowingCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, FollowActivity::class.java)
|
||||
.putExtra("title", "Following")
|
||||
.putExtra("userId", user.id),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
binding.profileAnimeCount.text = user.statistics.anime.count.toString()
|
||||
binding.profileAnimeCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity, Intent(this@ProfileActivity, ListActivity::class.java)
|
||||
.putExtra("anime", true)
|
||||
.putExtra("userId", user.id)
|
||||
.putExtra("username", user.name), null
|
||||
)
|
||||
}
|
||||
profileAnimeCount.text = user.statistics.anime.count.toString()
|
||||
profileAnimeCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, ListActivity::class.java)
|
||||
.putExtra("anime", true)
|
||||
.putExtra("userId", user.id)
|
||||
.putExtra("username", user.name),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
binding.profileMangaCount.text = user.statistics.manga.count.toString()
|
||||
binding.profileMangaCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity, Intent(this@ProfileActivity, ListActivity::class.java)
|
||||
.putExtra("anime", false)
|
||||
.putExtra("userId", user.id)
|
||||
.putExtra("username", user.name), null
|
||||
)
|
||||
profileMangaCount.text = user.statistics.manga.count.toString()
|
||||
profileMangaCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this@ProfileActivity,
|
||||
Intent(this@ProfileActivity, ListActivity::class.java)
|
||||
.putExtra("anime", false)
|
||||
.putExtra("userId", user.id)
|
||||
.putExtra("username", user.name),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
profileCloseButton.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,29 +284,31 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
|
||||
val percentage = abs(i) * 100 / mMaxScrollSize
|
||||
|
||||
binding.profileUserAvatarContainer.visibility =
|
||||
if (binding.profileUserAvatarContainer.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
|
||||
if (percentage >= percent && !isCollapsed) {
|
||||
isCollapsed = true
|
||||
ObjectAnimator.ofFloat(binding.profileUserDataContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(binding.profileUserAvatarContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(binding.profileButtonContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
binding.profileBannerImage.pause()
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
ObjectAnimator.ofFloat(binding.profileUserDataContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(binding.profileUserAvatarContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(binding.profileButtonContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
with (bindingProfileAppBar) {
|
||||
profileUserAvatarContainer.visibility =
|
||||
if (profileUserAvatarContainer.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
|
||||
if (percentage >= percent && !isCollapsed) {
|
||||
isCollapsed = true
|
||||
ObjectAnimator.ofFloat(profileUserDataContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(profileUserAvatarContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(profileButtonContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
profileBannerImage.pause()
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
ObjectAnimator.ofFloat(profileUserDataContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(profileUserAvatarContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(profileButtonContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.profileBannerImage.resume()
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) profileBannerImage.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,22 +154,23 @@ class ProfileFragment : Fragment() {
|
||||
private fun setFavPeople() {
|
||||
if (favStaff.isEmpty()) {
|
||||
binding.profileFavStaffContainer.visibility = View.GONE
|
||||
} else {
|
||||
binding.profileFavStaffRecycler.adapter = AuthorAdapter(favStaff)
|
||||
binding.profileFavStaffRecycler.layoutManager = LinearLayoutManager(
|
||||
activity, LinearLayoutManager.HORIZONTAL, false
|
||||
)
|
||||
binding.profileFavStaffRecycler.layoutAnimation = LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
binding.profileFavStaffRecycler.adapter = AuthorAdapter(favStaff)
|
||||
binding.profileFavStaffRecycler.layoutManager = LinearLayoutManager(
|
||||
activity,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
if (favCharacter.isEmpty()) {
|
||||
binding.profileFavCharactersContainer.visibility = View.GONE
|
||||
} else {
|
||||
binding.profileFavCharactersRecycler.adapter = CharacterAdapter(favCharacter)
|
||||
binding.profileFavCharactersRecycler.layoutManager = LinearLayoutManager(
|
||||
activity, LinearLayoutManager.HORIZONTAL, false
|
||||
)
|
||||
binding.profileFavCharactersRecycler.layoutAnimation = LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
binding.profileFavCharactersRecycler.adapter = CharacterAdapter(favCharacter)
|
||||
binding.profileFavCharactersRecycler.layoutManager = LinearLayoutManager(
|
||||
activity,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
private fun initRecyclerView(
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package ani.dantotsu.profile.activity
|
||||
package ani.dantotsu.profile
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.databinding.ItemFollowerBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.setAnimation
|
||||
|
||||
|
||||
@@ -41,7 +40,7 @@ class UsersAdapter(private val user: ArrayList<User>) : RecyclerView.Adapter<Use
|
||||
setAnimation(b.root.context, b.root)
|
||||
val user = user[position]
|
||||
b.profileUserAvatar.loadImage(user.pfp)
|
||||
b.profileBannerImage.loadImage(user.banner)
|
||||
blurImage(b.profileBannerImage, user.banner ?: user.pfp)
|
||||
b.profileUserName.text = user.name
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.databinding.BottomSheetUsersBinding
|
||||
import ani.dantotsu.profile.activity.UsersAdapter
|
||||
import ani.dantotsu.settings.DevelopersAdapter
|
||||
|
||||
|
||||
class UsersDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
@@ -21,7 +21,7 @@ import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
class FeedActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityFeedBinding
|
||||
private var selected: Int = 0
|
||||
private lateinit var navBar: AnimatedBottomBar
|
||||
lateinit var navBar: AnimatedBottomBar
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -50,16 +50,21 @@ class FeedFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
activity = requireActivity()
|
||||
|
||||
binding.listRecyclerView.setBaseline((activity as ProfileActivity).navBar)
|
||||
|
||||
binding.listRecyclerView.adapter = adapter
|
||||
binding.listRecyclerView.layoutManager =
|
||||
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
binding.listProgressBar.visibility = ViewGroup.VISIBLE
|
||||
userId = arguments?.getInt("userId", -1)
|
||||
activityId = arguments?.getInt("activityId", -1) ?: -1
|
||||
if (userId == -1) userId = null
|
||||
global = arguments?.getBoolean("global", false) ?: false
|
||||
|
||||
val navBar = if (userId != null) {
|
||||
(activity as ProfileActivity).navBar
|
||||
}else{
|
||||
(activity as FeedActivity).navBar
|
||||
}
|
||||
binding.listRecyclerView.setBaseline(navBar)
|
||||
binding.listRecyclerView.adapter = adapter
|
||||
binding.listRecyclerView.layoutManager =
|
||||
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
binding.listProgressBar.visibility = ViewGroup.VISIBLE
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -67,7 +72,12 @@ class FeedFragment : Fragment() {
|
||||
super.onResume()
|
||||
if (this::binding.isInitialized) {
|
||||
binding.root.requestLayout()
|
||||
binding.listRecyclerView.setBaseline((activity as ProfileActivity).navBar)
|
||||
val navBar = if (userId != null) {
|
||||
(activity as ProfileActivity).navBar
|
||||
}else{
|
||||
(activity as FeedActivity).navBar
|
||||
}
|
||||
binding.listRecyclerView.setBaseline(navBar)
|
||||
if (!loadedFirstTime) {
|
||||
activity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val nulledId = if (activityId == -1) null else activityId
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
|
||||
import ani.dantotsu.databinding.FragmentExtensionsBinding
|
||||
import ani.dantotsu.settings.paging.AnimeExtensionAdapter
|
||||
import ani.dantotsu.settings.paging.AnimeExtensionsViewModel
|
||||
import ani.dantotsu.settings.paging.AnimeExtensionsViewModelFactory
|
||||
@@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
class AnimeExtensionsFragment : Fragment(),
|
||||
SearchQueryHandler, OnAnimeInstallClickListener {
|
||||
private var _binding: FragmentAnimeExtensionsBinding? = null
|
||||
private var _binding: FragmentExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val viewModel: AnimeExtensionsViewModel by viewModels {
|
||||
@@ -48,12 +48,12 @@ class AnimeExtensionsFragment : Fragment(),
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||
binding.allAnimeExtensionsRecyclerView.adapter = adapter
|
||||
binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
|
||||
binding.allExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||
binding.allExtensionsRecyclerView.adapter = adapter
|
||||
binding.allExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
(binding.allExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
|
||||
true
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -91,8 +91,8 @@ class AnimeExtensionsFragment : Fragment(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle("Installing extension")
|
||||
.setContentText("Step: $installStep")
|
||||
.setContentTitle(getString(R.string.installing_extension))
|
||||
.setContentText(getString(R.string.install_step, installStep))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
@@ -103,11 +103,11 @@ class AnimeExtensionsFragment : Fragment(),
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle("Installation failed: ${error.message}")
|
||||
.setContentText("Error: ${error.message}")
|
||||
.setContentTitle(getString(R.string.installation_failed, error.message))
|
||||
.setContentText(getString(R.string.error_message, error.message))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
snackString("Installation failed: ${error.message}")
|
||||
snackString(getString(R.string.installation_failed, error.message))
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
@@ -115,12 +115,12 @@ class AnimeExtensionsFragment : Fragment(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_download_24)
|
||||
.setContentTitle("Installation complete")
|
||||
.setContentText("The extension has been successfully installed.")
|
||||
.setContentTitle(getString(R.string.installation_complete))
|
||||
.setContentText(getString(R.string.extension_has_been_installed))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
viewModel.invalidatePager()
|
||||
snackString("Extension installed")
|
||||
snackString(getString(R.string.extension_installed))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,63 +6,13 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.connections.github.Contributors
|
||||
import ani.dantotsu.databinding.BottomSheetDevelopersBinding
|
||||
|
||||
class DevelopersDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetDevelopersBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val developers = arrayOf(
|
||||
Developer(
|
||||
"rebelonion",
|
||||
"https://avatars.githubusercontent.com/u/87634197?v=4",
|
||||
"Owner and Maintainer",
|
||||
"https://github.com/rebelonion"
|
||||
),
|
||||
Developer(
|
||||
"Aayush262",
|
||||
"https://avatars.githubusercontent.com/u/99584765?v=4",
|
||||
"Contributor",
|
||||
"https://github.com/aayush2622"
|
||||
),
|
||||
Developer(
|
||||
"AbandonedCart",
|
||||
"https://avatars.githubusercontent.com/u/1173913?v=4",
|
||||
"Contributor",
|
||||
"https://github.com/AbandonedCart"
|
||||
),
|
||||
Developer(
|
||||
"Sadwhy",
|
||||
"https://avatars.githubusercontent.com/u/99601717?v=4",
|
||||
"Contributor",
|
||||
"https://github.com/Sadwhy"
|
||||
),
|
||||
Developer(
|
||||
"Wai What",
|
||||
"https://avatars.githubusercontent.com/u/149729762?v=4",
|
||||
"Icon Designer",
|
||||
"https://github.com/WaiWhat"
|
||||
),
|
||||
Developer(
|
||||
"MarshMeadow",
|
||||
"https://avatars.githubusercontent.com/u/88599122?v=4",
|
||||
"Beta Icon Designer",
|
||||
"https://github.com/MarshMeadow?tab=repositories"
|
||||
),
|
||||
Developer(
|
||||
"Zaxx69",
|
||||
"https://avatars.githubusercontent.com/u/138523882?v=4",
|
||||
"Telegram Admin",
|
||||
"https://github.com/Zaxx69"
|
||||
),
|
||||
Developer(
|
||||
"Arif Alam",
|
||||
"https://avatars.githubusercontent.com/u/70383209?v=4",
|
||||
"Head Discord Moderator",
|
||||
"https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
),
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -74,7 +24,7 @@ class DevelopersDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.devsRecyclerView.adapter = DevelopersAdapter(developers)
|
||||
binding.devsRecyclerView.adapter = DevelopersAdapter(Contributors().getContributors())
|
||||
binding.devsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.BottomSheetDiscordRpcBinding
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
class DiscordDialogFragment: BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetDiscordRpcBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetDiscordRpcBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
when (PrefManager.getCustomVal("discord_mode", "dantotsu")) {
|
||||
"nothing" -> binding.radioNothing.isChecked= true
|
||||
"dantotsu" -> binding.radioDantotsu.isChecked = true
|
||||
"anilist" -> binding.radioAnilist.isChecked = true
|
||||
else -> binding.radioAnilist.isChecked = true
|
||||
}
|
||||
|
||||
binding.anilistLinkPreview.text = getString(R.string.anilist_link, PrefManager.getVal<String>(PrefName.AnilistUserName))
|
||||
|
||||
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||
val mode = when (checkedId) {
|
||||
binding.radioNothing.id -> "nothing"
|
||||
binding.radioDantotsu.id -> "dantotsu"
|
||||
binding.radioAnilist.id -> "anilist"
|
||||
else -> "dantotsu"
|
||||
}
|
||||
PrefManager.setCustomVal("discord_mode", mode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -7,21 +7,13 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.github.Forks
|
||||
import ani.dantotsu.databinding.BottomSheetDevelopersBinding
|
||||
|
||||
class ForksDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetDevelopersBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val developers = arrayOf(
|
||||
Developer(
|
||||
"Dantotsu",
|
||||
"https://avatars.githubusercontent.com/u/87634197?v=4",
|
||||
"rebelonion",
|
||||
"https://github.com/rebelonion/Dantotsu"
|
||||
),
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -34,7 +26,7 @@ class ForksDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.devsTitle.setText(R.string.forks)
|
||||
binding.devsRecyclerView.adapter = DevelopersAdapter(developers)
|
||||
binding.devsRecyclerView.adapter = DevelopersAdapter(Forks().getForks())
|
||||
binding.devsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
|
||||
import ani.dantotsu.databinding.FragmentExtensionsBinding
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
@@ -49,7 +49,7 @@ import java.util.Locale
|
||||
class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
|
||||
|
||||
private var _binding: FragmentAnimeExtensionsBinding? = null
|
||||
private var _binding: FragmentExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
private val skipIcons: Boolean = PrefManager.getVal(PrefName.SkipExtensionIcons)
|
||||
@@ -183,9 +183,9 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
extensionsRecyclerView = binding.allAnimeExtensionsRecyclerView
|
||||
extensionsRecyclerView = binding.allExtensionsRecyclerView
|
||||
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
extensionsRecyclerView.adapter = extensionsAdapter
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
||||
import ani.dantotsu.databinding.FragmentExtensionsBinding
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
@@ -48,7 +48,7 @@ import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
private var _binding: FragmentMangaExtensionsBinding? = null
|
||||
private var _binding: FragmentExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
private val skipIcons: Boolean = PrefManager.getVal(PrefName.SkipExtensionIcons)
|
||||
@@ -181,9 +181,9 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
extensionsRecyclerView = binding.allMangaExtensionsRecyclerView
|
||||
extensionsRecyclerView = binding.allExtensionsRecyclerView
|
||||
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
extensionsRecyclerView.adapter = extensionsAdapter
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
||||
import ani.dantotsu.databinding.FragmentExtensionsBinding
|
||||
import ani.dantotsu.settings.paging.MangaExtensionAdapter
|
||||
import ani.dantotsu.settings.paging.MangaExtensionsViewModel
|
||||
import ani.dantotsu.settings.paging.MangaExtensionsViewModelFactory
|
||||
@@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaExtensionsFragment : Fragment(),
|
||||
SearchQueryHandler, OnMangaInstallClickListener {
|
||||
private var _binding: FragmentMangaExtensionsBinding? = null
|
||||
private var _binding: FragmentExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val viewModel: MangaExtensionsViewModel by viewModels {
|
||||
@@ -49,12 +49,12 @@ class MangaExtensionsFragment : Fragment(),
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||
binding.allMangaExtensionsRecyclerView.adapter = adapter
|
||||
binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
|
||||
binding.allExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||
binding.allExtensionsRecyclerView.adapter = adapter
|
||||
binding.allExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
(binding.allExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
|
||||
true
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -92,8 +92,8 @@ class MangaExtensionsFragment : Fragment(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle("Installing extension")
|
||||
.setContentText("Step: $installStep")
|
||||
.setContentTitle(getString(R.string.installing_extension))
|
||||
.setContentText(getString(R.string.install_step, installStep))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
@@ -104,11 +104,11 @@ class MangaExtensionsFragment : Fragment(),
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle("Installation failed: ${error.message}")
|
||||
.setContentText("Error: ${error.message}")
|
||||
.setContentTitle(getString(R.string.installation_failed, error.message))
|
||||
.setContentText(getString(R.string.error_message, error.message))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
snackString("Installation failed: ${error.message}")
|
||||
snackString(getString(R.string.installation_failed, error.message))
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
@@ -116,12 +116,12 @@ class MangaExtensionsFragment : Fragment(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_download_24)
|
||||
.setContentTitle("Installation complete")
|
||||
.setContentText("The extension has been successfully installed.")
|
||||
.setContentTitle(getString(R.string.installation_complete))
|
||||
.setContentText(getString(R.string.extension_has_been_installed))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
viewModel.invalidatePager()
|
||||
snackString("Extension installed")
|
||||
snackString(getString(R.string.extension_installed))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,11 +137,6 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
||||
binding.playerSettingsAutoSkipOpEd.isEnabled = isChecked
|
||||
}
|
||||
|
||||
binding.playerSettingsTimeStampsAutoHide.isChecked = PrefManager.getVal(PrefName.AutoHideTimeStamps)
|
||||
binding.playerSettingsTimeStampsAutoHide.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.AutoHideTimeStamps, isChecked)
|
||||
}
|
||||
|
||||
binding.playerSettingsTimeStampsProxy.isChecked =
|
||||
PrefManager.getVal(PrefName.UseProxyForTimeStamps)
|
||||
binding.playerSettingsTimeStampsProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||
@@ -152,6 +147,13 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
||||
PrefManager.getVal(PrefName.ShowTimeStampButton)
|
||||
binding.playerSettingsShowTimeStamp.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.ShowTimeStampButton, isChecked)
|
||||
binding.playerSettingsTimeStampsAutoHide.isEnabled = isChecked
|
||||
}
|
||||
|
||||
binding.playerSettingsTimeStampsAutoHide.isChecked = PrefManager.getVal(PrefName.AutoHideTimeStamps)
|
||||
binding.playerSettingsTimeStampsAutoHide.isEnabled = binding.playerSettingsShowTimeStamp.isChecked
|
||||
binding.playerSettingsTimeStampsAutoHide.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.AutoHideTimeStamps, isChecked)
|
||||
}
|
||||
|
||||
// Auto
|
||||
@@ -475,7 +477,7 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
||||
updateSubPreview()
|
||||
}
|
||||
}
|
||||
binding.subtitleTest.setOnChangeListener(object: Xpandable.OnChangeListener {
|
||||
binding.subtitleTest.addOnChangeListener(object: Xpandable.OnChangeListener {
|
||||
override fun onExpand() {
|
||||
updateSubPreview()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.BRAND
|
||||
import android.os.Build.DEVICE
|
||||
@@ -13,23 +14,25 @@ import android.os.Build.VERSION.CODENAME
|
||||
import android.os.Build.VERSION.RELEASE
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
@@ -47,9 +50,8 @@ import ani.dantotsu.databinding.ActivitySettingsExtensionsBinding
|
||||
import ani.dantotsu.databinding.ActivitySettingsMangaBinding
|
||||
import ani.dantotsu.databinding.ActivitySettingsNotificationsBinding
|
||||
import ani.dantotsu.databinding.ActivitySettingsThemeBinding
|
||||
import ani.dantotsu.databinding.ItemRepositoryBinding
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.downloadsPermission
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.MediaType
|
||||
@@ -78,19 +80,27 @@ import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.LauncherWrapper
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import eltos.simpledialogfragment.SimpleDialog
|
||||
import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE
|
||||
import eltos.simpledialogfragment.color.SimpleColorDialog
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
@@ -99,6 +109,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
|
||||
}
|
||||
lateinit var binding: ActivitySettingsBinding
|
||||
lateinit var launcher: LauncherWrapper
|
||||
private lateinit var bindingAccounts: ActivitySettingsAccountsBinding
|
||||
private lateinit var bindingTheme: ActivitySettingsThemeBinding
|
||||
private lateinit var bindingExtensions: ActivitySettingsExtensionsBinding
|
||||
@@ -109,7 +120,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
private lateinit var bindingAbout: ActivitySettingsAboutBinding
|
||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||
private var cursedCounter = 0
|
||||
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
||||
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
|
||||
|
||||
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -161,6 +175,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
}
|
||||
}
|
||||
}
|
||||
val contract = ActivityResultContracts.OpenDocumentTree()
|
||||
launcher = LauncherWrapper(this, contract)
|
||||
|
||||
binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
|
||||
binding.settingsVersion.setOnLongClickListener {
|
||||
@@ -207,6 +223,15 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
settingsAnilistUsername.visibility = View.VISIBLE
|
||||
settingsAnilistUsername.text = Anilist.username
|
||||
settingsAnilistAvatar.loadImage(Anilist.avatar)
|
||||
settingsAnilistAvatar.setOnClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
val anilistLink = getString(
|
||||
R.string.anilist_link,
|
||||
PrefManager.getVal<String>(PrefName.AnilistUserName)
|
||||
)
|
||||
openLinkInBrowser(anilistLink)
|
||||
true
|
||||
}
|
||||
|
||||
settingsMALLoginRequired.visibility = View.GONE
|
||||
settingsMALLogin.visibility = View.VISIBLE
|
||||
@@ -222,6 +247,12 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
settingsMALUsername.visibility = View.VISIBLE
|
||||
settingsMALUsername.text = MAL.username
|
||||
settingsMALAvatar.loadImage(MAL.avatar)
|
||||
settingsMALAvatar.setOnClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
val myanilistLink = getString(R.string.myanilist_link, MAL.username)
|
||||
openLinkInBrowser(myanilistLink)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
settingsMALAvatar.setImageResource(R.drawable.ic_round_person_24)
|
||||
settingsMALUsername.visibility = View.GONE
|
||||
@@ -248,6 +279,12 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
val username = PrefManager.getVal(PrefName.DiscordUserName, null as String?)
|
||||
if (id != null && avatar != null) {
|
||||
settingsDiscordAvatar.loadImage("https://cdn.discordapp.com/avatars/$id/$avatar.png")
|
||||
settingsDiscordAvatar.setOnClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
val discordLink = getString(R.string.discord_link, id)
|
||||
openLinkInBrowser(discordLink)
|
||||
true
|
||||
}
|
||||
}
|
||||
settingsDiscordUsername.visibility = View.VISIBLE
|
||||
settingsDiscordUsername.text =
|
||||
@@ -259,18 +296,18 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
reload()
|
||||
}
|
||||
|
||||
imageSwitcher.visibility = View.VISIBLE
|
||||
settingsImageSwitcher.visibility = View.VISIBLE
|
||||
var initialStatus = when (PrefManager.getVal<String>(PrefName.DiscordStatus)) {
|
||||
"online" -> R.drawable.discord_status_online
|
||||
"idle" -> R.drawable.discord_status_idle
|
||||
"dnd" -> R.drawable.discord_status_dnd
|
||||
else -> R.drawable.discord_status_online
|
||||
}
|
||||
imageSwitcher.setImageResource(initialStatus)
|
||||
settingsImageSwitcher.setImageResource(initialStatus)
|
||||
|
||||
val zoomInAnimation =
|
||||
AnimationUtils.loadAnimation(this@SettingsActivity, R.anim.bounce_zoom)
|
||||
imageSwitcher.setOnClickListener {
|
||||
settingsImageSwitcher.setOnClickListener {
|
||||
var status = "online"
|
||||
initialStatus = when (initialStatus) {
|
||||
R.drawable.discord_status_online -> {
|
||||
@@ -292,11 +329,16 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
}
|
||||
|
||||
PrefManager.setVal(PrefName.DiscordStatus, status)
|
||||
imageSwitcher.setImageResource(initialStatus)
|
||||
imageSwitcher.startAnimation(zoomInAnimation)
|
||||
settingsImageSwitcher.setImageResource(initialStatus)
|
||||
settingsImageSwitcher.startAnimation(zoomInAnimation)
|
||||
}
|
||||
settingsImageSwitcher.setOnLongClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
DiscordDialogFragment().show(supportFragmentManager, "dialog")
|
||||
true
|
||||
}
|
||||
} else {
|
||||
imageSwitcher.visibility = View.GONE
|
||||
settingsImageSwitcher.visibility = View.GONE
|
||||
settingsDiscordAvatar.setImageResource(R.drawable.ic_round_person_24)
|
||||
settingsDiscordUsername.visibility = View.GONE
|
||||
settingsDiscordLogin.setText(R.string.login)
|
||||
@@ -429,11 +471,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(MediaType.ANIME)
|
||||
DownloadService.sendRemoveAllDownloads(
|
||||
this@SettingsActivity,
|
||||
ExoplayerDownloadService::class.java,
|
||||
false
|
||||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||
@@ -454,6 +491,11 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
settingsShowYt.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.ShowYtButton, isChecked)
|
||||
}
|
||||
settingsIncludeAnimeList.isChecked = PrefManager.getVal(PrefName.IncludeAnimeList)
|
||||
settingsIncludeAnimeList.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.IncludeAnimeList, isChecked)
|
||||
restartApp(binding.root)
|
||||
}
|
||||
|
||||
var previousEp: View = when (PrefManager.getVal<Int>(PrefName.AnimeDefaultView)) {
|
||||
0 -> settingsEpList
|
||||
@@ -541,9 +583,178 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
settingsChpCompact.setOnClickListener {
|
||||
uiChp(1, it)
|
||||
}
|
||||
|
||||
settingsIncludeMangaList.isChecked = PrefManager.getVal(PrefName.IncludeMangaList)
|
||||
settingsIncludeMangaList.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.IncludeMangaList, isChecked)
|
||||
restartApp(binding.root)
|
||||
}
|
||||
}
|
||||
|
||||
bindingExtensions = ActivitySettingsExtensionsBinding.bind(binding.root).apply {
|
||||
|
||||
fun setExtensionOutput() {
|
||||
animeRepoInventory.removeAllViews()
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).forEach { item ->
|
||||
val view = ItemRepositoryBinding.inflate(
|
||||
LayoutInflater.from(animeRepoInventory.context), animeRepoInventory, true
|
||||
)
|
||||
view.repositoryItem.text = item.replace("https://raw.githubusercontent.com/", "")
|
||||
view.repositoryItem.setOnClickListener {
|
||||
AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
.setTitle("Delete Anime Repository")
|
||||
.setMessage(item)
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||
val anime = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item)
|
||||
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
setExtensionOutput()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
view.repositoryItem.setOnLongClickListener {
|
||||
copyToClipboard(item, true)
|
||||
true
|
||||
}
|
||||
}
|
||||
animeRepoInventory.isVisible = animeRepoInventory.childCount > 0
|
||||
mangaRepoInventory.removeAllViews()
|
||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).forEach { item ->
|
||||
val view = ItemRepositoryBinding.inflate(
|
||||
LayoutInflater.from(mangaRepoInventory.context), mangaRepoInventory, true
|
||||
)
|
||||
view.repositoryItem.text = item.replace("https://raw.githubusercontent.com/", "")
|
||||
view.repositoryItem.setOnClickListener {
|
||||
AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
.setTitle("Delete Manga Repository")
|
||||
.setMessage(item)
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||
val manga = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).minus(item)
|
||||
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
setExtensionOutput()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
view.repositoryItem.setOnLongClickListener {
|
||||
copyToClipboard(item, true)
|
||||
true
|
||||
}
|
||||
}
|
||||
mangaRepoInventory.isVisible = mangaRepoInventory.childCount > 0
|
||||
}
|
||||
|
||||
fun processUserInput(input: String, mediaType: MediaType) {
|
||||
val entry = if (input.endsWith("/") || input.endsWith("index.min.json"))
|
||||
input.substring(0, input.lastIndexOf("/")) else input
|
||||
if (mediaType == MediaType.ANIME) {
|
||||
val anime =
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
|
||||
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
if (mediaType == MediaType.MANGA) {
|
||||
val manga =
|
||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry)
|
||||
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
setExtensionOutput()
|
||||
}
|
||||
|
||||
fun processEditorAction(dialog: AlertDialog, editText: EditText, mediaType: MediaType) {
|
||||
editText.setOnEditorActionListener { textView, action, keyEvent ->
|
||||
if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE ||
|
||||
(keyEvent?.action == KeyEvent.ACTION_UP
|
||||
&& keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)
|
||||
) {
|
||||
processUserInput(textView.text.toString(), mediaType)
|
||||
dialog.dismiss()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
setExtensionOutput()
|
||||
animeAddRepository.setOnClickListener {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null)
|
||||
val editText =
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply {
|
||||
hint = getString(R.string.anime_add_repository)
|
||||
}
|
||||
val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
.setTitle(R.string.add_repository)
|
||||
.setMessage("Add additional repo for anime extensions")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||
processUserInput(editText.text.toString(), MediaType.ANIME)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton(getString(R.string.reset)) { dialog, _ ->
|
||||
PrefManager.removeVal(PrefName.DefaultUserAgent)
|
||||
editText.setText("")
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
|
||||
processEditorAction(alertDialog, editText, MediaType.ANIME)
|
||||
alertDialog.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
mangaAddRepository.setOnClickListener {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null)
|
||||
val editText =
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply {
|
||||
hint = getString(R.string.manga_add_repository)
|
||||
}
|
||||
val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
.setTitle(R.string.add_repository)
|
||||
.setView(dialogView)
|
||||
.setMessage("Add additional repo for manga extensions")
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||
processUserInput(editText.text.toString(), MediaType.MANGA)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton(getString(R.string.reset)) { dialog, _ ->
|
||||
PrefManager.removeVal(PrefName.DefaultUserAgent)
|
||||
editText.setText("")
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
|
||||
processEditorAction(alertDialog, editText, MediaType.MANGA)
|
||||
alertDialog.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
settingsForceLegacyInstall.isChecked =
|
||||
extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY
|
||||
settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked ->
|
||||
@@ -628,18 +839,18 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
val filteredLocations = Location.entries.filter { it.exportable }
|
||||
selectedArray.addAll(List(filteredLocations.size - 1) { false })
|
||||
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
.setTitle(R.string.import_export_settings)
|
||||
.setTitle(R.string.backup_restore)
|
||||
.setMultiChoiceItems(
|
||||
filteredLocations.map { it.name }.toTypedArray(),
|
||||
selectedArray.toBooleanArray()
|
||||
) { _, which, isChecked ->
|
||||
selectedArray[which] = isChecked
|
||||
}
|
||||
.setPositiveButton(R.string.button_import) { dialog, _ ->
|
||||
.setPositiveButton(R.string.button_restore) { dialog, _ ->
|
||||
openDocumentLauncher.launch(arrayOf("*/*"))
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.button_export) { dialog, _ ->
|
||||
.setNegativeButton(R.string.button_backup) { dialog, _ ->
|
||||
if (!selectedArray.contains(true)) {
|
||||
toast(R.string.no_location_selected)
|
||||
return@setNegativeButton
|
||||
@@ -678,27 +889,19 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
}
|
||||
|
||||
settingsExtensionDns.setText(exDns[PrefManager.getVal(PrefName.DohProvider)])
|
||||
settingsExtensionDns.setAdapter(ArrayAdapter(this@SettingsActivity, R.layout.item_dropdown, exDns))
|
||||
settingsExtensionDns.setAdapter(
|
||||
ArrayAdapter(
|
||||
this@SettingsActivity,
|
||||
R.layout.item_dropdown,
|
||||
exDns
|
||||
)
|
||||
)
|
||||
settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
|
||||
PrefManager.setVal(PrefName.DohProvider, i)
|
||||
settingsExtensionDns.clearFocus()
|
||||
restartApp(binding.root)
|
||||
}
|
||||
|
||||
settingsDownloadInSd.isChecked = PrefManager.getVal(PrefName.SdDl)
|
||||
settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(this@SettingsActivity, null)
|
||||
if (arrayOfFiles.size > 1 && arrayOfFiles[1] != null) {
|
||||
PrefManager.setVal(PrefName.SdDl, true)
|
||||
} else {
|
||||
settingsDownloadInSd.isChecked = false
|
||||
PrefManager.setVal(PrefName.SdDl, true)
|
||||
snackString(getString(R.string.noSdFound))
|
||||
}
|
||||
} else PrefManager.setVal(PrefName.SdDl, true)
|
||||
}
|
||||
|
||||
settingsContinueMedia.isChecked = PrefManager.getVal(PrefName.ContinueMedia)
|
||||
settingsContinueMedia.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.ContinueMedia, isChecked)
|
||||
@@ -713,6 +916,48 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
settingsRecentlyListOnly.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.RecentlyListOnly, isChecked)
|
||||
}
|
||||
settingsAdultAnimeOnly.isChecked = PrefManager.getVal(PrefName.AdultOnly)
|
||||
settingsAdultAnimeOnly.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setVal(PrefName.AdultOnly, isChecked)
|
||||
restartApp(binding.root)
|
||||
}
|
||||
|
||||
settingsDownloadLocation.setOnClickListener {
|
||||
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
.setTitle(R.string.change_download_location)
|
||||
.setMessage(R.string.download_location_msg)
|
||||
.setPositiveButton(R.string.ok) { dialog, _ ->
|
||||
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
launcher.registerForCallback { success ->
|
||||
if (success) {
|
||||
toast(getString(R.string.please_wait))
|
||||
val newUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
Injekt.get<DownloadsManager>().moveDownloadsDir(
|
||||
this@SettingsActivity,
|
||||
Uri.parse(oldUri), Uri.parse(newUri)
|
||||
) { finished, message ->
|
||||
if (finished) {
|
||||
toast(getString(R.string.success))
|
||||
} else {
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast(getString(R.string.error))
|
||||
}
|
||||
}
|
||||
launcher.launch()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
var previousStart: View = when (PrefManager.getVal<Int>(PrefName.DefaultStartUpTab)) {
|
||||
0 -> uiSettingsAnime
|
||||
@@ -742,7 +987,12 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
}
|
||||
|
||||
settingsUi.setOnClickListener {
|
||||
startActivity(Intent(this@SettingsActivity, UserInterfaceSettingsActivity::class.java))
|
||||
startActivity(
|
||||
Intent(
|
||||
this@SettingsActivity,
|
||||
UserInterfaceSettingsActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,7 +1016,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
getString(R.string.subscriptions_checking_time_s, timeNames[i])
|
||||
PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime)
|
||||
dialog.dismiss()
|
||||
TaskScheduler.create(this@SettingsActivity,
|
||||
TaskScheduler.create(
|
||||
this@SettingsActivity,
|
||||
PrefManager.getVal(PrefName.UseAlarmManager)
|
||||
).scheduleAllTasks(this@SettingsActivity)
|
||||
}.show()
|
||||
@@ -774,7 +1025,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
}
|
||||
|
||||
settingsSubscriptionsTime.setOnLongClickListener {
|
||||
TaskScheduler.create(this@SettingsActivity,
|
||||
TaskScheduler.create(
|
||||
this@SettingsActivity,
|
||||
PrefManager.getVal(PrefName.UseAlarmManager)
|
||||
).scheduleAllTasks(this@SettingsActivity)
|
||||
true
|
||||
@@ -788,7 +1040,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
else getString(R.string.do_not_update)
|
||||
}
|
||||
settingsAnilistSubscriptionsTime.text =
|
||||
getString(R.string.anilist_notifications_checking_time, aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)])
|
||||
getString(
|
||||
R.string.anilist_notifications_checking_time,
|
||||
aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
|
||||
)
|
||||
settingsAnilistSubscriptionsTime.setOnClickListener {
|
||||
|
||||
val selected = PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval)
|
||||
@@ -799,7 +1054,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
settingsAnilistSubscriptionsTime.text =
|
||||
getString(R.string.anilist_notifications_checking_time, aItems[i])
|
||||
dialog.dismiss()
|
||||
TaskScheduler.create(this@SettingsActivity,
|
||||
TaskScheduler.create(
|
||||
this@SettingsActivity,
|
||||
PrefManager.getVal(PrefName.UseAlarmManager)
|
||||
).scheduleAllTasks(this@SettingsActivity)
|
||||
}
|
||||
@@ -810,7 +1066,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
|
||||
settingsAnilistNotifications.setOnClickListener {
|
||||
val types = NotificationType.entries.map { it.name }
|
||||
val filteredTypes = PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes).toMutableSet()
|
||||
val filteredTypes =
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes).toMutableSet()
|
||||
val selected = types.map { filteredTypes.contains(it) }.toBooleanArray()
|
||||
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
.setTitle(R.string.anilist_notification_filters)
|
||||
@@ -837,7 +1094,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
}
|
||||
|
||||
settingsCommentSubscriptionsTime.text =
|
||||
getString(R.string.comment_notification_checking_time, cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)])
|
||||
getString(
|
||||
R.string.comment_notification_checking_time,
|
||||
cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)]
|
||||
)
|
||||
settingsCommentSubscriptionsTime.setOnClickListener {
|
||||
val selected = PrefManager.getVal<Int>(PrefName.CommentNotificationInterval)
|
||||
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
|
||||
@@ -847,7 +1107,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
settingsCommentSubscriptionsTime.text =
|
||||
getString(R.string.comment_notification_checking_time, cItems[i])
|
||||
dialog.dismiss()
|
||||
TaskScheduler.create(this@SettingsActivity,
|
||||
TaskScheduler.create(
|
||||
this@SettingsActivity,
|
||||
PrefManager.getVal(PrefName.UseAlarmManager)
|
||||
).scheduleAllTasks(this@SettingsActivity)
|
||||
}
|
||||
@@ -878,7 +1139,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
PrefManager.setVal(PrefName.UseAlarmManager, true)
|
||||
if (SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
|
||||
val intent = Intent("android.settings.REQUEST_SCHEDULE_EXACT_ALARM")
|
||||
val intent =
|
||||
Intent("android.settings.REQUEST_SCHEDULE_EXACT_ALARM")
|
||||
startActivity(intent)
|
||||
settingsNotificationsCheckingSubscriptions.isChecked = true
|
||||
}
|
||||
@@ -896,7 +1158,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
} else {
|
||||
PrefManager.setVal(PrefName.UseAlarmManager, false)
|
||||
TaskScheduler.create(this@SettingsActivity, true).cancelAllTasks()
|
||||
TaskScheduler.create(this@SettingsActivity, false).scheduleAllTasks(this@SettingsActivity)
|
||||
TaskScheduler.create(this@SettingsActivity, false)
|
||||
.scheduleAllTasks(this@SettingsActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1068,6 +1331,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
callback(null)
|
||||
}
|
||||
.create()
|
||||
|
||||
fun handleOkAction() {
|
||||
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
||||
if (editText?.text?.isNotBlank() == true) {
|
||||
@@ -1126,4 +1390,4 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||
?: "Unknown Architecture"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
|
||||
OfflineView(Pref(Location.General, Int::class, 0)),
|
||||
DownloadManager(Pref(Location.General, Int::class, 0)),
|
||||
NSFWExtension(Pref(Location.General, Boolean::class, true)),
|
||||
SdDl(Pref(Location.General, Boolean::class, false)),
|
||||
ContinueMedia(Pref(Location.General, Boolean::class, true)),
|
||||
SearchSources(Pref(Location.General, Boolean::class, true)),
|
||||
RecentlyListOnly(Pref(Location.General, Boolean::class, false)),
|
||||
@@ -29,6 +28,9 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
|
||||
"Mozilla/5.0 (Linux; Android 13; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
|
||||
)
|
||||
),
|
||||
AnimeExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
|
||||
MangaExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
|
||||
SharedRepositories(Pref(Location.General, Boolean::class, false)),
|
||||
AnimeSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
|
||||
AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
|
||||
MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
|
||||
@@ -40,6 +42,9 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
|
||||
LastAnilistNotificationId(Pref(Location.General, Int::class, 0)),
|
||||
AnilistFilteredTypes(Pref(Location.General, Set::class, setOf<String>())),
|
||||
UseAlarmManager(Pref(Location.General, Boolean::class, false)),
|
||||
IncludeAnimeList(Pref(Location.General, Boolean::class, true)),
|
||||
IncludeMangaList(Pref(Location.General, Boolean::class, true)),
|
||||
AdultOnly(Pref(Location.General, Boolean::class, false)),
|
||||
|
||||
//User Interface
|
||||
UseOLED(Pref(Location.UI, Boolean::class, false)),
|
||||
@@ -78,6 +83,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
|
||||
CommentSortOrder(Pref(Location.UI, String::class, "newest")),
|
||||
FollowerLayout(Pref(Location.UI, Int::class, 0)),
|
||||
|
||||
|
||||
//Player
|
||||
DefaultSpeed(Pref(Location.Player, Int::class, 5)),
|
||||
CursedSpeeds(Pref(Location.Player, Boolean::class, false)),
|
||||
@@ -178,6 +184,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
|
||||
RecentGlobalNotification(Pref(Location.Irrelevant, Int::class, 0)),
|
||||
CommentNotificationStore(Pref(Location.Irrelevant, List::class, listOf<CommentStore>())),
|
||||
UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)),
|
||||
DownloadsDir(Pref(Location.Irrelevant, String::class, "")),
|
||||
|
||||
//Protected
|
||||
DiscordToken(Pref(Location.Protected, String::class, "")),
|
||||
|
||||
22
app/src/main/java/ani/dantotsu/util/CountUpTimer.kt
Normal file
22
app/src/main/java/ani/dantotsu/util/CountUpTimer.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package ani.dantotsu.util
|
||||
|
||||
import android.os.CountDownTimer
|
||||
|
||||
// https://stackoverflow.com/a/40422151/461982
|
||||
abstract class CountUpTimer protected constructor(
|
||||
private val duration: Long
|
||||
) : CountDownTimer(duration, INTERVAL_MS) {
|
||||
abstract fun onTick(second: Int)
|
||||
override fun onTick(msUntilFinished: Long) {
|
||||
val second = ((duration - msUntilFinished) / 1000).toInt()
|
||||
onTick(second)
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
onTick(duration / 1000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INTERVAL_MS: Long = 1000
|
||||
}
|
||||
}
|
||||
132
app/src/main/java/ani/dantotsu/util/StoragePermissions.kt
Normal file
132
app/src/main/java/ani/dantotsu/util/StoragePermissions.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package ani.dantotsu.util
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
|
||||
class StoragePermissions {
|
||||
companion object {
|
||||
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val requiredPermissions = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||
}.toTypedArray()
|
||||
|
||||
return if (requiredPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
requiredPermissions,
|
||||
DOWNLOADS_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun hasDirAccess(context: Context, path: String): Boolean {
|
||||
val uri = pathToUri(path)
|
||||
return context.contentResolver.persistedUriPermissions.any {
|
||||
it.uri == uri && it.isReadPermission && it.isWritePermission
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun hasDirAccess(context: Context, uri: Uri): Boolean {
|
||||
return context.contentResolver.persistedUriPermissions.any {
|
||||
it.uri == uri && it.isReadPermission && it.isWritePermission
|
||||
}
|
||||
}
|
||||
|
||||
fun hasDirAccess(context: Context): Boolean {
|
||||
val path = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
return hasDirAccess(context, path)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.accessAlertDialog(launcher: LauncherWrapper,
|
||||
force: Boolean = false,
|
||||
complete: (Boolean) -> Unit
|
||||
) {
|
||||
if ((PrefManager.getVal<String>(PrefName.DownloadsDir).isNotEmpty() || hasDirAccess(this)) && !force) {
|
||||
complete(true)
|
||||
return
|
||||
}
|
||||
val builder = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
builder.setTitle(getString(R.string.dir_access))
|
||||
builder.setMessage(getString(R.string.dir_access_msg))
|
||||
builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||
launcher.registerForCallback(complete)
|
||||
launcher.launch()
|
||||
dialog.dismiss()
|
||||
}
|
||||
builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
complete(false)
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
private fun pathToUri(path: String): Uri {
|
||||
return Uri.parse(path)
|
||||
}
|
||||
|
||||
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LauncherWrapper(
|
||||
activity: AppCompatActivity,
|
||||
contract: ActivityResultContracts.OpenDocumentTree)
|
||||
{
|
||||
private var launcher: ActivityResultLauncher<Uri?>
|
||||
var complete: (Boolean) -> Unit = {}
|
||||
init{
|
||||
launcher = activity.registerForActivityResult(contract) { uri ->
|
||||
if (uri != null) {
|
||||
activity.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
|
||||
if (StoragePermissions.hasDirAccess(activity, uri)) {
|
||||
PrefManager.setVal(PrefName.DownloadsDir, uri.toString())
|
||||
complete(true)
|
||||
} else {
|
||||
toast(activity.getString(R.string.dir_error))
|
||||
complete(false)
|
||||
}
|
||||
} else {
|
||||
toast(activity.getString(R.string.dir_error))
|
||||
complete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerForCallback(callback: (Boolean) -> Unit) {
|
||||
complete = callback
|
||||
}
|
||||
|
||||
fun launch() {
|
||||
launcher.launch(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package ani.dantotsu.widgets.statistics
|
||||
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.StatisticsWidgetConfigureBinding
|
||||
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.widgets.upcoming.UpcomingWidget
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import eltos.simpledialogfragment.SimpleDialog
|
||||
import eltos.simpledialogfragment.color.SimpleColorDialog
|
||||
|
||||
/**
|
||||
* The configuration screen for the [ProfileStatsWidget] AppWidget.
|
||||
*/
|
||||
class ProfileStatsConfigure : AppCompatActivity(),
|
||||
SimpleDialog.OnDialogResultListener {
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
private var isMonetEnabled = false
|
||||
private var onClickListener = View.OnClickListener {
|
||||
val context = this@ProfileStatsConfigure
|
||||
|
||||
// It is the responsibility of the configuration activity to update the app widget
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
//updateAppWidget(context, appWidgetManager, appWidgetId)
|
||||
|
||||
|
||||
ProfileStatsWidget.updateAppWidget(
|
||||
context,
|
||||
appWidgetManager,
|
||||
appWidgetId
|
||||
)
|
||||
|
||||
// Make sure we pass back the original appWidgetId
|
||||
val resultValue = Intent()
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
finish()
|
||||
}
|
||||
private lateinit var binding: StatisticsWidgetConfigureBinding
|
||||
|
||||
public override fun onCreate(icicle: Bundle?) {
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
super.onCreate(icicle)
|
||||
|
||||
// Set the result to CANCELED. This will cause the widget host to cancel
|
||||
// out of the widget placement if the user presses the back button.
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
binding = StatisticsWidgetConfigureBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val prefs = getSharedPreferences(ProfileStatsWidget.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val topBackground = prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
|
||||
(binding.topBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(topBackground)
|
||||
binding.topBackgroundButton.setOnClickListener {
|
||||
val tag = ProfileStatsWidget.PREF_BACKGROUND_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(topBackground)
|
||||
.colors(
|
||||
this@ProfileStatsConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@ProfileStatsConfigure, tag)
|
||||
}
|
||||
val bottomBackground = prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
|
||||
(binding.bottomBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(bottomBackground)
|
||||
binding.bottomBackgroundButton.setOnClickListener {
|
||||
val tag = ProfileStatsWidget.PREF_BACKGROUND_FADE
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(bottomBackground)
|
||||
.colors(
|
||||
this@ProfileStatsConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@ProfileStatsConfigure, tag)
|
||||
}
|
||||
val titleColor = prefs.getInt(ProfileStatsWidget.PREF_TITLE_TEXT_COLOR, Color.WHITE)
|
||||
(binding.titleColorButton as MaterialButton).iconTint = ColorStateList.valueOf(titleColor)
|
||||
binding.titleColorButton.setOnClickListener {
|
||||
val tag = ProfileStatsWidget.PREF_TITLE_TEXT_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(titleColor)
|
||||
.colors(
|
||||
this@ProfileStatsConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@ProfileStatsConfigure, tag)
|
||||
}
|
||||
val statsColor = prefs.getInt(ProfileStatsWidget.PREF_STATS_TEXT_COLOR, Color.WHITE)
|
||||
(binding.statsColorButton as MaterialButton).iconTint = ColorStateList.valueOf(statsColor)
|
||||
binding.statsColorButton.setOnClickListener {
|
||||
val tag = ProfileStatsWidget.PREF_STATS_TEXT_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(statsColor)
|
||||
.colors(
|
||||
this@ProfileStatsConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@ProfileStatsConfigure, tag)
|
||||
}
|
||||
binding.useAppTheme.setOnCheckedChangeListener { _, isChecked ->
|
||||
isMonetEnabled = isChecked
|
||||
if (isChecked) {
|
||||
binding.topBackgroundButton.visibility = View.GONE
|
||||
binding.bottomBackgroundButton.visibility = View.GONE
|
||||
binding.titleColorButton.visibility = View.GONE
|
||||
binding.statsColorButton.visibility = View.GONE
|
||||
themeColors()
|
||||
|
||||
} else {
|
||||
binding.topBackgroundButton.visibility = View.VISIBLE
|
||||
binding.bottomBackgroundButton.visibility = View.VISIBLE
|
||||
binding.titleColorButton.visibility = View.VISIBLE
|
||||
binding.statsColorButton.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
binding.addButton.setOnClickListener(onClickListener)
|
||||
|
||||
// Find the widget id from the intent.
|
||||
val intent = intent
|
||||
val extras = intent.extras
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
)
|
||||
}
|
||||
|
||||
// If this activity was started with an intent without an app widget ID, finish with an error.
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun themeColors() {
|
||||
val typedValueSurface = TypedValue()
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSurface,
|
||||
typedValueSurface,
|
||||
true
|
||||
)
|
||||
val backgroundColor = typedValueSurface.data
|
||||
|
||||
val typedValuePrimary = TypedValue()
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorPrimary,
|
||||
typedValuePrimary,
|
||||
true
|
||||
)
|
||||
val textColor = typedValuePrimary.data
|
||||
|
||||
val typedValueOutline = TypedValue()
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOutline,
|
||||
typedValueOutline,
|
||||
true
|
||||
)
|
||||
val subTextColor = typedValueOutline.data
|
||||
|
||||
getSharedPreferences(ProfileStatsWidget.PREFS_NAME, Context.MODE_PRIVATE).edit().apply {
|
||||
putInt(ProfileStatsWidget.PREF_BACKGROUND_COLOR, backgroundColor)
|
||||
putInt(ProfileStatsWidget.PREF_BACKGROUND_FADE, backgroundColor)
|
||||
putInt(ProfileStatsWidget.PREF_TITLE_TEXT_COLOR, textColor)
|
||||
putInt(ProfileStatsWidget.PREF_STATS_TEXT_COLOR, subTextColor)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
|
||||
if (which == SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE) {
|
||||
if (!isMonetEnabled) {
|
||||
when (dialogTag) {
|
||||
ProfileStatsWidget.PREF_BACKGROUND_COLOR -> {
|
||||
getSharedPreferences(
|
||||
ProfileStatsWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
ProfileStatsWidget.PREF_BACKGROUND_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.topBackgroundButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
|
||||
ProfileStatsWidget.PREF_BACKGROUND_FADE -> {
|
||||
getSharedPreferences(
|
||||
ProfileStatsWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
ProfileStatsWidget.PREF_BACKGROUND_FADE,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.bottomBackgroundButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
|
||||
ProfileStatsWidget.PREF_TITLE_TEXT_COLOR -> {
|
||||
getSharedPreferences(
|
||||
ProfileStatsWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
ProfileStatsWidget.PREF_TITLE_TEXT_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.titleColorButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
|
||||
ProfileStatsWidget.PREF_STATS_TEXT_COLOR -> {
|
||||
getSharedPreferences(
|
||||
ProfileStatsWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
ProfileStatsWidget.PREF_STATS_TEXT_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.statsColorButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package ani.dantotsu.widgets.statistics
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import ani.dantotsu.MainActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.BitmapUtil
|
||||
import ani.dantotsu.widgets.WidgetSizeProvider
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Implementation of App Widget functionality.
|
||||
*/
|
||||
class ProfileStatsWidget : AppWidgetProvider() {
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId)
|
||||
}
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
}
|
||||
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun downloadImageAsBitmap(imageUrl: String): Bitmap? {
|
||||
var bitmap: Bitmap? = null
|
||||
|
||||
runBlocking(Dispatchers.IO) {
|
||||
var inputStream: InputStream? = null
|
||||
var urlConnection: HttpURLConnection? = null
|
||||
try {
|
||||
val url = URL(imageUrl)
|
||||
urlConnection = url.openConnection() as HttpURLConnection
|
||||
urlConnection.requestMethod = "GET"
|
||||
urlConnection.connect()
|
||||
|
||||
if (urlConnection.responseCode == HttpURLConnection.HTTP_OK) {
|
||||
inputStream = urlConnection.inputStream
|
||||
bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
urlConnection?.disconnect()
|
||||
}
|
||||
}
|
||||
return bitmap?.let { BitmapUtil.roundCorners(it) }
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun updateAppWidget(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int
|
||||
) {
|
||||
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val backgroundColor =
|
||||
prefs.getInt(PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
|
||||
val backgroundFade = prefs.getInt(PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
|
||||
val titleTextColor = prefs.getInt(PREF_TITLE_TEXT_COLOR, Color.WHITE)
|
||||
val statsTextColor = prefs.getInt(PREF_STATS_TEXT_COLOR, Color.WHITE)
|
||||
|
||||
val gradientDrawable = ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.linear_gradient_black,
|
||||
null
|
||||
) as GradientDrawable
|
||||
gradientDrawable.colors = intArrayOf(backgroundColor, backgroundFade)
|
||||
val widgetSizeProvider = WidgetSizeProvider(context)
|
||||
var (width, height) = widgetSizeProvider.getWidgetsSize(appWidgetId)
|
||||
if (width > 0 && height > 0) {
|
||||
gradientDrawable.cornerRadius = 64f
|
||||
} else {
|
||||
width = 300
|
||||
height = 300
|
||||
}
|
||||
|
||||
launchIO {
|
||||
val userPref = PrefManager.getVal(PrefName.AnilistUserId, "")
|
||||
if (userPref.isNotEmpty()) {
|
||||
val respond = Anilist.query.getUserProfile(userPref.toInt())
|
||||
respond?.data?.user?.let { user ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val views = RemoteViews(context.packageName, R.layout.statistics_widget).apply {
|
||||
setImageViewBitmap(
|
||||
R.id.backgroundView,
|
||||
BitmapUtil.convertDrawableToBitmap(
|
||||
gradientDrawable,
|
||||
width,
|
||||
height
|
||||
)
|
||||
)
|
||||
setOnClickPendingIntent(
|
||||
R.id.userAvatar,
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
1,
|
||||
Intent(context, ProfileStatsConfigure::class.java).apply {
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
setTextColor(R.id.userLabel, titleTextColor)
|
||||
setTextColor(R.id.topLeftItem, titleTextColor)
|
||||
setTextColor(R.id.topLeftLabel, statsTextColor)
|
||||
setTextColor(R.id.topRightItem, titleTextColor)
|
||||
setTextColor(R.id.topRightLabel, statsTextColor)
|
||||
setTextColor(R.id.bottomLeftItem, titleTextColor)
|
||||
setTextColor(R.id.bottomLeftLabel, statsTextColor)
|
||||
setTextColor(R.id.bottomRightItem, titleTextColor)
|
||||
setTextColor(R.id.bottomRightLabel, statsTextColor)
|
||||
|
||||
setImageViewBitmap(
|
||||
R.id.userAvatar,
|
||||
user.avatar?.medium?.let { it1 -> downloadImageAsBitmap(it1) }
|
||||
)
|
||||
setTextViewText(
|
||||
R.id.userLabel,
|
||||
context.getString(R.string.user_stats, user.name)
|
||||
)
|
||||
|
||||
setTextViewText(
|
||||
R.id.topLeftItem,
|
||||
user.statistics.anime.count.toString()
|
||||
)
|
||||
setTextViewText(
|
||||
R.id.topLeftLabel,
|
||||
context.getString(R.string.anime_watched)
|
||||
)
|
||||
|
||||
setTextViewText(
|
||||
R.id.topRightItem,
|
||||
user.statistics.anime.episodesWatched.toString()
|
||||
)
|
||||
setTextViewText(
|
||||
R.id.topRightLabel,
|
||||
context.getString(R.string.episodes_watched_n)
|
||||
)
|
||||
|
||||
setTextViewText(
|
||||
R.id.bottomLeftItem,
|
||||
user.statistics.manga.count.toString()
|
||||
)
|
||||
setTextViewText(
|
||||
R.id.bottomLeftLabel,
|
||||
context.getString(R.string.manga_read)
|
||||
)
|
||||
|
||||
setTextViewText(
|
||||
R.id.bottomRightItem,
|
||||
user.statistics.manga.chaptersRead.toString()
|
||||
)
|
||||
setTextViewText(
|
||||
R.id.bottomRightLabel,
|
||||
context.getString(R.string.chapters_read_n)
|
||||
)
|
||||
|
||||
val intent = Intent(context, ProfileActivity::class.java)
|
||||
.putExtra("userId", userPref.toInt())
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context, 0, intent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
setOnClickPendingIntent(R.id.widgetContainer, pendingIntent)
|
||||
}
|
||||
// Instruct the widget manager to update the widget
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
} ?: showLoginCascade(context, appWidgetManager, appWidgetId)
|
||||
} else showLoginCascade(context, appWidgetManager, appWidgetId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showLoginCascade(
|
||||
context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int
|
||||
) {
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val views = RemoteViews(context.packageName, R.layout.statistics_widget)
|
||||
|
||||
views.setTextViewText(R.id.topLeftItem, "")
|
||||
views.setTextViewText(
|
||||
R.id.topLeftLabel,
|
||||
context.getString(R.string.please)
|
||||
)
|
||||
|
||||
views.setTextViewText(R.id.topRightItem, "")
|
||||
views.setTextViewText(
|
||||
R.id.topRightLabel,
|
||||
context.getString(R.string.log_in)
|
||||
)
|
||||
|
||||
views.setTextViewText(
|
||||
R.id.bottomLeftItem,
|
||||
context.getString(R.string.or_join)
|
||||
)
|
||||
views.setTextViewText(R.id.bottomLeftLabel, "")
|
||||
|
||||
views.setTextViewText(
|
||||
R.id.bottomRightItem,
|
||||
context.getString(R.string.anilist)
|
||||
)
|
||||
views.setTextViewText(R.id.bottomRightLabel, "")
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context, 0, intent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
views.setOnClickPendingIntent(R.id.widgetContainer, pendingIntent)
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
}
|
||||
|
||||
const val PREFS_NAME = "ani.dantotsu.widgets.ResumableWidget"
|
||||
const val PREF_BACKGROUND_COLOR = "background_color"
|
||||
const val PREF_BACKGROUND_FADE = "background_fade"
|
||||
const val PREF_TITLE_TEXT_COLOR = "title_text_color"
|
||||
const val PREF_STATS_TEXT_COLOR = "stats_text_color"
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import ani.dantotsu.MainActivity
|
||||
import ani.dantotsu.R
|
||||
@@ -19,7 +18,7 @@ import ani.dantotsu.widgets.WidgetSizeProvider
|
||||
|
||||
/**
|
||||
* Implementation of App Widget functionality.
|
||||
* App Widget Configuration implemented in [UpcomingWidgetConfigureActivity]
|
||||
* App Widget Configuration implemented in [UpcomingWidgetConfigure]
|
||||
*/
|
||||
class UpcomingWidget : AppWidgetProvider() {
|
||||
override fun onUpdate(
|
||||
@@ -69,8 +68,8 @@ class UpcomingWidget : AppWidgetProvider() {
|
||||
): RemoteViews {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val backgroundColor =
|
||||
prefs.getInt(PREF_BACKGROUND_COLOR, ContextCompat.getColor(context, R.color.theme))
|
||||
val backgroundFade = prefs.getInt(PREF_BACKGROUND_FADE, Color.GRAY)
|
||||
prefs.getInt(PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
|
||||
val backgroundFade = prefs.getInt(PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
|
||||
val titleTextColor = prefs.getInt(PREF_TITLE_TEXT_COLOR, Color.WHITE)
|
||||
val countdownTextColor = prefs.getInt(PREF_COUNTDOWN_TEXT_COLOR, Color.WHITE)
|
||||
|
||||
@@ -80,14 +79,14 @@ class UpcomingWidget : AppWidgetProvider() {
|
||||
}
|
||||
val gradientDrawable = ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.gradient_background,
|
||||
R.drawable.linear_gradient_black,
|
||||
null
|
||||
) as GradientDrawable
|
||||
gradientDrawable.colors = intArrayOf(backgroundColor, backgroundFade)
|
||||
val widgetSizeProvider = WidgetSizeProvider(context)
|
||||
var (width, height) = widgetSizeProvider.getWidgetsSize(appWidgetId)
|
||||
if (width > 0 && height > 0) {
|
||||
gradientDrawable.cornerRadius = 50f
|
||||
gradientDrawable.cornerRadius = 64f
|
||||
} else {
|
||||
width = 300
|
||||
height = 300
|
||||
@@ -118,7 +117,7 @@ class UpcomingWidget : AppWidgetProvider() {
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
1,
|
||||
Intent(context, UpcomingWidgetConfigureActivity::class.java).apply {
|
||||
Intent(context, UpcomingWidgetConfigure::class.java).apply {
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package ani.dantotsu.widgets.upcoming
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.UpcomingWidgetConfigureBinding
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import eltos.simpledialogfragment.SimpleDialog
|
||||
import eltos.simpledialogfragment.color.SimpleColorDialog
|
||||
|
||||
/**
|
||||
* The configuration screen for the [UpcomingWidget] AppWidget.
|
||||
*/
|
||||
class UpcomingWidgetConfigure : AppCompatActivity(),
|
||||
SimpleDialog.OnDialogResultListener {
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
private var isMonetEnabled = false
|
||||
private var onClickListener = View.OnClickListener {
|
||||
val context = this@UpcomingWidgetConfigure
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
|
||||
updateAppWidget(
|
||||
context,
|
||||
appWidgetManager,
|
||||
appWidgetId,
|
||||
)
|
||||
|
||||
val resultValue = Intent()
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
finish()
|
||||
}
|
||||
private lateinit var binding: UpcomingWidgetConfigureBinding
|
||||
|
||||
public override fun onCreate(icicle: Bundle?) {
|
||||
ThemeManager(this).applyTheme()
|
||||
super.onCreate(icicle)
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
binding = UpcomingWidgetConfigureBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val prefs = getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val topBackground = prefs.getInt(UpcomingWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
|
||||
(binding.topBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(topBackground)
|
||||
binding.topBackgroundButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_BACKGROUND_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(topBackground)
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigure, tag)
|
||||
}
|
||||
val bottomBackground = prefs.getInt(UpcomingWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
|
||||
(binding.bottomBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(bottomBackground)
|
||||
binding.bottomBackgroundButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_BACKGROUND_FADE
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(bottomBackground)
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigure, tag)
|
||||
}
|
||||
val titleTextColor = prefs.getInt(UpcomingWidget.PREF_TITLE_TEXT_COLOR, Color.WHITE)
|
||||
(binding.titleColorButton as MaterialButton).iconTint = ColorStateList.valueOf(titleTextColor)
|
||||
binding.titleColorButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_TITLE_TEXT_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(titleTextColor)
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigure, tag)
|
||||
}
|
||||
val countdownTextColor = prefs.getInt(UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR, Color.WHITE)
|
||||
(binding.countdownColorButton as MaterialButton).iconTint = ColorStateList.valueOf(countdownTextColor)
|
||||
binding.countdownColorButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(countdownTextColor)
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigure,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigure, tag)
|
||||
}
|
||||
binding.useAppTheme.setOnCheckedChangeListener { _, isChecked ->
|
||||
isMonetEnabled = isChecked
|
||||
if (isChecked) {
|
||||
binding.topBackgroundButton.visibility = View.GONE
|
||||
binding.bottomBackgroundButton.visibility = View.GONE
|
||||
binding.titleColorButton.visibility = View.GONE
|
||||
binding.countdownColorButton.visibility = View.GONE
|
||||
themeColors()
|
||||
|
||||
} else {
|
||||
binding.topBackgroundButton.visibility = View.VISIBLE
|
||||
binding.bottomBackgroundButton.visibility = View.VISIBLE
|
||||
binding.titleColorButton.visibility = View.VISIBLE
|
||||
binding.countdownColorButton.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
binding.addButton.setOnClickListener(onClickListener)
|
||||
|
||||
val intent = intent
|
||||
val extras = intent.extras
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
)
|
||||
}
|
||||
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun themeColors() {
|
||||
val typedValueSurface = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValueSurface, true)
|
||||
val backgroundColor = typedValueSurface.data
|
||||
|
||||
val typedValuePrimary = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValuePrimary, true)
|
||||
val textColor = typedValuePrimary.data
|
||||
|
||||
val typedValueOutline = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValueOutline, true)
|
||||
val subTextColor = typedValueOutline.data
|
||||
|
||||
getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE).edit().apply {
|
||||
putInt(UpcomingWidget.PREF_BACKGROUND_COLOR, backgroundColor)
|
||||
putInt(UpcomingWidget.PREF_BACKGROUND_FADE, backgroundColor)
|
||||
putInt(UpcomingWidget.PREF_TITLE_TEXT_COLOR, textColor)
|
||||
putInt(UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR, subTextColor)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
|
||||
if (which == SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE) {
|
||||
if (!isMonetEnabled) {
|
||||
when (dialogTag) {
|
||||
UpcomingWidget.PREF_BACKGROUND_COLOR -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_BACKGROUND_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.topBackgroundButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
|
||||
UpcomingWidget.PREF_BACKGROUND_FADE -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_BACKGROUND_FADE,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.bottomBackgroundButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
|
||||
UpcomingWidget.PREF_TITLE_TEXT_COLOR -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_TITLE_TEXT_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.titleColorButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
|
||||
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
(binding.countdownColorButton as MaterialButton).iconTint =
|
||||
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package ani.dantotsu.widgets.upcoming
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.UpcomingWidgetConfigureBinding
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eltos.simpledialogfragment.SimpleDialog
|
||||
import eltos.simpledialogfragment.color.SimpleColorDialog
|
||||
|
||||
/**
|
||||
* The configuration screen for the [UpcomingWidget] AppWidget.
|
||||
*/
|
||||
class UpcomingWidgetConfigureActivity : AppCompatActivity(),
|
||||
SimpleDialog.OnDialogResultListener {
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
private var onClickListener = View.OnClickListener {
|
||||
val context = this@UpcomingWidgetConfigureActivity
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
|
||||
updateAppWidget(
|
||||
context,
|
||||
appWidgetManager,
|
||||
appWidgetId,
|
||||
)
|
||||
|
||||
val resultValue = Intent()
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
finish()
|
||||
}
|
||||
private lateinit var binding: UpcomingWidgetConfigureBinding
|
||||
|
||||
public override fun onCreate(icicle: Bundle?) {
|
||||
ThemeManager(this).applyTheme()
|
||||
super.onCreate(icicle)
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
binding = UpcomingWidgetConfigureBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val prefs = getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
binding.topBackgroundButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_BACKGROUND_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(
|
||||
prefs.getInt(
|
||||
UpcomingWidget.PREF_BACKGROUND_COLOR,
|
||||
ContextCompat.getColor(this, R.color.theme)
|
||||
)
|
||||
)
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigureActivity,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigureActivity, tag)
|
||||
}
|
||||
binding.bottomBackgroundButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_BACKGROUND_FADE
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(prefs.getInt(UpcomingWidget.PREF_BACKGROUND_FADE, Color.GRAY))
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigureActivity,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.setupColorWheelAlpha(true)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigureActivity, tag)
|
||||
}
|
||||
binding.titleColorButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_TITLE_TEXT_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(prefs.getInt(UpcomingWidget.PREF_TITLE_TEXT_COLOR, Color.WHITE))
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigureActivity,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigureActivity, tag)
|
||||
}
|
||||
binding.countdownColorButton.setOnClickListener {
|
||||
val tag = UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR
|
||||
SimpleColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(
|
||||
prefs.getInt(
|
||||
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR,
|
||||
Color.WHITE
|
||||
)
|
||||
)
|
||||
.colors(
|
||||
this@UpcomingWidgetConfigureActivity,
|
||||
SimpleColorDialog.MATERIAL_COLOR_PALLET
|
||||
)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this@UpcomingWidgetConfigureActivity, tag)
|
||||
}
|
||||
|
||||
binding.addButton.setOnClickListener(onClickListener)
|
||||
|
||||
val intent = intent
|
||||
val extras = intent.extras
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
)
|
||||
}
|
||||
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
|
||||
if (which == SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE) {
|
||||
when (dialogTag) {
|
||||
UpcomingWidget.PREF_BACKGROUND_COLOR -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_BACKGROUND_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
UpcomingWidget.PREF_BACKGROUND_FADE -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_BACKGROUND_FADE,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
UpcomingWidget.PREF_TITLE_TEXT_COLOR -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_TITLE_TEXT_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR -> {
|
||||
getSharedPreferences(
|
||||
UpcomingWidget.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.putInt(
|
||||
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR,
|
||||
extras.getInt(SimpleColorDialog.COLOR)
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||
import okhttp3.Headers
|
||||
@@ -69,7 +70,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", network.defaultUserAgentProvider())
|
||||
add("User-Agent", defaultUserAgentProvider())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ class ExtensionUpdateNotifier(private val context: Context) {
|
||||
Notifications.CHANNEL_EXTENSIONS_UPDATE,
|
||||
) {
|
||||
setContentTitle(
|
||||
"Extension updates available"
|
||||
context.getString(R.string.extension_updates_available)
|
||||
)
|
||||
val extNames = names.joinToString(", ")
|
||||
setContentText(extNames)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.extension.anime.api
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
@@ -32,42 +34,55 @@ internal class AnimeExtensionGithubApi {
|
||||
preferenceStore.getLong("last_ext_check", 0)
|
||||
}
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<AnimeExtension.Available> {
|
||||
return withIOContext {
|
||||
val githubResponse = if (requiresFallbackSource) {
|
||||
null
|
||||
} else {
|
||||
|
||||
val extensions: ArrayList<AnimeExtension.Available> = arrayListOf()
|
||||
|
||||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
|
||||
if (repos.isEmpty()) {
|
||||
repos.add("https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo")
|
||||
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos.toSet())
|
||||
}
|
||||
|
||||
repos.forEach {
|
||||
try {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.awaitSuccess()
|
||||
val githubResponse = try {
|
||||
networkService.client
|
||||
.newCall(GET("${it}/index.min.json"))
|
||||
.awaitSuccess()
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Failed to get repo: $it")
|
||||
Logger.log(e)
|
||||
null
|
||||
}
|
||||
|
||||
val response = githubResponse ?: run {
|
||||
networkService.client
|
||||
.newCall(GET(fallbackRepoUrl(it) + "/index.min.json"))
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val repoExtensions = with(json) {
|
||||
response
|
||||
.parseAs<List<AnimeExtensionJsonObject>>()
|
||||
.toExtensions(it)
|
||||
}
|
||||
|
||||
// Sanity check - a small number of extensions probably means something broke
|
||||
// with the repo generator
|
||||
if (repoExtensions.size < 10) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
extensions.addAll(repoExtensions)
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Failed to get extensions from GitHub")
|
||||
requiresFallbackSource = true
|
||||
null
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
val response = githubResponse ?: run {
|
||||
networkService.client
|
||||
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val extensions = with(json) {
|
||||
response
|
||||
.parseAs<List<AnimeExtensionJsonObject>>()
|
||||
.toExtensions()
|
||||
}
|
||||
|
||||
// Sanity check - a small number of extensions probably means something broke
|
||||
// with the repo generator
|
||||
if (extensions.size < 10) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
extensions
|
||||
}
|
||||
}
|
||||
@@ -111,7 +126,7 @@ internal class AnimeExtensionGithubApi {
|
||||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun List<AnimeExtensionJsonObject>.toExtensions(): List<AnimeExtension.Available> {
|
||||
private fun List<AnimeExtensionJsonObject>.toExtensions(repository: String): List<AnimeExtension.Available> {
|
||||
return this
|
||||
.filter {
|
||||
val libVersion = it.extractLibVersion()
|
||||
@@ -130,7 +145,8 @@ internal class AnimeExtensionGithubApi {
|
||||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.toAnimeExtensionSources().orEmpty(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
|
||||
repository = repository,
|
||||
iconUrl = "${repository}/icon/${it.pkg}.png",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -147,15 +163,27 @@ internal class AnimeExtensionGithubApi {
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: AnimeExtension.Available): String {
|
||||
return "${getUrlPrefix()}apk/${extension.apkName}"
|
||||
return "${extension.repository}/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
private fun getUrlPrefix(): String {
|
||||
return if (requiresFallbackSource) {
|
||||
FALLBACK_REPO_URL_PREFIX
|
||||
} else {
|
||||
REPO_URL_PREFIX
|
||||
private fun fallbackRepoUrl(repoUrl: String): String? {
|
||||
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
|
||||
val strippedRepoUrl =
|
||||
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
|
||||
val repoUrlParts = strippedRepoUrl.split("/")
|
||||
if (repoUrlParts.size < 3) {
|
||||
return null
|
||||
}
|
||||
val repoOwner = repoUrlParts[1]
|
||||
val repoName = repoUrlParts[2]
|
||||
fallbackRepoUrl += "$repoOwner/$repoName"
|
||||
val repoBranch = if (repoUrlParts.size > 3) {
|
||||
repoUrlParts[3]
|
||||
} else {
|
||||
"main"
|
||||
}
|
||||
fallbackRepoUrl += "@$repoBranch"
|
||||
return fallbackRepoUrl
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,11 +191,6 @@ private fun AnimeExtensionJsonObject.extractLibVersion(): Double {
|
||||
return version.substringBeforeLast('.').toDouble()
|
||||
}
|
||||
|
||||
private const val REPO_URL_PREFIX =
|
||||
"https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo/"
|
||||
private const val FALLBACK_REPO_URL_PREFIX =
|
||||
"https://gcore.jsdelivr.net/gh/aniyomiorg/aniyomi-extensions@repo/"
|
||||
|
||||
@Serializable
|
||||
private data class AnimeExtensionJsonObject(
|
||||
val name: String,
|
||||
|
||||
@@ -47,6 +47,7 @@ sealed class AnimeExtension {
|
||||
val sources: List<AvailableAnimeSources>,
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val repository: String
|
||||
) : AnimeExtension()
|
||||
|
||||
data class Untrusted(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.extension.manga.api
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
@@ -32,42 +34,55 @@ internal class MangaExtensionGithubApi {
|
||||
preferenceStore.getLong("last_ext_check", 0)
|
||||
}
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<MangaExtension.Available> {
|
||||
return withIOContext {
|
||||
val githubResponse = if (requiresFallbackSource) {
|
||||
null
|
||||
} else {
|
||||
|
||||
val extensions: ArrayList<MangaExtension.Available> = arrayListOf()
|
||||
|
||||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
||||
if (repos.isEmpty()) {
|
||||
repos.add("https://raw.githubusercontent.com/keiyoushi/extensions/main")
|
||||
PrefManager.setVal(PrefName.MangaExtensionRepos, repos.toSet())
|
||||
}
|
||||
|
||||
repos.forEach {
|
||||
try {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.awaitSuccess()
|
||||
val githubResponse = try {
|
||||
networkService.client
|
||||
.newCall(GET("${it}/index.min.json"))
|
||||
.awaitSuccess()
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Failed to get repo: $it")
|
||||
Logger.log(e)
|
||||
null
|
||||
}
|
||||
|
||||
val response = githubResponse ?: run {
|
||||
networkService.client
|
||||
.newCall(GET(fallbackRepoUrl(it) + "/index.min.json"))
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val repoExtensions = with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(it)
|
||||
}
|
||||
|
||||
// Sanity check - a small number of extensions probably means something broke
|
||||
// with the repo generator
|
||||
if (repoExtensions.size < 10) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
extensions.addAll(repoExtensions)
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Failed to get extensions from GitHub")
|
||||
requiresFallbackSource = true
|
||||
null
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
val response = githubResponse ?: run {
|
||||
networkService.client
|
||||
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val extensions = with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions()
|
||||
}
|
||||
|
||||
// Sanity check - a small number of extensions probably means something broke
|
||||
// with the repo generator
|
||||
if (extensions.size < 100) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
extensions
|
||||
}
|
||||
}
|
||||
@@ -110,7 +125,7 @@ internal class MangaExtensionGithubApi {
|
||||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun List<ExtensionJsonObject>.toExtensions(): List<MangaExtension.Available> {
|
||||
private fun List<ExtensionJsonObject>.toExtensions(repository: String): List<MangaExtension.Available> {
|
||||
return this
|
||||
.filter {
|
||||
val libVersion = it.extractLibVersion()
|
||||
@@ -129,7 +144,8 @@ internal class MangaExtensionGithubApi {
|
||||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.toExtensionSources().orEmpty(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
|
||||
repository = repository,
|
||||
iconUrl = "${repository}/icon/${it.pkg}.png",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -146,25 +162,33 @@ internal class MangaExtensionGithubApi {
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: MangaExtension.Available): String {
|
||||
return "${getUrlPrefix()}apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
private fun getUrlPrefix(): String {
|
||||
return if (requiresFallbackSource) {
|
||||
FALLBACK_REPO_URL_PREFIX
|
||||
} else {
|
||||
REPO_URL_PREFIX
|
||||
}
|
||||
return "${extension.repository}/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
private fun ExtensionJsonObject.extractLibVersion(): Double {
|
||||
return version.substringBeforeLast('.').toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/keiyoushi/extensions/main/"
|
||||
private const val FALLBACK_REPO_URL_PREFIX =
|
||||
"https://gcore.jsdelivr.net/gh/keiyoushi/extensions@main/"
|
||||
private fun fallbackRepoUrl(repoUrl: String): String? {
|
||||
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
|
||||
val strippedRepoUrl =
|
||||
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
|
||||
val repoUrlParts = strippedRepoUrl.split("/")
|
||||
if (repoUrlParts.size < 3) {
|
||||
return null
|
||||
}
|
||||
val repoOwner = repoUrlParts[1]
|
||||
val repoName = repoUrlParts[2]
|
||||
fallbackRepoUrl += "$repoOwner/$repoName"
|
||||
val repoBranch = if (repoUrlParts.size > 3) {
|
||||
repoUrlParts[3]
|
||||
} else {
|
||||
"main"
|
||||
}
|
||||
fallbackRepoUrl += "@$repoBranch"
|
||||
return fallbackRepoUrl
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
|
||||
@@ -47,6 +47,7 @@ sealed class MangaExtension {
|
||||
val sources: List<AvailableMangaSources>,
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val repository: String
|
||||
) : MangaExtension()
|
||||
|
||||
data class Untrusted(
|
||||
|
||||
@@ -89,5 +89,7 @@ class NetworkHelper(
|
||||
responseParser = Mapper
|
||||
)
|
||||
|
||||
fun defaultUserAgentProvider() = PrefManager.getVal<String>(PrefName.DefaultUserAgent)
|
||||
companion object {
|
||||
fun defaultUserAgentProvider() = PrefManager.getVal<String>(PrefName.DefaultUserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||
@@ -69,7 +70,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", network.defaultUserAgentProvider())
|
||||
add("User-Agent", defaultUserAgentProvider())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user