diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml
index a1277018..7165aeed 100644
--- a/.github/workflows/beta.yml
+++ b/.github/workflows/beta.yml
@@ -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 }}
diff --git a/app/build.gradle b/app/build.gradle
index b4ab706b..97c05e9b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fdab5a4c..934bd7b3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -73,12 +73,24 @@
android:resource="@xml/upcoming_widget_info" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
= 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(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.checkId(id: Int): Boolean {
this.forEach {
if (it.value.id == id) {
diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt
index 8a45b6e3..08f6aac6 100644
--- a/app/src/main/java/ani/dantotsu/MainActivity.kt
+++ b/app/src/main/java/ani/dantotsu/MainActivity.kt
@@ -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(R.id.userAgentTextBox)?.hint = "Password"
val subtitleTextView = dialogView.findViewById(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")
diff --git a/app/src/main/java/ani/dantotsu/Network.kt b/app/src/main/java/ani/dantotsu/Network.kt
index 91840b3e..1c26b3bb 100644
--- a/app/src/main/java/ani/dantotsu/Network.kt
+++ b/app/src/main/java/ani/dantotsu/Network.kt
@@ -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().defaultUserAgentProvider()
+ defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
)
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
index 804343f6..65fbb3cd 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
@@ -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 executeQuery(
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
index 4e02d228..fa4a3090 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
@@ -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, 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
)
)
}
@@ -885,18 +893,23 @@ class AnilistQueries {
sort: String? = null,
genres: MutableList? = null,
tags: MutableList? = null,
+ status: String? = null,
+ source: String? = null,
format: String? = null,
+ countryOfOrigin: String? = null,
isAdult: Boolean = false,
onList: Boolean? = null,
excludedGenres: MutableList? = null,
excludedTags: MutableList? = 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, variables, true)?.data?.page
if (response?.media != null) {
val responseArray = arrayListOf()
@@ -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(
+ """{
+ 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(
+ """{
+ 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? {
@@ -1074,21 +1146,6 @@ Page(page:$page,perPage:50) {
}""".replace("\n", " ").replace(""" """, "")
return executeQuery(query, force = true)?.data?.page
}
- if (smaller) {
- val response = execute()?.airingSchedules ?: return null
- val idArr = mutableListOf()
- 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()
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()
+ while (hasNextPage) {
+ page++
+ hasNextPage = executeQuery(
+ 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(
"""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"
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt
index a4092b91..a802a164 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt
@@ -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>(null)
fun getAnimeContinue(): LiveData> = animeContinue
- suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData> =
MutableLiveData>(null)
fun getAnimeFav(): LiveData> = animeFav
- suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData> =
MutableLiveData>(null)
fun getAnimePlanned(): LiveData> = animePlanned
- suspend fun setAnimePlanned() =
- animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
private val mangaContinue: MutableLiveData> =
MutableLiveData>(null)
fun getMangaContinue(): LiveData> = mangaContinue
- suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData> =
MutableLiveData>(null)
fun getMangaFav(): LiveData> = mangaFav
- suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData> =
MutableLiveData>(null)
fun getMangaPlanned(): LiveData> = mangaPlanned
- suspend fun setMangaPlanned() =
- mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
private val recommendation: MutableLiveData> =
MutableLiveData>(null)
fun getRecommendation(): LiveData> = 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> =
- MutableLiveData>(null)
-
- fun getUpdated(): LiveData> = updated
- suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
private val animePopular = MutableLiveData(null)
+
fun getPopular(): LiveData = 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> =
+ MutableLiveData>(null)
+ fun getUpdated(): LiveData> = updated
+
+ private val popularMovies: MutableLiveData> =
+ MutableLiveData>(null)
+ fun getMovies(): LiveData> = popularMovies
+
+ private val topRatedAnime: MutableLiveData> =
+ MutableLiveData>(null)
+ fun getTopRated(): LiveData> = topRatedAnime
+
+ private val mostFavAnime: MutableLiveData> =
+ MutableLiveData>(null)
+ fun getMostFav(): LiveData> = 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()
+ 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> =
- MutableLiveData>(null)
-
- fun getTrendingNovel(): LiveData> = updated
- suspend fun loadTrendingNovel() =
- updated.postValue(
- Anilist.query.search(
- type,
- perPage = 10,
- sort = Anilist.sortBy[2],
- format = "NOVEL"
- )?.results
- )
private val mangaPopular = MutableLiveData(null)
fun getPopular(): LiveData = 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> =
+ MutableLiveData>(null)
+ fun getPopularManga(): LiveData> = popularManga
+
+ private val popularManhwa: MutableLiveData> =
+ MutableLiveData>(null)
+ fun getPopularManhwa(): LiveData> = popularManhwa
+
+ private val popularNovel: MutableLiveData> =
+ MutableLiveData>(null)
+ fun getPopularNovel(): LiveData> = popularNovel
+
+ private val topRatedManga: MutableLiveData> =
+ MutableLiveData>(null)
+ fun getTopRated(): LiveData> = topRatedManga
+
+ private val mostFavManga: MutableLiveData> =
+ MutableLiveData>(null)
+ fun getMostFav(): LiveData> = 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> = animeFav
- private val listImages: MutableLiveData> =
- MutableLiveData>(arrayListOf())
-
- fun getListImages(): LiveData> = 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(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)
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt b/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt
index 32bc1757..8458cc7e 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt
@@ -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? = null,
var excludedGenres: MutableList? = null,
var tags: MutableList? = null,
var excludedTags: MutableList? = 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,
@@ -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)
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Character.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Character.kt
index e0539085..ec5a9ef4 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Character.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Character.kt
@@ -55,7 +55,7 @@ data class CharacterConnection(
@SerialName("nodes") var nodes: List?,
// 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?,
+ @SerialName("voiceActors") var voiceActors: List?,
// The voice actors of the character with role date
// @SerialName("voiceActorRoles") var voiceActorRoles: List?,
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
index 58445406..20f35035 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
@@ -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")
diff --git a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt
new file mode 100644
index 00000000..7d811ba3
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt
@@ -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()
+ 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? = 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
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt
index a114c1e5..4dd0e903 100644
--- a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt
+++ b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt
@@ -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,
diff --git a/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt b/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt
new file mode 100644
index 00000000..f796e38a
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt
@@ -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 {
+ var developers = arrayOf()
+ runBlocking(Dispatchers.IO) {
+ val repo = getAppString(R.string.repo)
+ val res = client.get("https://api.github.com/repos/$repo/contributors")
+ .parsed().map {
+ Mapper.json.decodeFromJsonElement(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
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/connections/github/Forks.kt b/app/src/main/java/ani/dantotsu/connections/github/Forks.kt
new file mode 100644
index 00000000..a074eea0
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/connections/github/Forks.kt
@@ -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 {
+ var forks = arrayOf()
+ runBlocking(Dispatchers.IO) {
+ val res = client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks")
+ .parsed().map {
+ Mapper.json.decodeFromJsonElement(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
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
index 4eb7f5a3..fa01cc79 100644
--- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
+++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
@@ -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) //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(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()
+}
diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
index 5d9c7a5b..daaa200e 100644
--- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
@@ -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().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().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().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 {
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 = ConcurrentLinkedQueue()
@Volatile
diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
index 64329759..6c21cb86 100644
--- a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
@@ -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(R.id.downloadedList)
- val layoutcompact = view.findViewById(R.id.downloadedGrid)
+ val layoutCompact = view.findViewById(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()
- 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()
+ 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 {
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
diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
index 92e811bd..08ddb3a7 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
@@ -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>()
val deferredMap = mutableMapOf>()
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 {
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) {
diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
index 99250edf..1d912887 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
@@ -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()
- 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()
- 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()
+ 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()
+ 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 {
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,
diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
index fc432313..123a54c1 100644
--- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
@@ -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 {
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) {
diff --git a/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
deleted file mode 100644
index 4add998c..00000000
--- a/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
+++ /dev/null
@@ -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,
- notMetRequirements: Int
- ): Notification =
- DownloadNotificationHelper(this, "download_service").buildProgressNotification(
- this,
- R.drawable.mono,
- null,
- null,
- downloads,
- notMetRequirements
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt
index 258c123a..ed146b90 100644
--- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt
+++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt
@@ -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()
- val dataSourceFactory = DataSource.Factory {
- //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
- val networkHelper = Injekt.get()
- 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()
- 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(
diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
index c7434c96..0be7a3e2 100644
--- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
@@ -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
diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt
index 2aacbda4..9861113a 100644
--- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt
@@ -195,24 +195,67 @@ class AnimePageAdapter : RecyclerView.Adapter 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
diff --git a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
index 24eedc27..17c4dec8 100644
--- a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
@@ -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
diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt
index ac823a5f..27f4c8ea 100644
--- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt
@@ -178,25 +178,76 @@ class MangaPageAdapter : RecyclerView.Adapter>? = null
+ var id: Int,
+ var name: String?,
+ var image: String?,
+ var role: String?,
+ var yearMedia: MutableMap>? = null,
+ var character: ArrayList? = null
) : Serializable
diff --git a/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt b/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt
index 22bc54f1..6c5ee146 100644
--- a/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt
@@ -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()
+ val concatAdapter = ConcatAdapter()
+ val map = author!!.yearMedia ?: return@observe
+ val keys = map.keys.toTypedArray()
+ var pos = 0
- val titlePosition = arrayListOf()
- 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)
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt b/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt
index 56086b20..c7ac9645 100644
--- a/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt
@@ -15,7 +15,8 @@ import ani.dantotsu.setAnimation
import java.io.Serializable
class AuthorAdapter(
- private val authorList: ArrayList
+ private val authorList: ArrayList,
+ private val isVA: Boolean = false,
) : RecyclerView.Adapter() {
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(
diff --git a/app/src/main/java/ani/dantotsu/media/Character.kt b/app/src/main/java/ani/dantotsu/media/Character.kt
index e774ed30..48746505 100644
--- a/app/src/main/java/ani/dantotsu/media/Character.kt
+++ b/app/src/main/java/ani/dantotsu/media/Character.kt
@@ -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? = null
+ var roles: ArrayList? = null,
+ val voiceActor: ArrayList? = null,
) : Serializable
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
index e82036eb..d97e138d 100644
--- a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
@@ -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
diff --git a/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt b/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt
index ec111dc3..d3d5d811 100644
--- a/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt
@@ -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
diff --git a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
index 78d264ee..8fa4e1fa 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
@@ -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
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
index a7b8858f..4f93cdde 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
@@ -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
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
index 00d3224d..609de856 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
@@ -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)
}
diff --git a/app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt b/app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
new file mode 100644
index 00000000..07d151a0
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
@@ -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 = "(? "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
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt
index 677d700f..394ddf0a 100644
--- a/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt
+++ b/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt
@@ -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 = MutableLiveData(null)
+ fun getVoiceActor(): LiveData = voiceActor
+ suspend fun loadVoiceActor(m: Author) {
+ if (voiceActor.value == null) voiceActor.postValue(Anilist.query.getVoiceActorsDetails(m))
+ }
private val calendar: MutableLiveData