Merge branch 'dev' into balista

This commit is contained in:
ibo
2024-04-05 19:09:01 +02:00
committed by GitHub
173 changed files with 6930 additions and 3360 deletions

View File

@@ -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 }}

View File

@@ -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'

View File

@@ -73,12 +73,24 @@
android:resource="@xml/upcoming_widget_info" />
</receiver>
<activity
android:name=".widgets.upcoming.UpcomingWidgetConfigureActivity"
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".widgets.statistics.ProfileStatsWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/statistics_widget_info" />
</receiver>
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
<activity
@@ -115,6 +127,14 @@
android:name=".settings.ExtensionsActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".widgets.statistics.ProfileStatsConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
<activity
android:name=".profile.ProfileActivity"
android:parentActivityName=".MainActivity"
@@ -350,16 +370,6 @@
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".download.video.ExoplayerDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<service
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
android:exported="false"

View File

@@ -92,6 +92,7 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
@@ -102,6 +103,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
@@ -134,9 +136,12 @@ import io.noties.markwon.html.TagHandlerNoOp
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -214,7 +219,8 @@ fun initActivity(a: Activity) {
window,
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) {
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
@@ -619,9 +625,14 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(size).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
}
}
}
@@ -876,31 +887,6 @@ fun savePrefs(
}
}
fun downloadsPermission(activity: AppCompatActivity): Boolean {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val requiredPermissions = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
activity,
requiredPermissions,
DOWNLOADS_PERMISSION_REQUEST_CODE
)
false
} else {
true
}
}
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
val contentUri = FileProvider.getUriForFile(
@@ -1004,6 +990,54 @@ fun countDown(media: Media, view: ViewGroup) {
}
}
fun sinceWhen(media: Media, view: ViewGroup) {
CoroutineScope(Dispatchers.IO).launch {
MangaUpdates().search(media.name ?: media.nameRomaji, media.startDate)?.let {
val latestChapter = it.metadata.series.latestChapter ?: it.record.chapter?.let { chapter ->
if (chapter.contains("-"))
chapter.split("-")[1].trim()
else
chapter
}?.toInt()
val timeSince = (System.currentTimeMillis() -
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
withContext(Dispatchers.Main) {
val v =
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
object : CountUpTimer(86400000) {
override fun onTick(second: Int) {
val a = second + timeSince
v.mediaCountdown.text = currActivity()?.getString(
R.string.time_format,
a / 86400,
a % 86400 / 3600,
a % 86400 % 3600 / 60,
a % 86400 % 3600 % 60
)
}
override fun onFinish() {
// The legend will never die.
}
}.start()
}
}
}
}
fun displayTimer(media: Media, view: ViewGroup) {
when {
media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> { } // No timer yet
}
}
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
this.forEach {
if (it.value.id == id) {

View File

@@ -3,6 +3,7 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Animatable
@@ -18,8 +19,8 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
@@ -54,6 +55,7 @@ import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.activity.NotificationActivity
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName
@@ -228,17 +230,6 @@ class MainActivity : AppCompatActivity() {
}
}
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
Toast.makeText(
this,
"You have extension updates available!",
Toast.LENGTH_LONG
).show()
}
binding.root.isMotionEventSplittingEnabled = false
lifecycleScope.launch {
@@ -282,6 +273,16 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
}
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
selectedOption = if (fragment != null) {
when (fragment) {
@@ -448,7 +449,7 @@ class MainActivity : AppCompatActivity() {
}
}
}
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
/*lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
@@ -457,7 +458,7 @@ class MainActivity : AppCompatActivity() {
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
}
}
}
}*/ //TODO: remove this
}
override fun onRestart() {
@@ -482,7 +483,7 @@ class MainActivity : AppCompatActivity() {
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
subtitleTextView?.visibility = View.VISIBLE
subtitleTextView?.text = "Enter your password to decrypt the file"
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Enter Password")

View File

@@ -9,6 +9,7 @@ import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -40,7 +41,7 @@ fun initializeNetwork() {
defaultHeaders = mapOf(
"User-Agent" to
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
)

View File

@@ -7,6 +7,7 @@ import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
@@ -40,20 +41,54 @@ object Anilist {
"SCORE_DESC",
"POPULARITY_DESC",
"TRENDING_DESC",
"START_DATE_DESC",
"TITLE_ENGLISH",
"TITLE_ENGLISH_DESC",
"SCORE"
)
val source = listOf(
"ORIGINAL",
"MANGA",
"LIGHT NOVEL",
"VISUAL NOVEL",
"VIDEO GAME",
"OTHER",
"NOVEL",
"DOUJINSHI",
"ANIME",
"WEB NOVEL",
"LIVE ACTION",
"GAME",
"COMIC",
"MULTIMEDIA PROJECT",
"PICTURE BOOK"
)
val animeStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"CANCELLED"
)
val mangaStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"HIATUS",
"CANCELLED"
)
val seasons = listOf(
"WINTER", "SPRING", "SUMMER", "FALL"
)
val anime_formats = listOf(
val animeFormats = listOf(
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
)
val manga_formats = listOf(
val mangaFormats = listOf(
"MANGA", "NOVEL", "ONE SHOT"
)
@@ -117,6 +152,9 @@ object Anilist {
episodesWatched = null
chapterRead = null
PrefManager.removeVal(PrefName.AnilistToken)
//logout from comments api
CommentsAPI.logout()
}
suspend inline fun <reified T : Any> executeQuery(

View File

@@ -72,7 +72,7 @@ class AnilistQueries {
media.cameFromContinue = false
val query =
"""{Media(id:${media.id}){id favourites popularity mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
"""{Media(id:${media.id}){id favourites popularity mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true)
@@ -139,7 +139,15 @@ class AnilistQueries {
?: "SUPPORTING"
else -> i.role.toString()
}
},
voiceActor = i.voiceActors?.map {
Author(
id = it.id,
name = it.name?.userPreferred,
image = it.image?.medium,
role = it.languageV2
)
} as ArrayList<Author>
)
)
}
@@ -885,18 +893,23 @@ class AnilistQueries {
sort: String? = null,
genres: MutableList<String>? = null,
tags: MutableList<String>? = null,
status: String? = null,
source: String? = null,
format: String? = null,
countryOfOrigin: String? = null,
isAdult: Boolean = false,
onList: Boolean? = null,
excludedGenres: MutableList<String>? = null,
excludedTags: MutableList<String>? = null,
startYear: Int? = null,
seasonYear: Int? = null,
season: String? = null,
id: Int? = null,
hd: Boolean = false,
adultOnly: Boolean = false
): SearchResults? {
val query = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
pageInfo {
total
@@ -941,14 +954,19 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}
""".replace("\n", " ").replace(""" """, "")
val variables = """{"type":"$type","isAdult":$isAdult
${if (adultOnly) ""","isAdult":true""" else ""}
${if (onList != null) ""","onList":$onList""" else ""}
${if (page != null) ""","page":"$page"""" else ""}
${if (id != null) ""","id":"$id"""" else ""}
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
${if (season != null) ""","season":"$season"""" else ""}
${if (search != null) ""","search":"$search"""" else ""}
${if (source != null) ""","source":"$source"""" else ""}
${if (sort != null) ""","sort":"$sort"""" else ""}
${if (status != null) ""","status":"$status"""" else ""}
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
${
if (excludedGenres?.isNotEmpty() == true)
@@ -980,7 +998,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
else ""
}
}""".replace("\n", " ").replace(""" """, "")
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
if (response?.media != null) {
val responseArray = arrayListOf<Media>()
@@ -1012,7 +1029,11 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
excludedGenres = excludedGenres,
tags = tags,
excludedTags = excludedTags,
status = status,
source = source,
format = format,
countryOfOrigin = countryOfOrigin,
startYear = startYear,
seasonYear = seasonYear,
season = season,
results = responseArray,
@@ -1022,9 +1043,60 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}
return null
}
private val onListAnime = (if(PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace("\"", "")
private val isAdult = (if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
private fun recentAnimeUpdates(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
}
private fun trendingMovies(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedAnime(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavAnime(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadAnimeList(): Query.AnimeList?{
return executeQuery<Query.AnimeList>(
"""{
recentUpdates:${recentAnimeUpdates()}
trendingMovies:${trendingMovies()}
topRated:${topRatedAnime()}
mostFav:${mostFavAnime()}
}""".trimIndent(), force = true
)
}
private val onListManga = (if(PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace("\"", "")
private fun trendingManga(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingManhwa(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingNovel(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedManga(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavManga(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadMangaList(): Query.MangaList?{
return executeQuery<Query.MangaList>(
"""{
trendingManga:${trendingManga()}
trendingManhwa:${trendingManhwa()}
trendingNovel:${trendingNovel()}
topRated:${topRatedManga()}
mostFav:${mostFavManga()}
}""".trimIndent(), force = true
)
}
suspend fun recentlyUpdated(
smaller: Boolean = true,
greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000
): MutableList<Media>? {
@@ -1074,21 +1146,6 @@ Page(page:$page,perPage:50) {
}""".replace("\n", " ").replace(""" """, "")
return executeQuery<Query.Page>(query, force = true)?.data?.page
}
if (smaller) {
val response = execute()?.airingSchedules ?: return null
val idArr = mutableListOf<Int>()
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
return response.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
}
}.toMutableList()
} else {
var i = 1
val list = mutableListOf<Media>()
var res: Page? = null
@@ -1108,7 +1165,6 @@ Page(page:$page,perPage:50) {
i++
}
return list.reversed().toMutableList()
}
}
suspend fun getCharacterDetails(character: Character): Character {
@@ -1331,7 +1387,52 @@ Page(page:$page,perPage:50) {
author.yearMedia = yearMedia
return author
}
suspend fun getVoiceActorsDetails(author: Author): Author {
fun query(page: Int = 0) = """ {
Staff(id:${author.id}) {
id
characters(page: $page,sort:FAVOURITES_DESC) {
pageInfo{
hasNextPage
}
nodes{
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
}
}
}
}""".replace("\n", " ").replace(""" """, "")
var hasNextPage = true
var page = 0
val characters = arrayListOf<Character>()
while (hasNextPage) {
page++
hasNextPage = executeQuery<Query.Author>(
query(page),
force = true
)?.data?.author?.characters?.let {
it.nodes?.forEach { i ->
characters.add(Character(i.id, i.name?.userPreferred, i.image?.large, i.image?.medium, "", false))
}
it.pageInfo?.hasNextPage == true
} ?: false
}
author.character = characters
return author
}
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
@@ -1394,15 +1495,10 @@ Page(page:$page,perPage:50) {
"""{
favoriteAnime:${userFavMediaQuery(true, 1, id)}
favoriteManga:${userFavMediaQuery(false, 1, id)}
animeMediaList:${bannerImageQuery("ANIME", id)}
mangaMediaList:${bannerImageQuery("MANGA", id)}
}""".trimIndent(), force = true
)
}
private fun bannerImageQuery(type: String, id: Int?): String {
return """MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } }"""
}
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
val reset = if (resetNotification) "true" else "false"

View File

@@ -5,6 +5,8 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.webkit.internal.ApiFeature.P
import androidx.webkit.internal.StartupApiFeature
import ani.dantotsu.BuildConfig
import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord
@@ -58,45 +60,36 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
suspend fun setAnimePlanned() =
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
suspend fun setMangaPlanned() =
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
private val recommendation: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
suspend fun initHomePage() {
val res = Anilist.query.initHomePage()
@@ -144,18 +137,15 @@ class AnilistAnimeViewModel : ViewModel() {
sort = Anilist.sortBy[2],
season = season,
seasonYear = year,
hd = true
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)?.results
)
}
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
private val animePopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular
suspend fun loadPopular(
type: String,
@@ -170,7 +160,8 @@ class AnilistAnimeViewModel : ViewModel() {
search = searchVal,
onList = if (onList) null else false,
sort = sort,
genres = genres
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
}
@@ -185,13 +176,60 @@ class AnilistAnimeViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList
r.onList,
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
)
)
var loaded: Boolean = false
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
private val popularMovies: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
suspend fun loadAll() {
val res = Anilist.query.loadAnimeList()?.data
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
res?.apply{
val idArr = mutableListOf<Int>()
updated.postValue(recentUpdates?.airingSchedules?.mapNotNull {i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
idArr.add(it.id)
Media(it)
}else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)){
idArr.add(it.id)
Media(it)
}else if ((listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
}else null
else null
}
}?.toMutableList() ?: arrayListOf())
popularMovies.postValue(trendingMovies?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
topRatedAnime.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
mostFavAnime.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
}
}
}
class AnilistMangaViewModel : ViewModel() {
@@ -209,23 +247,11 @@ class AnilistMangaViewModel : ViewModel() {
type,
perPage = 10,
sort = Anilist.sortBy[2],
hd = true
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)?.results
)
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
suspend fun loadTrendingNovel() =
updated.postValue(
Anilist.query.search(
type,
perPage = 10,
sort = Anilist.sortBy[2],
format = "NOVEL"
)?.results
)
private val mangaPopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular
@@ -242,7 +268,8 @@ class AnilistMangaViewModel : ViewModel() {
search = searchVal,
onList = if (onList) null else false,
sort = sort,
genres = genres
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
}
@@ -257,17 +284,53 @@ class AnilistMangaViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
r.season,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
var loaded: Boolean = false
private val popularManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
private val popularManhwa: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
private val popularNovel: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
private val topRatedManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
private val mostFavManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
suspend fun loadAll() {
val response = Anilist.query.loadMangaList()?.data
response?.apply {
popularManga.postValue(trendingManga?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
popularManhwa.postValue(trendingManhwa?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
popularNovel.postValue(trendingNovel?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
topRatedManga.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
mostFavManga.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
}
}
}
class AnilistSearch : ViewModel() {
@@ -286,13 +349,17 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
r.season,
)
)
@@ -305,11 +372,15 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
)
@@ -347,11 +418,6 @@ class ProfileViewModel : ViewModel() {
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
private val listImages: MutableLiveData<ArrayList<String?>> =
MutableLiveData<ArrayList<String?>>(arrayListOf())
fun getListImages(): LiveData<ArrayList<String?>> = listImages
suspend fun setData(id: Int) {
val res = Anilist.query.initProfilePage(id)
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
@@ -367,30 +433,11 @@ class ProfileViewModel : ViewModel() {
}
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
val bannerImages = arrayListOf<String?>(null, null)
val animeRandom = res?.data?.animeMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[0] = animeRandom
val mangaRandom = res?.data?.mangaMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[1] = mangaRandom
listImages.postValue(bannerImages)
}
fun refresh() {
mangaFav.postValue(mangaFav.value)
animeFav.postValue(animeFav.value)
listImages.postValue(listImages.value)
}
}

View File

@@ -11,13 +11,17 @@ data class SearchResults(
var onList: Boolean? = null,
var perPage: Int? = null,
var search: String? = null,
var countryOfOrigin :String? = null,
var sort: String? = null,
var genres: MutableList<String>? = null,
var excludedGenres: MutableList<String>? = null,
var tags: MutableList<String>? = null,
var excludedTags: MutableList<String>? = null,
var status: String? = null,
var source: String? = null,
var format: String? = null,
var seasonYear: Int? = null,
var startYear: Int? = null,
var season: String? = null,
var page: Int = 1,
var results: MutableList<Media>,
@@ -37,12 +41,24 @@ data class SearchResults(
)
)
}
status?.let {
list.add(SearchChip("STATUS", currContext()!!.getString(R.string.filter_status, it)))
}
source?.let {
list.add(SearchChip("SOURCE", currContext()!!.getString(R.string.filter_source, it)))
}
format?.let {
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
}
countryOfOrigin?.let {
list.add(SearchChip("COUNTRY", currContext()!!.getString(R.string.filter_country, it)))
}
season?.let {
list.add(SearchChip("SEASON", it))
}
startYear?.let {
list.add(SearchChip("START_YEAR", it.toString()))
}
seasonYear?.let {
list.add(SearchChip("SEASON_YEAR", it.toString()))
}
@@ -74,8 +90,12 @@ data class SearchResults(
fun removeChip(chip: SearchChip) {
when (chip.type) {
"SORT" -> sort = null
"STATUS" -> status = null
"SOURCE" -> source = null
"FORMAT" -> format = null
"COUNTRY" -> countryOfOrigin = null
"SEASON" -> season = null
"START_YEAR" -> startYear = null
"SEASON_YEAR" -> seasonYear = null
"GENRE" -> genres?.remove(chip.text)
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)

View File

@@ -55,7 +55,7 @@ data class CharacterConnection(
@SerialName("nodes") var nodes: List<Character>?,
// The pagination information
// @SerialName("pageInfo") var pageInfo: PageInfo?,
@SerialName("pageInfo") var pageInfo: PageInfo?,
) : java.io.Serializable
@Serializable
@@ -72,7 +72,7 @@ data class CharacterEdge(
@SerialName("name") var name: String?,
// The voice actors of the character
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
@SerialName("voiceActors") var voiceActors: List<Staff>?,
// The voice actors of the character with role date
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,

View File

@@ -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")

View File

@@ -0,0 +1,98 @@
package ani.dantotsu.connections.bakaupdates
import ani.dantotsu.client
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.tryWithSuspend
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okio.ByteString.Companion.encode
import org.json.JSONException
import org.json.JSONObject
import java.nio.charset.Charset
class MangaUpdates {
private val Int?.dateFormat get() = String.format("%02d", this)
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
suspend fun search(title: String, startDate: FuzzyDate?) : MangaUpdatesResponse.Results? {
return tryWithSuspend {
val query = JSONObject().apply {
try {
put("search", title.encode(Charset.forName("UTF-8")))
startDate?.let {
put(
"start_date",
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
)
}
put("include_metadata", true)
} catch (e: JSONException) {
e.printStackTrace()
}
}
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
res.results?.forEach{ println("MangaUpdates: $it") }
res.results?.first { it.metadata.series.lastUpdated?.timestamp != null }
}
}
@Serializable
data class MangaUpdatesResponse(
@SerialName("total_hits")
val totalHits: Int?,
@SerialName("page")
val page: Int?,
@SerialName("per_page")
val perPage: Int?,
val results: List<Results>? = null
) {
@Serializable
data class Results(
val record: Record,
val metadata: MetaData
) {
@Serializable
data class Record(
@SerialName("id")
val id: Int,
@SerialName("title")
val title: String,
@SerialName("volume")
val volume: String?,
@SerialName("chapter")
val chapter: String?,
@SerialName("release_date")
val releaseDate: String
)
@Serializable
data class MetaData(
val series: Series
) {
@Serializable
data class Series(
@SerialName("series_id")
val seriesId: Long?,
@SerialName("title")
val title: String?,
@SerialName("latest_chapter")
val latestChapter: Int?,
@SerialName("last_updated")
val lastUpdated: LastUpdated?
) {
@Serializable
data class LastUpdated(
@SerialName("timestamp")
val timestamp: Long,
@SerialName("as_rfc3339")
val asRfc3339: String,
@SerialName("as_string")
val asString: String
)
}
}
}
}
}

View File

@@ -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,

View File

@@ -0,0 +1,84 @@
package ani.dantotsu.connections.github
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.getAppString
import ani.dantotsu.settings.Developer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
class Contributors {
fun getContributors(): Array<Developer> {
var developers = arrayOf<Developer>()
runBlocking(Dispatchers.IO) {
val repo = getAppString(R.string.repo)
val res = client.get("https://api.github.com/repos/$repo/contributors")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.find { it.login == "rebelonion"}?.let { first ->
developers = developers.plus(
Developer(
first.login,
first.avatarUrl,
"Owner and Maintainer",
first.htmlUrl
)
).plus(arrayOf(
Developer(
"Wai What",
"https://avatars.githubusercontent.com/u/149729762?v=4",
"Icon Designer",
"https://github.com/WaiWhat"
),
Developer(
"MarshMeadow",
"https://avatars.githubusercontent.com/u/88599122?v=4",
"Beta Icon Designer",
"https://github.com/MarshMeadow?tab=repositories"
),
Developer(
"Zaxx69",
"https://avatars.githubusercontent.com/u/138523882?v=4",
"Telegram Admin",
"https://github.com/Zaxx69"
),
Developer(
"Arif Alam",
"https://avatars.githubusercontent.com/u/70383209?v=4",
"Head Discord Moderator",
"https://youtube.com/watch?v=dQw4w9WgXcQ"
)
))
}
res.filter {it.login != "rebelonion"}.forEach {
developers = developers.plus(
Developer(
it.login,
it.avatarUrl,
"Contributor",
it.htmlUrl
)
)
}
}
return developers
}
@Serializable
data class GithubResponse(
@SerialName("login")
val login: String,
@SerialName("avatar_url")
val avatarUrl: String,
@SerialName("html_url")
val htmlUrl: String
)
}

View File

@@ -0,0 +1,55 @@
package ani.dantotsu.connections.github
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.getAppString
import ani.dantotsu.settings.Developer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
class Forks {
fun getForks(): Array<Developer> {
var forks = arrayOf<Developer>()
runBlocking(Dispatchers.IO) {
val res = client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.forEach {
forks = forks.plus(
Developer(
it.name,
it.owner.avatarUrl,
it.owner.login,
it.htmlUrl
)
)
}
}
return forks
}
@Serializable
data class GithubResponse(
@SerialName("name")
val name: String,
val owner: Owner,
@SerialName("html_url")
val htmlUrl: String,
) {
@Serializable
data class Owner(
@SerialName("login")
val login: String,
@SerialName("avatar_url")
val avatarUrl: String
)
}
}

View File

@@ -1,14 +1,25 @@
package ani.dantotsu.download
import android.content.Context
import android.os.Environment
import android.widget.Toast
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.media.MediaType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.findFolder
import com.anggrayudi.storage.file.moveFileTo
import com.anggrayudi.storage.file.moveFolderTo
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable
class DownloadsManager(private val context: Context) {
@@ -42,27 +53,29 @@ class DownloadsManager(private val context: Context) {
saveDownloads()
}
fun removeDownload(downloadedType: DownloadedType) {
fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
downloadsList.remove(downloadedType)
removeDirectory(downloadedType)
CoroutineScope(Dispatchers.IO).launch {
removeDirectory(downloadedType)
withContext(Dispatchers.Main) {
onFinished()
}
}
saveDownloads()
}
fun removeMedia(title: String, type: MediaType) {
val subDirectory = type.asText()
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
if (directory.exists()) {
val deleted = directory.deleteRecursively()
val baseDirectory = getBaseDirectory(context, type)
val directory = baseDirectory?.findFolder(title)
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
snackString("Successfully deleted")
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
snackString("Failed to delete directory")
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
snackString("Directory does not exist")
cleanDownloads()
}
when (type) {
@@ -89,23 +102,17 @@ class DownloadsManager(private val context: Context) {
private fun cleanDownload(type: MediaType) {
// remove all folders that are not in the downloads list
val subDirectory = type.asText()
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory"
)
val directory = getBaseDirectory(context, type)
val downloadsSubLists = when (type) {
MediaType.MANGA -> mangaDownloadedTypes
MediaType.ANIME -> animeDownloadedTypes
else -> novelDownloadedTypes
}
if (directory.exists()) {
if (directory?.exists() == true && directory.isDirectory) {
val files = directory.listFiles()
if (files != null) {
for (file in files) {
if (!downloadsSubLists.any { it.title == file.name }) {
file.deleteRecursively()
}
for (file in files) {
if (!downloadsSubLists.any { it.title == file.name }) {
file.deleteRecursively(context, false)
}
}
}
@@ -113,27 +120,57 @@ class DownloadsManager(private val context: Context) {
val iterator = downloadsList.iterator()
while (iterator.hasNext()) {
val download = iterator.next()
val downloadDir = File(directory, download.title)
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
val downloadDir = directory?.findFolder(download.title)
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
iterator.remove()
}
}
}
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
{
val jsonString = gson.toJson(downloadsList)
val file = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/downloads.json"
)
if (file.parentFile?.exists() == false) {
file.parentFile?.mkdirs()
fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
try {
if (oldUri == newUri) {
finished(false, "Source and destination are the same")
return
}
CoroutineScope(Dispatchers.IO).launch {
val oldBase =
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
val newBase =
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
val folder =
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object:
FolderCallback() {
override fun onFailed(errorCode: ErrorCode) {
when (errorCode) {
ErrorCode.CANCELED -> finished(false, "Move canceled")
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(false, "Cannot create file in target")
ErrorCode.INVALID_TARGET_FOLDER -> finished(true, "Invalid target folder") // seems to still work
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(false, "No space left on target path")
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(false, "Source folder not found")
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(false, "Storage permission denied")
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(false, "Target folder cannot have same path with source folder")
else -> finished(false, "Failed to move downloads: $errorCode")
}
Logger.log("Failed to move downloads: $errorCode")
super.onFailed(errorCode)
}
override fun onCompleted(result: Result) {
finished(true, "Successfully moved downloads")
super.onCompleted(result)
}
})
}
} catch (e: Exception) {
snackString("Error: ${e.message}")
finished(false, "Failed to move downloads: ${e.message}")
return
}
if (!file.exists()) {
file.createNewFile()
}
file.writeText(jsonString)
}
fun queryDownload(downloadedType: DownloadedType): Boolean {
@@ -149,98 +186,35 @@ class DownloadsManager(private val context: Context) {
}
private fun removeDirectory(downloadedType: DownloadedType) {
val directory = when (downloadedType.type) {
MediaType.MANGA -> {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
}
MediaType.ANIME -> {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
}
else -> {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
}
val baseDirectory = getBaseDirectory(context, downloadedType.type)
val directory =
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
// Check if the directory exists and delete it recursively
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
snackString("Successfully deleted")
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
val directory = when (downloadedType.type) {
MediaType.MANGA -> {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
}
MediaType.ANIME -> {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
}
else -> {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
}
val destination = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
)
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
if (copied) {
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
snackString("Failed to delete directory")
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
snackString("Directory does not exist")
}
}
fun purgeDownloads(type: MediaType) {
val directory = when (type) {
MediaType.MANGA -> {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
}
MediaType.ANIME -> {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
}
else -> {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
}
}
if (directory.exists()) {
val deleted = directory.deleteRecursively()
val directory = getBaseDirectory(context, type)
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
snackString("Successfully deleted")
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
snackString("Failed to delete directory")
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
snackString("Directory does not exist")
}
downloadsList.removeAll { it.type == type }
@@ -248,59 +222,95 @@ class DownloadsManager(private val context: Context) {
}
companion object {
const val novelLocation = "Dantotsu/Novel"
const val mangaLocation = "Dantotsu/Manga"
const val animeLocation = "Dantotsu/Anime"
private const val BASE_LOCATION = "Dantotsu"
private const val MANGA_SUB_LOCATION = "Manga"
private const val ANIME_SUB_LOCATION = "Anime"
private const val NOVEL_SUB_LOCATION = "Novel"
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
fun getDirectory(
context: Context,
type: MediaType,
title: String,
chapter: String? = null
): File {
fun String?.findValidName(): String {
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
}
/**
* Get and create a base directory for the given type
* @param context the context
* @param type the type of media
* @return the base directory
*/
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
return when (type) {
MediaType.MANGA -> {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title"
)
}
base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
}
MediaType.ANIME -> {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title"
)
}
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
}
else -> {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title"
)
}
base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
}
}
}
/**
* Get and create a subdirectory for the given type
* @param context the context
* @param type the type of media
* @param title the title of the media
* @param chapter the chapter of the media
* @return the subdirectory
*/
fun getSubDirectory(
context: Context,
type: MediaType,
overwrite: Boolean,
title: String,
chapter: String? = null
): DocumentFile? {
val baseDirectory = getBaseDirectory(context, type) ?: return null
return if (chapter != null) {
baseDirectory.findOrCreateFolder(title, false)
?.findOrCreateFolder(chapter, overwrite)
} else {
baseDirectory.findOrCreateFolder(title, overwrite)
}
}
fun getDirSize(context: Context, type: MediaType, title: String, chapter: String? = null): Long {
val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0
var size = 0L
directory.listFiles().forEach {
size += it.length()
}
return size
}
private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean
): DocumentFile? {
return if (overwrite) {
findFolder(name.findValidName())?.delete()
createDirectory(name.findValidName())
} else {
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
}
}
}
}
data class DownloadedType(val title: String, val chapter: String, val type: MediaType) : Serializable
data class DownloadedType(
val pTitle: String, val pChapter: String, val type: MediaType
) : Serializable {
val title: String
get() = pTitle.findValidName()
val chapter: String
get() = pChapter.findValidName()
}

View File

@@ -9,24 +9,21 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
@@ -36,6 +33,12 @@ import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.FFmpegKitConfig
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.SessionState
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -46,7 +49,6 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
@@ -56,13 +58,12 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class AnimeDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
@@ -88,6 +89,7 @@ class AnimeDownloaderService : Service() {
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(100, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
@@ -156,27 +158,14 @@ class AnimeDownloaderService : Service() {
@UnstableApi
fun cancelDownload(taskName: String) {
val url =
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
if (url.isEmpty()) {
snackString("Failed to cancel download")
return
val sessionIds =
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
.map { it.sessionId }.toMutableList()
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
sessionIds.forEach {
FFmpegKit.cancel(it)
}
currentTasks.removeAll { it.getTaskName() == taskName }
DownloadService.sendSetStopReason(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
false
)
DownloadService.sendRemoveDownload(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
false
)
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[taskName]?.cancel()
@@ -209,7 +198,7 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
try {
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
//val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
@@ -220,18 +209,80 @@ class AnimeDownloaderService : Service() {
true
}
builder.setContentText("Downloading ${task.title} - ${task.episode}")
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
)
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
?: throw Exception("Failed to create output file")
var percent = 0
var totalLength = 0.0
val path = FFmpegKitConfig.getSafParameterForWrite(
this@AnimeDownloaderService,
outputFile.uri
)
val headersStringBuilder = StringBuilder().append(" ")
task.video.file.headers.forEach {
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
}
headersStringBuilder.append(" ")
FFprobeKit.executeAsync(
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"",
{
Logger.log("FFprobeKit: $it")
}, {
if (it.message.toDoubleOrNull() != null) {
totalLength = it.message.toDouble()
}
})
var request = "-headers"
val headers = headersStringBuilder.toString()
if (task.video.file.headers.isNotEmpty()) {
request += headers
}
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
println("Request: $request")
val ffTask =
FFmpegKit.executeAsync(request,
{ session ->
val state: SessionState = session.state
val returnCode = session.returnCode
// CALLED WHEN SESSION IS EXECUTED
Logger.log(
java.lang.String.format(
"FFmpeg process exited with state %s and rc %s.%s",
state,
returnCode,
session.failStackTrace
)
)
}, {
// CALLED WHEN SESSION PRINTS LOGS
Logger.log(it.message)
}) {
// CALLED WHEN SESSION GENERATES STATISTICS
val timeInMilliseconds = it.time
if (timeInMilliseconds > 0 && totalLength > 0) {
percent = ((it.time / 1000) / totalLength * 100).toInt()
}
Logger.log("Statistics: $it")
}
task.sessionId = ffTask.sessionId
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask.sessionId
saveMediaInfo(task)
task.subtitle?.let {
@@ -245,86 +296,115 @@ class AnimeDownloaderService : Service() {
)
)
}
val downloadStarted =
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
if (!downloadStarted) {
Logger.log("Download failed to start")
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed to start")
broadcastDownloadFailed(task.episode)
return@withContext
}
// periodically check if the download is complete
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
Logger.log("Download failed")
builder.setContentText("${task.title} - ${task.episode} Download failed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed")
Logger.log("Download failed: ${download.failureReason}")
downloadsManager.removeDownload(
DownloadedType(
while (ffTask.state != SessionState.COMPLETED) {
if (ffTask.state == SessionState.FAILED) {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode,
MediaType.ANIME,
task.episode
)
)
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${download.failureReason}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
Logger.log("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
Logger.log("Download stopped")
builder.setContentText("${task.title} - ${task.episode} Download stopped")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download stopped")
break
}
broadcastDownloadProgress(
task.episode,
download.percentDownloaded.toInt()
} Download failed"
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffTask.failStackTrace}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
builder.setProgress(
100, percent.coerceAtMost(99),
false
)
broadcastDownloadProgress(
task.episode,
percent.coerceAtMost(99)
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
kotlinx.coroutines.delay(2000)
}
if (ffTask.state == SessionState.COMPLETED) {
if (ffTask.returnCode.isValueError) {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
return@withContext
}
Logger.log("Download completed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download completed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed")
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
@@ -337,35 +417,24 @@ class AnimeDownloaderService : Service() {
}
}
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun hasDownloadStarted(
downloadManager: DownloadManager,
task: AnimeDownloadTask,
timeout: Long
): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
return true
}
// Delay between each poll
kotlinx.coroutines.delay(500)
}
return false
}
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: AnimeDownloadTask) {
CoroutineScope(Dispatchers.IO).launch {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/${task.title}"
)
val episodeDirectory = File(directory, task.episode)
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val directory =
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val episodeDirectory =
getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
)
?: throw Exception("Directory not found")
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -399,14 +468,25 @@ class AnimeDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
try {
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@AnimeDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -417,13 +497,16 @@ class AnimeDownloaderService : Service() {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -490,14 +573,15 @@ class AnimeDownloaderService : Service() {
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
var sessionId: Long = -1
) {
fun getTaskName(): String {
return "$title - $episode"
return "${title.replace("/", "")}/${episode.replace("/", "")}"
}
companion object {
fun getTaskName(title: String, episode: String): String {
return "$title - $episode"
return "${title.replace("/", "")}/${episode.replace("/", "")}"
}
}
}
@@ -511,7 +595,6 @@ class AnimeDownloaderService : Service() {
object AnimeServiceDataSingleton {
var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

@@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -25,6 +24,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import ani.dantotsu.R
import ani.dantotsu.bottomBar
@@ -33,6 +33,7 @@ import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -44,6 +45,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -55,9 +57,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@@ -66,6 +72,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -112,10 +119,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
})
var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutcompact
1 -> layoutCompact
else -> layoutList
}
selected.alpha = 1f
@@ -136,7 +143,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
grid()
}
layoutcompact.setOnClickListener {
layoutCompact.setOnClickListener {
selected(it as ImageView)
style = 1
PrefManager.setVal(PrefName.OfflineView, style)
@@ -156,11 +163,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@OptIn(UnstableApi::class)
private fun grid() {
gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
@@ -168,20 +175,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title.findValidName() }
media?.let {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@let
lifecycleScope.launch {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@launch
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
} ?: run {
snackString("no media found")
}
@@ -204,13 +213,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
for (mediaId in mediaIds) {
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
.removeDownload(mediaId.toString())
}
getDownloads()
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
@@ -238,7 +241,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -261,7 +263,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -281,25 +282,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private fun getDownloads() {
downloads = listOf()
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
if (downloadsJob.isActive) {
downloadsJob.cancel()
}
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
}
downloads = newAnimeDownloads
}
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = downloadedType.type.asText()
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* Load media.json file from the directory and convert it to Media class
* @param downloadedType DownloadedType object
* @return Media object
*/
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
return try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -311,8 +326,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
val media = directory?.findFile("media.json")
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
?: return null
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
@@ -322,22 +342,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
}
}
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
/**
* Load OfflineAnimeModel from the directory
* @param downloadedType DownloadedType object
* @return OfflineAnimeModel object
*/
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = downloadedType.type.asText()
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover?.exists() == true) {
cover.uri
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore

View File

@@ -10,17 +10,18 @@ import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.manga.ImageData
@@ -31,6 +32,9 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
@@ -51,8 +55,6 @@ import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
@@ -189,13 +191,20 @@ class MangaDownloaderService : Service() {
true
}
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
)?.deleteRecursively(this@MangaDownloaderService)
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
@@ -263,24 +272,18 @@ class MangaDownloaderService : Service() {
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try {
// Define the directory within the private external storage space
val directory = File(
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$title/$chapter"
)
if (!directory.exists()) {
directory.mkdirs()
}
// Create a file reference within that directory for your image
val file = File(directory, fileName)
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
?: throw Exception("Directory not found")
directory.findFile(fileName)?.forceDelete(this)
// Create a file reference within that directory for the image
val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
// Use a FileOutputStream to write the bitmap to the file
FileOutputStream(file).use { outputStream ->
file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
} catch (e: Exception) {
println("Exception while saving image: ${e.message}")
snackString("Exception while saving image: ${e.message}")
@@ -291,13 +294,12 @@ class MangaDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
val directory =
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -312,7 +314,10 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.writeText(jsonString)
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
@@ -327,7 +332,7 @@ class MangaDownloaderService : Service() {
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -337,14 +342,16 @@ class MangaDownloaderService : Service() {
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {

View File

@@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -23,6 +22,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
@@ -30,6 +30,7 @@ import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -41,6 +42,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -48,9 +50,13 @@ import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
@@ -59,6 +65,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -148,11 +155,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun grid() {
gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text =
@@ -164,14 +171,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
lifecycleScope.launch {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
}
} ?: run {
snackString("no media found")
}
@@ -194,9 +202,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
@@ -225,7 +230,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -248,7 +252,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -268,42 +271,62 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() {
downloads = listOf()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
if (downloadsJob.isActive) {
downloadsJob.cancel()
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
downloads = listOf()
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
downloads += newNovelDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
}
downloads += newNovelDownloads
}
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = downloadedType.type.asText()
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* Load media.json file from the directory and convert it to Media class
* @param downloadedType DownloadedType object
* @return Media object
*/
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
return try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
val media = directory?.findFile("media.json")
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
@@ -313,22 +336,22 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText()
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover?.exists() == true) {
cover.uri
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
@@ -336,14 +359,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
val readchapter = (mediaModel.userProgress ?: "~").toString()
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val readChapter = (mediaModel.userProgress ?: "~").toString()
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val chapters = " Chapters"
return OfflineMangaModel(
title,
score,
totalchapter,
readchapter,
totalChapter,
readChapter,
type,
chapters,
isOngoing,

View File

@@ -16,15 +16,19 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -250,24 +254,25 @@ class NovelDownloaderService : Service() {
if (!response.isSuccessful) {
throw IOException("Failed to download file: ${response.message}")
}
val directory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title,
task.chapter
) ?: throw Exception("Directory not found")
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
val file = File(
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
)
// Create directories if they don't exist
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
// Overwrite existing file
if (file.exists()) file.delete()
val file = directory.createFile("application/epub+zip", "0.epub")
?: throw Exception("File not created")
//download cover
task.coverUrl?.let {
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
}
val outputStream = this@NovelDownloaderService.contentResolver.openOutputStream(file.uri) ?: throw Exception("Could not open OutputStream")
val sink = file.sink().buffer()
val sink = outputStream.sink().buffer()
val responseBody = response.body
val totalBytes = responseBody.contentLength()
var downloadedBytes = 0L
@@ -352,13 +357,16 @@ class NovelDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
val directory =
DownloadsManager.getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -372,33 +380,47 @@ class NovelDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
try {
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@NovelDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(
Dispatchers.IO
) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
Logger.log("Downloading url $url")
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {

View File

@@ -1,37 +0,0 @@
package ani.dantotsu.download.video
import android.app.Notification
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.PlatformScheduler
import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R
@UnstableApi
class ExoplayerDownloadService :
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
companion object {
private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1
}
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
override fun getForegroundNotification(
downloads: MutableList<Download>,
notMetRequirements: Int
): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this,
R.drawable.mono,
null,
null,
downloads,
notMetRequirements
)
}

View File

@@ -53,140 +53,6 @@ import java.util.concurrent.Executors
@SuppressLint("UnsafeOptInUsageError")
object Helper {
private var simpleCache: SimpleCache? = null
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory {
val dataSource: HttpDataSource =
OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
video.file.headers.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
dataSource
}
val mimeType = when (video.format) {
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
VideoType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.APPLICATION_MP4
}
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
var sub: MediaItem.SubtitleConfiguration? = null
if (subtitle != null) {
sub = MediaItem.SubtitleConfiguration
.Builder(Uri.parse(subtitle.file.url))
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle.type) {
SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
}
)
.build()
}
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
val mediaItem = builder.build()
val downloadHelper = DownloadHelper.forMediaItem(
context,
mediaItem,
DefaultRenderersFactory(context),
dataSourceFactory
)
downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
ExoplayerDownloadService::class.java,
it,
false
)
}
}
override fun onPrepareError(helper: DownloadHelper, e: IOException) {
logError(e)
}
})
}
private var download: DownloadManager? = null
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Synchronized
@UnstableApi
fun downloadManager(context: Context): DownloadManager {
return download ?: let {
val database = Injekt.get<StandaloneDatabaseProvider>()
val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
val networkHelper = Injekt.get<NetworkHelper>()
val okHttpClient = networkHelper.client
val dataSource: HttpDataSource =
OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
dataSource
}
val threadPoolSize = Runtime.getRuntime().availableProcessors()
val executorService = Executors.newFixedThreadPool(threadPoolSize)
val downloadManager = DownloadManager(
context,
database,
getSimpleCache(context),
dataSourceFactory,
executorService
).apply {
requirements =
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3
}
downloadManager.addListener( //for testing
object : DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
when (download.state) {
Download.STATE_COMPLETED -> Logger.log("Download Completed")
Download.STATE_FAILED -> Logger.log("Download Failed")
Download.STATE_STOPPED -> Logger.log("Download Stopped")
Download.STATE_QUEUED -> Logger.log("Download Queued")
Download.STATE_DOWNLOADING -> Logger.log("Download Downloading")
Download.STATE_REMOVING -> Logger.log("Download Removing")
Download.STATE_RESTARTING -> Logger.log("Download Restarting")
}
}
}
)
downloadManager
}
}
private var downloadDirectory: File? = null
@Synchronized
private fun getDownloadDirectory(context: Context): File {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null)
if (downloadDirectory == null) {
downloadDirectory = context.filesDir
}
}
return downloadDirectory!!
}
@OptIn(UnstableApi::class)
fun startAnimeDownloadService(
context: Context,
@@ -225,15 +91,6 @@ object Helper {
.setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ ->
DownloadService.sendRemoveDownload(
context,
ExoplayerDownloadService::class.java,
PrefManager.getAnimeDownloadPreferences().getString(
animeDownloadTask.getTaskName(),
""
) ?: "",
false
)
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
@@ -243,12 +100,13 @@ object Helper {
episode,
MediaType.ANIME
)
)
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
) {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
}
.setNegativeButton("No") { _, _ -> }
@@ -263,18 +121,6 @@ object Helper {
}
}
@OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache {
return if (simpleCache == null) {
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val database = Injekt.get<StandaloneDatabaseProvider>()
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
simpleCache!!
} else {
simpleCache!!
}
}
private fun isNotificationPermissionGranted(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return ActivityCompat.checkSelfPermission(

View File

@@ -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

View File

@@ -195,24 +195,67 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
fun updateRecent(adaptor: MediaAdaptor) {
binding.animeUpdatedProgressBar.visibility = View.GONE
binding.animeUpdatedRecyclerView.adapter = adaptor
binding.animeUpdatedRecyclerView.layoutManager =
binding.apply{
init(
adaptor,
animeUpdatedRecyclerView,
animeUpdatedProgressBar,
animeRecently
)
animePopular.visibility = View.VISIBLE
animePopular.startAnimation(setSlideUp())
if (adaptor.itemCount == 0) {
animeRecentlyContainer.visibility = View.GONE
}
}
}
fun updateMovies(adaptor: MediaAdaptor) {
binding.apply{
init(
adaptor,
animeMoviesRecyclerView,
animeMoviesProgressBar,
animeMovies
)
}
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply{
init(
adaptor,
animeTopRatedRecyclerView,
animeTopRatedProgressBar,
animeTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply{
init(
adaptor,
animeMostFavRecyclerView,
animeMostFavProgressBar,
animeMostFav
)
}
}
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
LinearLayoutManager(
binding.animeUpdatedRecyclerView.context,
recyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
binding.animeRecently.visibility = View.VISIBLE
binding.animeRecently.startAnimation(setSlideUp())
binding.animeUpdatedRecyclerView.layoutAnimation =
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp())
}
fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) {
trendingBinding.userAvatar.loadImage(Anilist.avatar)

View File

@@ -80,8 +80,8 @@ class HomeFragment : Fragment() {
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
binding.homeUserAvatar.loadImage(Anilist.avatar)
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
blurImage(binding.homeUserBg, Anilist.bg)
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen, Anilist.bg)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
@@ -137,6 +137,7 @@ class HomeFragment : Fragment() {
bottomMargin = navBarHeight
}
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
binding.homeTopContainer.updatePadding(top = statusBarHeight)
var reached = false

View File

@@ -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

View File

@@ -178,25 +178,76 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
}
fun updateTrendingManga(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingMangaRecyclerView,
mangaTrendingMangaProgressBar,
mangaTrendingManga
)
}
}
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingManhwaRecyclerView,
mangaTrendingManhwaProgressBar,
mangaTrendingManhwa
)
}
}
fun updateNovel(adaptor: MediaAdaptor) {
binding.mangaNovelProgressBar.visibility = View.GONE
binding.mangaNovelRecyclerView.adapter = adaptor
binding.mangaNovelRecyclerView.layoutManager =
binding.apply {
init(
adaptor,
mangaNovelRecyclerView,
mangaNovelProgressBar,
mangaNovel
)
}
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTopRatedRecyclerView,
mangaTopRatedProgressBar,
mangaTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaMostFavRecyclerView,
mangaMostFavProgressBar,
mangaMostFav
)
mangaPopular.visibility = View.VISIBLE
mangaPopular.startAnimation(setSlideUp())
}
}
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
LinearLayoutManager(
binding.mangaNovelRecyclerView.context,
recyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
binding.mangaNovel.visibility = View.VISIBLE
binding.mangaNovel.startAnimation(setSlideUp())
binding.mangaNovelRecyclerView.layoutAnimation =
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp())
}
fun updateAvatar() {

View File

@@ -3,9 +3,10 @@ package ani.dantotsu.media
import java.io.Serializable
data class Author(
val id: Int,
val name: String?,
val image: String?,
val role: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
var id: Int,
var name: String?,
var image: String?,
var role: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
var character: ArrayList<Character>? = null
) : Serializable

View File

@@ -32,7 +32,7 @@ class AuthorActivity : AppCompatActivity() {
private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null
private var loaded = false
private var isVoiceArtist: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -55,43 +55,59 @@ class AuthorActivity : AppCompatActivity() {
binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
isVoiceArtist = intent.getBooleanExtra("isVoiceArtist", false)
if (isVoiceArtist) {
model.getVoiceActor().observe(this) {
if (it != null) {
author = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
binding.studioRecycler.adapter = CharacterAdapter(author!!.character ?: arrayListOf())
binding.studioRecycler.layoutManager = GridLayoutManager(
this,
(screenWidth / 120f).toInt()
)
}
}
}else{
model.getAuthor().observe(this) {
if (it != null) {
author = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
model.getAuthor().observe(this) {
if (it != null) {
author = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe
val keys = map.keys.toTypedArray()
var pos = 0
val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe
val keys = map.keys.toTypedArray()
var pos = 0
val gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position in titlePosition) {
true -> gridSize
else -> 1
val gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position in titlePosition) {
true -> gridSize
else -> 1
}
}
}
}
for (i in keys.indices) {
val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
titlePosition.add(pos)
pos += (empty + medias.size + 1)
for (i in keys.indices) {
val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
titlePosition.add(pos)
pos += (empty + medias.size + 1)
concatAdapter.addAdapter(TitleAdapter("${keys[i]} (${medias.size})"))
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty))
}
concatAdapter.addAdapter(TitleAdapter("${keys[i]} (${medias.size})"))
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty))
}
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
}
}
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
@@ -99,7 +115,7 @@ class AuthorActivity : AppCompatActivity() {
if (it) {
scope.launch {
if (author != null)
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
withContext(Dispatchers.IO) { if (isVoiceArtist) model.loadVoiceActor(author!!) else model.loadAuthor(author!!)}
live.postValue(false)
}
}

View File

@@ -15,7 +15,8 @@ import ani.dantotsu.setAnimation
import java.io.Serializable
class AuthorAdapter(
private val authorList: ArrayList<Author>
private val authorList: ArrayList<Author>,
private val isVA: Boolean = false,
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding =
@@ -43,7 +44,7 @@ class AuthorAdapter(
Intent(
itemView.context,
AuthorActivity::class.java
).putExtra("author", author as Serializable),
).putExtra("author", author as Serializable).putExtra("isVoiceArtist", isVA),
ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity,
Pair.create(

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.media
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Query
import org.checkerframework.checker.units.qual.A
import java.io.Serializable
data class Character(
@@ -14,5 +16,6 @@ data class Character(
var age: String? = null,
var gender: String? = null,
var dateOfBirth: FuzzyDate? = null,
var roles: ArrayList<Media>? = null
var roles: ArrayList<Media>? = null,
val voiceActor: ArrayList<Author>? = null,
) : Serializable

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -0,0 +1,146 @@
package ani.dantotsu.media
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
object MediaNameAdapter {
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
private const val REGEX_EPISODE =
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val soft = subdubMatcher.group(1)
val subdub = subdubMatcher.group(2)
val bed = subdubMatcher.group(3) ?: ""
val toggled = when (typeToSetTo) {
SubDubType.SUB -> "sub"
SubDubType.DUB -> "dub"
SubDubType.NULL -> ""
}
val toggledCasePreserved =
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
?.isUpperCase() == true
) toggled.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.ROOT
) else it.toString()
} else toggled
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
} else {
null
}
}
fun getSubDub(text: String): SubDubType {
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
when (subdub) {
"sub" -> SubDubType.SUB
"dub" -> SubDubType.DUB
else -> SubDubType.NULL
}
} else {
SubDubType.NULL
}
}
enum class SubDubType {
SUB, DUB, NULL
}
fun findSeasonNumber(text: String): Int? {
val seasonPattern: Pattern = Pattern.compile(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
null
}
}
fun findEpisodeNumber(text: String): Float? {
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, Pattern.CASE_INSENSITIVE)
val episodeMatcher: Matcher = episodePattern.matcher(text)
return if (episodeMatcher.find()) {
if (episodeMatcher.group(2) != null) {
episodeMatcher.group(2)?.toFloat()
} else {
val failedEpisodeNumberPattern: Pattern =
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedEpisodeNumberMatcher: Matcher =
failedEpisodeNumberPattern.matcher(text)
if (failedEpisodeNumberMatcher.find()) {
failedEpisodeNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
} else {
null
}
}
fun removeEpisodeNumber(text: String): String {
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "").ifEmpty {
text
}
val letterPattern = Regex("[a-zA-Z]")
return if (letterPattern.containsMatchIn(removedNumber)) {
removedNumber
} else {
text
}
}
fun removeEpisodeNumberCompletely(text: String): String {
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern =
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
fun findChapterNumber(text: String): Float? {
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) {
matcher.group(2)?.toFloat()
} else {
val failedChapterNumberPattern: Pattern =
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedChapterNumberMatcher: Matcher =
failedChapterNumberPattern.matcher(text)
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
}
}

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import ani.dantotsu.connections.anilist.Anilist
import org.checkerframework.checker.units.qual.A
import java.text.DateFormat
import java.util.Date
@@ -25,12 +26,16 @@ class OtherDetailsViewModel : ViewModel() {
suspend fun loadAuthor(m: Author) {
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
}
private val voiceActor: MutableLiveData<Author> = MutableLiveData(null)
fun getVoiceActor(): LiveData<Author> = voiceActor
suspend fun loadVoiceActor(m: Author) {
if (voiceActor.value == null) voiceActor.postValue(Anilist.query.getVoiceActorsDetails(m))
}
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() {
val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL)
val map = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String, MutableList<Int>>()

View File

@@ -70,11 +70,16 @@ class SearchActivity : AppCompatActivity() {
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
results = mutableListOf(),
hasNextPage = false
)
@@ -133,8 +138,12 @@ class SearchActivity : AppCompatActivity() {
excludedTags = it.excludedTags
tags = it.tags
season = it.season
startYear = it.startYear
seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
}

View File

@@ -13,6 +13,8 @@ import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
@@ -45,6 +47,19 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -93,7 +108,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
activity.updateChips = { it.update() }
}
@@ -103,6 +118,59 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
}
binding.searchFilter.setOnLongClickListener {
val popupMenu = PopupMenu(activity, binding.searchFilter)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
}
true
}
popupMenu.show()
true
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
@@ -257,7 +325,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
}
class SearchChipAdapter(val activity: SearchActivity) :
class SearchChipAdapter(val activity: SearchActivity, private val searchAdapter: SearchAdapter) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
@@ -274,11 +342,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position]
holder.binding.root.apply {
text = chip.text
text = chip.text.replace("_", " ")
setOnClickListener {
activity.result.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
}
}
}
@@ -287,6 +356,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
fun update() {
chips = activity.result.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
override fun getItemCount(): Int = chips.size

View File

@@ -1,11 +1,15 @@
package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnimationUtils
import android.widget.ArrayAdapter
import android.widget.PopupMenu
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -17,6 +21,9 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
import ani.dantotsu.databinding.ItemChipBinding
import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Calendar
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
@@ -38,6 +45,54 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var exGenres = mutableListOf<String>()
private var selectedTags = mutableListOf<String>()
private var exTags = mutableListOf<String>()
private fun updateChips() {
binding.searchFilterGenres.adapter?.notifyDataSetChanged()
binding.searchFilterTags.adapter?.notifyDataSetChanged()
}
private fun startBounceZoomAnimation(view: View? = null) {
val targetView = view ?: binding.sortByFilter
val bounceZoomAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
targetView.startAnimation(bounceZoomAnimation)
}
private fun setSortByFilterImage() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.sortByFilter.setImageResource(filterDrawable)
}
private fun resetSearchFilter() {
activity.result.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
selectedGenres.clear()
exGenres.clear()
selectedTags.clear()
exTags.clear()
binding.searchStatus.setText("")
binding.searchSource.setText("")
binding.searchFormat.setText("")
binding.searchSeason.setText("")
binding.searchYear.setText("")
binding.searchStatus.clearFocus()
binding.searchFormat.clearFocus()
binding.searchSeason.clearFocus()
binding.searchYear.clearFocus()
updateChips()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -47,14 +102,149 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf()
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
resetSearchFilter()
}
binding.resetSearchFilter.setOnLongClickListener {
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
val bounceAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
binding.resetSearchFilter.startAnimation(bounceAnimation)
binding.resetSearchFilter.postDelayed({
resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch {
activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
excludedTags = exTags
}
activity.updateChips.invoke()
activity.search()
dismiss()
}
}, 500)
true
}
binding.sortByFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
startBounceZoomAnimation()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation()
}
}
true
}
popupMenu.show()
}
binding.countryFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.country_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.country_global -> {
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_china -> {
activity.result.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_japan -> {
activity.result.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
}
true
}
popupMenu.show()
}
binding.searchFilterApply.setOnClickListener {
activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
sort = binding.searchSortBy.text.toString().ifBlank { null }
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
season = binding.searchSeason.text.toString().ifBlank { null }
seasonYear = binding.searchYear.text.toString().toIntOrNull()
if (activity.result.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else {
startYear = binding.searchYear.text.toString().toIntOrNull()
}
sort = activity.result.sort
countryOfOrigin = activity.result.countryOfOrigin
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@@ -67,15 +257,22 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchFilterCancel.setOnClickListener {
dismiss()
}
binding.searchSortBy.setText(activity.result.sort?.let {
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
})
binding.searchSortBy.setAdapter(
val format = if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
binding.searchStatus.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
resources.getStringArray(R.array.sort_by)
format
)
)
binding.searchSource.setText(activity.result.source?.replace("_", " "))
binding.searchSource.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
Anilist.source.toTypedArray()
)
)
@@ -84,11 +281,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
if (activity.result.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString())
} else {
binding.searchYear.setText(activity.result.startYear?.toString())
}
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
else {
binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setAdapter(
@@ -98,16 +309,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
Anilist.seasons.toTypedArray()
)
)
binding.searchYear.setText(activity.result.seasonYear?.toString())
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
}
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->

View File

@@ -5,6 +5,7 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString
import com.anggrayudi.storage.file.openOutputStream
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -51,21 +52,17 @@ class SubtitleDownloader {
downloadedType: DownloadedType
) {
try {
val directory = DownloadsManager.getDirectory(
val directory = DownloadsManager.getSubDirectory(
context,
downloadedType.type,
false,
downloadedType.title,
downloadedType.chapter
)
if (!directory.exists()) { //just in case
directory.mkdirs()
}
) ?: throw Exception("Could not create directory")
val type = loadSubtitleType(url)
val subtiteFile = File(directory, "subtitle.${type}")
if (subtiteFile.exists()) {
subtiteFile.delete()
}
subtiteFile.createNewFile()
directory.findFile("subtitle.${type}")?.delete()
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
?: throw Exception("Could not create subtitle file")
val client = Injekt.get<NetworkHelper>().client
val request = Request.Builder().url(url).build()
@@ -77,7 +74,8 @@ class SubtitleDownloader {
}
reponse.body.byteStream().use { input ->
subtiteFile.outputStream().use { output ->
subtitleFile.openOutputStream(context, false).use { output ->
if (output == null) throw Exception("Could not open output stream")
input.copyTo(output)
}
}

View File

@@ -1,127 +0,0 @@
package ani.dantotsu.media.anime
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
class AnimeNameAdapter {
companion object {
const val episodeRegex =
"(episode|episodio|ep|e)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
const val failedEpisodeNumberRegex =
"(?<!part\\s)\\b(\\d+)\\b"
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val soft = subdubMatcher.group(1)
val subdub = subdubMatcher.group(2)
val bed = subdubMatcher.group(3) ?: ""
val toggled = when (typeToSetTo) {
SubDubType.SUB -> "sub"
SubDubType.DUB -> "dub"
SubDubType.NULL -> ""
}
val toggledCasePreserved =
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
?.isUpperCase() == true
) toggled.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.ROOT
) else it.toString()
} else toggled
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
} else {
null
}
}
fun getSubDub(text: String): SubDubType {
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
when (subdub) {
"sub" -> SubDubType.SUB
"dub" -> SubDubType.DUB
else -> SubDubType.NULL
}
} else {
SubDubType.NULL
}
}
enum class SubDubType {
SUB, DUB, NULL
}
fun findSeasonNumber(text: String): Int? {
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
null
}
}
fun findEpisodeNumber(text: String): Float? {
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
val episodeMatcher: Matcher = episodePattern.matcher(text)
return if (episodeMatcher.find()) {
if (episodeMatcher.group(2) != null) {
episodeMatcher.group(2)?.toFloat()
} else {
val failedEpisodeNumberPattern: Pattern =
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
val failedEpisodeNumberMatcher: Matcher =
failedEpisodeNumberPattern.matcher(text)
if (failedEpisodeNumberMatcher.find()) {
failedEpisodeNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
} else {
null
}
}
fun removeEpisodeNumber(text: String): String {
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "").ifEmpty {
text
}
val letterPattern = Regex("[a-zA-Z]")
return if (letterPattern.containsMatchIn(removedNumber)) {
removedNumber
} else {
text
}
}
fun removeEpisodeNumberCompletely(text: String): String {
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern =
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
}
}

View File

@@ -17,15 +17,16 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.countDown
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.displayTimer
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.openSettings
import ani.dantotsu.others.LanguageMapper
@@ -51,7 +52,7 @@ class AnimeWatchAdapter(
private val fragment: AnimeWatchFragment,
private val watchSources: WatchSources
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
@@ -403,7 +404,7 @@ class AnimeWatchAdapter(
}
val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
@@ -436,7 +437,8 @@ class AnimeWatchAdapter(
val sourceFound = media.anime.episodes!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) {
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText(binding.animeSource.adapter
@@ -452,6 +454,7 @@ class AnimeWatchAdapter(
fragment.loadEpisodes(nextIndex, false)
}
}
binding.animeSource.setOnClickListener { autoSelect = false }
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE
@@ -497,8 +500,7 @@ class AnimeWatchAdapter(
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
//Timer
countDown(media, binding.animeSourceContainer)
displayTimer(media, binding.animeSourceContainer)
}
}
}

View File

@@ -14,6 +14,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
@@ -34,13 +35,14 @@ import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.dp
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
@@ -53,6 +55,8 @@ import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@@ -224,7 +228,7 @@ class AnimeWatchFragment : Fragment() {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc =
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title
@@ -421,7 +425,19 @@ class AnimeWatchFragment : Fragment() {
}
fun onAnimeEpisodeDownloadClick(i: String) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
activity?.let{
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
} else {
snackString("Permission is required to download")
}
}
} else {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
}
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
@@ -441,8 +457,9 @@ class AnimeWatchFragment : Fragment() {
i,
MediaType.ANIME
)
)
episodeAdapter.purgeDownload(i)
) {
episodeAdapter.purgeDownload(i)
}
}
@OptIn(UnstableApi::class)
@@ -453,20 +470,15 @@ class AnimeWatchFragment : Fragment() {
i,
MediaType.ANIME
)
)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
episodeAdapter.deleteDownload(i)
}
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
@@ -530,7 +542,7 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName()) {
if (download.title == media.mainName().findValidName()) {
episodeAdapter.stopDownload(download.chapter)
}
}

View File

@@ -10,7 +10,6 @@ import androidx.annotation.OptIn
import androidx.core.view.isVisible
import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.updateProgress
@@ -18,9 +17,12 @@ import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
import com.bumptech.glide.Glide
@@ -55,15 +57,7 @@ class EpisodeAdapter(
var arr: List<Episode> = arrayListOf(),
var offlineMode: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var index: DownloadIndex
init {
if (offlineMode) {
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
}
}
val context = fragment.requireContext()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) {
@@ -102,7 +96,7 @@ class EpisodeAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position]
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
} else {
ep.number
} ?: ""
@@ -247,17 +241,8 @@ class EpisodeAdapter(
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
media.mainName(),
episodeNumber
)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
val size = try {
val download = index.getDownload(id)
bytesToHuman(download?.bytesDownloaded ?: 0)
bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
} catch (e: Exception) {
null
}

View File

@@ -104,7 +104,7 @@ import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.video.Helper
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.dp
import ani.dantotsu.getCurrentBrightnessValue
import ani.dantotsu.hideSystemBars
@@ -113,6 +113,8 @@ import ani.dantotsu.isOnline
import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.okHttpClient
import ani.dantotsu.others.AniSkip
@@ -393,7 +395,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
isCastApiAvailable = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
try {
castContext = CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
castContext =
CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
castPlayer = CastPlayer(castContext!!)
castPlayer!!.setSessionAvailabilityListener(this)
} catch (e: Exception) {
@@ -441,41 +444,43 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN)
if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) {
if (PrefManager.getVal(PrefName.RotationPlayer)) {
orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
when (orientation) {
in 45..135 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
if (PrefManager.getVal(PrefName.RotationPlayer)) {
orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
when (orientation) {
in 45..135 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
}
in 225..315 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
in 315..360, in 0..45 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
}
in 225..315 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
in 315..360, in 0..45 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
}
orientationListener?.enable()
}
orientationListener?.enable()
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
exoRotate.setOnClickListener {
requestedOrientation = rotation
it.visibility = View.GONE
}
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
exoRotate.setOnClickListener {
requestedOrientation = rotation
it.visibility = View.GONE
}
}
setupSubFormatting(playerView)
@@ -998,7 +1003,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
episodeTitleArr = arrayListOf()
episodes.forEach {
val episode = it.value
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
val cleanedTitle = MediaNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotBlank() && cleanedTitle != "null") ": $cleanedTitle" else ""}")
}
@@ -1083,35 +1088,48 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
lifecycleScope.launch {
val presence = RPC.createPresence(RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let {
RPC.Link(
media.userPreferredName,
it
)
},
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
val buttons = when (discordMode) {
"nothing" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
getString(R.string.github)
)
"dantotsu" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
)
"anilist" -> {
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
val anilistLink = "https://anilist.co/user/$userId/"
mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("View My AniList", anilistLink)
)
}
else -> mutableListOf()
}
val presence = RPC.createPresence(
RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let {
RPC.Link(
media.userPreferredName,
it
)
},
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
)
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
}
@@ -1119,7 +1137,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
startService(intent)
}
}
updateProgress()
}
}
@@ -1156,7 +1173,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (PrefManager.getVal(PrefName.Cast)) {
playerView.findViewById<CustomCastButton>(R.id.exo_cast).apply {
visibility = View.VISIBLE
if(PrefManager.getVal(PrefName.UseInternalCast)) {
if (PrefManager.getVal(PrefName.UseInternalCast)) {
try {
CastButtonFactory.setUpMediaRouteButton(context, this)
dialogFactory = CustomCastThemeFactory()
@@ -1319,7 +1336,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
)
@Suppress("UNCHECKED_CAST")
val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
val list = (PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>).toMutableList()
if (list.contains(media.id)) list.remove(media.id)
list.add(media.id)
PrefManager.setCustomVal("continueAnimeList", list)
@@ -1413,7 +1434,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
cacheFactory = CacheDataSource.Factory().apply {
setCache(Helper.getSimpleCache(this@ExoplayerView))
setCache(VideoCache.getInstance(this@ExoplayerView))
if (ext.server.offline) {
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
} else {
@@ -1430,15 +1451,28 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val downloadedMediaItem = if (ext.server.offline) {
val key = ext.server.name
downloadId = PrefManager.getAnimeDownloadPreferences()
.getString(key, null)
if (downloadId != null) {
Helper.downloadManager(this)
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
val titleName = ext.server.name.split("/").first()
val episodeName = ext.server.name.split("/").last()
val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
if (directory != null) {
val files = directory.listFiles()
println(files)
val docFile = directory.listFiles().firstOrNull {
it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
}
if (docFile != null) {
val uri = docFile.uri
MediaItem.Builder().setUri(uri).setMimeType(mimeType).build()
} else {
snackString("File not found")
null
}
} else {
snackString("Download not found")
snackString("Directory not found")
null
}
} else null
mediaItem = if (downloadedMediaItem == null) {
@@ -1813,7 +1847,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (!functionstarted && !disappeared && PrefManager.getVal(PrefName.AutoHideTimeStamps)) {
disappearSkip()
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)){
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)) {
skipTimeButton.visibility = View.VISIBLE
exoSkip.visibility = View.GONE
skipTimeText.text = new.skipType.getType()
@@ -2152,11 +2186,16 @@ class CustomCastButton : MediaRouteButton {
fun setCastCallback(castCallback: () -> Unit) {
this.castCallback = castCallback
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun performClick(): Boolean {
return if (PrefManager.getVal(PrefName.UseInternalCast)) {

View File

@@ -30,10 +30,13 @@ import ani.dantotsu.currActivity
import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.video.Helper
import ani.dantotsu.hideSystemBars
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Subtitle
@@ -376,6 +379,45 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} else {
binding.urlDownload.visibility = View.GONE
}
val subtitles = extractor.subtitles
if (subtitles.isNotEmpty()) {
binding.urlSub.visibility = View.VISIBLE
} else {
binding.urlSub.visibility = View.GONE
}
binding.urlSub.setOnClickListener {
if (subtitles.isNotEmpty()) {
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle")
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { _, which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton("Download") { dialog, _ ->
scope.launch {
if (subtitleToDownload != null) {
SubtitleDownloader.downloadSubtitle(
requireContext(),
subtitleToDownload!!.file.url,
DownloadedType(media!!.mainName(), media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number, MediaType.ANIME)
)
}
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else {
snackString("No Subtitles Available")
}
}
binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name

View File

@@ -5,6 +5,7 @@ import android.content.Intent
import android.graphics.Color
import android.view.View
import android.widget.Toast
import android.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
@@ -35,13 +36,15 @@ import java.util.TimeZone
import kotlin.math.abs
import kotlin.math.sqrt
class CommentItem(val comment: Comment,
private val markwon: Markwon,
val parentSection: Section,
private val commentsFragment: CommentsFragment,
private val backgroundColor: Int,
val commentDepth: Int
) : BindableItem<ItemCommentsBinding>() {
class CommentItem(
val comment: Comment,
private val markwon: Markwon,
val parentSection: Section,
private val commentsFragment: CommentsFragment,
private val backgroundColor: Int,
val commentDepth: Int
) :
BindableItem<ItemCommentsBinding>() {
lateinit var binding: ItemCommentsBinding
val adapter = GroupieAdapter()
private var subCommentIds: MutableList<Int> = mutableListOf()
@@ -63,9 +66,6 @@ class CommentItem(val comment: Comment,
val isUserComment = CommentsAPI.userId == comment.userId
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
markwon.setMarkdown(viewBinding.commentText, comment.content)
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
if (comment.tag == null) {
viewBinding.commentUserTagLayout.visibility = View.GONE
@@ -157,41 +157,71 @@ class CommentItem(val comment: Comment,
Toast.makeText(context, "Owner", Toast.LENGTH_SHORT).show()
}
}
viewBinding.commentDelete.setOnClickListener {
dialogBuilder(getAppString(R.string.delete_comment), getAppString(R.string.delete_comment_confirm)) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString(R.string.comment_deleted)
parentSection.remove(this@CommentItem)
}
}
}
}
viewBinding.commentBanUser.setOnClickListener {
dialogBuilder(getAppString(R.string.ban_user), getAppString(R.string.ban_user_confirm)) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString(R.string.user_banned)
}
}
}
}
viewBinding.commentReport.setOnClickListener {
dialogBuilder(getAppString(R.string.report_comment), getAppString(R.string.report_comment_confirm)) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.reportComment(
comment.commentId,
comment.username,
commentsFragment.mediaName,
comment.userId
)
if (success) {
snackString(R.string.comment_reported)
viewBinding.commentInfo.setOnClickListener {
val popup = PopupMenu(commentsFragment.requireContext(), viewBinding.commentInfo)
popup.menuInflater.inflate(R.menu.profile_details_menu, popup.menu)
popup.menu.findItem(R.id.commentDelete)?.isVisible = isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod
popup.menu.findItem(R.id.commentBanUser)?.isVisible = (CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment
popup.menu.findItem(R.id.commentReport)?.isVisible = !isUserComment
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.commentReport -> {
dialogBuilder(
getAppString(R.string.report_comment),
getAppString(R.string.report_comment_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.reportComment(
comment.commentId,
comment.username,
commentsFragment.mediaName,
comment.userId
)
if (success) {
snackString(R.string.comment_reported)
}
}
}
true
}
R.id.commentDelete -> {
dialogBuilder(
getAppString(R.string.delete_comment),
getAppString(R.string.delete_comment_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString(R.string.comment_deleted)
parentSection.remove(this@CommentItem)
}
}
}
true
}
R.id.commentBanUser -> {
dialogBuilder(
getAppString(R.string.ban_user),
getAppString(R.string.ban_user_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString(R.string.user_banned)
}
}
}
true
}
else -> {
false
}
}
}
popup.show()
}
//fill the icon if the user has liked the comment
setVoteButtons(viewBinding)
@@ -227,7 +257,6 @@ class CommentItem(val comment: Comment,
comment.upvotes -= 1
}
comment.downvotes += if (voteType == -1) 1 else -1
notifyChanged()
}
}

View File

@@ -75,7 +75,7 @@ class CommentsFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity() as MediaDetailsActivity
binding.commentsList.setBaseline(activity.navBar, activity.binding.commentInputLayout)
binding.commentsListContainer.setBaseline(activity.navBar, activity.binding.commentInputLayout)
//get the media id from the intent
val mediaId = arguments?.getInt("mediaId") ?: -1
@@ -370,7 +370,6 @@ class CommentsFragment : Fragment() {
override fun onResume() {
super.onResume()
tag = null
binding.commentsList.setBaseline(activity.navBar, activity.binding.commentInputLayout)
section.groups.forEach {
if (it is CommentItem && it.containsGif()) {
it.notifyChanged()

View File

@@ -15,6 +15,7 @@ import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.setAnimation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -267,10 +268,10 @@ class MangaChapterAdapter(
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root)
val ep = arr[position]
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
val parsedNumber = MediaNameAdapter.findChapterNumber(ep.number)?.toInt()
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
if (media.userProgress != null) {
if ((MangaNameAdapter.findChapterNumber(ep.number)
if ((MediaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
)
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
@@ -279,7 +280,7 @@ class MangaChapterAdapter(
binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
MediaNameAdapter.findChapterNumber(ep.number).toString()
)
true
}
@@ -315,7 +316,7 @@ class MangaChapterAdapter(
} else binding.itemChapterTitle.visibility = View.VISIBLE
if (media.userProgress != null) {
if ((MangaNameAdapter.findChapterNumber(ep.number)
if ((MediaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
) {
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
@@ -326,7 +327,7 @@ class MangaChapterAdapter(
binding.root.setOnLongClickListener {
updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
MediaNameAdapter.findChapterNumber(ep.number).toString()
)
true
}

View File

@@ -1,29 +0,0 @@
package ani.dantotsu.media.manga
import java.util.regex.Matcher
import java.util.regex.Pattern
class MangaNameAdapter {
companion object {
private const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
private const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
fun findChapterNumber(text: String): Float? {
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) {
matcher.group(2)?.toFloat()
} else {
val failedChapterNumberPattern: Pattern =
Pattern.compile(filedChapterNumberRegex, Pattern.CASE_INSENSITIVE)
val failedChapterNumberMatcher: Matcher =
failedChapterNumberPattern.matcher(text)
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
}
}
}

View File

@@ -26,6 +26,7 @@ import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.openSettings
@@ -385,8 +386,8 @@ class MangaReadAdapter(
)
}
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
val startChapter = MediaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MediaNameAdapter.findChapterNumber(names[last - 1])
val startChapterString = if (startChapter != null) {
"Ch.$startChapter"
} else {
@@ -448,7 +449,7 @@ class MangaReadAdapter(
chapter.scanlator !in hiddenScanlators
}
val formattedChapters = filteredChapters.map {
MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
MediaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
}
if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)]

View File

@@ -16,6 +16,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@@ -34,6 +35,7 @@ import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton
import ani.dantotsu.dp
@@ -41,6 +43,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper
@@ -55,6 +58,8 @@ import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
@@ -189,7 +194,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
)
for (download in downloadManager.mangaDownloadedTypes) {
if (download.title == media.mainName()) {
if (download.title == media.mainName().findValidName()) {
chapterAdapter.stopDownload(download.chapter)
}
}
@@ -227,7 +232,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language
val progressChapterIndex = (chapters?.indexOfFirst {
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} ?: 0) + 1
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
@@ -433,51 +438,65 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun onMangaChapterDownloadClick(i: String) {
if (!isNotificationPermissionGranted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
val parser =
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
CoroutineScope(Dispatchers.IO).launch {
val images = parser.imageList(chapter.sChapter)
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.mainName(),
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
retries = 2,
simultaneousDownloads = 2
activity?.let {
if (!isNotificationPermissionGranted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
it,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
fun continueDownload() {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
val parser =
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
CoroutineScope(Dispatchers.IO).launch {
val images = parser.imageList(chapter.sChapter)
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.mainName(),
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
retries = 2,
simultaneousDownloads = 2
)
// If the service is not already running, start it
if (!MangaServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
// If the service is not already running, start it
if (!MangaServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
MangaServiceDataSingleton.isServiceRunning = true
}
// Inform the adapter that the download has started
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
}
}
MangaServiceDataSingleton.isServiceRunning = true
}
// Inform the adapter that the download has started
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
}
}
}
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
continueDownload()
} else {
snackString("Permission is required to download")
}
}
} else {
continueDownload()
}
}
}
@@ -499,8 +518,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
i,
MediaType.MANGA
)
)
chapterAdapter.deleteDownload(i)
) {
chapterAdapter.deleteDownload(i)
}
}
fun onMangaChapterStopDownloadClick(i: String) {
@@ -517,8 +537,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
i,
MediaType.MANGA
)
)
chapterAdapter.purgeDownload(i)
) {
chapterAdapter.purgeDownload(i)
}
}
private val downloadStatusReceiver = object : BroadcastReceiver() {

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
@@ -36,7 +37,18 @@ abstract class BaseImageAdapter(
chapter: MangaChapter
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val settings = activity.defaultSettings
val images = chapter.images()
private val chapterImages = chapter.images()
var images = chapterImages
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
images = if (settings.layout == CurrentReaderSettings.Layouts.PAGED
&& settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) {
chapterImages.reversed()
} else {
chapterImages
}
super.onAttachedToRecyclerView(recyclerView)
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
@@ -165,6 +177,10 @@ abstract class BaseImageAdapter(
it.load(localFile.absoluteFile)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else if (link.url.startsWith("content://")) {
it.load(Uri.parse(link.url))
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else {
mangaCache.get(link.url)?.let { imageData ->
val bitmap = imageData.fetchAndProcessImage(
@@ -175,6 +191,7 @@ abstract class BaseImageAdapter(
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
}
}
}
?.let {
@@ -207,5 +224,4 @@ abstract class BaseImageAdapter(
return newBitmap
}
}
}

View File

@@ -57,9 +57,9 @@ import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaSingleton
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaImage
@@ -129,6 +129,11 @@ class MangaReaderActivity : AppCompatActivity() {
var sliding = false
var isAnimating = false
private val directionRLBT get() = defaultSettings.direction == RIGHT_TO_LEFT
|| defaultSettings.direction == BOTTOM_TO_TOP
private val directionPagedBT get() = defaultSettings.layout == CurrentReaderSettings.Layouts.PAGED
&& defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP
override fun onAttachedToWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
val displayCutout = window.decorView.rootWindowInsets.displayCutout
@@ -180,7 +185,7 @@ class MangaReaderActivity : AppCompatActivity() {
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
onBackPressedDispatcher.addCallback(this) {
val chapter = (MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
val chapter = (MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
?.minus(1L) ?: 0).toString()
if (chapter == "0.0" && PrefManager.getVal(PrefName.ChapterZeroReader)
// Not asking individually or incognito
@@ -224,8 +229,13 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
?: 1))
else
binding.mangaReaderPager.currentItem =
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP ) {
binding.mangaReaderPager.currentItem =
(maxChapterPage.toInt() - value.toInt()) / (dualPage { 2 } ?: 1)
} else {
binding.mangaReaderPager.currentItem =
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
}
pageSliderHide()
}
}
@@ -331,7 +341,7 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderNextChapter.setOnClickListener {
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
if (directionRLBT) {
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
else snackString(getString(R.string.first_chapter))
} else {
@@ -344,7 +354,7 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderPreviousChapter.performClick()
}
binding.mangaReaderPreviousChapter.setOnClickListener {
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
if (directionRLBT) {
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
else snackString(getString(R.string.next_chapter_not_found))
} else {
@@ -361,16 +371,12 @@ class MangaReaderActivity : AppCompatActivity() {
PrefManager.setCustomVal("${media.id}_current_chp", chap.number)
currentChapterIndex = chaptersArr.indexOf(chap.number)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
if (directionRLBT) {
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
} else {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
}
applySettings()
val context = this
@@ -378,6 +384,25 @@ class MangaReaderActivity : AppCompatActivity() {
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
lifecycleScope.launch {
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
val buttons = when (discordMode) {
"nothing" -> mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
)
"dantotsu" -> mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link("Read on Dantotsu", getString(R.string.dantotsu))
)
"anilist" -> {
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
val anilistLink = "https://anilist.co/user/$userId/"
mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link("View My AniList", anilistLink)
)
}
else -> mutableListOf()
}
val presence = RPC.createPresence(
RPC.Companion.RPCData(
applicationId = Discord.application_Id,
@@ -386,20 +411,9 @@ class MangaReaderActivity : AppCompatActivity() {
details = chap.title?.takeIf { it.isNotEmpty() }
?: getString(R.string.chapter_num, chap.number),
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
largeImage = media.cover?.let { cover ->
RPC.Link(media.userPreferredName, cover)
},
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
getString(R.string.github)
)
)
largeImage = media.cover?.let { cover -> RPC.Link(media.userPreferredName, cover) },
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
)
)
val intent = Intent(context, DiscordService::class.java).apply {
@@ -455,7 +469,11 @@ class MangaReaderActivity : AppCompatActivity() {
currentChapterPage = PrefManager.getCustomVal("${media.id}_${chapter.number}", 1L)
val chapImages = chapter.images()
val chapImages = if (directionPagedBT) {
chapter.images().reversed()
} else {
chapter.images()
}
maxChapterPage = 0
if (chapImages.isNotEmpty()) {
@@ -479,7 +497,11 @@ class MangaReaderActivity : AppCompatActivity() {
}
val currentPage = currentChapterPage.toInt()
val currentPage = if (directionPagedBT) {
maxChapterPage - currentChapterPage + 1
} else {
currentChapterPage
}.toInt()
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)) {
binding.mangaReaderSwipy.vertical = true
@@ -508,10 +530,10 @@ class MangaReaderActivity : AppCompatActivity() {
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderNextChapter.performClick()
binding.mangaReaderPreviousChapter.performClick()
}
binding.mangaReaderSwipy.onBottomSwiped = {
binding.mangaReaderPreviousChapter.performClick()
binding.mangaReaderNextChapter.performClick()
}
}
binding.mangaReaderSwipy.topBeingSwiped = { value ->
@@ -620,7 +642,7 @@ class MangaReaderActivity : AppCompatActivity() {
RecyclerView.VERTICAL
else
RecyclerView.HORIZONTAL,
!(defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == LEFT_TO_RIGHT)
directionRLBT
)
manager.preloadItemCount = 5
@@ -637,6 +659,8 @@ class MangaReaderActivity : AppCompatActivity() {
else false
}
manager.setStackFromEnd(defaultSettings.direction == BOTTOM_TO_TOP)
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
defaultSettings.apply {
@@ -691,9 +715,7 @@ class MangaReaderActivity : AppCompatActivity() {
visibility = View.VISIBLE
adapter = imageAdapter
layoutDirection =
if (defaultSettings.direction == BOTTOM_TO_TOP || defaultSettings.direction == RIGHT_TO_LEFT)
View.LAYOUT_DIRECTION_RTL
else View.LAYOUT_DIRECTION_LTR
if (directionRLBT) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
orientation =
if (defaultSettings.direction == LEFT_TO_RIGHT || defaultSettings.direction == RIGHT_TO_LEFT)
ViewPager2.ORIENTATION_HORIZONTAL
@@ -782,7 +804,7 @@ class MangaReaderActivity : AppCompatActivity() {
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
if (screenWidth / 5 in x + 1..<y) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
PressPos.RIGHT
} else {
PressPos.LEFT
@@ -790,7 +812,7 @@ class MangaReaderActivity : AppCompatActivity() {
}
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
PressPos.LEFT
} else {
PressPos.RIGHT
@@ -886,9 +908,10 @@ class MangaReaderActivity : AppCompatActivity() {
}
}
binding.mangaReaderSlider.layoutDirection =
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP)
if (directionRLBT)
View.LAYOUT_DIRECTION_RTL
else View.LAYOUT_DIRECTION_LTR
else
View.LAYOUT_DIRECTION_LTR
shouldShow?.apply { isContVisible = !this }
if (isContVisible) {
isContVisible = false
@@ -896,12 +919,7 @@ class MangaReaderActivity : AppCompatActivity() {
isAnimating = true
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
.setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(
binding.mangaReaderBottomLayout,
"translationY",
0f,
128f
)
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
@@ -921,7 +939,11 @@ class MangaReaderActivity : AppCompatActivity() {
}
private var loading = false
fun updatePageNumber(page: Long) {
fun updatePageNumber(pageNumber: Long) {
var page = pageNumber
if (directionPagedBT) {
page = maxChapterPage - pageNumber + 1
}
if (currentChapterPage != page) {
currentChapterPage = page
PrefManager.setCustomVal("${media.id}_${chapter.number}", page)
@@ -969,7 +991,7 @@ class MangaReaderActivity : AppCompatActivity() {
PrefManager.setCustomVal("${media.id}_save_progress", true)
updateProgress(
media,
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
dialog.dismiss()
@@ -991,7 +1013,7 @@ class MangaReaderActivity : AppCompatActivity() {
)
updateProgress(
media,
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
runnable.run()
@@ -1086,4 +1108,4 @@ class MangaReaderActivity : AppCompatActivity() {
}
return true
}
}
}

View File

@@ -20,6 +20,7 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
@@ -94,23 +95,23 @@ class NovelReadFragment : Fragment(),
)
)
) {
val file = File(
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
)
if (!file.exists()) return false
val fileUri = FileProvider.getUriForFile(
requireContext(),
"${requireContext().packageName}.provider",
file
)
val intent = Intent(context, NovelReaderActivity::class.java).apply {
action = Intent.ACTION_VIEW
setDataAndType(fileUri, "application/epub+zip")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
try {
val directory =
DownloadsManager.getSubDirectory(context?:currContext()!!, MediaType.NOVEL, false, novel.name)
val file = directory?.findFile(novel.name)
if (file?.exists() == false) return false
val fileUri = file?.uri ?: return false
val intent = Intent(context, NovelReaderActivity::class.java).apply {
action = Intent.ACTION_VIEW
setDataAndType(fileUri, "application/epub+zip")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
startActivity(intent)
return true
} catch (e: Exception) {
Logger.log(e)
return false
}
startActivity(intent)
return true
} else {
return false
}
@@ -135,7 +136,7 @@ class NovelReadFragment : Fragment(),
novel.name,
MediaType.NOVEL
)
)
) {}
}
private val downloadStatusReceiver = object : BroadcastReceiver() {

View File

@@ -46,11 +46,11 @@ class CommentNotificationTask : Task {
)
notifications =
notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
notifications?.filter { !it.type.isGlobal() || it.notificationId > recentGlobal }
?.toMutableList()
val newRecentGlobal =
notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
notifications?.filter { it.type.isGlobal() }?.maxOfOrNull { it.notificationId }
if (newRecentGlobal != null) {
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
}
@@ -313,4 +313,6 @@ class CommentNotificationTask : Task {
null
}
}
private fun Int?.isGlobal() = this == 3 || this == 420
}

View File

@@ -3,13 +3,11 @@ package ani.dantotsu.notifications.subscription
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.Selected
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.Episode
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources
@@ -105,7 +103,7 @@ class SubscriptionHelper {
}
return chp?.apply {
selected.latest = MangaNameAdapter.findChapterNumber(number) ?: 0f
selected.latest = MediaNameAdapter.findChapterNumber(number) ?: 0f
saveSelected(id, selected)
}
}

View File

@@ -36,16 +36,8 @@ object Download {
}
private fun getDownloadDir(context: Context): File {
val direct: File
if (PrefManager.getVal(PrefName.SdDl)) {
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
val parentDirectory = arrayOfFiles[1].toString()
direct = File(parentDirectory)
if (!direct.exists()) direct.mkdirs()
} else {
direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
if (!direct.exists()) direct.mkdirs()
}
val direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
if (!direct.exists()) direct.mkdirs()
return direct
}
@@ -96,52 +88,10 @@ object Download {
when (PrefManager.getVal(PrefName.DownloadManager) as Int) {
1 -> oneDM(context, file, notif ?: fileName)
2 -> adm(context, file, fileName, folder)
else -> defaultDownload(context, file, fileName, folder, notif ?: fileName)
else -> oneDM(context, file, notif ?: fileName)
}
}
private fun defaultDownload(
context: Context,
file: FileUrl,
fileName: String,
folder: String,
notif: String
) {
val manager =
context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager
val request: DownloadManager.Request = DownloadManager.Request(Uri.parse(file.url))
file.headers.forEach {
request.addRequestHeader(it.key, it.value)
}
CoroutineScope(Dispatchers.IO).launch {
try {
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
if (PrefManager.getVal(PrefName.SdDl) && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString() + folder
val direct = File(parentDirectory)
if (!direct.exists()) direct.mkdirs()
request.setDestinationUri(Uri.fromFile(File("$parentDirectory$fileName")))
} else {
val direct = File(Environment.DIRECTORY_DOWNLOADS + "/Dantotsu$folder")
if (!direct.exists()) direct.mkdirs()
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
"/Dantotsu$folder$fileName"
)
}
request.setTitle(notif)
manager.enqueue(request)
toast(currContext()?.getString(R.string.started_downloading, notif))
} catch (e: SecurityException) {
toast(currContext()?.getString(R.string.permission_required))
} catch (e: Exception) {
toast(e.toString())
}
}
}
private fun oneDM(context: Context, file: FileUrl, notif: String) {
val appName =
if (isPackageInstalled("idm.internet.download.manager.plus", context.packageManager)) {

View File

@@ -12,7 +12,6 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetImageBinding
import ani.dantotsu.downloadsPermission
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
@@ -22,6 +21,7 @@ import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.shareImage
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.ImageSource
import kotlinx.coroutines.launch

View File

@@ -12,8 +12,8 @@ import ani.dantotsu.R
class Xpandable @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
var expanded: Boolean = false
private var listener: OnChangeListener? = null
private var expanded: Boolean = false
private var listeners: ArrayList<OnChangeListener> = arrayListOf()
init {
context.withStyledAttributes(attrs, R.styleable.Xpandable) {
@@ -50,7 +50,9 @@ class Xpandable @JvmOverloads constructor(
}
}
postDelayed({
listener?.onRetract()
listeners.forEach{
it.onRetract()
}
}, 300)
}
@@ -64,13 +66,19 @@ class Xpandable @JvmOverloads constructor(
}
}
postDelayed({
listener?.onExpand()
listeners.forEach{
it.onExpand()
}
}, 300)
}
@Suppress("unused")
fun setOnChangeListener(listener: OnChangeListener) {
this.listener = listener
fun addOnChangeListener(listener: OnChangeListener) {
listeners.add(listener)
}
fun removeListener(listener: OnChangeListener) {
listeners.remove(listener)
}
interface OnChangeListener {

View File

@@ -3,7 +3,7 @@ package ani.dantotsu.parsers
import android.content.Context
import ani.dantotsu.FileUrl
import ani.dantotsu.currContext
import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.snackString
@@ -73,12 +73,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
configurableSource.getPreferenceKey(),
Context.MODE_PRIVATE
)
sharedPreferences.all.filterValues { AnimeNameAdapter.getSubDub(it.toString()) != AnimeNameAdapter.Companion.SubDubType.NULL }
sharedPreferences.all.filterValues { MediaNameAdapter.getSubDub(it.toString()) != MediaNameAdapter.SubDubType.NULL }
.forEach { value ->
return when (AnimeNameAdapter.getSubDub(value.value.toString())) {
AnimeNameAdapter.Companion.SubDubType.SUB -> false
AnimeNameAdapter.Companion.SubDubType.DUB -> true
AnimeNameAdapter.Companion.SubDubType.NULL -> false
return when (MediaNameAdapter.getSubDub(value.value.toString())) {
MediaNameAdapter.SubDubType.SUB -> false
MediaNameAdapter.SubDubType.DUB -> true
MediaNameAdapter.SubDubType.NULL -> false
}
}
}
@@ -92,8 +92,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val configurableSource = extension.sources[sourceLanguage] as? ConfigurableAnimeSource
?: return
val type = when (setDub) {
true -> AnimeNameAdapter.Companion.SubDubType.DUB
false -> AnimeNameAdapter.Companion.SubDubType.SUB
true -> MediaNameAdapter.SubDubType.DUB
false -> MediaNameAdapter.SubDubType.SUB
}
currContext()?.let { context ->
val sharedPreferences =
@@ -101,9 +101,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
configurableSource.getPreferenceKey(),
Context.MODE_PRIVATE
)
sharedPreferences.all.filterValues { AnimeNameAdapter.getSubDub(it.toString()) != AnimeNameAdapter.Companion.SubDubType.NULL }
sharedPreferences.all.filterValues { MediaNameAdapter.getSubDub(it.toString()) != MediaNameAdapter.SubDubType.NULL }
.forEach { value ->
val setValue = AnimeNameAdapter.setSubDub(value.value.toString(), type)
val setValue = MediaNameAdapter.setSubDub(value.value.toString(), type)
if (setValue != null) {
sharedPreferences.edit().putString(value.key, setValue).apply()
}
@@ -122,9 +122,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
Context.MODE_PRIVATE
)
sharedPreferences.all.filterValues {
AnimeNameAdapter.setSubDub(
MediaNameAdapter.setSubDub(
it.toString(),
AnimeNameAdapter.Companion.SubDubType.NULL
MediaNameAdapter.SubDubType.NULL
) != null
}
.forEach { _ -> return true }
@@ -150,7 +150,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val sortedEpisodes = if (res[0].episode_number == -1f) {
// Find the number in the string and sort by that number
val sortedByStringNumber = res.sortedBy {
val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
val matchResult = MediaNameAdapter.findEpisodeNumber(it.name)
val number = matchResult ?: Float.MAX_VALUE
it.episode_number = number // Store the found number in episode_number
number
@@ -171,13 +171,13 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
var episodeCounter = 1f
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
val seasonGroups =
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
res.groupBy { MediaNameAdapter.findSeasonNumber(it.name) ?: 0 }
seasonGroups.keys.sortedBy { it }
.flatMap { season ->
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
val potentialNumber =
AnimeNameAdapter.findEpisodeNumber(episode.name)
MediaNameAdapter.findEpisodeNumber(episode.name)
if (potentialNumber != null) {
episode.episode_number = potentialNumber
} else {

View File

@@ -1,7 +1,7 @@
package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.media.MediaNameAdapter
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
@@ -33,9 +33,9 @@ abstract class MangaParser : BaseParser() {
): MangaChapter? {
val chapter = loadChapters(mangaLink, extra, sManga)
val max = chapter
.maxByOrNull { MangaNameAdapter.findChapterNumber(it.number) ?: 0f }
.maxByOrNull { MediaNameAdapter.findChapterNumber(it.number) ?: 0f }
return max
?.takeIf { latest < (MangaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
?.takeIf { latest < (MediaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
}
/**

View File

@@ -1,11 +1,14 @@
package ani.dantotsu.parsers
import android.app.Application
import android.net.Uri
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
@@ -18,6 +21,7 @@ import java.util.Locale
class OfflineAnimeParser : AnimeParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
private val context = Injekt.get<Application>()
override val name = "Offline"
override val saveName = "Offline"
@@ -29,22 +33,19 @@ class OfflineAnimeParser : AnimeParser() {
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/$animeLink"
)
val directory = getSubDirectory(context, MediaType.ANIME, false, animeLink)
//get all of the folder names and add them to the list
val episodes = mutableListOf<Episode>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (directory?.exists() == true) {
directory.listFiles().forEach {
//put the title and episdode number in the extra data
val extraData = mutableMapOf<String, String>()
extraData["title"] = animeLink
extraData["episode"] = it.name
extraData["episode"] = it.name!!
if (it.isDirectory) {
val episode = Episode(
it.name,
"$animeLink - ${it.name}",
it.name!!,
getTaskName(animeLink,it.name!!),
it.name,
null,
null,
@@ -54,7 +55,7 @@ class OfflineAnimeParser : AnimeParser() {
episodes.add(episode)
}
}
episodes.sortBy { AnimeNameAdapter.findEpisodeNumber(it.number) }
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
return episodes
}
return emptyList()
@@ -131,18 +132,19 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
private fun getSubtitle(title: String, episode: String): List<Subtitle>? {
currContext()?.let {
DownloadsManager.getDirectory(
DownloadsManager.getSubDirectory(
it,
MediaType.ANIME,
false,
title,
episode
).listFiles()?.forEach { file ->
if (file.name.contains("subtitle")) {
)?.listFiles()?.forEach { file ->
if (file.name?.contains("subtitle") == true) {
return listOf(
Subtitle(
"Downloaded Subtitle",
Uri.fromFile(file).toString(),
determineSubtitletype(file.absolutePath)
file.uri.toString(),
determineSubtitletype(file.name ?: "")
)
)
}

View File

@@ -1,9 +1,12 @@
package ani.dantotsu.parsers
import android.app.Application
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
@@ -14,6 +17,7 @@ import java.io.File
class OfflineMangaParser : MangaParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
private val context = Injekt.get<Application>()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
@@ -23,17 +27,14 @@ class OfflineMangaParser : MangaParser() {
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$mangaLink"
)
val directory = getSubDirectory(context, MediaType.MANGA, false, mangaLink)
//get all of the folder names and add them to the list
val chapters = mutableListOf<MangaChapter>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (directory?.exists() == true) {
directory.listFiles().forEach {
if (it.isDirectory) {
val chapter = MangaChapter(
it.name,
it.name!!,
"$mangaLink/${it.name}",
it.name,
null,
@@ -43,23 +44,22 @@ class OfflineMangaParser : MangaParser() {
chapters.add(chapter)
}
}
chapters.sortBy { MangaNameAdapter.findChapterNumber(it.number) }
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
return chapters
}
return emptyList()
}
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$chapterLink"
)
val title = chapterLink.split("/").first()
val chapter = chapterLink.split("/").last()
val directory = getSubDirectory(context, MediaType.MANGA, false, title, chapter)
val images = mutableListOf<MangaImage>()
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
if (directory.exists()) {
directory.listFiles()?.forEach {
if (directory?.exists() == true) {
directory.listFiles().forEach {
if (it.isFile) {
val image = MangaImage(it.absolutePath, false, null)
val image = MangaImage(it.uri.toString(), false, null)
images.add(image)
}
}

View File

@@ -1,9 +1,12 @@
package ani.dantotsu.parsers
import android.app.Application
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -11,6 +14,7 @@ import java.io.File
class OfflineNovelParser : NovelParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
private val context = Injekt.get<Application>()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
@@ -21,24 +25,21 @@ class OfflineNovelParser : NovelParser() {
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
//link should be a directory
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/$link"
)
val directory = getSubDirectory(context, MediaType.NOVEL, false, link)
val chapters = mutableListOf<Book>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (directory?.exists() == true) {
directory.listFiles().forEach {
if (it.isDirectory) {
val chapter = Book(
it.name,
it.absolutePath + "/cover.jpg",
it.name?:"Unknown",
it.uri.toString(),
null,
listOf(it.absolutePath + "/0.epub")
listOf(it.uri.toString())
)
chapters.add(chapter)
}
}
chapters.sortBy { MangaNameAdapter.findChapterNumber(it.name) }
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.name) }
return chapters.first()
}
return Book(
@@ -60,20 +61,16 @@ class OfflineNovelParser : NovelParser() {
val returnList: MutableList<ShowResponse> = mutableListOf()
for (title in returnTitles) {
//need to search the subdirectories for the ShowResponses
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/$title"
)
val directory = getSubDirectory(context, MediaType.NOVEL, false, title)
val names = mutableListOf<String>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (directory?.exists() == true) {
directory.listFiles().forEach {
if (it.isDirectory) {
names.add(it.name)
names.add(it.name?: "Unknown")
}
}
}
val cover =
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
val cover = directory?.findFile("cover.jpg")?.uri.toString()
names.forEach {
returnList.add(ShowResponse(it, it, cover))
}

View File

@@ -22,6 +22,7 @@ import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ActivityProfileBinding
import ani.dantotsu.databinding.ItemProfileAppBarBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.media.user.ListActivity
@@ -45,6 +46,7 @@ import kotlin.math.abs
class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
lateinit var binding: ActivityProfileBinding
private lateinit var bindingProfileAppBar: ItemProfileAppBarBinding
private var selected: Int = 0
lateinit var navBar: AnimatedBottomBar
@@ -108,145 +110,165 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
binding.profileViewPager.setCurrentItem(selected, true)
}
})
val userLevel = intent.getStringExtra("userLVL") ?: ""
binding.followButton.isGone = user.id == Anilist.userid || Anilist.userid == null
binding.followButton.text = getString(
when {
user.isFollowing -> R.string.unfollow
user.isFollower -> R.string.follows_you
else -> R.string.follow
}
)
if (user.isFollowing && user.isFollower) binding.followButton.text = getString(R.string.mutual)
binding.followButton.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val res = Anilist.query.toggleFollow(user.id)
if (res?.data?.toggleFollow != null) {
withContext(Dispatchers.Main) {
snackString(R.string.success)
user.isFollowing = res.data.toggleFollow.isFollowing
binding.followButton.text = getString(
when {
user.isFollowing -> R.string.unfollow
user.isFollower -> R.string.follows_you
else -> R.string.follow
}
)
if (user.isFollowing && user.isFollower)
binding.followButton.text = getString(R.string.mutual)
bindingProfileAppBar = ItemProfileAppBarBinding.bind(binding.root).apply {
val userLevel = intent.getStringExtra("userLVL") ?: ""
followButton.isGone =
user.id == Anilist.userid || Anilist.userid == null
followButton.text = getString(
when {
user.isFollowing -> R.string.unfollow
user.isFollower -> R.string.follows_you
else -> R.string.follow
}
)
if (user.isFollowing && user.isFollower) followButton.text =
getString(R.string.mutual)
followButton.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val res = Anilist.query.toggleFollow(user.id)
if (res?.data?.toggleFollow != null) {
withContext(Dispatchers.Main) {
snackString(R.string.success)
user.isFollowing = res.data.toggleFollow.isFollowing
followButton.text = getString(
when {
user.isFollowing -> R.string.unfollow
user.isFollower -> R.string.follows_you
else -> R.string.follow
}
)
if (user.isFollowing && user.isFollower)
followButton.text = getString(R.string.mutual)
}
}
}
}
}
binding.profileProgressBar.visibility = View.GONE
binding.profileAppBar.visibility = View.VISIBLE
binding.profileMenuButton.setOnClickListener {
val popup = PopupMenu(this@ProfileActivity, binding.profileMenuButton)
popup.menuInflater.inflate(R.menu.menu_profile, popup.menu)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_view_following -> {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", "Following")
.putExtra("userId", user.id),
null
)
true
}
binding.profileProgressBar.visibility = View.GONE
profileAppBar.visibility = View.VISIBLE
profileMenuButton.setOnClickListener {
val popup = PopupMenu(this@ProfileActivity, profileMenuButton)
popup.menuInflater.inflate(R.menu.menu_profile, popup.menu)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_view_following -> {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", "Following")
.putExtra("userId", user.id),
null
)
true
}
R.id.action_view_followers -> {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", "Followers")
.putExtra("userId", user.id),
null
)
true
}
R.id.action_view_followers -> {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", "Followers")
.putExtra("userId", user.id),
null
)
true
}
R.id.action_view_on_anilist -> {
openLinkInBrowser("https://anilist.co/user/${user.name}")
true
}
R.id.action_view_on_anilist -> {
openLinkInBrowser("https://anilist.co/user/${user.name}")
true
}
else -> false
else -> false
}
}
popup.show()
}
popup.show()
}
binding.profileUserAvatar.loadImage(user.avatar?.medium)
binding.profileUserAvatar.setOnLongClickListener {
ImageViewDialog.newInstance(
this@ProfileActivity,
"${user.name}'s [Avatar]",
user.avatar?.medium
profileUserAvatar.loadImage(user.avatar?.medium)
profileUserAvatar.setOnLongClickListener {
ImageViewDialog.newInstance(
this@ProfileActivity,
"${user.name}'s [Avatar]",
user.avatar?.medium
)
}
val userLevelText = "${user.name} $userLevel"
profileUserName.text = userLevelText
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
if (bannerAnimations) profileBannerImage else profileBannerImageNoKen,
user.bannerImage ?: user.avatar?.medium
)
}
profileBannerImage.updateLayoutParams { height += statusBarHeight }
profileBannerImageNoKen.updateLayoutParams { height += statusBarHeight }
profileBannerGradient.updateLayoutParams { height += statusBarHeight }
profileCloseButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
profileMenuButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
profileButtonContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
profileBannerImage.setOnLongClickListener {
ImageViewDialog.newInstance(
this@ProfileActivity,
user.name + " [Banner]",
user.bannerImage
)
}
val userLevelText = "${user.name} $userLevel"
binding.profileUserName.text = userLevelText
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.profileBannerImage.pause()
blurImage(binding.profileBannerImage, user.bannerImage ?: user.avatar?.medium)
binding.profileBannerImage.updateLayoutParams { height += statusBarHeight }
binding.profileBannerGradient.updateLayoutParams { height += statusBarHeight }
binding.profileMenuButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.profileButtonContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.profileBannerImage.setOnLongClickListener {
ImageViewDialog.newInstance(
this@ProfileActivity,
user.name + " [Banner]",
user.bannerImage
)
}
mMaxScrollSize = binding.profileAppBar.totalScrollRange
binding.profileAppBar.addOnOffsetChangedListener(this@ProfileActivity)
mMaxScrollSize = profileAppBar.totalScrollRange
profileAppBar.addOnOffsetChangedListener(this@ProfileActivity)
binding.profileFollowerCount.text = followers.toString()
binding.profileFollowerCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", getString(R.string.followers))
.putExtra("userId", user.id),
null
)
}
profileFollowerCount.text = followers.toString()
profileFollowerCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", getString(R.string.followers))
.putExtra("userId", user.id),
null
)
}
binding.profileFollowingCount.text = following.toString()
binding.profileFollowingCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", "Following")
.putExtra("userId", user.id),
null
)
}
profileFollowingCount.text = following.toString()
profileFollowingCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, FollowActivity::class.java)
.putExtra("title", "Following")
.putExtra("userId", user.id),
null
)
}
binding.profileAnimeCount.text = user.statistics.anime.count.toString()
binding.profileAnimeCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity, Intent(this@ProfileActivity, ListActivity::class.java)
.putExtra("anime", true)
.putExtra("userId", user.id)
.putExtra("username", user.name), null
)
}
profileAnimeCount.text = user.statistics.anime.count.toString()
profileAnimeCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, ListActivity::class.java)
.putExtra("anime", true)
.putExtra("userId", user.id)
.putExtra("username", user.name),
null
)
}
binding.profileMangaCount.text = user.statistics.manga.count.toString()
binding.profileMangaCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity, Intent(this@ProfileActivity, ListActivity::class.java)
.putExtra("anime", false)
.putExtra("userId", user.id)
.putExtra("username", user.name), null
)
profileMangaCount.text = user.statistics.manga.count.toString()
profileMangaCountContainer.setOnClickListener {
ContextCompat.startActivity(
this@ProfileActivity,
Intent(this@ProfileActivity, ListActivity::class.java)
.putExtra("anime", false)
.putExtra("userId", user.id)
.putExtra("username", user.name),
null
)
}
profileCloseButton.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
}
}
@@ -262,29 +284,31 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize
binding.profileUserAvatarContainer.visibility =
if (binding.profileUserAvatarContainer.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
ObjectAnimator.ofFloat(binding.profileUserDataContainer, "translationX", screenWidth)
.setDuration(duration).start()
ObjectAnimator.ofFloat(binding.profileUserAvatarContainer, "translationX", screenWidth)
.setDuration(duration).start()
ObjectAnimator.ofFloat(binding.profileButtonContainer, "translationX", screenWidth)
.setDuration(duration).start()
binding.profileBannerImage.pause()
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
ObjectAnimator.ofFloat(binding.profileUserDataContainer, "translationX", 0f)
.setDuration(duration).start()
ObjectAnimator.ofFloat(binding.profileUserAvatarContainer, "translationX", 0f)
.setDuration(duration).start()
ObjectAnimator.ofFloat(binding.profileButtonContainer, "translationX", 0f)
.setDuration(duration).start()
with (bindingProfileAppBar) {
profileUserAvatarContainer.visibility =
if (profileUserAvatarContainer.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
ObjectAnimator.ofFloat(profileUserDataContainer, "translationX", screenWidth)
.setDuration(duration).start()
ObjectAnimator.ofFloat(profileUserAvatarContainer, "translationX", screenWidth)
.setDuration(duration).start()
ObjectAnimator.ofFloat(profileButtonContainer, "translationX", screenWidth)
.setDuration(duration).start()
profileBannerImage.pause()
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
ObjectAnimator.ofFloat(profileUserDataContainer, "translationX", 0f)
.setDuration(duration).start()
ObjectAnimator.ofFloat(profileUserAvatarContainer, "translationX", 0f)
.setDuration(duration).start()
ObjectAnimator.ofFloat(profileButtonContainer, "translationX", 0f)
.setDuration(duration).start()
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.profileBannerImage.resume()
if (PrefManager.getVal(PrefName.BannerAnimations)) profileBannerImage.resume()
}
}
}

View File

@@ -154,22 +154,23 @@ class ProfileFragment : Fragment() {
private fun setFavPeople() {
if (favStaff.isEmpty()) {
binding.profileFavStaffContainer.visibility = View.GONE
} else {
binding.profileFavStaffRecycler.adapter = AuthorAdapter(favStaff)
binding.profileFavStaffRecycler.layoutManager = LinearLayoutManager(
activity, LinearLayoutManager.HORIZONTAL, false
)
binding.profileFavStaffRecycler.layoutAnimation = LayoutAnimationController(setSlideIn(), 0.25f)
}
binding.profileFavStaffRecycler.adapter = AuthorAdapter(favStaff)
binding.profileFavStaffRecycler.layoutManager = LinearLayoutManager(
activity,
LinearLayoutManager.HORIZONTAL,
false
)
if (favCharacter.isEmpty()) {
binding.profileFavCharactersContainer.visibility = View.GONE
} else {
binding.profileFavCharactersRecycler.adapter = CharacterAdapter(favCharacter)
binding.profileFavCharactersRecycler.layoutManager = LinearLayoutManager(
activity, LinearLayoutManager.HORIZONTAL, false
)
binding.profileFavCharactersRecycler.layoutAnimation = LayoutAnimationController(setSlideIn(), 0.25f)
}
binding.profileFavCharactersRecycler.adapter = CharacterAdapter(favCharacter)
binding.profileFavCharactersRecycler.layoutManager = LinearLayoutManager(
activity,
LinearLayoutManager.HORIZONTAL,
false
)
}
private fun initRecyclerView(

View File

@@ -1,14 +1,13 @@
package ani.dantotsu.profile.activity
package ani.dantotsu.profile
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.blurImage
import ani.dantotsu.databinding.ItemFollowerBinding
import ani.dantotsu.loadImage
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
@@ -41,7 +40,7 @@ class UsersAdapter(private val user: ArrayList<User>) : RecyclerView.Adapter<Use
setAnimation(b.root.context, b.root)
val user = user[position]
b.profileUserAvatar.loadImage(user.pfp)
b.profileBannerImage.loadImage(user.banner)
blurImage(b.profileBannerImage, user.banner ?: user.pfp)
b.profileUserName.text = user.name
}

View File

@@ -7,8 +7,6 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.databinding.BottomSheetUsersBinding
import ani.dantotsu.profile.activity.UsersAdapter
import ani.dantotsu.settings.DevelopersAdapter
class UsersDialogFragment : BottomSheetDialogFragment() {

View File

@@ -21,7 +21,7 @@ import nl.joery.animatedbottombar.AnimatedBottomBar
class FeedActivity : AppCompatActivity() {
private lateinit var binding: ActivityFeedBinding
private var selected: Int = 0
private lateinit var navBar: AnimatedBottomBar
lateinit var navBar: AnimatedBottomBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -50,16 +50,21 @@ class FeedFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity()
binding.listRecyclerView.setBaseline((activity as ProfileActivity).navBar)
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
binding.listProgressBar.visibility = ViewGroup.VISIBLE
userId = arguments?.getInt("userId", -1)
activityId = arguments?.getInt("activityId", -1) ?: -1
if (userId == -1) userId = null
global = arguments?.getBoolean("global", false) ?: false
val navBar = if (userId != null) {
(activity as ProfileActivity).navBar
}else{
(activity as FeedActivity).navBar
}
binding.listRecyclerView.setBaseline(navBar)
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
binding.listProgressBar.visibility = ViewGroup.VISIBLE
}
@SuppressLint("ClickableViewAccessibility")
@@ -67,7 +72,12 @@ class FeedFragment : Fragment() {
super.onResume()
if (this::binding.isInitialized) {
binding.root.requestLayout()
binding.listRecyclerView.setBaseline((activity as ProfileActivity).navBar)
val navBar = if (userId != null) {
(activity as ProfileActivity).navBar
}else{
(activity as FeedActivity).navBar
}
binding.listRecyclerView.setBaseline(navBar)
if (!loadedFirstTime) {
activity.lifecycleScope.launch(Dispatchers.IO) {
val nulledId = if (activityId == -1) null else activityId

View File

@@ -13,7 +13,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
import ani.dantotsu.databinding.FragmentExtensionsBinding
import ani.dantotsu.settings.paging.AnimeExtensionAdapter
import ani.dantotsu.settings.paging.AnimeExtensionsViewModel
import ani.dantotsu.settings.paging.AnimeExtensionsViewModelFactory
@@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get
class AnimeExtensionsFragment : Fragment(),
SearchQueryHandler, OnAnimeInstallClickListener {
private var _binding: FragmentAnimeExtensionsBinding? = null
private var _binding: FragmentExtensionsBinding? = null
private val binding get() = _binding!!
private val viewModel: AnimeExtensionsViewModel by viewModels {
@@ -48,12 +48,12 @@ class AnimeExtensionsFragment : Fragment(),
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = false
binding.allAnimeExtensionsRecyclerView.adapter = adapter
binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
binding.allExtensionsRecyclerView.isNestedScrollingEnabled = false
binding.allExtensionsRecyclerView.adapter = adapter
binding.allExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
true
lifecycleScope.launch {
@@ -91,8 +91,8 @@ class AnimeExtensionsFragment : Fragment(),
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Installing extension")
.setContentText("Step: $installStep")
.setContentTitle(getString(R.string.installing_extension))
.setContentText(getString(R.string.install_step, installStep))
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
},
@@ -103,11 +103,11 @@ class AnimeExtensionsFragment : Fragment(),
Notifications.CHANNEL_DOWNLOADER_ERROR
)
.setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Installation failed: ${error.message}")
.setContentText("Error: ${error.message}")
.setContentTitle(getString(R.string.installation_failed, error.message))
.setContentText(getString(R.string.error_message, error.message))
.setPriority(NotificationCompat.PRIORITY_HIGH)
notificationManager.notify(1, builder.build())
snackString("Installation failed: ${error.message}")
snackString(getString(R.string.installation_failed, error.message))
},
{
val builder = NotificationCompat.Builder(
@@ -115,12 +115,12 @@ class AnimeExtensionsFragment : Fragment(),
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_download_24)
.setContentTitle("Installation complete")
.setContentText("The extension has been successfully installed.")
.setContentTitle(getString(R.string.installation_complete))
.setContentText(getString(R.string.extension_has_been_installed))
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
viewModel.invalidatePager()
snackString("Extension installed")
snackString(getString(R.string.extension_installed))
}
)
}

View File

@@ -6,63 +6,13 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.connections.github.Contributors
import ani.dantotsu.databinding.BottomSheetDevelopersBinding
class DevelopersDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetDevelopersBinding? = null
private val binding get() = _binding!!
private val developers = arrayOf(
Developer(
"rebelonion",
"https://avatars.githubusercontent.com/u/87634197?v=4",
"Owner and Maintainer",
"https://github.com/rebelonion"
),
Developer(
"Aayush262",
"https://avatars.githubusercontent.com/u/99584765?v=4",
"Contributor",
"https://github.com/aayush2622"
),
Developer(
"AbandonedCart",
"https://avatars.githubusercontent.com/u/1173913?v=4",
"Contributor",
"https://github.com/AbandonedCart"
),
Developer(
"Sadwhy",
"https://avatars.githubusercontent.com/u/99601717?v=4",
"Contributor",
"https://github.com/Sadwhy"
),
Developer(
"Wai What",
"https://avatars.githubusercontent.com/u/149729762?v=4",
"Icon Designer",
"https://github.com/WaiWhat"
),
Developer(
"MarshMeadow",
"https://avatars.githubusercontent.com/u/88599122?v=4",
"Beta Icon Designer",
"https://github.com/MarshMeadow?tab=repositories"
),
Developer(
"Zaxx69",
"https://avatars.githubusercontent.com/u/138523882?v=4",
"Telegram Admin",
"https://github.com/Zaxx69"
),
Developer(
"Arif Alam",
"https://avatars.githubusercontent.com/u/70383209?v=4",
"Head Discord Moderator",
"https://youtube.com/watch?v=dQw4w9WgXcQ"
),
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -74,7 +24,7 @@ class DevelopersDialogFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.devsRecyclerView.adapter = DevelopersAdapter(developers)
binding.devsRecyclerView.adapter = DevelopersAdapter(Contributors().getContributors())
binding.devsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
}

View File

@@ -0,0 +1,52 @@
package ani.dantotsu.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetDiscordRpcBinding
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
class DiscordDialogFragment: BottomSheetDialogFragment() {
private var _binding: BottomSheetDiscordRpcBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetDiscordRpcBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
when (PrefManager.getCustomVal("discord_mode", "dantotsu")) {
"nothing" -> binding.radioNothing.isChecked= true
"dantotsu" -> binding.radioDantotsu.isChecked = true
"anilist" -> binding.radioAnilist.isChecked = true
else -> binding.radioAnilist.isChecked = true
}
binding.anilistLinkPreview.text = getString(R.string.anilist_link, PrefManager.getVal<String>(PrefName.AnilistUserName))
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
val mode = when (checkedId) {
binding.radioNothing.id -> "nothing"
binding.radioDantotsu.id -> "dantotsu"
binding.radioAnilist.id -> "anilist"
else -> "dantotsu"
}
PrefManager.setCustomVal("discord_mode", mode)
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@@ -7,21 +7,13 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.connections.github.Forks
import ani.dantotsu.databinding.BottomSheetDevelopersBinding
class ForksDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetDevelopersBinding? = null
private val binding get() = _binding!!
private val developers = arrayOf(
Developer(
"Dantotsu",
"https://avatars.githubusercontent.com/u/87634197?v=4",
"rebelonion",
"https://github.com/rebelonion/Dantotsu"
),
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -34,7 +26,7 @@ class ForksDialogFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.devsTitle.setText(R.string.forks)
binding.devsRecyclerView.adapter = DevelopersAdapter(developers)
binding.devsRecyclerView.adapter = DevelopersAdapter(Forks().getForks())
binding.devsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
}

View File

@@ -24,7 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
import ani.dantotsu.databinding.FragmentExtensionsBinding
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
@@ -49,7 +49,7 @@ import java.util.Locale
class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentAnimeExtensionsBinding? = null
private var _binding: FragmentExtensionsBinding? = null
private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView
private val skipIcons: Boolean = PrefManager.getVal(PrefName.SkipExtensionIcons)
@@ -183,9 +183,9 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
extensionsRecyclerView = binding.allAnimeExtensionsRecyclerView
extensionsRecyclerView = binding.allExtensionsRecyclerView
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
extensionsRecyclerView.adapter = extensionsAdapter

View File

@@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
import ani.dantotsu.databinding.FragmentExtensionsBinding
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
@@ -48,7 +48,7 @@ import java.util.Collections
import java.util.Locale
class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentMangaExtensionsBinding? = null
private var _binding: FragmentExtensionsBinding? = null
private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView
private val skipIcons: Boolean = PrefManager.getVal(PrefName.SkipExtensionIcons)
@@ -181,9 +181,9 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
extensionsRecyclerView = binding.allMangaExtensionsRecyclerView
extensionsRecyclerView = binding.allExtensionsRecyclerView
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
extensionsRecyclerView.adapter = extensionsAdapter

View File

@@ -13,7 +13,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
import ani.dantotsu.databinding.FragmentExtensionsBinding
import ani.dantotsu.settings.paging.MangaExtensionAdapter
import ani.dantotsu.settings.paging.MangaExtensionsViewModel
import ani.dantotsu.settings.paging.MangaExtensionsViewModelFactory
@@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get
class MangaExtensionsFragment : Fragment(),
SearchQueryHandler, OnMangaInstallClickListener {
private var _binding: FragmentMangaExtensionsBinding? = null
private var _binding: FragmentExtensionsBinding? = null
private val binding get() = _binding!!
private val viewModel: MangaExtensionsViewModel by viewModels {
@@ -49,12 +49,12 @@ class MangaExtensionsFragment : Fragment(),
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
_binding = FragmentExtensionsBinding.inflate(inflater, container, false)
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = false
binding.allMangaExtensionsRecyclerView.adapter = adapter
binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
binding.allExtensionsRecyclerView.isNestedScrollingEnabled = false
binding.allExtensionsRecyclerView.adapter = adapter
binding.allExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled =
true
lifecycleScope.launch {
@@ -92,8 +92,8 @@ class MangaExtensionsFragment : Fragment(),
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Installing extension")
.setContentText("Step: $installStep")
.setContentTitle(getString(R.string.installing_extension))
.setContentText(getString(R.string.install_step, installStep))
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
},
@@ -104,11 +104,11 @@ class MangaExtensionsFragment : Fragment(),
Notifications.CHANNEL_DOWNLOADER_ERROR
)
.setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Installation failed: ${error.message}")
.setContentText("Error: ${error.message}")
.setContentTitle(getString(R.string.installation_failed, error.message))
.setContentText(getString(R.string.error_message, error.message))
.setPriority(NotificationCompat.PRIORITY_HIGH)
notificationManager.notify(1, builder.build())
snackString("Installation failed: ${error.message}")
snackString(getString(R.string.installation_failed, error.message))
},
{
val builder = NotificationCompat.Builder(
@@ -116,12 +116,12 @@ class MangaExtensionsFragment : Fragment(),
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_download_24)
.setContentTitle("Installation complete")
.setContentText("The extension has been successfully installed.")
.setContentTitle(getString(R.string.installation_complete))
.setContentText(getString(R.string.extension_has_been_installed))
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
viewModel.invalidatePager()
snackString("Extension installed")
snackString(getString(R.string.extension_installed))
}
)
}

View File

@@ -137,11 +137,6 @@ class PlayerSettingsActivity : AppCompatActivity() {
binding.playerSettingsAutoSkipOpEd.isEnabled = isChecked
}
binding.playerSettingsTimeStampsAutoHide.isChecked = PrefManager.getVal(PrefName.AutoHideTimeStamps)
binding.playerSettingsTimeStampsAutoHide.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.AutoHideTimeStamps, isChecked)
}
binding.playerSettingsTimeStampsProxy.isChecked =
PrefManager.getVal(PrefName.UseProxyForTimeStamps)
binding.playerSettingsTimeStampsProxy.setOnCheckedChangeListener { _, isChecked ->
@@ -152,6 +147,13 @@ class PlayerSettingsActivity : AppCompatActivity() {
PrefManager.getVal(PrefName.ShowTimeStampButton)
binding.playerSettingsShowTimeStamp.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.ShowTimeStampButton, isChecked)
binding.playerSettingsTimeStampsAutoHide.isEnabled = isChecked
}
binding.playerSettingsTimeStampsAutoHide.isChecked = PrefManager.getVal(PrefName.AutoHideTimeStamps)
binding.playerSettingsTimeStampsAutoHide.isEnabled = binding.playerSettingsShowTimeStamp.isChecked
binding.playerSettingsTimeStampsAutoHide.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.AutoHideTimeStamps, isChecked)
}
// Auto
@@ -475,7 +477,7 @@ class PlayerSettingsActivity : AppCompatActivity() {
updateSubPreview()
}
}
binding.subtitleTest.setOnChangeListener(object: Xpandable.OnChangeListener {
binding.subtitleTest.addOnChangeListener(object: Xpandable.OnChangeListener {
override fun onExpand() {
updateSubPreview()
}

View File

@@ -5,6 +5,7 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Build
import android.os.Build.BRAND
import android.os.Build.DEVICE
@@ -13,23 +14,25 @@ import android.os.Build.VERSION.CODENAME
import android.os.Build.VERSION.RELEASE
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.BuildConfig
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
@@ -47,9 +50,8 @@ import ani.dantotsu.databinding.ActivitySettingsExtensionsBinding
import ani.dantotsu.databinding.ActivitySettingsMangaBinding
import ani.dantotsu.databinding.ActivitySettingsNotificationsBinding
import ani.dantotsu.databinding.ActivitySettingsThemeBinding
import ani.dantotsu.databinding.ItemRepositoryBinding
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.downloadsPermission
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaType
@@ -78,19 +80,27 @@ import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
import ani.dantotsu.util.LauncherWrapper
import ani.dantotsu.util.Logger
import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
import com.google.android.material.textfield.TextInputEditText
import eltos.simpledialogfragment.SimpleDialog
import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE
import eltos.simpledialogfragment.color.SimpleColorDialog
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.random.Random
@@ -99,6 +109,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
}
lateinit var binding: ActivitySettingsBinding
lateinit var launcher: LauncherWrapper
private lateinit var bindingAccounts: ActivitySettingsAccountsBinding
private lateinit var bindingTheme: ActivitySettingsThemeBinding
private lateinit var bindingExtensions: ActivitySettingsExtensionsBinding
@@ -109,7 +120,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
private lateinit var bindingAbout: ActivitySettingsAboutBinding
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
private var cursedCounter = 0
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
@kotlin.OptIn(DelicateCoroutinesApi::class)
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -161,6 +175,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
}
}
}
val contract = ActivityResultContracts.OpenDocumentTree()
launcher = LauncherWrapper(this, contract)
binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
binding.settingsVersion.setOnLongClickListener {
@@ -207,6 +223,15 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsAnilistUsername.visibility = View.VISIBLE
settingsAnilistUsername.text = Anilist.username
settingsAnilistAvatar.loadImage(Anilist.avatar)
settingsAnilistAvatar.setOnClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val anilistLink = getString(
R.string.anilist_link,
PrefManager.getVal<String>(PrefName.AnilistUserName)
)
openLinkInBrowser(anilistLink)
true
}
settingsMALLoginRequired.visibility = View.GONE
settingsMALLogin.visibility = View.VISIBLE
@@ -222,6 +247,12 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsMALUsername.visibility = View.VISIBLE
settingsMALUsername.text = MAL.username
settingsMALAvatar.loadImage(MAL.avatar)
settingsMALAvatar.setOnClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val myanilistLink = getString(R.string.myanilist_link, MAL.username)
openLinkInBrowser(myanilistLink)
true
}
} else {
settingsMALAvatar.setImageResource(R.drawable.ic_round_person_24)
settingsMALUsername.visibility = View.GONE
@@ -248,6 +279,12 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
val username = PrefManager.getVal(PrefName.DiscordUserName, null as String?)
if (id != null && avatar != null) {
settingsDiscordAvatar.loadImage("https://cdn.discordapp.com/avatars/$id/$avatar.png")
settingsDiscordAvatar.setOnClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val discordLink = getString(R.string.discord_link, id)
openLinkInBrowser(discordLink)
true
}
}
settingsDiscordUsername.visibility = View.VISIBLE
settingsDiscordUsername.text =
@@ -259,18 +296,18 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
reload()
}
imageSwitcher.visibility = View.VISIBLE
settingsImageSwitcher.visibility = View.VISIBLE
var initialStatus = when (PrefManager.getVal<String>(PrefName.DiscordStatus)) {
"online" -> R.drawable.discord_status_online
"idle" -> R.drawable.discord_status_idle
"dnd" -> R.drawable.discord_status_dnd
else -> R.drawable.discord_status_online
}
imageSwitcher.setImageResource(initialStatus)
settingsImageSwitcher.setImageResource(initialStatus)
val zoomInAnimation =
AnimationUtils.loadAnimation(this@SettingsActivity, R.anim.bounce_zoom)
imageSwitcher.setOnClickListener {
settingsImageSwitcher.setOnClickListener {
var status = "online"
initialStatus = when (initialStatus) {
R.drawable.discord_status_online -> {
@@ -292,11 +329,16 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
}
PrefManager.setVal(PrefName.DiscordStatus, status)
imageSwitcher.setImageResource(initialStatus)
imageSwitcher.startAnimation(zoomInAnimation)
settingsImageSwitcher.setImageResource(initialStatus)
settingsImageSwitcher.startAnimation(zoomInAnimation)
}
settingsImageSwitcher.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
DiscordDialogFragment().show(supportFragmentManager, "dialog")
true
}
} else {
imageSwitcher.visibility = View.GONE
settingsImageSwitcher.visibility = View.GONE
settingsDiscordAvatar.setImageResource(R.drawable.ic_round_person_24)
settingsDiscordUsername.visibility = View.GONE
settingsDiscordLogin.setText(R.string.login)
@@ -429,11 +471,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
.setPositiveButton(R.string.yes) { dialog, _ ->
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.purgeDownloads(MediaType.ANIME)
DownloadService.sendRemoveAllDownloads(
this@SettingsActivity,
ExoplayerDownloadService::class.java,
false
)
dialog.dismiss()
}
.setNegativeButton(R.string.no) { dialog, _ ->
@@ -454,6 +491,11 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsShowYt.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.ShowYtButton, isChecked)
}
settingsIncludeAnimeList.isChecked = PrefManager.getVal(PrefName.IncludeAnimeList)
settingsIncludeAnimeList.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.IncludeAnimeList, isChecked)
restartApp(binding.root)
}
var previousEp: View = when (PrefManager.getVal<Int>(PrefName.AnimeDefaultView)) {
0 -> settingsEpList
@@ -541,9 +583,178 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsChpCompact.setOnClickListener {
uiChp(1, it)
}
settingsIncludeMangaList.isChecked = PrefManager.getVal(PrefName.IncludeMangaList)
settingsIncludeMangaList.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.IncludeMangaList, isChecked)
restartApp(binding.root)
}
}
bindingExtensions = ActivitySettingsExtensionsBinding.bind(binding.root).apply {
fun setExtensionOutput() {
animeRepoInventory.removeAllViews()
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).forEach { item ->
val view = ItemRepositoryBinding.inflate(
LayoutInflater.from(animeRepoInventory.context), animeRepoInventory, true
)
view.repositoryItem.text = item.replace("https://raw.githubusercontent.com/", "")
view.repositoryItem.setOnClickListener {
AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle("Delete Anime Repository")
.setMessage(item)
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
val anime = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
setExtensionOutput()
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions()
}
dialog.dismiss()
}
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
view.repositoryItem.setOnLongClickListener {
copyToClipboard(item, true)
true
}
}
animeRepoInventory.isVisible = animeRepoInventory.childCount > 0
mangaRepoInventory.removeAllViews()
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).forEach { item ->
val view = ItemRepositoryBinding.inflate(
LayoutInflater.from(mangaRepoInventory.context), mangaRepoInventory, true
)
view.repositoryItem.text = item.replace("https://raw.githubusercontent.com/", "")
view.repositoryItem.setOnClickListener {
AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle("Delete Manga Repository")
.setMessage(item)
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
val manga = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).minus(item)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
setExtensionOutput()
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager.findAvailableExtensions()
}
dialog.dismiss()
}
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
view.repositoryItem.setOnLongClickListener {
copyToClipboard(item, true)
true
}
}
mangaRepoInventory.isVisible = mangaRepoInventory.childCount > 0
}
fun processUserInput(input: String, mediaType: MediaType) {
val entry = if (input.endsWith("/") || input.endsWith("index.min.json"))
input.substring(0, input.lastIndexOf("/")) else input
if (mediaType == MediaType.ANIME) {
val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions()
}
}
if (mediaType == MediaType.MANGA) {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager.findAvailableExtensions()
}
}
setExtensionOutput()
}
fun processEditorAction(dialog: AlertDialog, editText: EditText, mediaType: MediaType) {
editText.setOnEditorActionListener { textView, action, keyEvent ->
if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE ||
(keyEvent?.action == KeyEvent.ACTION_UP
&& keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)
) {
processUserInput(textView.text.toString(), mediaType)
dialog.dismiss()
return@setOnEditorActionListener true
}
false
}
}
setExtensionOutput()
animeAddRepository.setOnClickListener {
val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null)
val editText =
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply {
hint = getString(R.string.anime_add_repository)
}
val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.add_repository)
.setMessage("Add additional repo for anime extensions")
.setView(dialogView)
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
processUserInput(editText.text.toString(), MediaType.ANIME)
dialog.dismiss()
}
.setNeutralButton(getString(R.string.reset)) { dialog, _ ->
PrefManager.removeVal(PrefName.DefaultUserAgent)
editText.setText("")
dialog.dismiss()
}
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
.create()
processEditorAction(alertDialog, editText, MediaType.ANIME)
alertDialog.show()
alertDialog.window?.setDimAmount(0.8f)
}
mangaAddRepository.setOnClickListener {
val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null)
val editText =
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply {
hint = getString(R.string.manga_add_repository)
}
val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.add_repository)
.setView(dialogView)
.setMessage("Add additional repo for manga extensions")
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
processUserInput(editText.text.toString(), MediaType.MANGA)
dialog.dismiss()
}
.setNeutralButton(getString(R.string.reset)) { dialog, _ ->
PrefManager.removeVal(PrefName.DefaultUserAgent)
editText.setText("")
dialog.dismiss()
}
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
.create()
processEditorAction(alertDialog, editText, MediaType.MANGA)
alertDialog.show()
alertDialog.window?.setDimAmount(0.8f)
}
settingsForceLegacyInstall.isChecked =
extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY
settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked ->
@@ -628,18 +839,18 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
val filteredLocations = Location.entries.filter { it.exportable }
selectedArray.addAll(List(filteredLocations.size - 1) { false })
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.import_export_settings)
.setTitle(R.string.backup_restore)
.setMultiChoiceItems(
filteredLocations.map { it.name }.toTypedArray(),
selectedArray.toBooleanArray()
) { _, which, isChecked ->
selectedArray[which] = isChecked
}
.setPositiveButton(R.string.button_import) { dialog, _ ->
.setPositiveButton(R.string.button_restore) { dialog, _ ->
openDocumentLauncher.launch(arrayOf("*/*"))
dialog.dismiss()
}
.setNegativeButton(R.string.button_export) { dialog, _ ->
.setNegativeButton(R.string.button_backup) { dialog, _ ->
if (!selectedArray.contains(true)) {
toast(R.string.no_location_selected)
return@setNegativeButton
@@ -678,27 +889,19 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
}
settingsExtensionDns.setText(exDns[PrefManager.getVal(PrefName.DohProvider)])
settingsExtensionDns.setAdapter(ArrayAdapter(this@SettingsActivity, R.layout.item_dropdown, exDns))
settingsExtensionDns.setAdapter(
ArrayAdapter(
this@SettingsActivity,
R.layout.item_dropdown,
exDns
)
)
settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
PrefManager.setVal(PrefName.DohProvider, i)
settingsExtensionDns.clearFocus()
restartApp(binding.root)
}
settingsDownloadInSd.isChecked = PrefManager.getVal(PrefName.SdDl)
settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
val arrayOfFiles = ContextCompat.getExternalFilesDirs(this@SettingsActivity, null)
if (arrayOfFiles.size > 1 && arrayOfFiles[1] != null) {
PrefManager.setVal(PrefName.SdDl, true)
} else {
settingsDownloadInSd.isChecked = false
PrefManager.setVal(PrefName.SdDl, true)
snackString(getString(R.string.noSdFound))
}
} else PrefManager.setVal(PrefName.SdDl, true)
}
settingsContinueMedia.isChecked = PrefManager.getVal(PrefName.ContinueMedia)
settingsContinueMedia.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.ContinueMedia, isChecked)
@@ -713,6 +916,48 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsRecentlyListOnly.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.RecentlyListOnly, isChecked)
}
settingsAdultAnimeOnly.isChecked = PrefManager.getVal(PrefName.AdultOnly)
settingsAdultAnimeOnly.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.AdultOnly, isChecked)
restartApp(binding.root)
}
settingsDownloadLocation.setOnClickListener {
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.change_download_location)
.setMessage(R.string.download_location_msg)
.setPositiveButton(R.string.ok) { dialog, _ ->
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
launcher.registerForCallback { success ->
if (success) {
toast(getString(R.string.please_wait))
val newUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
GlobalScope.launch(Dispatchers.IO) {
Injekt.get<DownloadsManager>().moveDownloadsDir(
this@SettingsActivity,
Uri.parse(oldUri), Uri.parse(newUri)
) { finished, message ->
if (finished) {
toast(getString(R.string.success))
} else {
toast(message)
}
}
}
} else {
toast(getString(R.string.error))
}
}
launcher.launch()
dialog.dismiss()
}
.setNeutralButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
}
var previousStart: View = when (PrefManager.getVal<Int>(PrefName.DefaultStartUpTab)) {
0 -> uiSettingsAnime
@@ -742,7 +987,12 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
}
settingsUi.setOnClickListener {
startActivity(Intent(this@SettingsActivity, UserInterfaceSettingsActivity::class.java))
startActivity(
Intent(
this@SettingsActivity,
UserInterfaceSettingsActivity::class.java
)
)
}
}
@@ -766,7 +1016,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
getString(R.string.subscriptions_checking_time_s, timeNames[i])
PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime)
dialog.dismiss()
TaskScheduler.create(this@SettingsActivity,
TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity)
}.show()
@@ -774,7 +1025,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
}
settingsSubscriptionsTime.setOnLongClickListener {
TaskScheduler.create(this@SettingsActivity,
TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity)
true
@@ -788,7 +1040,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
else getString(R.string.do_not_update)
}
settingsAnilistSubscriptionsTime.text =
getString(R.string.anilist_notifications_checking_time, aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)])
getString(
R.string.anilist_notifications_checking_time,
aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
)
settingsAnilistSubscriptionsTime.setOnClickListener {
val selected = PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval)
@@ -799,7 +1054,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsAnilistSubscriptionsTime.text =
getString(R.string.anilist_notifications_checking_time, aItems[i])
dialog.dismiss()
TaskScheduler.create(this@SettingsActivity,
TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity)
}
@@ -810,7 +1066,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsAnilistNotifications.setOnClickListener {
val types = NotificationType.entries.map { it.name }
val filteredTypes = PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes).toMutableSet()
val filteredTypes =
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes).toMutableSet()
val selected = types.map { filteredTypes.contains(it) }.toBooleanArray()
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.anilist_notification_filters)
@@ -837,7 +1094,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
}
settingsCommentSubscriptionsTime.text =
getString(R.string.comment_notification_checking_time, cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)])
getString(
R.string.comment_notification_checking_time,
cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)]
)
settingsCommentSubscriptionsTime.setOnClickListener {
val selected = PrefManager.getVal<Int>(PrefName.CommentNotificationInterval)
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
@@ -847,7 +1107,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsCommentSubscriptionsTime.text =
getString(R.string.comment_notification_checking_time, cItems[i])
dialog.dismiss()
TaskScheduler.create(this@SettingsActivity,
TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity)
}
@@ -878,7 +1139,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
PrefManager.setVal(PrefName.UseAlarmManager, true)
if (SDK_INT >= Build.VERSION_CODES.S) {
if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
val intent = Intent("android.settings.REQUEST_SCHEDULE_EXACT_ALARM")
val intent =
Intent("android.settings.REQUEST_SCHEDULE_EXACT_ALARM")
startActivity(intent)
settingsNotificationsCheckingSubscriptions.isChecked = true
}
@@ -896,7 +1158,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} else {
PrefManager.setVal(PrefName.UseAlarmManager, false)
TaskScheduler.create(this@SettingsActivity, true).cancelAllTasks()
TaskScheduler.create(this@SettingsActivity, false).scheduleAllTasks(this@SettingsActivity)
TaskScheduler.create(this@SettingsActivity, false)
.scheduleAllTasks(this@SettingsActivity)
}
}
}
@@ -1068,6 +1331,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
callback(null)
}
.create()
fun handleOkAction() {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
@@ -1126,4 +1390,4 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
?: "Unknown Architecture"
}
}
}
}

View File

@@ -13,7 +13,6 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
OfflineView(Pref(Location.General, Int::class, 0)),
DownloadManager(Pref(Location.General, Int::class, 0)),
NSFWExtension(Pref(Location.General, Boolean::class, true)),
SdDl(Pref(Location.General, Boolean::class, false)),
ContinueMedia(Pref(Location.General, Boolean::class, true)),
SearchSources(Pref(Location.General, Boolean::class, true)),
RecentlyListOnly(Pref(Location.General, Boolean::class, false)),
@@ -29,6 +28,9 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
"Mozilla/5.0 (Linux; Android 13; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
)
),
AnimeExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
MangaExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
SharedRepositories(Pref(Location.General, Boolean::class, false)),
AnimeSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
@@ -40,6 +42,9 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
LastAnilistNotificationId(Pref(Location.General, Int::class, 0)),
AnilistFilteredTypes(Pref(Location.General, Set::class, setOf<String>())),
UseAlarmManager(Pref(Location.General, Boolean::class, false)),
IncludeAnimeList(Pref(Location.General, Boolean::class, true)),
IncludeMangaList(Pref(Location.General, Boolean::class, true)),
AdultOnly(Pref(Location.General, Boolean::class, false)),
//User Interface
UseOLED(Pref(Location.UI, Boolean::class, false)),
@@ -78,6 +83,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
CommentSortOrder(Pref(Location.UI, String::class, "newest")),
FollowerLayout(Pref(Location.UI, Int::class, 0)),
//Player
DefaultSpeed(Pref(Location.Player, Int::class, 5)),
CursedSpeeds(Pref(Location.Player, Boolean::class, false)),
@@ -178,6 +184,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
RecentGlobalNotification(Pref(Location.Irrelevant, Int::class, 0)),
CommentNotificationStore(Pref(Location.Irrelevant, List::class, listOf<CommentStore>())),
UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)),
DownloadsDir(Pref(Location.Irrelevant, String::class, "")),
//Protected
DiscordToken(Pref(Location.Protected, String::class, "")),

View File

@@ -0,0 +1,22 @@
package ani.dantotsu.util
import android.os.CountDownTimer
// https://stackoverflow.com/a/40422151/461982
abstract class CountUpTimer protected constructor(
private val duration: Long
) : CountDownTimer(duration, INTERVAL_MS) {
abstract fun onTick(second: Int)
override fun onTick(msUntilFinished: Long) {
val second = ((duration - msUntilFinished) / 1000).toInt()
onTick(second)
}
override fun onFinish() {
onTick(duration / 1000)
}
companion object {
private const val INTERVAL_MS: Long = 1000
}
}

View File

@@ -0,0 +1,132 @@
package ani.dantotsu.util
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
class StoragePermissions {
companion object {
fun downloadsPermission(activity: AppCompatActivity): Boolean {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val requiredPermissions = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
activity,
requiredPermissions,
DOWNLOADS_PERMISSION_REQUEST_CODE
)
false
} else {
true
}
}
fun hasDirAccess(context: Context, path: String): Boolean {
val uri = pathToUri(path)
return context.contentResolver.persistedUriPermissions.any {
it.uri == uri && it.isReadPermission && it.isWritePermission
}
}
fun hasDirAccess(context: Context, uri: Uri): Boolean {
return context.contentResolver.persistedUriPermissions.any {
it.uri == uri && it.isReadPermission && it.isWritePermission
}
}
fun hasDirAccess(context: Context): Boolean {
val path = PrefManager.getVal<String>(PrefName.DownloadsDir)
return hasDirAccess(context, path)
}
fun AppCompatActivity.accessAlertDialog(launcher: LauncherWrapper,
force: Boolean = false,
complete: (Boolean) -> Unit
) {
if ((PrefManager.getVal<String>(PrefName.DownloadsDir).isNotEmpty() || hasDirAccess(this)) && !force) {
complete(true)
return
}
val builder = AlertDialog.Builder(this, R.style.MyPopup)
builder.setTitle(getString(R.string.dir_access))
builder.setMessage(getString(R.string.dir_access_msg))
builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
launcher.registerForCallback(complete)
launcher.launch()
dialog.dismiss()
}
builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
complete(false)
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
}
private fun pathToUri(path: String): Uri {
return Uri.parse(path)
}
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
}
}
class LauncherWrapper(
activity: AppCompatActivity,
contract: ActivityResultContracts.OpenDocumentTree)
{
private var launcher: ActivityResultLauncher<Uri?>
var complete: (Boolean) -> Unit = {}
init{
launcher = activity.registerForActivityResult(contract) { uri ->
if (uri != null) {
activity.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
if (StoragePermissions.hasDirAccess(activity, uri)) {
PrefManager.setVal(PrefName.DownloadsDir, uri.toString())
complete(true)
} else {
toast(activity.getString(R.string.dir_error))
complete(false)
}
} else {
toast(activity.getString(R.string.dir_error))
complete(false)
}
}
}
fun registerForCallback(callback: (Boolean) -> Unit) {
complete = callback
}
fun launch() {
launcher.launch(null)
}
}

View File

@@ -0,0 +1,268 @@
package ani.dantotsu.widgets.statistics
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R
import ani.dantotsu.databinding.StatisticsWidgetConfigureBinding
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.widgets.upcoming.UpcomingWidget
import com.google.android.material.button.MaterialButton
import eltos.simpledialogfragment.SimpleDialog
import eltos.simpledialogfragment.color.SimpleColorDialog
/**
* The configuration screen for the [ProfileStatsWidget] AppWidget.
*/
class ProfileStatsConfigure : AppCompatActivity(),
SimpleDialog.OnDialogResultListener {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private var isMonetEnabled = false
private var onClickListener = View.OnClickListener {
val context = this@ProfileStatsConfigure
// It is the responsibility of the configuration activity to update the app widget
val appWidgetManager = AppWidgetManager.getInstance(context)
//updateAppWidget(context, appWidgetManager, appWidgetId)
ProfileStatsWidget.updateAppWidget(
context,
appWidgetManager,
appWidgetId
)
// Make sure we pass back the original appWidgetId
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, resultValue)
finish()
}
private lateinit var binding: StatisticsWidgetConfigureBinding
public override fun onCreate(icicle: Bundle?) {
ThemeManager(this).applyTheme()
super.onCreate(icicle)
// Set the result to CANCELED. This will cause the widget host to cancel
// out of the widget placement if the user presses the back button.
setResult(RESULT_CANCELED)
binding = StatisticsWidgetConfigureBinding.inflate(layoutInflater)
setContentView(binding.root)
val prefs = getSharedPreferences(ProfileStatsWidget.PREFS_NAME, Context.MODE_PRIVATE)
val topBackground = prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
(binding.topBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(topBackground)
binding.topBackgroundButton.setOnClickListener {
val tag = ProfileStatsWidget.PREF_BACKGROUND_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(topBackground)
.colors(
this@ProfileStatsConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@ProfileStatsConfigure, tag)
}
val bottomBackground = prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
(binding.bottomBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(bottomBackground)
binding.bottomBackgroundButton.setOnClickListener {
val tag = ProfileStatsWidget.PREF_BACKGROUND_FADE
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(bottomBackground)
.colors(
this@ProfileStatsConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@ProfileStatsConfigure, tag)
}
val titleColor = prefs.getInt(ProfileStatsWidget.PREF_TITLE_TEXT_COLOR, Color.WHITE)
(binding.titleColorButton as MaterialButton).iconTint = ColorStateList.valueOf(titleColor)
binding.titleColorButton.setOnClickListener {
val tag = ProfileStatsWidget.PREF_TITLE_TEXT_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(titleColor)
.colors(
this@ProfileStatsConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@ProfileStatsConfigure, tag)
}
val statsColor = prefs.getInt(ProfileStatsWidget.PREF_STATS_TEXT_COLOR, Color.WHITE)
(binding.statsColorButton as MaterialButton).iconTint = ColorStateList.valueOf(statsColor)
binding.statsColorButton.setOnClickListener {
val tag = ProfileStatsWidget.PREF_STATS_TEXT_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(statsColor)
.colors(
this@ProfileStatsConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@ProfileStatsConfigure, tag)
}
binding.useAppTheme.setOnCheckedChangeListener { _, isChecked ->
isMonetEnabled = isChecked
if (isChecked) {
binding.topBackgroundButton.visibility = View.GONE
binding.bottomBackgroundButton.visibility = View.GONE
binding.titleColorButton.visibility = View.GONE
binding.statsColorButton.visibility = View.GONE
themeColors()
} else {
binding.topBackgroundButton.visibility = View.VISIBLE
binding.bottomBackgroundButton.visibility = View.VISIBLE
binding.titleColorButton.visibility = View.VISIBLE
binding.statsColorButton.visibility = View.VISIBLE
}
}
binding.addButton.setOnClickListener(onClickListener)
// Find the widget id from the intent.
val intent = intent
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
)
}
// If this activity was started with an intent without an app widget ID, finish with an error.
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}
}
private fun themeColors() {
val typedValueSurface = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValueSurface,
true
)
val backgroundColor = typedValueSurface.data
val typedValuePrimary = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorPrimary,
typedValuePrimary,
true
)
val textColor = typedValuePrimary.data
val typedValueOutline = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorOutline,
typedValueOutline,
true
)
val subTextColor = typedValueOutline.data
getSharedPreferences(ProfileStatsWidget.PREFS_NAME, Context.MODE_PRIVATE).edit().apply {
putInt(ProfileStatsWidget.PREF_BACKGROUND_COLOR, backgroundColor)
putInt(ProfileStatsWidget.PREF_BACKGROUND_FADE, backgroundColor)
putInt(ProfileStatsWidget.PREF_TITLE_TEXT_COLOR, textColor)
putInt(ProfileStatsWidget.PREF_STATS_TEXT_COLOR, subTextColor)
apply()
}
}
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
if (which == SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE) {
if (!isMonetEnabled) {
when (dialogTag) {
ProfileStatsWidget.PREF_BACKGROUND_COLOR -> {
getSharedPreferences(
ProfileStatsWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
ProfileStatsWidget.PREF_BACKGROUND_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.topBackgroundButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
ProfileStatsWidget.PREF_BACKGROUND_FADE -> {
getSharedPreferences(
ProfileStatsWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
ProfileStatsWidget.PREF_BACKGROUND_FADE,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.bottomBackgroundButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
ProfileStatsWidget.PREF_TITLE_TEXT_COLOR -> {
getSharedPreferences(
ProfileStatsWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
ProfileStatsWidget.PREF_TITLE_TEXT_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.titleColorButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
ProfileStatsWidget.PREF_STATS_TEXT_COLOR -> {
getSharedPreferences(
ProfileStatsWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
ProfileStatsWidget.PREF_STATS_TEXT_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.statsColorButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
}
}
}
return true
}
}

View File

@@ -0,0 +1,257 @@
package ani.dantotsu.widgets.statistics
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.widget.RemoteViews
import androidx.core.content.res.ResourcesCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.BitmapUtil
import ani.dantotsu.widgets.WidgetSizeProvider
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
/**
* Implementation of App Widget functionality.
*/
class ProfileStatsWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { appWidgetId ->
updateAppWidget(context, appWidgetManager, appWidgetId)
}
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
}
companion object {
private fun downloadImageAsBitmap(imageUrl: String): Bitmap? {
var bitmap: Bitmap? = null
runBlocking(Dispatchers.IO) {
var inputStream: InputStream? = null
var urlConnection: HttpURLConnection? = null
try {
val url = URL(imageUrl)
urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "GET"
urlConnection.connect()
if (urlConnection.responseCode == HttpURLConnection.HTTP_OK) {
inputStream = urlConnection.inputStream
bitmap = BitmapFactory.decodeStream(inputStream)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
inputStream?.close()
urlConnection?.disconnect()
}
}
return bitmap?.let { BitmapUtil.roundCorners(it) }
}
@OptIn(DelicateCoroutinesApi::class)
fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val backgroundColor =
prefs.getInt(PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
val backgroundFade = prefs.getInt(PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
val titleTextColor = prefs.getInt(PREF_TITLE_TEXT_COLOR, Color.WHITE)
val statsTextColor = prefs.getInt(PREF_STATS_TEXT_COLOR, Color.WHITE)
val gradientDrawable = ResourcesCompat.getDrawable(
context.resources,
R.drawable.linear_gradient_black,
null
) as GradientDrawable
gradientDrawable.colors = intArrayOf(backgroundColor, backgroundFade)
val widgetSizeProvider = WidgetSizeProvider(context)
var (width, height) = widgetSizeProvider.getWidgetsSize(appWidgetId)
if (width > 0 && height > 0) {
gradientDrawable.cornerRadius = 64f
} else {
width = 300
height = 300
}
launchIO {
val userPref = PrefManager.getVal(PrefName.AnilistUserId, "")
if (userPref.isNotEmpty()) {
val respond = Anilist.query.getUserProfile(userPref.toInt())
respond?.data?.user?.let { user ->
withContext(Dispatchers.Main) {
val views = RemoteViews(context.packageName, R.layout.statistics_widget).apply {
setImageViewBitmap(
R.id.backgroundView,
BitmapUtil.convertDrawableToBitmap(
gradientDrawable,
width,
height
)
)
setOnClickPendingIntent(
R.id.userAvatar,
PendingIntent.getActivity(
context,
1,
Intent(context, ProfileStatsConfigure::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
setTextColor(R.id.userLabel, titleTextColor)
setTextColor(R.id.topLeftItem, titleTextColor)
setTextColor(R.id.topLeftLabel, statsTextColor)
setTextColor(R.id.topRightItem, titleTextColor)
setTextColor(R.id.topRightLabel, statsTextColor)
setTextColor(R.id.bottomLeftItem, titleTextColor)
setTextColor(R.id.bottomLeftLabel, statsTextColor)
setTextColor(R.id.bottomRightItem, titleTextColor)
setTextColor(R.id.bottomRightLabel, statsTextColor)
setImageViewBitmap(
R.id.userAvatar,
user.avatar?.medium?.let { it1 -> downloadImageAsBitmap(it1) }
)
setTextViewText(
R.id.userLabel,
context.getString(R.string.user_stats, user.name)
)
setTextViewText(
R.id.topLeftItem,
user.statistics.anime.count.toString()
)
setTextViewText(
R.id.topLeftLabel,
context.getString(R.string.anime_watched)
)
setTextViewText(
R.id.topRightItem,
user.statistics.anime.episodesWatched.toString()
)
setTextViewText(
R.id.topRightLabel,
context.getString(R.string.episodes_watched_n)
)
setTextViewText(
R.id.bottomLeftItem,
user.statistics.manga.count.toString()
)
setTextViewText(
R.id.bottomLeftLabel,
context.getString(R.string.manga_read)
)
setTextViewText(
R.id.bottomRightItem,
user.statistics.manga.chaptersRead.toString()
)
setTextViewText(
R.id.bottomRightLabel,
context.getString(R.string.chapters_read_n)
)
val intent = Intent(context, ProfileActivity::class.java)
.putExtra("userId", userPref.toInt())
val pendingIntent = PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE
)
setOnClickPendingIntent(R.id.widgetContainer, pendingIntent)
}
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
} ?: showLoginCascade(context, appWidgetManager, appWidgetId)
} else showLoginCascade(context, appWidgetManager, appWidgetId)
}
}
private suspend fun showLoginCascade(
context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int
) {
withContext(Dispatchers.Main) {
val views = RemoteViews(context.packageName, R.layout.statistics_widget)
views.setTextViewText(R.id.topLeftItem, "")
views.setTextViewText(
R.id.topLeftLabel,
context.getString(R.string.please)
)
views.setTextViewText(R.id.topRightItem, "")
views.setTextViewText(
R.id.topRightLabel,
context.getString(R.string.log_in)
)
views.setTextViewText(
R.id.bottomLeftItem,
context.getString(R.string.or_join)
)
views.setTextViewText(R.id.bottomLeftLabel, "")
views.setTextViewText(
R.id.bottomRightItem,
context.getString(R.string.anilist)
)
views.setTextViewText(R.id.bottomRightLabel, "")
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widgetContainer, pendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
const val PREFS_NAME = "ani.dantotsu.widgets.ResumableWidget"
const val PREF_BACKGROUND_COLOR = "background_color"
const val PREF_BACKGROUND_FADE = "background_fade"
const val PREF_TITLE_TEXT_COLOR = "title_text_color"
const val PREF_STATS_TEXT_COLOR = "stats_text_color"
}
}

View File

@@ -10,7 +10,6 @@ import android.graphics.drawable.GradientDrawable
import android.net.Uri
import android.os.Bundle
import android.widget.RemoteViews
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
@@ -19,7 +18,7 @@ import ani.dantotsu.widgets.WidgetSizeProvider
/**
* Implementation of App Widget functionality.
* App Widget Configuration implemented in [UpcomingWidgetConfigureActivity]
* App Widget Configuration implemented in [UpcomingWidgetConfigure]
*/
class UpcomingWidget : AppWidgetProvider() {
override fun onUpdate(
@@ -69,8 +68,8 @@ class UpcomingWidget : AppWidgetProvider() {
): RemoteViews {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val backgroundColor =
prefs.getInt(PREF_BACKGROUND_COLOR, ContextCompat.getColor(context, R.color.theme))
val backgroundFade = prefs.getInt(PREF_BACKGROUND_FADE, Color.GRAY)
prefs.getInt(PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
val backgroundFade = prefs.getInt(PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
val titleTextColor = prefs.getInt(PREF_TITLE_TEXT_COLOR, Color.WHITE)
val countdownTextColor = prefs.getInt(PREF_COUNTDOWN_TEXT_COLOR, Color.WHITE)
@@ -80,14 +79,14 @@ class UpcomingWidget : AppWidgetProvider() {
}
val gradientDrawable = ResourcesCompat.getDrawable(
context.resources,
R.drawable.gradient_background,
R.drawable.linear_gradient_black,
null
) as GradientDrawable
gradientDrawable.colors = intArrayOf(backgroundColor, backgroundFade)
val widgetSizeProvider = WidgetSizeProvider(context)
var (width, height) = widgetSizeProvider.getWidgetsSize(appWidgetId)
if (width > 0 && height > 0) {
gradientDrawable.cornerRadius = 50f
gradientDrawable.cornerRadius = 64f
} else {
width = 300
height = 300
@@ -118,7 +117,7 @@ class UpcomingWidget : AppWidgetProvider() {
PendingIntent.getActivity(
context,
1,
Intent(context, UpcomingWidgetConfigureActivity::class.java).apply {
Intent(context, UpcomingWidgetConfigure::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

View File

@@ -0,0 +1,240 @@
package ani.dantotsu.widgets.upcoming
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R
import ani.dantotsu.databinding.UpcomingWidgetConfigureBinding
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.button.MaterialButton
import eltos.simpledialogfragment.SimpleDialog
import eltos.simpledialogfragment.color.SimpleColorDialog
/**
* The configuration screen for the [UpcomingWidget] AppWidget.
*/
class UpcomingWidgetConfigure : AppCompatActivity(),
SimpleDialog.OnDialogResultListener {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private var isMonetEnabled = false
private var onClickListener = View.OnClickListener {
val context = this@UpcomingWidgetConfigure
val appWidgetManager = AppWidgetManager.getInstance(context)
updateAppWidget(
context,
appWidgetManager,
appWidgetId,
)
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, resultValue)
finish()
}
private lateinit var binding: UpcomingWidgetConfigureBinding
public override fun onCreate(icicle: Bundle?) {
ThemeManager(this).applyTheme()
super.onCreate(icicle)
setResult(RESULT_CANCELED)
binding = UpcomingWidgetConfigureBinding.inflate(layoutInflater)
setContentView(binding.root)
val prefs = getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE)
val topBackground = prefs.getInt(UpcomingWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000"))
(binding.topBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(topBackground)
binding.topBackgroundButton.setOnClickListener {
val tag = UpcomingWidget.PREF_BACKGROUND_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(topBackground)
.colors(
this@UpcomingWidgetConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigure, tag)
}
val bottomBackground = prefs.getInt(UpcomingWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000"))
(binding.bottomBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(bottomBackground)
binding.bottomBackgroundButton.setOnClickListener {
val tag = UpcomingWidget.PREF_BACKGROUND_FADE
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(bottomBackground)
.colors(
this@UpcomingWidgetConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigure, tag)
}
val titleTextColor = prefs.getInt(UpcomingWidget.PREF_TITLE_TEXT_COLOR, Color.WHITE)
(binding.titleColorButton as MaterialButton).iconTint = ColorStateList.valueOf(titleTextColor)
binding.titleColorButton.setOnClickListener {
val tag = UpcomingWidget.PREF_TITLE_TEXT_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(titleTextColor)
.colors(
this@UpcomingWidgetConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigure, tag)
}
val countdownTextColor = prefs.getInt(UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR, Color.WHITE)
(binding.countdownColorButton as MaterialButton).iconTint = ColorStateList.valueOf(countdownTextColor)
binding.countdownColorButton.setOnClickListener {
val tag = UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(countdownTextColor)
.colors(
this@UpcomingWidgetConfigure,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigure, tag)
}
binding.useAppTheme.setOnCheckedChangeListener { _, isChecked ->
isMonetEnabled = isChecked
if (isChecked) {
binding.topBackgroundButton.visibility = View.GONE
binding.bottomBackgroundButton.visibility = View.GONE
binding.titleColorButton.visibility = View.GONE
binding.countdownColorButton.visibility = View.GONE
themeColors()
} else {
binding.topBackgroundButton.visibility = View.VISIBLE
binding.bottomBackgroundButton.visibility = View.VISIBLE
binding.titleColorButton.visibility = View.VISIBLE
binding.countdownColorButton.visibility = View.VISIBLE
}
}
binding.addButton.setOnClickListener(onClickListener)
val intent = intent
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
)
}
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}
}
private fun themeColors() {
val typedValueSurface = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValueSurface, true)
val backgroundColor = typedValueSurface.data
val typedValuePrimary = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValuePrimary, true)
val textColor = typedValuePrimary.data
val typedValueOutline = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValueOutline, true)
val subTextColor = typedValueOutline.data
getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE).edit().apply {
putInt(UpcomingWidget.PREF_BACKGROUND_COLOR, backgroundColor)
putInt(UpcomingWidget.PREF_BACKGROUND_FADE, backgroundColor)
putInt(UpcomingWidget.PREF_TITLE_TEXT_COLOR, textColor)
putInt(UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR, subTextColor)
apply()
}
}
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
if (which == SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE) {
if (!isMonetEnabled) {
when (dialogTag) {
UpcomingWidget.PREF_BACKGROUND_COLOR -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_BACKGROUND_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.topBackgroundButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
UpcomingWidget.PREF_BACKGROUND_FADE -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_BACKGROUND_FADE,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.bottomBackgroundButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
UpcomingWidget.PREF_TITLE_TEXT_COLOR -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_TITLE_TEXT_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.titleColorButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
(binding.countdownColorButton as MaterialButton).iconTint =
ColorStateList.valueOf(extras.getInt(SimpleColorDialog.COLOR))
}
}
}
}
return true
}
}

View File

@@ -1,195 +0,0 @@
package ani.dantotsu.widgets.upcoming
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.databinding.UpcomingWidgetConfigureBinding
import ani.dantotsu.themes.ThemeManager
import eltos.simpledialogfragment.SimpleDialog
import eltos.simpledialogfragment.color.SimpleColorDialog
/**
* The configuration screen for the [UpcomingWidget] AppWidget.
*/
class UpcomingWidgetConfigureActivity : AppCompatActivity(),
SimpleDialog.OnDialogResultListener {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private var onClickListener = View.OnClickListener {
val context = this@UpcomingWidgetConfigureActivity
val appWidgetManager = AppWidgetManager.getInstance(context)
updateAppWidget(
context,
appWidgetManager,
appWidgetId,
)
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, resultValue)
finish()
}
private lateinit var binding: UpcomingWidgetConfigureBinding
public override fun onCreate(icicle: Bundle?) {
ThemeManager(this).applyTheme()
super.onCreate(icicle)
setResult(RESULT_CANCELED)
binding = UpcomingWidgetConfigureBinding.inflate(layoutInflater)
setContentView(binding.root)
val prefs = getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE)
binding.topBackgroundButton.setOnClickListener {
val tag = UpcomingWidget.PREF_BACKGROUND_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(
prefs.getInt(
UpcomingWidget.PREF_BACKGROUND_COLOR,
ContextCompat.getColor(this, R.color.theme)
)
)
.colors(
this@UpcomingWidgetConfigureActivity,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigureActivity, tag)
}
binding.bottomBackgroundButton.setOnClickListener {
val tag = UpcomingWidget.PREF_BACKGROUND_FADE
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(prefs.getInt(UpcomingWidget.PREF_BACKGROUND_FADE, Color.GRAY))
.colors(
this@UpcomingWidgetConfigureActivity,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.setupColorWheelAlpha(true)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigureActivity, tag)
}
binding.titleColorButton.setOnClickListener {
val tag = UpcomingWidget.PREF_TITLE_TEXT_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(prefs.getInt(UpcomingWidget.PREF_TITLE_TEXT_COLOR, Color.WHITE))
.colors(
this@UpcomingWidgetConfigureActivity,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigureActivity, tag)
}
binding.countdownColorButton.setOnClickListener {
val tag = UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR
SimpleColorDialog().title(R.string.custom_theme)
.colorPreset(
prefs.getInt(
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR,
Color.WHITE
)
)
.colors(
this@UpcomingWidgetConfigureActivity,
SimpleColorDialog.MATERIAL_COLOR_PALLET
)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this@UpcomingWidgetConfigureActivity, tag)
}
binding.addButton.setOnClickListener(onClickListener)
val intent = intent
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
)
}
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}
}
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
if (which == SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE) {
when (dialogTag) {
UpcomingWidget.PREF_BACKGROUND_COLOR -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_BACKGROUND_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
}
UpcomingWidget.PREF_BACKGROUND_FADE -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_BACKGROUND_FADE,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
}
UpcomingWidget.PREF_TITLE_TEXT_COLOR -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_TITLE_TEXT_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
}
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR -> {
getSharedPreferences(
UpcomingWidget.PREFS_NAME,
Context.MODE_PRIVATE
).edit()
.putInt(
UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR,
extras.getInt(SimpleColorDialog.COLOR)
)
.apply()
}
}
}
return true
}
}

View File

@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import okhttp3.Headers
@@ -69,7 +70,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", network.defaultUserAgentProvider())
add("User-Agent", defaultUserAgentProvider())
}
/**

View File

@@ -15,7 +15,7 @@ class ExtensionUpdateNotifier(private val context: Context) {
Notifications.CHANNEL_EXTENSIONS_UPDATE,
) {
setContentTitle(
"Extension updates available"
context.getString(R.string.extension_updates_available)
)
val extNames = names.joinToString(", ")
setContentText(extNames)

View File

@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.extension.anime.api
import android.content.Context
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
@@ -32,42 +34,55 @@ internal class AnimeExtensionGithubApi {
preferenceStore.getLong("last_ext_check", 0)
}
private var requiresFallbackSource = false
suspend fun findExtensions(): List<AnimeExtension.Available> {
return withIOContext {
val githubResponse = if (requiresFallbackSource) {
null
} else {
val extensions: ArrayList<AnimeExtension.Available> = arrayListOf()
val repos =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
if (repos.isEmpty()) {
repos.add("https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo")
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos.toSet())
}
repos.forEach {
try {
networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
val githubResponse = try {
networkService.client
.newCall(GET("${it}/index.min.json"))
.awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get repo: $it")
Logger.log(e)
null
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET(fallbackRepoUrl(it) + "/index.min.json"))
.awaitSuccess()
}
val repoExtensions = with(json) {
response
.parseAs<List<AnimeExtensionJsonObject>>()
.toExtensions(it)
}
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (repoExtensions.size < 10) {
throw Exception()
}
extensions.addAll(repoExtensions)
} catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub")
requiresFallbackSource = true
null
Logger.log(e)
}
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
}
val extensions = with(json) {
response
.parseAs<List<AnimeExtensionJsonObject>>()
.toExtensions()
}
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (extensions.size < 10) {
throw Exception()
}
extensions
}
}
@@ -111,7 +126,7 @@ internal class AnimeExtensionGithubApi {
return extensionsWithUpdate
}
private fun List<AnimeExtensionJsonObject>.toExtensions(): List<AnimeExtension.Available> {
private fun List<AnimeExtensionJsonObject>.toExtensions(repository: String): List<AnimeExtension.Available> {
return this
.filter {
val libVersion = it.extractLibVersion()
@@ -130,7 +145,8 @@ internal class AnimeExtensionGithubApi {
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toAnimeExtensionSources().orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
repository = repository,
iconUrl = "${repository}/icon/${it.pkg}.png",
)
}
}
@@ -147,15 +163,27 @@ internal class AnimeExtensionGithubApi {
}
fun getApkUrl(extension: AnimeExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}"
return "${extension.repository}/apk/${extension.apkName}"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
private fun fallbackRepoUrl(repoUrl: String): String? {
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
val strippedRepoUrl =
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
val repoUrlParts = strippedRepoUrl.split("/")
if (repoUrlParts.size < 3) {
return null
}
val repoOwner = repoUrlParts[1]
val repoName = repoUrlParts[2]
fallbackRepoUrl += "$repoOwner/$repoName"
val repoBranch = if (repoUrlParts.size > 3) {
repoUrlParts[3]
} else {
"main"
}
fallbackRepoUrl += "@$repoBranch"
return fallbackRepoUrl
}
}
@@ -163,11 +191,6 @@ private fun AnimeExtensionJsonObject.extractLibVersion(): Double {
return version.substringBeforeLast('.').toDouble()
}
private const val REPO_URL_PREFIX =
"https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo/"
private const val FALLBACK_REPO_URL_PREFIX =
"https://gcore.jsdelivr.net/gh/aniyomiorg/aniyomi-extensions@repo/"
@Serializable
private data class AnimeExtensionJsonObject(
val name: String,

View File

@@ -47,6 +47,7 @@ sealed class AnimeExtension {
val sources: List<AvailableAnimeSources>,
val apkName: String,
val iconUrl: String,
val repository: String
) : AnimeExtension()
data class Untrusted(

View File

@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.extension.manga.api
import android.content.Context
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
@@ -32,42 +34,55 @@ internal class MangaExtensionGithubApi {
preferenceStore.getLong("last_ext_check", 0)
}
private var requiresFallbackSource = false
suspend fun findExtensions(): List<MangaExtension.Available> {
return withIOContext {
val githubResponse = if (requiresFallbackSource) {
null
} else {
val extensions: ArrayList<MangaExtension.Available> = arrayListOf()
val repos =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
if (repos.isEmpty()) {
repos.add("https://raw.githubusercontent.com/keiyoushi/extensions/main")
PrefManager.setVal(PrefName.MangaExtensionRepos, repos.toSet())
}
repos.forEach {
try {
networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
val githubResponse = try {
networkService.client
.newCall(GET("${it}/index.min.json"))
.awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get repo: $it")
Logger.log(e)
null
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET(fallbackRepoUrl(it) + "/index.min.json"))
.awaitSuccess()
}
val repoExtensions = with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(it)
}
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (repoExtensions.size < 10) {
throw Exception()
}
extensions.addAll(repoExtensions)
} catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub")
requiresFallbackSource = true
null
Logger.log(e)
}
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
}
val extensions = with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions()
}
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (extensions.size < 100) {
throw Exception()
}
extensions
}
}
@@ -110,7 +125,7 @@ internal class MangaExtensionGithubApi {
return extensionsWithUpdate
}
private fun List<ExtensionJsonObject>.toExtensions(): List<MangaExtension.Available> {
private fun List<ExtensionJsonObject>.toExtensions(repository: String): List<MangaExtension.Available> {
return this
.filter {
val libVersion = it.extractLibVersion()
@@ -129,7 +144,8 @@ internal class MangaExtensionGithubApi {
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toExtensionSources().orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
repository = repository,
iconUrl = "${repository}/icon/${it.pkg}.png",
)
}
}
@@ -146,25 +162,33 @@ internal class MangaExtensionGithubApi {
}
fun getApkUrl(extension: MangaExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
}
return "${extension.repository}/apk/${extension.apkName}"
}
private fun ExtensionJsonObject.extractLibVersion(): Double {
return version.substringBeforeLast('.').toDouble()
}
}
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/keiyoushi/extensions/main/"
private const val FALLBACK_REPO_URL_PREFIX =
"https://gcore.jsdelivr.net/gh/keiyoushi/extensions@main/"
private fun fallbackRepoUrl(repoUrl: String): String? {
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
val strippedRepoUrl =
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
val repoUrlParts = strippedRepoUrl.split("/")
if (repoUrlParts.size < 3) {
return null
}
val repoOwner = repoUrlParts[1]
val repoName = repoUrlParts[2]
fallbackRepoUrl += "$repoOwner/$repoName"
val repoBranch = if (repoUrlParts.size > 3) {
repoUrlParts[3]
} else {
"main"
}
fallbackRepoUrl += "@$repoBranch"
return fallbackRepoUrl
}
}
@Serializable
private data class ExtensionJsonObject(

View File

@@ -47,6 +47,7 @@ sealed class MangaExtension {
val sources: List<AvailableMangaSources>,
val apkName: String,
val iconUrl: String,
val repository: String
) : MangaExtension()
data class Untrusted(

View File

@@ -89,5 +89,7 @@ class NetworkHelper(
responseParser = Mapper
)
fun defaultUserAgentProvider() = PrefManager.getVal<String>(PrefName.DefaultUserAgent)
companion object {
fun defaultUserAgentProvider() = PrefManager.getVal<String>(PrefName.DefaultUserAgent)
}
}

View File

@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
@@ -69,7 +70,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", network.defaultUserAgentProvider())
add("User-Agent", defaultUserAgentProvider())
}
/**

Some files were not shown because too many files have changed in this diff Show More