Merge pull request #152 from rebelonion/dev

Dev
This commit is contained in:
rebel onion 2024-01-22 08:41:40 -06:00 committed by GitHub
commit 4508cada0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
230 changed files with 9161 additions and 3762 deletions

View file

@ -4,10 +4,14 @@ on:
push:
branches:
- dev
paths-ignore:
- '**/README.md'
jobs:
build:
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Checkout repo
@ -28,11 +32,17 @@ jobs:
java-version: 17
cache: gradle
- name: Decode Keystore File
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew assembleDebug
run: ./gradlew assembleDebug -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
uses: actions/upload-artifact@v3.0.0
@ -44,7 +54,7 @@ jobs:
shell: bash
run: |
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" )
curl -F "payload_json={\"content\":\" everyone **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
curl -F "payload_json={\"content\":\" Debug-Build **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
- name: Delete Old Pre-Releases
id: delete-pre-releases

View file

@ -21,7 +21,7 @@ android {
minSdk 23
targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "2.0.0-beta00-i"
versionName "2.0.0-beta01-iv1"
signingConfig signingConfigs.debug
}
@ -29,12 +29,12 @@ android {
debug {
applicationIdSuffix ".beta"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
debuggable true
debuggable System.getenv("CI") == null
}
release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
}
}
buildFeatures {
@ -54,19 +54,19 @@ android {
dependencies {
// Core
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.6.0'
implementation 'androidx.browser:browser:1.7.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.work:work-runtime-ktx:2.8.1"
implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.9.0'
// Glide
@ -90,9 +90,12 @@ dependencies {
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$exo_version"
//media3 casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.6.0"
// UI
implementation 'com.google.android.material:material:1.10.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'com.flaviofaria:kenburnsview:1.0.7'
@ -100,7 +103,7 @@ dependencies {
implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation "com.github.skydoves:colorpickerview:2.3.0"
implementation 'com.github.eltos:simpledialogfragments:v3.7'
// string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
@ -115,13 +118,14 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.3.0'
implementation 'ch.acra:acra-http:5.9.7'
implementation 'com.squareup.okio:okio:3.7.0'
implementation 'ch.acra:acra-http:5.11.3'
implementation 'org.jsoup:jsoup:1.15.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'app.cash.quickjs:quickjs-android:0.9.2'
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Dantotsu ß</string>
<string name="app_name">Dantotsu β</string>
</resources>

View file

@ -10,6 +10,8 @@
android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@ -17,25 +19,22 @@
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- For background jobs -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
<uses-permission
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" />
<queries>
@ -48,6 +47,7 @@
<application
android:name=".App"
android:allowBackup="true"
android:banner="@mipmap/ic_banner_foreground"
android:icon="${icon_placeholder}"
android:label="@string/app_name"
android:largeHeap="true"
@ -56,14 +56,28 @@
android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true"
tools:ignore="AllowBackup"
android:banner="@drawable/ic_banner_foreground">
tools:ignore="AllowBackup">
<receiver
android:name=".widgets.CurrentlyAiringWidget"
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/currently_airing_widget_info" />
</receiver>
<receiver android:name=".subcriptions.NotificationClickReceiver" />
<activity
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity"
android:name=".media.novel.novelreader.NovelReaderActivity"
android:configChanges="orientation|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" />
@ -71,13 +85,11 @@
<data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" />
<data android:mimeType="application/vnd.comicbook+zip" />
<data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.kf8" />
<data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.cbz" />
<data android:scheme="content" />
<data android:scheme="file" />
</intent-filter>
@ -103,9 +115,9 @@
<activity
android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" />
<activity android:name="ani.dantotsu.media.user.ListActivity" />
<activity android:name=".media.user.ListActivity" />
<activity
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity"
android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true"
android:exported="true"
android:label="@string/manga"
@ -118,7 +130,7 @@
<activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" />
<activity
android:name="ani.dantotsu.media.anime.ExoplayerView"
android:name=".media.anime.ExoplayerView"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:excludeFromRecents="true"
android:exported="true"
@ -127,7 +139,7 @@
android:supportsPictureInPicture="true"
tools:targetApi="n" />
<activity
android:name="ani.dantotsu.connections.anilist.Login"
android:name=".connections.anilist.Login"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
@ -144,7 +156,7 @@
</intent-filter>
</activity>
<activity
android:name="ani.dantotsu.connections.mal.Login"
android:name=".connections.mal.Login"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
@ -160,8 +172,8 @@
android:scheme="dantotsu" />
</intent-filter>
</activity>
<activity android:name="ani.dantotsu.connections.discord.Login"
<activity
android:name=".connections.discord.Login"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
@ -178,9 +190,26 @@
<data android:host="discord.dantotsu.com" />
</intent-filter>
</activity>
<activity
android:name="ani.dantotsu.connections.anilist.UrlMedia"
android:name=".others.webview.CookieCatcher"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask">
<intent-filter android:label="Discord Login for Dantotsu">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="dantotsu" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="discord.dantotsu.com" />
</intent-filter>
</activity>
<activity
android:name=".connections.anilist.UrlMedia"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
@ -216,23 +245,23 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".download.DownloadContainerActivity" />
<activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver
android:name=".subcriptions.AlarmReceiver"
@ -258,32 +287,49 @@
android:resource="@xml/provider_paths" />
</provider>
<service android:name=".download.video.MyDownloadService"
android:exported="false">
<service
android:name=".widgets.CurrentlyAiringRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="true" />
<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.manga.util.MangaExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<service android:name=".download.manga.MangaDownloaderService"
<service
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".download.novel.NovelDownloaderService"
<service
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".connections.discord.DiscordService"
<service
android:name=".download.manga.MangaDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".download.novel.NovelDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".download.anime.AnimeDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".connections.discord.DiscordService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
</application>
</manifest>

View file

@ -13,7 +13,9 @@ import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity
import com.google.android.material.color.DynamicColors
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.data.notification.Notifications
@ -58,6 +60,24 @@ class App : MultiDexApplication() {
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getBoolean("shared_user_id", true).let {
if (!it) return@let
val dUsername = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getString("discord_username", null)
val aUsername = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getString("anilist_username", null)
if (dUsername != null || aUsername != null) {
Firebase.crashlytics.setUserId("$dUsername - $aUsername")
}
}
FirebaseCrashlytics.getInstance().setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
@ -77,13 +97,13 @@ class App : MultiDexApplication() {
animeScope.launch {
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
}
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
}
val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch {
@ -91,7 +111,6 @@ class App : MultiDexApplication() {
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}
}

View file

@ -4,6 +4,8 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DatePickerDialog
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@ -28,6 +30,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.*
import android.widget.*
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp
@ -44,6 +47,7 @@ import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.NotificationClickReceiver
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
@ -53,6 +57,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.*
import nl.joery.animatedbottombar.AnimatedBottomBar
import java.io.*
@ -124,6 +130,13 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
}
} catch (e: Exception) {
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
//try to delete the file
try {
a?.deleteFile(fileName)
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().log("Failed to delete file $fileName")
FirebaseCrashlytics.getInstance().recordException(e)
}
e.printStackTrace()
}
return null
@ -601,7 +614,7 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
"$APPLICATION_ID.provider",
saveImage(
bitmap,
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title
) ?: return
)
@ -624,13 +637,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png")
return tryWith {
return try {
val fOut: OutputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
fOut.close()
scanFile(imageFile.absolutePath, currContext()!!)
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
imageFile
} catch (e: Exception) {
snackString("Failed to save image: ${e.localizedMessage}")
null
}
}
@ -673,7 +689,7 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
@SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
@ -783,6 +799,7 @@ fun toast(string: String?) {
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
try { //I have no idea why this sometimes crashes for some people...
if (s != null) {
(activity ?: currActivity())?.apply {
runOnUiThread {
@ -813,6 +830,10 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
}
logger(s)
}
} catch (e: Exception) {
logger(e.stackTraceToString())
FirebaseCrashlytics.getInstance().recordException(e)
}
}
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
@ -930,6 +951,33 @@ fun checkCountry(context: Context): Boolean {
}
}
const val INCOGNITO_CHANNEL_ID = 26
@SuppressLint("LaunchActivityFromNotification")
fun incognitoNotification(context: Context) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("incognito", false)
if (incognito) {
val intent = Intent(context, NotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
.setSmallIcon(R.drawable.ic_incognito_24)
.setContentTitle("Incognito Mode")
.setContentText("Disable Incognito Mode")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setOngoing(true)
notificationManager.notify(INCOGNITO_CHANNEL_ID, builder.build())
} else {
notificationManager.cancel(INCOGNITO_CHANNEL_ID)
}
}
suspend fun View.pop() {
currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()

View file

@ -1,6 +1,7 @@
package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
@ -11,12 +12,14 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
@ -26,11 +29,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment
@ -39,12 +45,14 @@ import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.themes.ThemeManager
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -54,17 +62,23 @@ import java.io.Serializable
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
private val scope = lifecycleScope
private var load = false
private var uiSettings = UserInterfaceSettings()
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
super.onCreate(savedInstanceState)
//get FRAGMENT_CLASS_NAME from intent
val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
@ -73,17 +87,59 @@ class MainActivity : AppCompatActivity() {
val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable
}
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("colorOverflow", false)
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
}
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
resources.getDimensionPixelSize(statusBarHeightId)
} catch (e: Exception) {
statusBarHeight
}
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = 11 * offset / 12
binding.incognito.layoutParams = layoutParams
incognitoLiveData = SharedPreferenceBooleanLiveData(
sharedPreferences,
"incognito",
false
)
incognitoLiveData.observe(this) {
if (it) {
val slideDownAnim = ObjectAnimator.ofFloat(
binding.incognito,
View.TRANSLATION_Y,
-(binding.incognito.height.toFloat() + statusBarHeight),
0f
)
slideDownAnim.duration = 200
slideDownAnim.start()
binding.incognito.visibility = View.VISIBLE
} else {
val slideUpAnim = ObjectAnimator.ofFloat(
binding.incognito,
View.TRANSLATION_Y,
0f,
-(binding.incognito.height.toFloat() + statusBarHeight)
)
slideUpAnim.duration = 200
slideUpAnim.start()
//wait for animation to finish
Handler(Looper.getMainLooper()).postDelayed(
{ binding.incognito.visibility = View.GONE },
200
)
}
}
incognitoNotification(this)
var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) {
@ -142,18 +198,33 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab
selectedOption = if (FRAGMENT_CLASS_NAME != null) {
when (FRAGMENT_CLASS_NAME) {
AnimeFragment::class.java.name -> 0
HomeFragment::class.java.name -> 1
MangaFragment::class.java.name -> 2
else -> 1
}
} else {
uiSettings.defaultStartUpTab
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
}
}
val offline = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("offlineMode", false)
if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java))
} else {
if (offline) {
snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java))
} else {
val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) {
model.genres.observe(this) { it ->
if (it != null) {
if (it) {
val navbar = binding.includedNavbar.navbar
@ -162,7 +233,8 @@ class MainActivity : AppCompatActivity() {
binding.mainProgressBar.visibility = View.GONE
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener {
@ -178,7 +250,12 @@ class MainActivity : AppCompatActivity() {
}
})
navbar.selectTabAt(selectedOption)
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
} else {
binding.mainProgressBar.visibility = View.GONE
}
@ -241,7 +318,26 @@ class MainActivity : AppCompatActivity() {
}
}
}
}
//TODO: Remove this
GlobalScope.launch(Dispatchers.IO) {
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
val download = downloadCursor.download
Log.e("Downloader", download.request.uri.toString())
Log.e("Downloader", download.request.id.toString())
Log.e("Downloader", download.request.mimeType.toString())
Log.e("Downloader", download.request.data.size.toString())
Log.e("Downloader", download.bytesDownloaded.toString())
Log.e("Downloader", download.state.toString())
Log.e("Downloader", download.failureReason.toString())
if (download.state == Download.STATE_FAILED) { //simple cleanup
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
}
}
}
}

View file

@ -3,7 +3,10 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application
import android.content.Context
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.parsers.novel.NovelExtensionManager
@ -27,6 +30,7 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
@OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
@ -51,6 +55,8 @@ class AppModule(val app: Application) : InjektModule {
}
}
addSingletonFactory { StandaloneDatabaseProvider(app) }
addSingletonFactory { MangaCache() }
ContextCompat.getMainExecutor(app).execute {

View file

@ -10,7 +10,6 @@ import ani.dantotsu.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
fun updateProgress(media: Media, number: String) {
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)

View file

@ -1,6 +1,7 @@
package ani.dantotsu.connections.anilist
import android.app.Activity
import android.content.Context
import ani.dantotsu.R
import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId
@ -10,6 +11,7 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.currContext
import ani.dantotsu.isOnline
import ani.dantotsu.loadData
import ani.dantotsu.logError
import ani.dantotsu.media.Author
@ -33,6 +35,13 @@ class AnilistQueries {
}.also { println("time : $it") }
val user = response?.data?.user ?: return false
currContext()?.let {
it.getSharedPreferences(it.getString(R.string.preference_file_key), Context.MODE_PRIVATE)
.edit()
.putString("anilist_username", user.name)
.apply()
}
Anilist.userid = user.id
Anilist.username = user.name
Anilist.bg = user.bannerImage
@ -239,9 +248,11 @@ class AnilistQueries {
else snackString(currContext()?.getString(R.string.what_did_you_open))
}
} else {
if (currContext()?.let { isOnline(it) } == true) {
snackString(currContext()?.getString(R.string.error_getting_data))
}
}
}
val mal = async {
if (media.idMAL != null) {
MalScraper.loadMedia(media)
@ -410,8 +421,9 @@ class AnilistQueries {
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
sorted["All"] = all
val sort = sortOrder ?: options?.rowOrder
val listsort = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getString("sort_order", "score")
val sort = listsort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) {
when (sort) {
"score" -> sorted[i]?.sortWith { b, a ->

View file

@ -1,25 +0,0 @@
package ani.dantotsu.download
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager
class DownloadContainerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
setContentView(R.layout.activity_container)
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")
val fragment = Class.forName(fragmentClassName).newInstance() as Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
}
}

View file

@ -15,43 +15,43 @@ class DownloadsManager(private val context: Context) {
private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList()
val mangaDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.MANGA }
val animeDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.ANIME }
val novelDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.NOVEL }
val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
val animeDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
val novelDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply()
}
private fun loadDownloads(): List<Download> {
private fun loadDownloads(): List<DownloadedType> {
val jsonString = prefs.getString("downloads_key", null)
return if (jsonString != null) {
val type = object : TypeToken<List<Download>>() {}.type
val type = object : TypeToken<List<DownloadedType>>() {}.type
gson.fromJson(jsonString, type)
} else {
emptyList()
}
}
fun addDownload(download: Download) {
downloadsList.add(download)
fun addDownload(downloadedType: DownloadedType) {
downloadsList.add(downloadedType)
saveDownloads()
}
fun removeDownload(download: Download) {
downloadsList.remove(download)
removeDirectory(download)
fun removeDownload(downloadedType: DownloadedType) {
downloadsList.remove(downloadedType)
removeDirectory(downloadedType)
saveDownloads()
}
fun removeMedia(title: String, type: Download.Type) {
val subDirectory = if (type == Download.Type.MANGA) {
fun removeMedia(title: String, type: DownloadedType.Type) {
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == Download.Type.ANIME) {
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
@ -71,21 +71,31 @@ class DownloadsManager(private val context: Context) {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
cleanDownloads()
}
downloadsList.removeAll { it.title == title }
when (type) {
DownloadedType.Type.MANGA -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
}
DownloadedType.Type.ANIME -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
}
DownloadedType.Type.NOVEL -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
}
}
saveDownloads()
}
private fun cleanDownloads() {
cleanDownload(Download.Type.MANGA)
cleanDownload(Download.Type.ANIME)
cleanDownload(Download.Type.NOVEL)
cleanDownload(DownloadedType.Type.MANGA)
cleanDownload(DownloadedType.Type.ANIME)
cleanDownload(DownloadedType.Type.NOVEL)
}
private fun cleanDownload(type: Download.Type) {
private fun cleanDownload(type: DownloadedType.Type) {
// remove all folders that are not in the downloads list
val subDirectory = if (type == Download.Type.MANGA) {
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == Download.Type.ANIME) {
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
@ -94,18 +104,18 @@ class DownloadsManager(private val context: Context) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory"
)
val downloadsSubList = if (type == Download.Type.MANGA) {
mangaDownloads
} else if (type == Download.Type.ANIME) {
animeDownloads
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
mangaDownloadedTypes
} else if (type == DownloadedType.Type.ANIME) {
animeDownloadedTypes
} else {
novelDownloads
novelDownloadedTypes
}
if (directory.exists()) {
val files = directory.listFiles()
if (files != null) {
for (file in files) {
if (!downloadsSubList.any { it.title == file.name }) {
if (!downloadsSubLists.any { it.title == file.name }) {
val deleted = file.deleteRecursively()
}
}
@ -122,11 +132,11 @@ class DownloadsManager(private val context: Context) {
}
}
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<Download>) //for debugging
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
{
val jsonString = gson.toJson(downloadsList)
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/downloads.json"
)
if (file.parentFile?.exists() == false) {
@ -138,25 +148,33 @@ class DownloadsManager(private val context: Context) {
file.writeText(jsonString)
}
fun queryDownload(download: Download): Boolean {
return downloadsList.contains(download)
fun queryDownload(downloadedType: DownloadedType): Boolean {
return downloadsList.contains(downloadedType)
}
private fun removeDirectory(download: Download) {
val directory = if (download.type == Download.Type.MANGA) {
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
return if (type == null) {
downloadsList.any { it.title == title && it.chapter == chapter }
} else {
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
}
}
private fun removeDirectory(downloadedType: DownloadedType) {
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}/${download.chapter}"
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}"
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${download.title}/${download.chapter}"
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
@ -173,26 +191,26 @@ class DownloadsManager(private val context: Context) {
}
}
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
val directory = if (download.type == Download.Type.MANGA) {
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}/${download.chapter}"
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}"
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${download.title}/${download.chapter}"
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
val destination = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${download.title}/${download.chapter}"
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
)
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
@ -206,10 +224,10 @@ class DownloadsManager(private val context: Context) {
}
}
fun purgeDownloads(type: Download.Type) {
val directory = if (type == Download.Type.MANGA) {
fun purgeDownloads(type: DownloadedType.Type) {
val directory = if (type == DownloadedType.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else if (type == Download.Type.ANIME) {
} else if (type == DownloadedType.Type.ANIME) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
@ -233,11 +251,51 @@ class DownloadsManager(private val context: Context) {
const val novelLocation = "Dantotsu/Novel"
const val mangaLocation = "Dantotsu/Manga"
const val animeLocation = "Dantotsu/Anime"
fun getDirectory(context: Context, type: DownloadedType.Type, title: String, chapter: String? = null): File {
return if (type == DownloadedType.Type.MANGA) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title"
)
}
} else if (type == DownloadedType.Type.ANIME) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title"
)
}
} else {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title"
)
}
}
}
}
}
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type {
MANGA,
ANIME,

View file

@ -0,0 +1,524 @@
package ani.dantotsu.download.anime
import android.Manifest
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
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.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.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.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
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.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
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
private lateinit var builder: NotificationCompat.Builder
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
private val downloadJobs = mutableMapOf<String, Job>()
private val mutex = Mutex()
private var isCurrentlyProcessing = false
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
return null
}
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(this)
builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Anime Download Progress")
setSmallIcon(R.drawable.ic_round_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIFICATION_ID, builder.build())
}
ContextCompat.registerReceiver(
this,
cancelReceiver,
IntentFilter(ACTION_CANCEL_DOWNLOAD),
ContextCompat.RECEIVER_EXPORTED
)
}
override fun onDestroy() {
super.onDestroy()
AnimeServiceDataSingleton.downloadQueue.clear()
downloadJobs.clear()
AnimeServiceDataSingleton.isServiceRunning = false
unregisterReceiver(cancelReceiver)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
snackString("Download started")
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
serviceScope.launch {
mutex.withLock {
if (!isCurrentlyProcessing) {
isCurrentlyProcessing = true
processQueue()
isCurrentlyProcessing = false
}
}
}
return START_NOT_STICKY
}
private fun processQueue() {
CoroutineScope(Dispatchers.Default).launch {
while (AnimeServiceDataSingleton.downloadQueue.isNotEmpty()) {
val task = AnimeServiceDataSingleton.downloadQueue.poll()
if (task != null) {
val job = launch { download(task) }
currentTasks.add(task)
mutex.withLock {
downloadJobs[task.getTaskName()] = job
}
job.join() // Wait for the job to complete before continuing to the next task
mutex.withLock {
downloadJobs.remove(task.getTaskName())
}
updateNotification() // Update the notification after each task is completed
}
if (AnimeServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty
}
}
}
}
}
@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
}
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()
downloadJobs.remove(taskName)
AnimeServiceDataSingleton.downloadQueue.removeAll { it.getTaskName() == taskName }
updateNotification() // Update the notification after cancellation
}
}
}
private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads"
} else {
"All downloads completed"
}
builder.setContentText(text)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
try {
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@AnimeDownloaderService,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
builder.setContentText("Downloading ${task.title} - ${task.episode}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
broadcastDownloadStarted(task.episode)
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
)
}
saveMediaInfo(task)
task.subtitle?.let {
SubtitleDownloader.downloadSubtitle(
this@AnimeDownloaderService,
it.file.url,
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
}
val downloadStarted =
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
if (!downloadStarted) {
logger("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("Download failed")
builder.setContentText("${task.title} - ${task.episode} Download failed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed")
logger("Download failed: ${download.failureReason}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
FirebaseCrashlytics.getInstance().recordException(
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("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed")
getSharedPreferences(
getString(R.string.anime_downloads),
Context.MODE_PRIVATE
).edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
logger("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()
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
kotlinx.coroutines.delay(2000)
}
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
logger("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
FirebaseCrashlytics.getInstance().recordException(e)
}
broadcastDownloadFailed(task.episode)
}
}
@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) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/${task.title}"
)
val episodeDirectory = File(directory, task.episode)
if (!directory.exists()) directory.mkdirs()
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val mediaJson = gson.toJson(task.sourceMedia)
val media = gson.fromJson(mediaJson, Media::class.java)
if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
if (task.episodeImage != null) {
media.anime?.episodes?.get(task.episode)?.let { episode ->
episode.thumb = downloadImage(
task.episodeImage,
episodeDirectory,
"episodeImage.jpg"
)?.let {
FileUrl(
it
)
}
}
downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg")
}
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("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 ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(
this@AnimeDownloaderService,
"Exception while saving ${name}: ${e.message}",
Toast.LENGTH_LONG
).show()
}
null
} finally {
connection?.disconnect()
}
}
private fun broadcastDownloadStarted(episodeNumber: String) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFinished(episodeNumber: String) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FINISHED).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFailed(episodeNumber: String) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FAILED).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadProgress(episodeNumber: String, progress: Int) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_PROGRESS).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
putExtra("progress", progress)
}
sendBroadcast(intent)
}
private val cancelReceiver = object : BroadcastReceiver() {
@androidx.annotation.OptIn(UnstableApi::class)
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
val taskName = intent.getStringExtra(EXTRA_TASK_NAME)
taskName?.let {
cancelDownload(it)
}
}
}
}
data class AnimeDownloadTask(
val title: String,
val episode: String,
val video: Video,
val subtitle: Subtitle? = null,
val sourceMedia: Media? = null,
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
) {
fun getTaskName(): String {
return "$title - $episode"
}
companion object {
fun getTaskName(title: String, episode: String): String {
return "$title - $episode"
}
}
}
companion object {
private const val NOTIFICATION_ID = 1103
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
const val EXTRA_TASK_NAME = "extra_task_name"
}
}
object AnimeServiceDataSingleton {
var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile
var isServiceRunning: Boolean = false
}

View file

@ -0,0 +1,112 @@
package ani.dantotsu.download.anime
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
class OfflineAnimeAdapter(
private val context: Context,
private var items: List<OfflineAnimeModel>,
private val searchListener: OfflineAnimeSearchListener
) : BaseAdapter() {
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineAnimeModel> = items
private var style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
override fun getCount(): Int {
return items.size
}
override fun getItem(position: Int): Any {
return items[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View = convertView ?: when (style) {
0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
}
val item = getItem(position) as OfflineAnimeModel
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val episodes = view.findViewById<TextView>(R.id.itemTotal)
episodes.text = " Episodes"
bannerView.setImageURI(item.banner)
totalepisodes.text = item.totalEpisodeList
} else if (style == 1) {
val watchedEpisodes =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
watchedEpisodes.text = item.watchedEpisode
totalepisodes.text = " | " + item.totalEpisode
}
// Bind item data to the views
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
type.text = item.type
typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image)
titleTextView.text = item.title
itemScore.text = item.score
if (item.isOngoing) {
ongoing.visibility = View.VISIBLE
} else {
ongoing.visibility = View.GONE
}
return view
}
fun onSearchQuery(query: String) {
// Implement the filtering logic here, for example:
items = if (query.isEmpty()) {
// Return the original list if the query is empty
originalItems
} else {
// Filter the list based on the query
originalItems.filter { it.title.contains(query, ignoreCase = true) }
}
notifyDataSetChanged() // Notify the adapter that the data set has changed
}
fun setItems(items: List<OfflineAnimeModel>) {
this.items = items
this.originalItems = items
notifyDataSetChanged()
}
fun notifyNewGrid() {
style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
notifyDataSetChanged()
}
}

View file

@ -0,0 +1,450 @@
package ani.dantotsu.download.anime
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.view.animation.OvershootInterpolator
import android.widget.AbsListView
import android.widget.AutoCompleteTextView
import android.widget.GridView
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import kotlin.math.max
import kotlin.math.min
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineAnimeModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total : TextView
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
textInputLayout.hint = "Anime"
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
if (!uiSettings.immersiveMode) {
view.rootView.fitsSystemWindows = true
}
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
searchView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onSearchQuery(s.toString())
}
})
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getInt("offline_view", 0)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutcompact
else -> layoutList
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
layoutList.setOnClickListener {
selected(it as ImageView)
style = 0
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putInt("offline_view", style!!)?.apply()
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView)
adapter.notifyNewGrid()
grid()
}
layoutcompact.setOnClickListener {
selected(it as ImageView)
style = 1
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putInt("offline_view", style!!)?.apply()
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView1)
adapter.notifyNewGrid()
grid()
}
gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
total = view.findViewById(R.id.total)
grid()
return view
}
@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)
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
gridView.setOnItemClickListener { _, _, position, _ ->
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@let
}
MediaDetailsActivity.mediaSingleton = mediaModel
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true)
)
} ?: run {
snackString("no media found")
}
}
gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val type: DownloadedType.Type =
DownloadedType.Type.ANIME
// Alert dialog to confirm deletion
val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
val mediaIds = requireContext().getSharedPreferences(
getString(R.string.anime_downloads),
Context.MODE_PRIVATE
)
?.all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
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
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
}
override fun onSearchQuery(query: String) {
adapter.onSearchQuery(query)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
val visible = false
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
}
scrollTop.setOnClickListener {
gridView.smoothScrollToPositionFromTop(0, 0)
}
// Assuming 'scrollTop' is a view that you want to hide/show
scrollTop.visibility = View.GONE
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < -height
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
}
})
initActivity(requireActivity())
}
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
downloads = listOf()
}
override fun onDestroy() {
super.onDestroy()
downloads = listOf()
}
override fun onStop() {
super.onStop()
downloads = listOf()
}
private fun getDownloads() {
downloads = listOf()
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val _downloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
}
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
null
}
}
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
val totalEpisode =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
val chapters = " Chapters"
val totalEpisodesList =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
?: "~").toString()
return OfflineAnimeModel(
title,
score,
totalEpisode,
totalEpisodesList,
watchedEpisodes,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
false,
false,
null,
null
)
}
}
}
interface OfflineAnimeSearchListener {
fun onSearchQuery(query: String)
}

View file

@ -0,0 +1,17 @@
package ani.dantotsu.download.anime
import android.net.Uri
data class OfflineAnimeModel(
val title: String,
val score: String,
val totalEpisode: String,
val totalEpisodeList: String,
val watchedEpisode: String,
val type: String,
val episodes: String,
val isOngoing: Boolean,
val isUserScored: Boolean,
val image: Uri?,
val banner: Uri?,
)

View file

@ -18,7 +18,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
@ -187,7 +187,8 @@ class MangaDownloaderService : Service() {
true
}
val deferredList = mutableListOf<Deferred<Bitmap?>>()
//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())
@ -196,15 +197,12 @@ class MangaDownloaderService : Service() {
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
// Limit the number of simultaneous downloads from the task
if (deferredList.size >= task.simultaneousDownloads) {
// Wait for all deferred to complete and clear the list
deferredList.awaitAll()
deferredList.clear()
if (deferredMap.size >= task.simultaneousDownloads) {
deferredMap.values.awaitAll()
deferredMap.clear()
}
// Download the image and add to deferred list
val deferred = async(Dispatchers.IO) {
deferredMap[index] = async(Dispatchers.IO) {
var bitmap: Bitmap? = null
var retryCount = 0
@ -217,7 +215,6 @@ class MangaDownloaderService : Service() {
retryCount++
}
// Cache the image if successful
if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
}
@ -233,12 +230,10 @@ class MangaDownloaderService : Service() {
bitmap
}
deferredList.add(deferred)
}
// Wait for any remaining deferred to complete
deferredList.awaitAll()
deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
@ -246,10 +241,10 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task)
downloadsManager.addDownload(
Download(
DownloadedType(
task.title,
task.chapter,
Download.Type.MANGA
DownloadedType.Type.MANGA
)
)
broadcastDownloadFinished(task.chapter)
@ -314,7 +309,16 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.writeText(jsonString)
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@MangaDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
}
}
}

View file

@ -1,11 +1,13 @@
package ani.dantotsu.download.manga
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
@ -19,6 +21,8 @@ class OfflineMangaAdapter(
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineMangaModel> = items
private var style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
override fun getCount(): Int {
return items.size
@ -32,23 +36,47 @@ class OfflineMangaAdapter(
return position.toLong()
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view = convertView
if (view == null) {
view = inflater.inflate(R.layout.item_media_compact, parent, false)
val view: View = convertView ?: when (style) {
0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
}
val item = getItem(position) as OfflineMangaModel
val imageView = view!!.findViewById<ImageView>(R.id.itemCompactImage)
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalchapter = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val chapters = view.findViewById<TextView>(R.id.itemTotal)
chapters.text = " Chapters"
bannerView.setImageURI(item.banner)
totalchapter.text = item.totalChapter
} else if (style == 1) {
val readchapter =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
readchapter.text = item.readChapter
totalchapter.text = " | " + item.totalChapter
}
// Bind item data to the views
// For example:
typeimage.setImageResource(if (item.type == "Novel") R.drawable.ic_round_book_24 else R.drawable.ic_round_import_contacts_24)
type.text = item.type
typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image)
titleTextView.text = item.title
itemScore.text = item.score
if (item.isOngoing) {
ongoing.visibility = View.VISIBLE
} else {
@ -74,4 +102,10 @@ class OfflineMangaAdapter(
this.originalItems = items
notifyDataSetChanged()
}
fun notifyNewGrid() {
style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
notifyDataSetChanged()
}
}

View file

@ -13,21 +13,33 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.view.animation.OvershootInterpolator
import android.widget.AbsListView
import android.widget.AutoCompleteTextView
import android.widget.GridView
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
@ -45,19 +57,24 @@ import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total : TextView
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
textInputLayout.hint = "Manga"
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
@ -69,16 +86,13 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
animeUserAvatar.setSafeOnClickListener {
val dialogFragment = SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
dialogFragment.show(
(it.context as AppCompatActivity).supportFragmentManager,
"dialog"
)
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
if (!uiSettings.immersiveMode) {
view.rootView.fitsSystemWindows = true
}
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
@ -98,17 +112,65 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
onSearchQuery(s.toString())
}
})
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getInt("offline_view", 0)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutcompact
else -> layoutList
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
layoutList.setOnClickListener {
selected(it as ImageView)
style = 0
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("offline_view", style!!).apply()
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView)
adapter.notifyNewGrid()
grid()
}
layoutcompact.setOnClickListener {
selected(it as ImageView)
style = 1
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("offline_view", style!!).apply()
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView1)
adapter.notifyNewGrid()
grid()
}
gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
total = view.findViewById(R.id.total)
grid()
return view
}
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)
gridView.adapter = adapter
gridView.setOnItemClickListener { parent, view, position, id ->
gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
gridView.setOnItemClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media =
downloadManager.mangaDownloads.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title }
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
@ -120,22 +182,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
gridView.setOnItemLongClickListener { parent, view, position, id ->
gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) {
Download.Type.MANGA
val type: DownloadedType.Type =
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
DownloadedType.Type.MANGA
} else {
Download.Type.NOVEL
DownloadedType.Type.NOVEL
}
// Alert dialog to confirm deletion
val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
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
@ -144,8 +209,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
dialog.window?.setDimAmount(0.8f)
true
}
return view
}
override fun onSearchQuery(query: String) {
@ -154,6 +217,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initActivity(requireActivity())
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
@ -170,7 +234,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
var visible = false
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
val visible = false
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
@ -187,9 +254,30 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
scrollTop.setOnClickListener {
//TODO: scroll to top
gridView.smoothScrollToPositionFromTop(0, 0)
}
// Assuming 'scrollTop' is a view that you want to hide/show
scrollTop.visibility = View.GONE
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < -height
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
}
})
}
override fun onResume() {
@ -215,19 +303,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() {
downloads = listOf()
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val _downloads = downloadManager.novelDownloads.filter { it.title == title }
val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
@ -236,17 +324,17 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
private fun getMedia(download: Download): Media? {
val type = if (download.type == Download.Type.MANGA) {
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}"
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
@ -266,40 +354,68 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel {
val type = if (download.type == Download.Type.MANGA) {
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}"
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
val mediaModel = getMedia(download)!!
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else {
null
}
val title = mediaModel.nameMAL ?: mediaModel.nameRomaji
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing = false
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
val readchapter = (mediaModel.userProgress ?: "~").toString()
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val chapters = " Chapters"
return OfflineMangaModel(
title,
score,
totalchapter,
readchapter,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel("unknown", "0", false, false, null)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
false,
false,
null,
null
)
}
}
}

View file

@ -5,7 +5,12 @@ import android.net.Uri
data class OfflineMangaModel(
val title: String,
val score: String,
val totalChapter: String,
val readChapter: String,
val type: String,
val chapters: String,
val isOngoing: Boolean,
val isUserScored: Boolean,
val image: Uri?
val image: Uri?,
val banner: Uri?
)

View file

@ -17,7 +17,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
@ -330,10 +330,10 @@ class NovelDownloaderService : Service() {
saveMediaInfo(task)
downloadsManager.addDownload(
Download(
DownloadedType(
task.title,
task.chapter,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)

View file

@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R
@UnstableApi
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) {
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

View file

@ -1,8 +1,19 @@
package ani.dantotsu.download.video
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
@ -15,6 +26,7 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadHelper
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
@ -22,7 +34,12 @@ import androidx.media3.exoplayer.scheduler.Requirements
import androidx.media3.ui.TrackSelectionDialogBuilder
import ani.dantotsu.R
import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.okHttpClient
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
@ -37,6 +54,8 @@ import java.util.concurrent.*
object Helper {
private var simpleCache: SimpleCache? = null
@SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory {
@ -82,27 +101,13 @@ object Helper {
)
downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
TrackSelectionDialogBuilder(
context, "Select thingy", helper.getTracks(0).groups
) { _, overrides ->
val params = TrackSelectionParameters.Builder(context)
overrides.forEach {
params.addOverride(it.value)
}
helper.addTrackSelection(0, params.build())
MyDownloadService
helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
MyDownloadService::class.java,
helper.getDownloadRequest(null),
ExoplayerDownloadService::class.java,
it,
false
)
}.apply {
setTheme(R.style.DialogTheme)
setTrackNameProvider {
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
}
build().show()
}
}
@ -114,13 +119,13 @@ object Helper {
private var download: DownloadManager? = null
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Synchronized
@UnstableApi
fun downloadManager(context: Context): DownloadManager {
return download ?: let {
val database = StandaloneDatabaseProvider(context)
val database = Injekt.get<StandaloneDatabaseProvider>()
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
@ -133,17 +138,42 @@ object Helper {
}
dataSource
}
DownloadManager(
val threadPoolSize = Runtime.getRuntime().availableProcessors()
val executorService = Executors.newFixedThreadPool(threadPoolSize)
val downloadManager = DownloadManager(
context,
database,
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
getSimpleCache(context),
dataSourceFactory,
Executor(Runnable::run)
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?
) {
if (download.state == Download.STATE_COMPLETED) {
Log.e("Downloader", "Download Completed")
} else if (download.state == Download.STATE_FAILED) {
Log.e("Downloader", "Download Failed")
} else if (download.state == Download.STATE_STOPPED) {
Log.e("Downloader", "Download Stopped")
} else if (download.state == Download.STATE_QUEUED) {
Log.e("Downloader", "Download Queued")
} else if (download.state == Download.STATE_DOWNLOADING) {
Log.e("Downloader", "Download Downloading")
}
}
}
)
downloadManager
}
}
@ -159,4 +189,108 @@ object Helper {
}
return downloadDirectory!!
}
@OptIn(UnstableApi::class)
fun startAnimeDownloadService(
context: Context,
title: String,
episode: String,
video: Video,
subtitle: Subtitle? = null,
sourceMedia: Media? = null,
episodeImage: String? = null
) {
if (!isNotificationPermissionGranted(context)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
context as Activity,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
title,
episode,
video,
subtitle,
sourceMedia,
episodeImage
)
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, DownloadedType.Type.ANIME)
if (downloadCheck) {
AlertDialog.Builder(context, R.style.MyPopup)
.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,
context.getSharedPreferences(
getString(context, R.string.anime_downloads),
Context.MODE_PRIVATE
).getString(
animeDownloadTask.getTaskName(),
""
) ?: "",
false
)
context.getSharedPreferences(
getString(context, R.string.anime_downloads),
Context.MODE_PRIVATE
).edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
DownloadedType.Type.ANIME
)
)
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
.setNegativeButton("No") { _, _ -> }
.show()
} else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
}
@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(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
}
return true
}
}

View file

@ -47,6 +47,7 @@ import kotlin.math.min
class AnimeFragment : Fragment() {
private var _binding: FragmentAnimeBinding? = null
private val binding get() = _binding!!
private lateinit var animePageAdapter: AnimePageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
@ -95,7 +96,7 @@ class AnimeFragment : Fragment() {
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val animePageAdapter = AnimePageAdapter()
animePageAdapter = AnimePageAdapter()
var loading = true
if (model.notSet) {
@ -277,6 +278,11 @@ class AnimeFragment : Fragment() {
override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
if (animePageAdapter.trendingViewPager != null) {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
super.onResume()
}
}

View file

@ -111,7 +111,6 @@ class HomeFragment : Fragment() {
snackString(currContext()?.getString(R.string.please_reload))
}
}
binding.homeUserAvatarContainer.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)

View file

@ -28,5 +28,6 @@ class LoginFragment : Fragment() {
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
}
}

View file

@ -43,6 +43,7 @@ import kotlin.math.min
class MangaFragment : Fragment() {
private var _binding: FragmentMangaBinding? = null
private val binding get() = _binding!!
private lateinit var mangaPageAdapter: MangaPageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
@ -90,7 +91,7 @@ class MangaFragment : Fragment() {
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val mangaPageAdapter = MangaPageAdapter()
mangaPageAdapter = MangaPageAdapter()
var loading = true
if (model.notSet) {
model.notSet = false
@ -251,6 +252,11 @@ class MangaFragment : Fragment() {
override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
//make sure mangaPageAdapter is initialized
if (mangaPageAdapter.trendingViewPager != null) {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
super.onResume()
}

View file

@ -4,8 +4,11 @@ import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach
@ -17,6 +20,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R
import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.databinding.ActivityNoInternetBinding
import ani.dantotsu.download.anime.OfflineAnimeFragment
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
@ -25,6 +29,7 @@ import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.others.LangSet
import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import nl.joery.animatedbottombar.AnimatedBottomBar
@ -56,10 +61,24 @@ class NoInternet : AppCompatActivity() {
}
var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) {
if (doubleBackToExitPressedOnce) {
finishAffinity()
}
doubleBackToExitPressedOnce = true
snackString(this@NoInternet.getString(R.string.back_to_exit))
Handler(Looper.getMainLooper()).postDelayed(
{ doubleBackToExitPressedOnce = false },
2000
)
}
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
@ -96,12 +115,11 @@ class NoInternet : AppCompatActivity() {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
when (position) {
0 -> return OfflineFragment()
1 -> return OfflineFragment()
2 -> return OfflineMangaFragment()
}
return LoginFragment()
return when (position) {
0 -> OfflineAnimeFragment()
2 -> OfflineMangaFragment()
else -> OfflineFragment()
}
}
}
}

View file

@ -4,11 +4,13 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
@ -16,8 +18,10 @@ import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@ -76,6 +80,10 @@ class CalendarActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
}
setContentView(binding.root)

View file

@ -118,6 +118,19 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
}
fun emptyMedia() = Media(
id = 0,
name = "No media found",
nameRomaji = "No media found",
userPreferredName = "",
isAdult = false,
isFav = false,
isListPrivate = false,
userScore = 0,
userStatus = "",
format = "",
)
object MediaSingleton {
var media: Media? = null
var bitmap: Bitmap? = null

View file

@ -13,7 +13,10 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
@ -136,7 +139,7 @@ class MediaAdaptor(
val media = mediaList?.get(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
@ -319,6 +322,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@ -332,6 +336,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@ -346,6 +351,7 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@ -361,12 +367,14 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
binding.itemCompactTitleContainer.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@ -375,7 +383,7 @@ class MediaAdaptor(
}
}
fun clicked(position: Int, bitmap: Bitmap? = null) {
fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position)
if (bitmap != null) MediaSingleton.bitmap = bitmap
@ -384,7 +392,13 @@ class MediaAdaptor(
Intent(activity, MediaDetailsActivity::class.java).putExtra(
"media",
media as Serializable
), null
), ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
Pair.create(
itemCompactImage,
ViewCompat.getTransitionName(activity.findViewById(R.id.itemCompactImage))!!
),
).toBundle()
)
}
}

View file

@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.SpannableStringBuilder
@ -42,7 +43,6 @@ import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized
import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings
@ -72,11 +72,17 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
LangSet.setLocale(this)
var media: Media = intent.getSerialized("media") ?: return
super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
if (media.name == "No media found") {
snackString(media.name)
onBackPressedDispatcher.onBackPressed()
return
}
mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null
super.onCreate(savedInstanceState)
binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat()
@ -85,12 +91,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
initActivity(this)
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
if (!uiSettings.immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@ -154,7 +159,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
})
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
if (this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("incognito", false)) {
binding.mediaTitle.text = " ${media.userPreferredName}"
binding.incognito.visibility = View.VISIBLE
}else {
binding.mediaTitle.text = media.userPreferredName
}
binding.mediaTitle.setOnLongClickListener {
copyToClipboard(media.userPreferredName)
true
@ -305,7 +316,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
adult = media.isAdult
tabLayout.menu.clear()
if (media.anime != null) {
viewPager.adapter =
@ -317,7 +327,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
)
if (media.format == "NOVEL") {
tabLayout.inflateMenu(R.menu.novel_menu_detail)
} else {
tabLayout.inflateMenu(R.menu.manga_menu_detail)
}
anime = false
}
@ -442,7 +456,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
.setDuration(duration).start()
binding.mediaBanner.pause()
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
@ -455,7 +468,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
.setDuration(duration).start()
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
}
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
false
@ -532,5 +544,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
image.alpha = if (disabled) 0.33f else 1f
}
}
companion object {
var mediaSingleton: Media? = null
}
}

View file

@ -242,7 +242,8 @@ class MediaDetailsViewModel : ViewModel() {
i: String,
manager: FragmentManager,
launch: Boolean = true,
prevEp: String? = null
prevEp: String? = null,
isDownload: Boolean = false
) {
Handler(Looper.getMainLooper()).post {
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
@ -254,13 +255,17 @@ class MediaDetailsViewModel : ViewModel() {
}
media.selected = this.loadSelected(media)
val selector =
SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
SelectorDialogFragment.newInstance(
media.selected!!.server,
launch,
prevEp,
isDownload
)
selector.show(manager, "dialog")
}
}
}
//Manga
var mangaReadSources: MangaReadSources? = null
@ -314,7 +319,8 @@ class MediaDetailsViewModel : ViewModel() {
val novelSources = NovelSources
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
suspend fun searchNovels(query: String, i: Int) {
val source = novelSources[i]
val position = if (i >= novelSources.list.size) 0 else i
val source = novelSources[position]
tryWithSuspend(post = true) {
if (source != null) {
novelResponses.postValue(source.search(query))

View file

@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
@ -59,6 +60,7 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
val offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("offlineMode", false) || !isOnline(requireContext())
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
@ -101,6 +103,7 @@ class MediaInfoFragment : Fragment() {
if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
if (!offline) {
binding.mediaInfoStudioContainer.setOnClickListener {
ContextCompat.startActivity(
requireActivity(),
@ -112,9 +115,11 @@ class MediaInfoFragment : Fragment() {
)
}
}
}
if (media.anime.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.anime.author!!.name
if (!offline) {
binding.mediaInfoAuthorContainer.setOnClickListener {
ContextCompat.startActivity(
requireActivity(),
@ -126,6 +131,7 @@ class MediaInfoFragment : Fragment() {
)
}
}
}
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
binding.mediaInfoTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
@ -137,6 +143,7 @@ class MediaInfoFragment : Fragment() {
if (media.manga.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.manga.author!!.name
if (!offline) {
binding.mediaInfoAuthorContainer.setOnClickListener {
ContextCompat.startActivity(
requireActivity(),
@ -149,6 +156,7 @@ class MediaInfoFragment : Fragment() {
}
}
}
}
val desc = HtmlCompat.fromHtml(
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
@ -189,7 +197,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
if (media.trailer != null) {
if (media.trailer != null && !offline) {
@Suppress("DEPRECATION")
class MyChrome : WebChromeClient() {
private var mCustomView: View? = null
@ -243,7 +251,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) {
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
val markWon = Markwon.builder(requireContext())
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
@ -304,7 +312,7 @@ class MediaInfoFragment : Fragment() {
}
}
if (media.genres.isNotEmpty()) {
if (media.genres.isNotEmpty() && !offline) {
val bind = ActivityGenreBinding.inflate(
LayoutInflater.from(context),
parent,
@ -335,7 +343,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
if (media.tags.isNotEmpty()) {
if (media.tags.isNotEmpty() && !offline) {
val bind = ItemTitleChipgroupBinding.inflate(
LayoutInflater.from(context),
parent,
@ -376,7 +384,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
if (!media.characters.isNullOrEmpty()) {
if (!media.characters.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
@ -393,7 +401,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
if (!media.relations.isNullOrEmpty()) {
if (!media.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate(
LayoutInflater.from(context),
@ -456,7 +464,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bindi.root)
}
if (!media.recommendations.isNullOrEmpty()) {
if (!media.recommendations.isNullOrEmpty() && !offline ) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,

View file

@ -16,7 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListBinding
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.materialswitch.MaterialSwitch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -196,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
}
media?.inCustomListsOf?.forEach {
SwitchMaterial(requireContext()).apply {
MaterialSwitch(requireContext()).apply {
isChecked = it.value
text = it.key
setOnCheckedChangeListener { _, isChecked ->

View file

@ -78,7 +78,7 @@ class SearchActivity : AppCompatActivity() {
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
val headerAdaptor = SearchAdapter(this)
val gridSize = (screenWidth / 124f).toInt()
val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
@ -159,7 +159,7 @@ class SearchActivity : AppCompatActivity() {
fun search() {
val size = model.searchResults.results.size
model.searchResults.results.clear()
runOnUiThread {
binding.searchRecyclerView.post {
mediaAdaptor.notifyItemRangeRemoved(0, size)
}

View file

@ -1,6 +1,8 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
@ -10,10 +12,14 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.saveData
@ -54,6 +60,14 @@ class SearchAdapter(private val activity: SearchActivity) :
}
binding.searchBar.hint = activity.result.type
if (currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("incognito", false ) == true){
val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? =
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
binding.searchBar.startIconDrawable = startIconDrawable
}
var adult = activity.result.isAdult
var listOnly = activity.result.onList

View file

@ -1,19 +1,23 @@
package ani.dantotsu.media
import android.content.Context
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class SubtitleDownloader {
companion object {
//doesn't really download the subtitles -\_(o_o)_/-
suspend fun downloadSubtitles(context: Context, url: String): SubtitleType =
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
withContext(Dispatchers.IO) {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val networkHelper = Injekt.get<NetworkHelper>()
@ -29,8 +33,8 @@ class SubtitleDownloader {
val subtitleType = when {
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT
responseBody.contains("[Script Info]") -> SubtitleType.ASS
responseBody.contains("WEBVTT") -> SubtitleType.VTT
else -> SubtitleType.SRT
}
@ -39,5 +43,41 @@ class SubtitleDownloader {
return@withContext SubtitleType.UNKNOWN
}
}
//actually downloads lol
suspend fun downloadSubtitle(context: Context, url: String, downloadedType: DownloadedType) {
try {
val directory = DownloadsManager.getDirectory(context, downloadedType.type, downloadedType.title, downloadedType.chapter)
if (!directory.exists()) { //just in case
directory.mkdirs()
}
val type = loadSubtitleType(context, url)
val subtiteFile = File(directory, "subtitle.${type}")
if (subtiteFile.exists()) {
subtiteFile.delete()
}
subtiteFile.createNewFile()
val client = Injekt.get<NetworkHelper>().client
val request = Request.Builder().url(url).build()
val reponse = client.newCall(request).execute()
if (!reponse.isSuccessful) {
snackString("Failed to download subtitle")
return
}
reponse.body.byteStream().use { input ->
subtiteFile.outputStream().use { output ->
input.copyTo(output)
}
}
} catch (e: Exception) {
snackString("Failed to download subtitle")
e.printStackTrace()
return
}
}
}
}

View file

@ -5,8 +5,13 @@ import java.util.regex.Pattern
class AnimeNameAdapter {
companion object {
const val episodeRegex =
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
const val failedEpisodeNumberRegex =
"(?<!part\\s)\\b(\\d+)\\b"
const val seasonRegex = "\\s+(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
fun findSeasonNumber(text: String): Int? {
val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)"
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
@ -16,5 +21,56 @@ class AnimeNameAdapter {
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 =
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
}
}

View file

@ -1,32 +1,41 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class AnimeWatchAdapter(
private val media: Media,
private val fragment: AnimeWatchFragment,
@ -41,6 +50,9 @@ class AnimeWatchAdapter(
return ViewHolder(bind)
}
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
@ -78,6 +90,17 @@ class AnimeWatchAdapter(
null
)
}
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
)
?.getBoolean("offlineMode", false) == true
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection
var source =
@ -147,8 +170,9 @@ class AnimeWatchAdapter(
}
}
//Icons
//Subscription
//subscribe
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
@ -167,44 +191,99 @@ class AnimeWatchAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id))
}
//Icons
//Nested Button
binding.animeNestedButton.setOnClickListener {
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
binding.animeSourceTop.setOnClickListener {
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener {
reversed = !reversed
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed)
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true
}
//Grids
var selected = when (style) {
0 -> binding.animeSourceList
1 -> binding.animeSourceGrid
2 -> binding.animeSourceCompact
else -> binding.animeSourceList
0 -> dialogBinding.animeSourceList
1 -> dialogBinding.animeSourceGrid
2 -> dialogBinding.animeSourceCompact
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Grid"
2 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageView) {
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
binding.animeSourceList.setOnClickListener {
selected(it as ImageView)
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
fragment.onIconPressed(style, reversed)
dialogBinding.layoutText.text = "List"
run = true
}
binding.animeSourceGrid.setOnClickListener {
selected(it as ImageView)
dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
fragment.onIconPressed(style, reversed)
dialogBinding.layoutText.text = "Grid"
run = true
}
binding.animeSourceCompact.setOnClickListener {
selected(it as ImageView)
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
fragment.onIconPressed(style, reversed)
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast("WebView not installed")
}
//start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser
val sourceHttp =
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
startActivity(fragment.requireContext(), intent, null)
}
}
}
//hidden
dialogBinding.animeScanlatorContainer.visibility = View.GONE
dialogBinding.animeDownloadContainer.visibility = View.GONE
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadEpisodes(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadEpisodes(source, true)
}
.create()
nestedDialog?.show()
}
binding.animeScanlatorTop.visibility = View.GONE
binding.animeDownloadTop.visibility = View.GONE
//Episode Handling
handleEpisodes()
}
@ -304,12 +383,15 @@ class AnimeWatchAdapter(
}
}
val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
@ -322,6 +404,7 @@ class AnimeWatchAdapter(
} else {
binding.animeSourceContinue.visibility = View.GONE
}
binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE
@ -336,7 +419,7 @@ class AnimeWatchAdapter(
}
}
fun setLanguageList(lang: Int, source: Int) {
private fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (watchSources is AnimeSources) {
val parser = watchSources[source] as? DynamicAnimeParser
@ -351,12 +434,16 @@ class AnimeWatchAdapter(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
binding?.animeSourceLanguage?.setAdapter(
ArrayAdapter(
val adapter = ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
parser.extension.sources.map { it.lang })
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)
}
}

View file

@ -2,6 +2,10 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@ -9,20 +13,29 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
@ -42,6 +55,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
@ -61,6 +76,8 @@ class AnimeWatchFragment : Fragment() {
private lateinit var headerAdapter: AnimeWatchAdapter
private lateinit var episodeAdapter: EpisodeAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f
private var progress = View.VISIBLE
@ -80,6 +97,21 @@ class AnimeWatchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
addAction(ACTION_DOWNLOAD_FAILED)
addAction(ACTION_DOWNLOAD_PROGRESS)
}
ContextCompat.registerReceiver(
requireContext(),
downloadStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp
@ -134,9 +166,17 @@ class AnimeWatchFragment : Fragment() {
if (!loaded) {
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
val offlineMode =
model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
episodeAdapter =
EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
EpisodeAdapter(
style ?: uiSettings.animeDefaultView,
media,
this,
offlineMode = offlineMode
)
binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter)
@ -169,11 +209,14 @@ class AnimeWatchFragment : Fragment() {
if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc =
episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
episode.title =
episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
episode.thumb =
episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title
?: episode.title else episode.title
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
?: FileUrl[media.cover]
}
}
@ -314,19 +357,20 @@ class AnimeWatchFragment : Fragment() {
if (show) View.GONE else View.VISIBLE
}
}
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
val names =
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext())
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
@ -343,10 +387,10 @@ class AnimeWatchFragment : Fragment() {
.commit()
}
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
.setOnDismissListener {
if (!itemSelected) {
changeUIVisibility(true)
return@setNegativeButton
}
}
.show()
dialog.window?.setDimAmount(0.8f)
@ -379,6 +423,93 @@ class AnimeWatchFragment : Fragment() {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
}
fun onAnimeEpisodeDownloadClick(i: String) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
val cancelIntent = Intent().apply {
action = AnimeDownloaderService.ACTION_CANCEL_DOWNLOAD
putExtra(
AnimeDownloaderService.EXTRA_TASK_NAME,
AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
)
}
requireContext().sendBroadcast(cancelIntent)
// Remove the download from the manager and update the UI
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
)
)
episodeAdapter.purgeDownload(i)
}
@OptIn(UnstableApi::class)
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
)
)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = requireContext().getSharedPreferences(
ContextCompat.getString(requireContext(), R.string.anime_downloads),
Context.MODE_PRIVATE
).getString(
taskName,
""
) ?: ""
requireContext().getSharedPreferences(
ContextCompat.getString(requireContext(), R.string.anime_downloads),
Context.MODE_PRIVATE
).edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!this@AnimeWatchFragment::episodeAdapter.isInitialized) return
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let { episodeAdapter.startDownload(it) }
}
ACTION_DOWNLOAD_FINISHED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let { episodeAdapter.stopDownload(it) }
}
ACTION_DOWNLOAD_FAILED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let {
episodeAdapter.purgeDownload(it)
}
}
ACTION_DOWNLOAD_PROGRESS -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
val progress = intent.getIntExtra("progress", 0)
chapterNumber?.let {
episodeAdapter.updateDownloadProgress(it, progress)
}
}
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
@ -391,6 +522,8 @@ class AnimeWatchFragment : Fragment() {
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
episodeAdapter.offlineMode = isDownloaded
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) {
@ -405,11 +538,20 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName()) {
episodeAdapter.stopDownload(download.chapter)
}
}
}
override fun onDestroy() {
model.watchSources?.flushText()
super.onDestroy()
try {
requireContext().unregisterReceiver(downloadStatusReceiver)
} catch (_: IllegalArgumentException) {
}
}
var state: Parcelable? = null
@ -424,4 +566,12 @@ class AnimeWatchFragment : Fragment() {
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
}
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
}
}

View file

@ -0,0 +1,43 @@
package ani.dantotsu.media.anime
import android.content.Context
import android.os.Bundle
import androidx.mediarouter.app.MediaRouteActionProvider
import androidx.mediarouter.app.MediaRouteChooserDialog
import androidx.mediarouter.app.MediaRouteChooserDialogFragment
import androidx.mediarouter.app.MediaRouteControllerDialog
import androidx.mediarouter.app.MediaRouteControllerDialogFragment
import androidx.mediarouter.app.MediaRouteDialogFactory
import ani.dantotsu.R
class CustomCastProvider(context: Context) : MediaRouteActionProvider(context) {
init {
dialogFactory = CustomCastThemeFactory()
}
}
class CustomCastThemeFactory : MediaRouteDialogFactory() {
override fun onCreateChooserDialogFragment(): MediaRouteChooserDialogFragment {
return CustomMediaRouterChooserDialogFragment()
}
override fun onCreateControllerDialogFragment(): MediaRouteControllerDialogFragment {
return CustomMediaRouteControllerDialogFragment()
}
}
class CustomMediaRouterChooserDialogFragment : MediaRouteChooserDialogFragment() {
override fun onCreateChooserDialog(
context: Context,
savedInstanceState: Bundle?
): MediaRouteChooserDialog =
MediaRouteChooserDialog(context, R.style.MyPopup)
}
class CustomMediaRouteControllerDialogFragment : MediaRouteControllerDialogFragment() {
override fun onCreateControllerDialog(
context: Context,
savedInstanceState: Bundle?
): MediaRouteControllerDialog =
MediaRouteControllerDialog(context, R.style.MyPopup)
}

View file

@ -14,7 +14,8 @@ data class Episode(
var selectedExtractor: String? = null,
var selectedVideo: Int = 0,
var selectedSubtitle: Int? = -1,
var extractors: MutableList<VideoExtractor>? = null,
var downloadProgress: String? = null,
@Transient var extractors: MutableList<VideoExtractor>? = null,
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
var allStreams: Boolean = false,
var watched: Long? = null,

View file

@ -1,19 +1,32 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.LinearLayout
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.ln
import kotlin.math.pow
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
val curr = loadData<Long>("${mediaId}_${ep}")
@ -32,13 +45,24 @@ fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep:
}
}
@OptIn(UnstableApi::class)
class EpisodeAdapter(
private var type: Int,
private val media: Media,
private val fragment: AnimeWatchFragment,
var arr: List<Episode> = arrayListOf()
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
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) {
0 -> EpisodeListViewHolder(
@ -76,12 +100,11 @@ class EpisodeAdapter(
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position]
val title =
"${
if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(
R.string.episode_singular
)
} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
} else {
ep.number
} ?: ""
when (holder) {
is EpisodeListViewHolder -> {
@ -93,7 +116,7 @@ class EpisodeAdapter(
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
if (ep.filler) {
binding.itemEpisodeFiller.visibility = View.VISIBLE
@ -102,6 +125,7 @@ class EpisodeAdapter(
binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE
}
holder.bind(ep.number, ep.downloadProgress)
binding.itemEpisodeDesc.visibility =
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: ""
@ -205,6 +229,80 @@ class EpisodeAdapter(
override fun getItemCount(): Int = arr.size
private val activeDownloads = mutableSetOf<String>()
private val downloadedEpisodes = mutableSetOf<String>()
fun startDownload(episodeNumber: String) {
activeDownloads.add(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
@OptIn(UnstableApi::class)
fun stopDownload(episodeNumber: String) {
activeDownloads.remove(episodeNumber)
downloadedEpisodes.add(episodeNumber)
// 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 = fragment.requireContext().getSharedPreferences(
ContextCompat.getString(fragment.requireContext(), R.string.anime_downloads),
Context.MODE_PRIVATE
).getString(
taskName,
""
) ?: ""
val size = try {
val download = index.getDownload(id)
bytesToHuman(download?.bytesDownloaded ?: 0)
} catch (e: Exception) {
null
}
arr[position].downloadProgress = "Downloaded" + if (size != null) ": ($size)" else ""
notifyItemChanged(position)
}
}
fun deleteDownload(episodeNumber: String) {
downloadedEpisodes.remove(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = null
notifyItemChanged(position)
}
}
fun purgeDownload(episodeNumber: String) {
activeDownloads.remove(episodeNumber)
downloadedEpisodes.remove(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = "Failed"
notifyItemChanged(position)
}
}
fun updateDownloadProgress(episodeNumber: String, progress: Int) {
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = "Downloading: $progress%"
notifyItemChanged(position)
}
}
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
@ -227,11 +325,27 @@ class EpisodeAdapter(
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>()
init {
itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
}
binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val episodeNumber = arr[bindingAdapterPosition].number
if (activeDownloads.contains(episodeNumber)) {
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
return@setOnClickListener
} else if (downloadedEpisodes.contains(episodeNumber)) {
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
return@setOnClickListener
} else {
fragment.onAnimeEpisodeDownloadClick(episodeNumber)
}
}
}
binding.itemEpisodeDesc.setOnClickListener {
if (binding.itemEpisodeDesc.maxLines == 3)
binding.itemEpisodeDesc.maxLines = 100
@ -239,11 +353,73 @@ class EpisodeAdapter(
binding.itemEpisodeDesc.maxLines = 3
}
}
fun bind(episodeNumber: String, progress: String?) {
if (progress != null) {
binding.itemDownloadStatus.visibility = View.VISIBLE
binding.itemDownloadStatus.text = progress
} else {
binding.itemDownloadStatus.visibility = View.GONE
binding.itemDownloadStatus.text = ""
}
if (activeDownloads.contains(episodeNumber)) {
// Show spinner
binding.itemDownload.setImageResource(R.drawable.ic_sync)
startOrContinueRotation(episodeNumber)
} else if (downloadedEpisodes.contains(episodeNumber)) {
binding.itemDownloadStatus.visibility = View.VISIBLE
// Show checkmark
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
//binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
binding.itemDownload.postDelayed({
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
binding.itemDownload.rotation = 0f
//binding.itemDownload.setColorFilter(typedValue2.data)
}, 1000)
} else {
binding.itemDownloadStatus.visibility = View.GONE
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
binding.itemDownload.rotation = 0f
}
}
private fun startOrContinueRotation(episodeNumber: String) {
if (!isRotationCoroutineRunningFor(episodeNumber)) {
val scope = fragment.lifecycle.coroutineScope
scope.launch {
// Add chapter number to active coroutines set
activeCoroutines.add(episodeNumber)
while (activeDownloads.contains(episodeNumber)) {
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
.setInterpolator(
LinearInterpolator()
).start()
delay(1000)
}
// Remove chapter number from active coroutines set
activeCoroutines.remove(episodeNumber)
}
}
}
private fun isRotationCoroutineRunningFor(episodeNumber: String): Boolean {
return episodeNumber in activeCoroutines
}
}
fun updateType(t: Int) {
type = t
}
private fun bytesToHuman(bytes: Long): String? {
if (bytes < 0) return null
val unit = 1000
if (bytes < unit) return "$bytes B"
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = ("KMGTPE")[exp - 1]
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
}
}

View file

@ -14,7 +14,6 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.hardware.Sensor
import android.hardware.SensorManager
import android.media.AudioManager
import android.media.AudioManager.*
@ -43,11 +42,15 @@ import androidx.core.math.MathUtils.clamp
import androidx.core.view.WindowCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.*
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
import androidx.media3.common.C.TRACK_TYPE_VIDEO
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
@ -58,6 +61,7 @@ import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaSession
import androidx.media3.ui.*
import androidx.media3.ui.CaptionStyleCompat.*
import androidx.mediarouter.app.MediaRouteButton
import ani.dantotsu.*
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
@ -67,12 +71,12 @@ import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.AniSkip.getType
import ani.dantotsu.others.Download.download
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.ResettableTimer
import ani.dantotsu.others.getSerialized
@ -82,6 +86,10 @@ import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import com.bumptech.glide.Glide
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.slider.Slider
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.lagradost.nicehttp.ignoreAllSSLErrors
@ -97,10 +105,9 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@UnstableApi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
class ExoplayerView : AppCompatActivity(), Player.Listener {
class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityListener {
private val resumeWindow = "resumeWindow"
private val resumePosition = "resumePosition"
@ -108,6 +115,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
private val playerOnPlay = "playerOnPlay"
private lateinit var exoPlayer: ExoPlayer
private var castPlayer: CastPlayer? = null
private var castContext: CastContext? = null
private var isCastApiAvailable = false
private lateinit var trackSelector: DefaultTrackSelector
private lateinit var cacheFactory: CacheDataSource.Factory
private lateinit var playbackParameters: PlaybackParameters
@ -145,6 +155,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
private var orientationListener: OrientationEventListener? = null
private var downloadId: String? = null
companion object {
var initialized = false
lateinit var media: Media
@ -328,6 +340,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
setContentView(binding.root)
//Initialize
isCastApiAvailable = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
try {
castContext = CastContext.getSharedInstance(this)
castPlayer = CastPlayer(castContext!!)
castPlayer!!.setSessionAvailabilityListener(this)
} catch (e: Exception) {
isCastApiAvailable = false
}
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars()
@ -387,7 +409,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
println(orientation)
if (orientation in 45..135) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility =
View.VISIBLE
@ -466,15 +487,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (isInitialized) {
isPlayerPlaying = exoPlayer.isPlaying
(exoPlay.drawable as Animatable?)?.start()
if (isPlayerPlaying) {
if (isPlayerPlaying || castPlayer?.isPlaying == true) {
Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay)
exoPlayer.pause()
castPlayer?.pause()
} else {
if (castPlayer?.isPlaying == false && castPlayer?.currentMediaItem != null) {
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
castPlayer?.play()
} else if (!isPlayerPlaying) {
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
exoPlayer.play()
}
}
}
}
// Picture-in-picture
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -812,18 +839,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
audioManager.setStreamVolume(STREAM_MUSIC, volume, 0)
volumeHide()
}
val fastForward = playerView.findViewById<TextView>(R.id.exo_fast_forward_text)
fun fastForward() {
isFastForwarding = true
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
snackString("Playing at ${exoPlayer.playbackParameters.speed}x speed")
fastForward.visibility = View.VISIBLE
fastForward.text = ("${exoPlayer.playbackParameters.speed}x")
}
fun stopFastForward() {
if (isFastForwarding) {
isFastForwarding = false
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
snackString("Playing at default speed: ${exoPlayer.playbackParameters.speed}x")
fastForward.visibility = View.GONE
}
}
@ -925,10 +953,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
episodeArr = episodes.keys.toList()
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
episodeTitleArr = arrayListOf()
episodeTitleArr = arrayListOf<String>()
episodes.forEach {
val episode = it.value
episodeTitleArr.add("${if (!episode.title.isNullOrEmpty() && episode.title != "null") "" else "Episode "}${episode.number}${if (episode.filler) " [Filler]" else ""}${if (!episode.title.isNullOrEmpty() && episode.title != "null") " : " + episode.title else ""}")
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotBlank() && cleanedTitle != "null") ": $cleanedTitle" else ""}")
}
//Episode Change
@ -1074,10 +1103,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
//Cast
if (settings.cast) {
playerView.findViewById<ImageButton>(R.id.exo_cast).apply {
playerView.findViewById<MediaRouteButton>(R.id.exo_cast).apply {
visibility = View.VISIBLE
setSafeOnClickListener {
try {
CastButtonFactory.setUpMediaRouteButton(context, this)
dialogFactory = CustomCastThemeFactory()
} catch (e: Exception) {
isCastApiAvailable = false
}
setOnLongClickListener {
cast()
true
}
}
}
@ -1101,7 +1137,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (settings.cursedSpeeds)
arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
else
arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f)
arrayOf(
0.25f,
0.33f,
0.5f,
0.66f,
0.75f,
1f,
1.15f,
1.25f,
1.33f,
1.5f,
1.66f,
1.75f,
2f
)
val speedsName = speeds.map { "${it}x" }.toTypedArray()
var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed
@ -1156,14 +1206,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
preloading = false
val incognito = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("incognito", false) ?: false
val showProgressDialog =
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
?: true else false
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.auto_update, media.userPreferredName))
.setMessage(getString(R.string.incognito_will_not_update))
.apply {
if (incognito) {
setMessage(getString(R.string.incognito_will_not_update))
}
setOnCancelListener { hideSystemBars() }
setCancelable(false)
setPositiveButton(getString(R.string.yes)) { dialog, _ ->
@ -1232,7 +1286,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (subtitle?.type == SubtitleType.UNKNOWN) {
val context = this
runBlocking {
val type = SubtitleDownloader.downloadSubtitles(context, subtitle!!.file.url)
val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
val fileUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration
.Builder(fileUri)
@ -1250,8 +1304,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
println("sub: $sub")
} else {
val subUri = Uri.parse((subtitle!!.file.url))
sub = MediaItem.SubtitleConfiguration
.Builder(Uri.parse(subtitle!!.file.url))
.Builder(subUri)
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle?.type) {
@ -1270,18 +1325,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
ext.onVideoPlayed(video)
}
val but = playerView.findViewById<ImageButton>(R.id.exo_download)
if (video?.format == VideoType.CONTAINER || (loadData<Int>("settings_download_manager")
?: 0) != 0
) {
but.visibility = View.VISIBLE
but.setOnClickListener {
download(this, episode, animeTitle.text.toString())
}
} else {
but.visibility = View.GONE
}
val simpleCache = VideoCache.getInstance(this)
val httpClient = okHttpClient.newBuilder().apply {
ignoreAllSSLErrors()
@ -1298,10 +1341,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
dataSource
}
val dafuckDataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
cacheFactory = CacheDataSource.Factory().apply {
setCache(simpleCache)
setCache(Helper.getSimpleCache(this@ExoplayerView))
if (ext.server.offline) {
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
} else {
setUpstreamDataSourceFactory(dataSourceFactory)
}
setCacheWriteDataSinkFactory(null)
}
val mimeType = when (video?.format) {
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
@ -1309,6 +1358,20 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
else -> MimeTypes.APPLICATION_MP4
}
val downloadedMediaItem = if (ext.server.offline) {
val key = ext.server.name
downloadId = getSharedPreferences(getString(R.string.anime_downloads), MODE_PRIVATE)
.getString(key, null)
if (downloadId != null) {
Helper.downloadManager(this)
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
} else {
snackString("Download not found")
null
}
} else null
mediaItem = if (downloadedMediaItem == null) {
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
logger("url: ${video!!.file.url}")
logger("mimeType: $mimeType")
@ -1317,7 +1380,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
builder.setSubtitleConfigurations(listofnotnullsubs)
}
mediaItem = builder.build()
builder.build()
} else {
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
if (sub != null) {
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build()
addedSubsDownloadedMediaItem.setSubtitleConfigurations(immutableListOf(addLanguage))
episode.selectedSubtitle = 0
}
addedSubsDownloadedMediaItem.build()
}
//Source
exoSource.setOnClickListener {
@ -1479,7 +1553,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
super.onPause()
orientationListener?.disable()
if (isInitialized) {
if (castPlayer?.isPlaying == false) {
playerView.player?.pause()
}
saveData(
"${media.id}_${media.anime!!.selectedEpisode}",
exoPlayer.currentPosition,
@ -1500,7 +1576,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
override fun onStop() {
if (castPlayer?.isPlaying == false) {
playerView.player?.pause()
}
super.onStop()
}
@ -1793,7 +1871,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
// Enter PiP Mode
@Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.N)
private fun enterPipMode() {
wasPlaying = isPlayerPlaying
if (!pipEnabled) return
@ -1866,6 +1943,55 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
}
private fun startCastPlayer() {
if (!isCastApiAvailable) {
snackString("Cast API not available")
return
}
//make sure mediaItem is initialized and castPlayer is not null
if (!this::mediaItem.isInitialized || castPlayer == null) return
castPlayer?.setMediaItem(mediaItem)
castPlayer?.prepare()
playerView.player = castPlayer
exoPlayer.stop()
castPlayer?.addListener(object : Player.Listener {
//if the player is paused changed, we want to update the UI
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (playWhenReady) {
(exoPlay.drawable as Animatable?)?.start()
Glide.with(this@ExoplayerView)
.load(R.drawable.anim_play_to_pause)
.into(exoPlay)
} else {
(exoPlay.drawable as Animatable?)?.start()
Glide.with(this@ExoplayerView)
.load(R.drawable.anim_pause_to_play)
.into(exoPlay)
}
}
})
}
private fun startExoPlayer() {
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
playerView.player = exoPlayer
castPlayer?.stop()
}
override fun onCastSessionAvailable() {
if (isCastApiAvailable) {
startCastPlayer()
}
}
override fun onCastSessionUnavailable() {
startExoPlayer()
}
@SuppressLint("ViewConstructor")
class ExtendedTimeBar(
context: Context,

View file

@ -1,6 +1,7 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
@ -20,17 +21,21 @@ import ani.dantotsu.*
import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.DecimalFormat
class SelectorDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSelectorBinding? = null
private val binding get() = _binding!!
@ -42,6 +47,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
private var makeDefault = false
private var selected: String? = null
private var launch: Boolean? = null
private var isDownloadMenu: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -49,6 +55,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
selected = it.getString("server")
launch = it.getBoolean("launch", true)
prevEpisode = it.getString("prev")
isDownloadMenu = it.getBoolean("isDownload")
}
}
@ -76,8 +83,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
episode = ep
if (ep != null) {
if (isDownloadMenu == true) {
binding.selectorMakeDefault.visibility = View.GONE
}
if (selected != null) {
if (selected != null && isDownloadMenu == false) {
binding.selectorListContainer.visibility = View.GONE
binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorAutoText.text = selected
@ -95,7 +105,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun load() {
val size =
if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
ep.extractors?.firstOrNull()?.videos?.size
} else {
ep.extractors?.find { it.server.name == selected }?.videos?.size
}
if (size != null && size >= media!!.selected!!.video) {
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
selected
@ -145,6 +160,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
ep.extractorCallback = {
scope.launch {
adapter.add(it)
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
}
}
model.getEpisode().observe(this) {
@ -164,6 +182,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} else {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
adapter.addAll(ep.extractors)
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
binding.selectorProgressBar.visibility = View.GONE
}
}
@ -179,7 +200,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
prevEpisode = null
dismiss()
if (launch!!) {
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java)
ExoplayerView.media = media
@ -214,7 +235,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val extractor = links[position]
holder.binding.streamName.text = extractor.server.name
holder.binding.streamName.text = ""//extractor.server.name
holder.binding.streamName.visibility = View.GONE
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
@ -235,6 +257,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
notifyItemRangeInserted(0, extractors.size)
}
fun performClick(position: Int) {
try { //bandaid fix for crash
val extractor = links[position]
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = 0
startExoplayer(media!!)
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().recordException(e)
}
}
private inner class StreamViewHolder(val binding: ItemStreamBinding) :
RecyclerView.ViewHolder(binding.root)
}
@ -256,23 +290,106 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding
val video = extractor.videos[position]
binding.urlQuality.text =
if (video.quality != null) "${video.quality}p" else "Default Quality"
binding.urlNote.text = video.extraNote ?: ""
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
if (isDownloadMenu == true) {
binding.urlDownload.visibility = View.VISIBLE
} else {
binding.urlDownload.visibility = View.GONE
}
binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
val selectedVideo =
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
val subtitles = extractor.subtitles
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
if (subtitles.isNotEmpty()) {
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle")
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { dialog, which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton("Download") { _, _ ->
dialog?.dismiss()
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
currActivity()!!,
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
} else {
snackString("No Video Selected")
}
}
.setNegativeButton("Skip") { dialog, _ ->
subtitleToDownload = null
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
currActivity()!!,
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
} else {
snackString("No Video Selected")
}
dialog.dismiss()
}
.setNeutralButton("Cancel") { dialog, _ ->
subtitleToDownload = null
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else {
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
requireActivity(),
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
} else {
snackString("No Video Selected")
}
}
dismiss()
}
binding.urlDownload.setOnLongClickListener {
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
if ((loadData<Int>("settings_download_manager") ?: 0) != 0) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position
download(
requireActivity(),
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
media!!.userPreferredName
)
dismiss()
} else {
snackString("No Download Manager Selected")
}
true
}
if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
@ -281,12 +398,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
"#.##"
).format(video.size ?: 0).toString() + " MB"))
} else {
binding.urlQuality.text = "Multi Quality"
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
binding.urlDownload.visibility = View.GONE
}
}
binding.urlNote.visibility = View.VISIBLE
binding.urlNote.text = video.format.name
binding.urlQuality.text = extractor.server.name
}
override fun getItemCount(): Int = extractor.videos.size
@ -295,6 +410,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setSafeOnClickListener {
if (isDownloadMenu == true) {
binding.urlDownload.performClick()
return@setSafeOnClickListener
}
tryWith(true) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
extractor.server.name
@ -326,13 +445,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun newInstance(
server: String? = null,
la: Boolean = true,
prev: String? = null
prev: String? = null,
isDownload: Boolean
): SelectorDialogFragment =
SelectorDialogFragment().apply {
arguments = Bundle().apply {
putString("server", server)
putBoolean("launch", la)
putString("prev", prev)
putBoolean("isDownload", isDownload)
}
}
}

View file

@ -6,7 +6,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.fragment.app.activityViewModels
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment
@ -60,6 +62,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
)
)
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding
if (position == 0) {

View file

@ -141,8 +141,8 @@ class MangaChapterAdapter(
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>()
val typedValue1 = TypedValue()
val typedValue2 = TypedValue()
private val typedValue1 = TypedValue()
private val typedValue2 = TypedValue()
fun bind(chapterNumber: String, progress: String?) {
if (progress != null) {
binding.itemChapterTitle.visibility = View.VISIBLE
@ -167,6 +167,7 @@ class MangaChapterAdapter(
} else {
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
binding.itemDownload.rotation = 0f
}
}
@ -235,7 +236,7 @@ class MangaChapterAdapter(
input.maxValue = itemCount - bindingAdapterPosition
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { dialog, which ->
alertDialog.setPositiveButton("OK") { _, _ ->
downloadNChaptersFrom(bindingAdapterPosition, input.value)
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }

View file

@ -5,16 +5,25 @@ import java.util.regex.Pattern
class MangaNameAdapter {
companion object {
const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
fun findChapterNumber(text: String): Float? {
val regex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)"
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
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

@ -2,34 +2,41 @@ package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.NumberPicker
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class MangaReadAdapter(
private val media: Media,
private val fragment: MangaReadFragment,
@ -47,6 +54,8 @@ class MangaReadAdapter(
return ViewHolder(bind)
}
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
@ -60,7 +69,17 @@ class MangaReadAdapter(
null
)
}
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
)
?.getBoolean("offlineMode", false) == true
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection
var source =
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
@ -117,7 +136,7 @@ class MangaReadAdapter(
}
}
//Subscription
//Grids
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
@ -136,24 +155,103 @@ class MangaReadAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id))
}
//Icons
binding.animeSourceGrid.visibility = View.GONE
binding.animeNestedButton.setOnClickListener {
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
binding.animeSourceTop.setOnClickListener {
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener {
reversed = !reversed
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed)
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true
}
binding.animeScanlatorTop.setOnClickListener {
val dialogView =
//Grids
dialogBinding.animeSourceGrid.visibility = View.GONE
var selected = when (style) {
0 -> dialogBinding.animeSourceList
1 -> dialogBinding.animeSourceCompact
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.text = "List"
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast("WebView not installed")
}
//start CookieCatcher activity
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
ContextCompat.startActivity(fragment.requireContext(), intent, null)
}
}
}
//Multi download
dialogBinding.downloadNo.text = "0"
dialogBinding.animeDownloadTop.setOnClickListener {
//Alert dialog asking for the number of chapters to download
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
alertDialog.setTitle("Multi Chapter Downloader")
alertDialog.setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext())
input.minValue = 1
input.maxValue = 20
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { _, _ ->
dialogBinding.downloadNo.text = "${input.value}"
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
val dialog = alertDialog.show()
dialog.window?.setDimAmount(0.8f)
}
//Scanlator
dialogBinding.animeScanlatorContainer.visibility =
if (options.count() > 1) View.VISIBLE else View.GONE
dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 =
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
val checkboxContainer =
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
// Dynamically add checkboxes
options.forEach { option ->
val checkBox = CheckBox(currContext()).apply {
text = option
@ -170,8 +268,8 @@ class MangaReadAdapter(
// Create AlertDialog
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
.setView(dialogView)
.setPositiveButton("OK") { dialog, which ->
.setView(dialogView2)
.setPositiveButton("OK") { _, _ ->
//add unchecked to hidden
hiddenScanlators.clear()
for (i in 0 until checkboxContainer.childCount) {
@ -188,46 +286,25 @@ class MangaReadAdapter(
dialog.window?.setDimAmount(0.8f)
}
binding.animeDownloadTop.setOnClickListener {
//Alert dialog asking for the number of chapters to download
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
alertDialog.setTitle("Multi Chapter Downloader")
alertDialog.setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext())
input.minValue = 1
input.maxValue = 20
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { dialog, which ->
fragment.multiDownload(input.value)
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (dialogBinding.downloadNo.text != "0") {
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
val dialog = alertDialog.show()
dialog.window?.setDimAmount(0.8f)
if (refresh) fragment.loadChapters(source, true)
}
var selected = when (style) {
0 -> binding.animeSourceList
1 -> binding.animeSourceCompact
else -> binding.animeSourceList
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadChapters(source, true)
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
.setOnCancelListener {
if (refresh) fragment.loadChapters(source, true)
}
binding.animeSourceList.setOnClickListener {
selected(it as ImageView)
style = 0
fragment.onIconPressed(style, reversed)
.create()
nestedDialog?.show()
}
binding.animeSourceCompact.setOnClickListener {
selected(it as ImageView)
style = 1
fragment.onIconPressed(style, reversed)
}
//Chapter Handling
handleChapters()
}
@ -259,6 +336,7 @@ class MangaReadAdapter(
0
)
}
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
val startChapterString = if (startChapter != null) {
@ -370,7 +448,7 @@ class MangaReadAdapter(
}
}
fun setLanguageList(lang: Int, source: Int) {
private fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (mangaReadSources is MangaSources) {
val parser = mangaReadSources[source] as? DynamicMangaParser
@ -385,12 +463,16 @@ class MangaReadAdapter(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
binding?.animeSourceLanguage?.setAdapter(
ArrayAdapter(
val adapter = ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
parser.extension.sources.map { it.lang })
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)
}
}

View file

@ -29,7 +29,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton
@ -37,6 +37,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser
@ -166,7 +167,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
chapterAdapter =
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
for (download in downloadManager.mangaDownloads) {
for (download in downloadManager.mangaDownloadedTypes) {
chapterAdapter.stopDownload(download.chapter)
}
@ -202,19 +203,24 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val selected = media.userProgress
val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language
val progressChapterIndex = chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected }?:0
if (progressChapterIndex < 0 || n < 1) return
val chaptersToDownload = chapters?.subList(
progressChapterIndex + 1,
progressChapterIndex + n + 1
)
if (chaptersToDownload != null) {
val progressChapterIndex = (chapters?.indexOfFirst {
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} ?: 0) + 1
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
// Calculate the end index
val endIndex = minOf(progressChapterIndex + n, chapters.size)
//make sure there are enough chapters
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
for (chapter in chaptersToDownload) {
onMangaChapterDownloadClick(chapter.title!!)
}
}
}
private fun updateChapters() {
val loadedChapters = model.getMangaChapters().value
@ -260,7 +266,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
}
fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
private fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
val scanlators = mutableListOf<String>()
if (chap != null) {
val chapters = chap.values
@ -355,19 +361,20 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (show) View.GONE else View.VISIBLE
}
}
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
val names =
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext())
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
@ -382,10 +389,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
.addToBackStack(null)
.commit()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
.setOnDismissListener {
if (!itemSelected) {
changeUIVisibility(true)
return@setNegativeButton
}
}
.show()
dialog.window?.setDimAmount(0.8f)
@ -440,7 +447,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.nameMAL ?: media.nameRomaji,
title = media.mainName(),
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
@ -481,10 +488,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
fun onMangaChapterRemoveDownloadClick(i: String) {
downloadManager.removeDownload(
Download(
media.nameMAL ?: media.nameRomaji,
DownloadedType(
media.mainName(),
i,
Download.Type.MANGA
DownloadedType.Type.MANGA
)
)
chapterAdapter.deleteDownload(i)
@ -499,10 +506,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Remove the download from the manager and update the UI
downloadManager.removeDownload(
Download(
media.nameMAL ?: media.nameRomaji,
DownloadedType(
media.mainName(),
i,
Download.Type.MANGA
DownloadedType.Type.MANGA
)
)
chapterAdapter.purgeDownload(i)

View file

@ -89,7 +89,8 @@ abstract class BaseImageAdapter(
}
} else {
val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
override fun onSingleClick(event: MotionEvent) = activity.handleController()
override fun onSingleClick(event: MotionEvent) =
activity.handleController(event = event)
})
view.findViewById<View>(R.id.imgProgCover).apply {
setOnTouchListener { _, event ->
@ -112,6 +113,9 @@ abstract class BaseImageAdapter(
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
}
abstract fun isZoomed(): Boolean
abstract fun setZoom(zoom: Float)
abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object {

View file

@ -50,7 +50,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
if (model.loadMangaChapterImages(
chp,
m.selected!!,
m.nameMAL ?: m.nameRomaji
m.mainName()
)
) {
val activity = currActivity()

View file

@ -91,4 +91,16 @@ open class ImageAdapter(
}
override fun getItemCount(): Int = images.size
override fun isZoomed(): Boolean {
val imageView =
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
return imageView.scale > imageView.minScale
}
override fun setZoom(zoom: Float) {
val imageView =
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
imageView.setScaleAndCenter(zoom, imageView.center)
}
}

View file

@ -3,8 +3,10 @@ package ani.dantotsu.media.manga.mangareader
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.content.res.Resources
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
@ -218,7 +220,7 @@ class MangaReaderActivity : AppCompatActivity() {
val mangaSources = MangaSources
val scope = lifecycleScope
scope.launch(Dispatchers.IO) {
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow, this@MangaReaderActivity)
}
model.mangaReadSources = mangaSources
} else {
@ -253,7 +255,8 @@ class MangaReaderActivity : AppCompatActivity() {
}
showProgressDialog =
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
?: true else false
//Chapter Change
fun change(index: Int) {
@ -358,7 +361,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages(
chapter,
media.selected!!,
media.nameMAL ?: media.nameRomaji
media.mainName()
)
}
}
@ -701,8 +704,60 @@ class MangaReaderActivity : AppCompatActivity() {
goneTimer.schedule(timerTask, controllerDuration)
}
fun handleController(shouldShow: Boolean? = null) {
enum class pressPos {
LEFT, RIGHT, CENTER
}
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
var pressLocation = pressPos.CENTER
if (!sliding) {
if (event != null && settings.default.layout == PAGED) {
if (event.action != MotionEvent.ACTION_UP) return
val x = event.rawX.toInt()
val y = event.rawY.toInt()
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 (settings.default.direction == RIGHT_TO_LEFT) {
pressPos.RIGHT
} else {
pressPos.LEFT
}
}
//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 (settings.default.direction == RIGHT_TO_LEFT) {
pressPos.LEFT
} else {
pressPos.RIGHT
}
}
}
// if pressLocation is left or right go to previous or next page (paged mode only)
if (pressLocation == pressPos.LEFT) {
if (binding.mangaReaderPager.currentItem > 0) {
//if the current images zoomed in, go back to normal before going to previous page
if (imageAdapter?.isZoomed() == true) {
imageAdapter?.setZoom(1f)
}
binding.mangaReaderPager.currentItem -= 1
return
}
} else if (pressLocation == pressPos.RIGHT) {
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
//if the current images zoomed in, go back to normal before going to next page
if (imageAdapter?.isZoomed() == true) {
imageAdapter?.setZoom(1f)
}
//if right to left, go to previous page
binding.mangaReaderPager.currentItem += 1
return
}
}
if (!settings.showSystemBars) {
hideBars()
checkNotch()
@ -786,7 +841,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!,
media.nameMAL ?: media.nameRomaji,
media.mainName(),
false
)
loading = false
@ -795,17 +850,28 @@ class MangaReaderActivity : AppCompatActivity() {
private fun progress(runnable: Runnable) {
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
showProgressDialog =
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
?: true else false
if (showProgressDialog) {
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
checkbox.setOnCheckedChangeListener { _, isChecked ->
saveData("${media.id}_progressDialog", isChecked)
saveData("${media.id}_progressDialog", !isChecked)
showProgressDialog = !isChecked
}
val incognito =
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("incognito", false) ?: false
AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.title_update_progress))
.apply {
if (incognito) {
setMessage(getString(R.string.incognito_will_not_update))
}
}
.setView(dialogView)
.setCancelable(false)
.setPositiveButton(getString(R.string.yes)) { dialog, _ ->

View file

@ -22,7 +22,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton
@ -67,7 +67,7 @@ class NovelReadFragment : Fragment(),
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
Log.e("downloadTrigger", novelDownloadPackage.link)
val downloadTask = NovelDownloaderService.DownloadTask(
title = media.nameMAL ?: media.nameRomaji,
title = media.mainName(),
chapter = novelDownloadPackage.novelName,
downloadLink = novelDownloadPackage.link,
originalLink = novelDownloadPackage.originalLink,
@ -92,16 +92,16 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
if (downloadsManager.queryDownload(
Download(
media.nameMAL ?: media.nameRomaji,
DownloadedType(
media.mainName(),
novel.name,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
) {
val file = File(
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.novelLocation}/${media.nameMAL ?: media.nameRomaji}/${novel.name}/0.epub"
"${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
)
if (!file.exists()) return false
val fileUri = FileProvider.getUriForFile(
@ -124,10 +124,10 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheck(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
return downloadsManager.queryDownload(
Download(
media.nameMAL ?: media.nameRomaji,
DownloadedType(
media.mainName(),
novel.name,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
}
@ -135,10 +135,10 @@ class NovelReadFragment : Fragment(),
override fun deleteDownload(novel: ShowResponse) {
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.removeDownload(
Download(
media.nameMAL ?: media.nameRomaji,
DownloadedType(
media.mainName(),
novel.name,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
}
@ -247,8 +247,7 @@ class NovelReadFragment : Fragment(),
headerAdapter.progress?.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.IO) {
if (auto || query == "") model.autoSearchNovels(media)
//else model.searchNovels(query, source)
else model.autoSearchNovels(media) //testing
else model.searchNovels(query, source)
}
searching = true
if (save) {

View file

@ -42,7 +42,11 @@ class NovelResponseAdapter(
.into(binding.itemEpisodeImage)
val typedValue = TypedValue()
fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true)
fragment.requireContext().theme?.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
typedValue,
true
)
val color = typedValue.data
binding.itemEpisodeTitle.text = novel.name
@ -98,14 +102,19 @@ class NovelResponseAdapter(
}
binding.root.setOnLongClickListener {
val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme)
val builder = androidx.appcompat.app.AlertDialog.Builder(
fragment.requireContext(),
R.style.DialogTheme
)
builder.setTitle("Delete ${novel.name}?")
builder.setMessage("Are you sure you want to delete ${novel.name}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadedCheckCallback.deleteDownload(novel)
deleteDownload(novel.link)
snackString("Deleted ${novel.name}")
if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) {
if (binding.itemEpisodeFiller.text.toString()
.contains("Download", ignoreCase = true)
) {
binding.itemEpisodeFiller.text = ""
}
}

View file

@ -15,6 +15,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.OvershootInterpolator
import android.webkit.WebView
import android.widget.AdapterView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
@ -36,7 +37,7 @@ import ani.dantotsu.saveData
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.CurrentNovelReaderSettings
import ani.dantotsu.settings.CurrentReaderSettings
import ani.dantotsu.settings.NovelReaderSettings
import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
@ -62,7 +63,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
private lateinit var binding: ActivityNovelReaderBinding
private val scope = lifecycleScope
lateinit var settings: NovelReaderSettings
lateinit var settings: ReaderSettings
private lateinit var uiSettings: UserInterfaceSettings
private var notchHeight: Int? = null
@ -139,16 +140,31 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
}
@SuppressLint("WebViewApiAvailability")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//check for supported webview
val webViewVersion = WebViewCompat.getCurrentWebViewPackage(this)?.versionName
val webViewVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WebView.getCurrentWebViewPackage()?.versionName
} else {
WebViewCompat.getCurrentWebViewPackage(this)?.versionName
}
val firstVersion = webViewVersion?.split(".")?.firstOrNull()?.toIntOrNull()
if (webViewVersion == null || firstVersion == null || firstVersion < 87) {
Toast.makeText(this, "Please update WebView from PlayStore", Toast.LENGTH_LONG).show()
val text = if (webViewVersion == null) {
"Could not find webView installed"
} else if (firstVersion == null) {
"Could not find WebView Version Number: $webViewVersion"
} else if (firstVersion < 87) { //false positive?
"Webview Versiom: $firstVersion. PLease update"
} else {
"Please update WebView from PlayStore"
}
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
//open playstore
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
intent.data =
Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
startActivity(intent)
//stop reader
finish()
@ -159,9 +175,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
ThemeManager(this).applyTheme()
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
setContentView(binding.root)
settings = loadData("novel_reader_settings", this)
?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) }
settings = loadData("reader_settings", this)
?: ReaderSettings().apply { saveData("reader_settings", this) }
uiSettings = loadData("ui_settings", this)
?: UserInterfaceSettings().also { saveData("ui_settings", it) }
@ -271,7 +286,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
binding.bookReader.getAppearance {
currentTheme = it
themes.add(0, it)
settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default
settings.defaultLN =
loadData("${sanitizedBookId}_current_settings") ?: settings.defaultLN
applySettings()
}
@ -323,7 +339,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP)
if (!settings.default.volumeButtons)
if (!settings.defaultLN.volumeButtons)
return false
if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeUp?.invoke()
@ -333,7 +349,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
if (!settings.default.volumeButtons)
if (!settings.defaultLN.volumeButtons)
return false
if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeDown?.invoke()
@ -349,13 +365,18 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
fun applySettings() {
saveData("${sanitizedBookId}_current_settings", settings.default)
saveData("${sanitizedBookId}_current_settings", settings.defaultLN)
hideBars()
if (settings.defaultLN.useOledTheme) {
themes.forEach { theme ->
theme.darkBg = Color.parseColor("#000000")
}
}
currentTheme =
themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) }
themes.first { it.name.equals(settings.defaultLN.currentThemeName, ignoreCase = true) }
when (settings.default.layout) {
when (settings.defaultLN.layout) {
CurrentNovelReaderSettings.Layouts.PAGED -> {
currentTheme?.flow = ReaderFlow.PAGINATED
}
@ -366,22 +387,22 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
when (settings.default.dualPageMode) {
when (settings.defaultLN.dualPageMode) {
CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1
CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2
CurrentReaderSettings.DualPageModes.Force -> requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
currentTheme?.lineHeight = settings.default.lineHeight
currentTheme?.gap = settings.default.margin
currentTheme?.maxInlineSize = settings.default.maxInlineSize
currentTheme?.maxBlockSize = settings.default.maxBlockSize
currentTheme?.useDark = settings.default.useDarkTheme
currentTheme?.lineHeight = settings.defaultLN.lineHeight
currentTheme?.gap = settings.defaultLN.margin
currentTheme?.maxInlineSize = settings.defaultLN.maxInlineSize
currentTheme?.maxBlockSize = settings.defaultLN.maxBlockSize
currentTheme?.useDark = settings.defaultLN.useDarkTheme
currentTheme?.let { binding.bookReader.setAppearance(it) }
if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (settings.defaultLN.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

View file

@ -30,8 +30,7 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as NovelReaderActivity
val settings = activity.settings.default
val settings = activity.settings.defaultLN
val themeLabels = activity.themes.map { it.name }
binding.themeSelect.adapter =
NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels)
@ -49,7 +48,11 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
binding.useOledTheme.isChecked = settings.useOledTheme
binding.useOledTheme.setOnCheckedChangeListener { _, isChecked ->
settings.useOledTheme = isChecked
activity.applySettings()
}
val layoutList = listOf(
binding.paged,
binding.continuous
@ -173,6 +176,20 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
binding.maxBlockSize.setText(value.toString())
activity.applySettings()
}
}
binding.incrementMaxBlockSize.setOnClickListener {
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
settings.maxBlockSize = value + 10
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
activity.applySettings()
}
binding.decrementMaxBlockSize.setOnClickListener {
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
settings.maxBlockSize = value - 10
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
activity.applySettings()
}
binding.useDarkTheme.isChecked = settings.useDarkTheme

View file

@ -1,23 +1,29 @@
package ani.dantotsu.media.user
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@ -74,13 +80,16 @@ class ListActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
}
setContentView(binding.root)
val anime = intent.getBooleanExtra("anime", true)
binding.listTitle.text =
intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@ListActivity.selectedTabIdx = tab?.position ?: 0
@ -145,7 +154,8 @@ class ListActivity : AppCompatActivity() {
R.id.release -> "release"
else -> null
}
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putString("sort_order", sort)?.apply()
binding.listProgressBar.visibility = View.VISIBLE
binding.listViewPager.adapter = null
scope.launch {

View file

@ -46,7 +46,7 @@ class ListFragment : Fragment() {
binding.listRecyclerView.layoutManager =
GridLayoutManager(
requireContext(),
if (grid!!) (screenWidth / 124f).toInt() else 1
if (grid!!) (screenWidth / 120f).toInt() else 1
)
binding.listRecyclerView.adapter = adapter
}

View file

@ -1,11 +1,13 @@
package ani.dantotsu.offline
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentOfflineBinding
import ani.dantotsu.isOnline
import ani.dantotsu.navBarHeight
@ -13,6 +15,7 @@ import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight
class OfflineFragment : Fragment() {
private var offline = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -23,6 +26,11 @@ class OfflineFragment : Fragment() {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("offlineMode", false) ?: false
binding.noInternet.text =
if (offline) "Offline Mode" else getString(R.string.no_internet)
binding.refreshButton.visibility = if (offline) View.GONE else View.VISIBLE
binding.refreshButton.setOnClickListener {
if (isOnline(requireContext())) {
startMainActivity(requireActivity())
@ -30,4 +38,10 @@ class OfflineFragment : Fragment() {
}
return binding.root
}
override fun onResume() {
super.onResume()
offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("offlineMode", false) ?: false
}
}

View file

@ -2,3 +2,4 @@ package ani.dantotsu.others
const val DisabledReports = false
//Setting this to false, will allow sending crash reports to Dantotsu's Firebase Crashlytics
//if you want a custom build without crash reporting, set this to true

View file

@ -6,26 +6,38 @@ import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.tryWithSuspend
import com.google.gson.Gson
import com.lagradost.nicehttp.NiceResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
object Kitsu {
private suspend fun getKitsuData(query: String): KitsuResponse? {
val headers = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json",
"Accept-Encoding" to "gzip, deflate",
"Accept-Language" to "en-US,en;q=0.5",
"Host" to "kitsu.io",
"Connection" to "keep-alive",
"DNT" to "1",
"Origin" to "https://kitsu.io"
"Origin" to "https://kitsu.io",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "cross-site",
)
val json = tryWithSuspend {
client.post(
val response = tryWithSuspend {
val res = client.post(
"https://kitsu.io/api/graphql",
headers,
data = mapOf("query" to query)
)
res
}
return json?.parsed()
val json = decodeToString(response)
val gson = Gson()
return gson.fromJson(json, KitsuResponse::class.java)
}
suspend fun getKitsuEpisodesDetails(media: Media): Map<String, Episode>? {
@ -54,14 +66,14 @@ query {
}
}
}
}"""
}""".trimIndent()
val result = getKitsuData(query) ?: return null
logger("Kitsu : result=$result", print)
media.idKitsu = result.data?.lookupMapping?.id
return (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
val num = ep?.num?.toString() ?: return@mapNotNull null
val a = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
val num = ep?.number?.toString() ?: return@mapNotNull null
num to Episode(
number = num,
title = ep.titles?.canonical,
@ -69,6 +81,25 @@ query {
thumb = FileUrl[ep.thumbnail?.original?.url],
)
}.toMap()
logger("Kitsu : a=$a", print)
return a
}
private fun decodeToString(res: NiceResponse?): String? {
return when (res?.headers?.get("Content-Encoding")) {
"gzip" -> {
res.body.byteStream().use { inputStream ->
GZIPInputStream(inputStream).use { gzipInputStream ->
InputStreamReader(gzipInputStream).use { reader ->
reader.readText()
}
}
}
}
else -> {
res?.body?.string()
}
}
}
@Serializable
@ -93,7 +124,7 @@ query {
@Serializable
data class Node(
@SerialName("number") val num: Long? = null,
@SerialName("number") val number: Int? = null,
@SerialName("titles") val titles: Titles? = null,
@SerialName("description") val description: Description? = null,
@SerialName("thumbnail") val thumbnail: Thumbnail? = null

View file

@ -6,25 +6,116 @@ class LanguageMapper {
fun mapLanguageCodeToName(code: String): String {
return when (code) {
"all" -> "Multi"
"af" -> "Afrikaans"
"am" -> "Amharic"
"ar" -> "Arabic"
"as" -> "Assamese"
"az" -> "Azerbaijani"
"be" -> "Belarusian"
"bg" -> "Bulgarian"
"bn" -> "Bengali"
"bs" -> "Bosnian"
"ca" -> "Catalan"
"ceb" -> "Cebuano"
"cs" -> "Czech"
"da" -> "Danish"
"de" -> "German"
"el" -> "Greek"
"en" -> "English"
"en-Us" -> "English (United States)"
"eo" -> "Esperanto"
"es" -> "Spanish"
"es-419" -> "Spanish (Latin America)"
"et" -> "Estonian"
"eu" -> "Basque"
"fa" -> "Persian"
"fi" -> "Finnish"
"fil" -> "Filipino"
"fo" -> "Faroese"
"fr" -> "French"
"ga" -> "Irish"
"gn" -> "Guarani"
"gu" -> "Gujarati"
"ha" -> "Hausa"
"he" -> "Hebrew"
"hi" -> "Hindi"
"hr" -> "Croatian"
"ht" -> "Haitian Creole"
"hu" -> "Hungarian"
"hy" -> "Armenian"
"id" -> "Indonesian"
"ig" -> "Igbo"
"is" -> "Icelandic"
"it" -> "Italian"
"ja" -> "Japanese"
"jv" -> "Javanese"
"ka" -> "Georgian"
"kk" -> "Kazakh"
"km" -> "Khmer"
"kn" -> "Kannada"
"ko" -> "Korean"
"ku" -> "Kurdish"
"ky" -> "Kyrgyz"
"la" -> "Latin"
"lb" -> "Luxembourgish"
"lo" -> "Lao"
"lt" -> "Lithuanian"
"lv" -> "Latvian"
"mg" -> "Malagasy"
"mi" -> "Maori"
"mk" -> "Macedonian"
"ml" -> "Malayalam"
"mn" -> "Mongolian"
"mo" -> "Moldovan"
"mr" -> "Marathi"
"ms" -> "Malay"
"mt" -> "Maltese"
"my" -> "Burmese"
"ne" -> "Nepali"
"nl" -> "Dutch"
"no" -> "Norwegian"
"ny" -> "Chichewa"
"pl" -> "Polish"
"pt" -> "Portuguese"
"pt-BR" -> "Portuguese (Brazil)"
"pt-PT" -> "Portuguese (Portugal)"
"ps" -> "Pashto"
"ro" -> "Romanian"
"rm" -> "Romansh"
"ru" -> "Russian"
"sd" -> "Sindhi"
"sh" -> "Serbo-Croatian"
"si" -> "Sinhala"
"sk" -> "Slovak"
"sl" -> "Slovenian"
"sm" -> "Samoan"
"sn" -> "Shona"
"so" -> "Somali"
"sq" -> "Albanian"
"sr" -> "Serbian"
"st" -> "Southern Sotho"
"sv" -> "Swedish"
"sw" -> "Swahili"
"ta" -> "Tamil"
"te" -> "Telugu"
"tg" -> "Tajik"
"th" -> "Thai"
"ti" -> "Tigrinya"
"tk" -> "Turkmen"
"tl" -> "Tagalog"
"to" -> "Tongan"
"tr" -> "Turkish"
"uk" -> "Ukrainian"
"ur" -> "Urdu"
"uz" -> "Uzbek"
"vi" -> "Vietnamese"
"yo" -> "Yoruba"
"zh" -> "Chinese"
"zh-Hans" -> "Chinese (Simplified)"
else -> ""
"zh-Hant" -> "Chinese (Traditional)"
"zh-Habt" -> "Chinese (Hakka)"
"zu" -> "Zulu"
else -> code
}
}

View file

@ -50,7 +50,7 @@ object MalScraper {
}
}
} catch (e: Exception) {
if (e is TimeoutCancellationException) snackString(currContext()?.getString(R.string.error_loading_mal_data))
// if (e is TimeoutCancellationException) snackString(currContext()?.getString(R.string.error_loading_mal_data))
}
}
}

View file

@ -0,0 +1,112 @@
package ani.dantotsu.others
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
abstract class SharedPreferenceLiveData<T>(
val sharedPrefs: SharedPreferences,
val key: String,
val defValue: T
) : LiveData<T>() {
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == this.key) {
value = getValueFromPreferences(key, defValue)
}
}
abstract fun getValueFromPreferences(key: String, defValue: T): T
override fun onActive() {
super.onActive()
value = getValueFromPreferences(key, defValue)
sharedPrefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}
override fun onInactive() {
sharedPrefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
super.onInactive()
}
}
class SharedPreferenceIntLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Int) :
SharedPreferenceLiveData<Int>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Int): Int =
sharedPrefs.getInt(key, defValue)
}
class SharedPreferenceStringLiveData(
sharedPrefs: SharedPreferences,
key: String,
defValue: String
) :
SharedPreferenceLiveData<String>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: String): String =
sharedPrefs.getString(key, defValue).toString()
}
class SharedPreferenceBooleanLiveData(
sharedPrefs: SharedPreferences,
key: String,
defValue: Boolean
) :
SharedPreferenceLiveData<Boolean>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean =
sharedPrefs.getBoolean(key, defValue)
}
class SharedPreferenceFloatLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Float) :
SharedPreferenceLiveData<Float>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Float): Float =
sharedPrefs.getFloat(key, defValue)
}
class SharedPreferenceLongLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Long) :
SharedPreferenceLiveData<Long>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Long): Long =
sharedPrefs.getLong(key, defValue)
}
class SharedPreferenceStringSetLiveData(
sharedPrefs: SharedPreferences,
key: String,
defValue: Set<String>
) :
SharedPreferenceLiveData<Set<String>>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Set<String>): Set<String> =
sharedPrefs.getStringSet(key, defValue)?.toSet() ?: defValue
}
fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData<Int> {
return SharedPreferenceIntLiveData(this, key, defValue)
}
fun SharedPreferences.stringLiveData(
key: String,
defValue: String
): SharedPreferenceLiveData<String> {
return SharedPreferenceStringLiveData(this, key, defValue)
}
fun SharedPreferences.booleanLiveData(
key: String,
defValue: Boolean
): SharedPreferenceLiveData<Boolean> {
return SharedPreferenceBooleanLiveData(this, key, defValue)
}
fun SharedPreferences.floatLiveData(key: String, defValue: Float): SharedPreferenceLiveData<Float> {
return SharedPreferenceFloatLiveData(this, key, defValue)
}
fun SharedPreferences.longLiveData(key: String, defValue: Long): SharedPreferenceLiveData<Long> {
return SharedPreferenceLongLiveData(this, key, defValue)
}
fun SharedPreferences.stringSetLiveData(
key: String,
defValue: Set<String>
): SharedPreferenceLiveData<Set<String>> {
return SharedPreferenceStringSetLiveData(this, key, defValue)
}

View file

@ -4,9 +4,11 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -14,7 +16,9 @@ import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ActivityImageSearchBinding
import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
@ -49,10 +53,13 @@ class ImageSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
initActivity(this)
ThemeManager(this).applyTheme()
binding = ActivityImageSearchBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.uploadImage.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.uploadImage.setOnClickListener {
viewModel.clearResults()
imageSelectionLauncher.launch("image/*")

View file

@ -0,0 +1,60 @@
package ani.dantotsu.others.webview
import android.annotation.SuppressLint
import android.app.Application
import android.os.Build
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R
import ani.dantotsu.themes.ThemeManager
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CookieCatcher : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
//get url from intent
val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = Application.getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
setContentView(R.layout.activity_discord)
val webView = findViewById<WebView>(R.id.discordWebview)
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
cookies.setAcceptThirdPartyCookies(webView, true)
webView.apply {
settings.javaScriptEnabled = true
settings.databaseEnabled = true
settings.domStorageEnabled = true
}
WebView.setWebContentsDebuggingEnabled(true)
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return super.shouldOverrideUrlLoading(view, request)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
}
}
webView.loadUrl(url)
}
}

View file

@ -11,6 +11,9 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.FileUrl
import ani.dantotsu.databinding.BottomSheetWebviewBinding
import ani.dantotsu.defaultHeaders
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
@ -30,7 +33,8 @@ abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
dismiss()
}
val cookies: CookieManager = CookieManager.getInstance()
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
//CookieManager.getInstance()
override fun onCreateView(
inflater: LayoutInflater,

View file

@ -1,5 +1,6 @@
package ani.dantotsu.parsers
import android.content.Context
import ani.dantotsu.Lazier
import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@ -8,24 +9,52 @@ import kotlinx.coroutines.flow.first
object AnimeSources : WatchSources() {
override var list: List<Lazier<BaseParser>> = emptyList()
var pinnedAnimeSources: Set<String> = emptySet()
suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.Installed>>, context: Context) {
val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
pinnedAnimeSources = sharedPrefs.getStringSet("pinned_anime_sources", emptySet()) ?: emptySet()
suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions)
list = createParsersFromExtensions(initialExtensions) + Lazier(
{ OfflineAnimeParser() },
"Downloaded"
)
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions)
list = sortPinnedAnimeSources(createParsersFromExtensions(extensions), pinnedAnimeSources) + Lazier(
{ OfflineAnimeParser() },
"Downloaded"
)
}
}
fun performReorderAnimeSources() {
//remove the downloaded source from the list to avoid duplicates
list = list.filter { it.name != "Downloaded" }
list = sortPinnedAnimeSources(list, pinnedAnimeSources) + Lazier(
{ OfflineAnimeParser() },
"Downloaded"
)
}
private fun createParsersFromExtensions(extensions: List<AnimeExtension.Installed>): List<Lazier<BaseParser>> {
return extensions.map { extension ->
val name = extension.name
Lazier({ DynamicAnimeParser(extension) }, name)
}
}
private fun sortPinnedAnimeSources(Sources: List<Lazier<BaseParser>>, pinnedAnimeSources: Set<String>): List<Lazier<BaseParser>> {
//find the pinned sources
val pinnedSources = Sources.filter { pinnedAnimeSources.contains(it.name) }
//find the unpinned sources
val unpinnedSources = Sources.filter { !pinnedAnimeSources.contains(it.name) }
//put the pinned sources at the top of the list
return pinnedSources + unpinnedSources
}
}

View file

@ -9,23 +9,22 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import ani.dantotsu.FileUrl
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -40,7 +39,10 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
@ -81,16 +83,16 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
}
if (source is AnimeCatalogueSource) {
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
?: return emptyList())
try {
val res = source.getEpisodeList(sAnime)
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 = "\\d+".toRegex().find(it.name)
val number = matchResult?.value?.toFloat() ?: Float.MAX_VALUE
val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
val number = matchResult ?: Float.MAX_VALUE
it.episode_number = number // Store the found number in episode_number
number
}
@ -109,10 +111,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
// 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 }
seasonGroups.keys.sorted().flatMap { season ->
seasonGroups.keys.sortedBy { it.toInt() }
.flatMap { season ->
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
episode.episode_number = episodeCounter++
val potentialNumber =
AnimeNameAdapter.findEpisodeNumber(episode.name)
if (potentialNumber != null) {
episode.episode_number = potentialNumber
} else {
episode.episode_number = episodeCounter
}
episodeCounter++
}
episode
} ?: emptyList()
@ -120,12 +130,10 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
}
return sortedEpisodes.map { SEpisodeToEpisode(it) }
} catch (e: Exception) {
println("Exception: $e")
logger("Exception: $e")
}
return emptyList()
}
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
}
override suspend fun loadVideoServers(
episodeLink: String,
@ -137,7 +145,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList()
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
?: return emptyList())
return try {
val videos = source.getVideoList(sEpisode)
@ -159,15 +168,16 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList()
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
?: return emptyList())
return try {
val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle()
logger("query: $query")
convertAnimesPageToShowResponse(res)
} catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
.show()
snackString( "Failed to bypass Cloudflare")
}
emptyList()
} catch (e: Exception) {
@ -202,7 +212,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
}
return Episode(
if (episodeNumberInt.toInt() != -1) {
episodeNumberInt.toString()
if (sEpisode.episode_number % 1 == 0f) {
episodeNumberInt.toInt().toString()
} else {
sEpisode.episode_number.toString()
}
} else {
sEpisode.name
},
@ -277,7 +291,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
var imageDataList: List<ImageData> = listOf()
val ret = coroutineScope {
try {
println("source.name " + source.name)
logger("source.name " + source.name)
val res = source.getPageList(sChapter)
val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
@ -295,8 +309,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} catch (e: Exception) {
logger("loadImages Exception: $e")
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
.show()
snackString("Failed to load images: $e")
emptyList()
}
}
@ -310,29 +323,30 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
var imageDataList: List<ImageData> = listOf()
coroutineScope {
return coroutineScope {
try {
println("source.name " + source.name)
logger("source.name " + source.name)
val res = source.getPageList(sChapter)
val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
val semaphore = Semaphore(5)
val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) {
imageDataList += ImageData(page, source)
semaphore.withPermit {
ImageData(page, source)
}
}
}
deferreds.awaitAll()
} catch (e: Exception) {
logger("loadImages Exception: $e")
snackString("Failed to load images: $e")
emptyList()
}
}
return imageDataList
}
suspend fun fetchAndProcessImage(
@ -344,8 +358,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
try {
// Fetch the image
val response = httpSource.getImage(page)
println("Response: ${response.code}")
println("Response: ${response.message}")
logger("Response: ${response.code}")
logger("Response: ${response.message}")
// Convert the Response to an InputStream
val inputStream = response.body.byteStream()
@ -353,7 +367,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
inputStream.close()
ani.dantotsu.media.manga.saveImage(
bitmap,
context.contentResolver,
@ -365,7 +379,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
return@withContext bitmap
} catch (e: Exception) {
// Handle any exceptions
println("An error occurred: ${e.message}")
logger("An error occurred: ${e.message}")
return@withContext null
}
}
@ -395,10 +409,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
)
}
inputStream?.close()
inputStream.close()
} catch (e: Exception) {
// Handle any exceptions
println("An error occurred: ${e.message}")
logger("An error occurred: ${e.message}")
}
}
}
@ -445,7 +459,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
} catch (e: Exception) {
// Handle exception here
println("Exception while saving image: ${e.message}")
logger("Exception while saving image: ${e.message}")
}
}
@ -465,8 +479,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
.show()
snackString("Failed to bypass Cloudflare")
}
emptyList()
} catch (e: Exception) {
@ -603,13 +616,18 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName)
// this solves a problem no one has, so I'm commenting it out for now
//if (format == null) {
// val networkHelper = Injekt.get<NetworkHelper>()
// format = headRequest(videoUrl, networkHelper)
//}
}
// If the format is still undetermined, log an error or handle it appropriately
// If the format is still undetermined, log an error
if (format == null) {
logger("Unknown video format: $videoUrl")
FirebaseCrashlytics.getInstance()
.recordException(Exception("Unknown video format: $videoUrl"))
//FirebaseCrashlytics.getInstance()
// .recordException(Exception("Unknown video format: $videoUrl"))
format = VideoType.CONTAINER
}
val headersMap: Map<String, String> =
@ -620,12 +638,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
number,
format,
FileUrl(videoUrl, headersMap),
aniVideo.totalContentLength.toDouble()
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
)
}
private fun getVideoType(fileName: String): VideoType? {
return when {
val type = when {
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
".mkv",
ignoreCase = true
@ -635,6 +653,47 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> null
}
return type
}
private fun headRequest(fileName: String, networkHelper: NetworkHelper): VideoType? {
return try {
logger("attempting head request for $fileName")
val request = Request.Builder()
.url(fileName)
.head()
.build()
networkHelper.client.newCall(request).execute().use { response ->
val contentType = response.header("Content-Type")
val contentDisposition = response.header("Content-Disposition")
if (contentType != null) {
when {
contentType.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
contentType.contains("dash", ignoreCase = true) -> VideoType.DASH
contentType.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
else -> null
}
} else if (contentDisposition != null) {
when {
contentDisposition.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
contentDisposition.contains("dash", ignoreCase = true) -> VideoType.DASH
contentDisposition.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
else -> null
}
} else {
logger("failed head request for $fileName")
null
}
}
} catch (e: Exception) {
logger("Exception in headRequest: $e")
null
}
}
private fun TrackToSubtitle(track: Track): Subtitle {
@ -646,7 +705,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
}
private fun findSubtitleType(url: String): SubtitleType? {
private fun findSubtitleType(url: String): SubtitleType {
// First, try to determine the type based on the URL file extension
val type: SubtitleType = when {
url.endsWith(".vtt", true) -> SubtitleType.VTT

View file

@ -56,7 +56,7 @@ abstract class BaseParser {
* **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response: ShowResponse? = loadSavedShowResponse(mediaObj.id)
if (response != null && this !is OfflineMangaParser) {
if (response != null && this !is OfflineMangaParser && this !is OfflineAnimeParser) {
saveShowResponse(mediaObj.id, response, true)
} else {
setUserText("Searching : ${mediaObj.mainName()}")
@ -156,9 +156,9 @@ abstract class BaseParser {
}
fun checkIfVariablesAreEmpty() {
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `hostUrl` for the Parser")
if (name.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `name` for the Parser")
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `saveName` for the Parser")
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
if (name.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
}
open var showUserText = ""

View file

@ -16,6 +16,9 @@ abstract class WatchSources : BaseSources() {
?: EmptyAnimeParser()
}
fun isDownloadedSource(i: Int): Boolean {
return get(i) is OfflineAnimeParser
}
suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap<String, Episode> {
return tryWithSuspend(true) {
@ -46,6 +49,19 @@ abstract class WatchSources : BaseSources() {
sEpisode = it.sEpisode
)
}
} else if (parser is OfflineAnimeParser) {
parser.loadEpisodes(showLink, extra, SAnime.create()).forEach {
map[it.number] = Episode(
it.number,
it.link,
it.title,
it.description,
it.thumbnail,
it.isFiller,
extra = it.extra,
sEpisode = it.sEpisode
)
}
}
}
return map

View file

@ -1,5 +1,6 @@
package ani.dantotsu.parsers
import android.content.Context
import ani.dantotsu.Lazier
import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
@ -7,12 +8,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList()
var pinnedMangaSources: Set<String> = emptySet()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>, context: Context) {
val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
pinnedMangaSources = sharedPrefs.getStringSet("pinned_manga_sources", emptySet()) ?: emptySet()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) + Lazier(
@ -22,19 +24,37 @@ object MangaSources : MangaReadSources() {
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions) + Lazier(
list = sortPinnedMangaSources(createParsersFromExtensions(extensions), pinnedMangaSources) + Lazier(
{ OfflineMangaParser() },
"Downloaded"
)
}
}
fun performReorderMangaSources() {
//remove the downloaded source from the list to avoid duplicates
list = list.filter { it.name != "Downloaded" }
list = sortPinnedMangaSources(list, pinnedMangaSources) + Lazier(
{ OfflineMangaParser() },
"Downloaded"
)
}
private fun createParsersFromExtensions(extensions: List<MangaExtension.Installed>): List<Lazier<BaseParser>> {
return extensions.map { extension ->
val name = extension.name
Lazier({ DynamicMangaParser(extension) }, name)
}
}
private fun sortPinnedMangaSources(Sources: List<Lazier<BaseParser>>, pinnedMangaSources: Set<String>): List<Lazier<BaseParser>> {
//find the pinned sources
val pinnedSources = Sources.filter { pinnedMangaSources.contains(it.name) }
//find the unpinned sources
val unpinnedSources = Sources.filter { !pinnedMangaSources.contains(it.name) }
//put the pinned sources at the top of the list
return pinnedSources + unpinnedSources
}
}
object HMangaSources : MangaReadSources() {

View file

@ -0,0 +1,161 @@
package ani.dantotsu.parsers
import android.net.Uri
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.Locale
class OfflineAnimeParser : AnimeParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
override val name = "Offline"
override val saveName = "Offline"
override val hostUrl = "Offline"
override val isDubAvailableSeparately = false
override val isNSFW = false
override suspend fun loadEpisodes(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/$animeLink"
)
//get all of the folder names and add them to the list
val episodes = mutableListOf<Episode>()
if (directory.exists()) {
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
if (it.isDirectory) {
val episode = Episode(
it.name,
"$animeLink - ${it.name}",
it.name,
null,
null,
extra = extraData,
sEpisode = SEpisodeImpl()
)
episodes.add(episode)
}
}
episodes.sortBy { AnimeNameAdapter.findEpisodeNumber(it.number) }
return episodes
}
return emptyList()
}
override suspend fun loadVideoServers(
episodeLink: String,
extra: Map<String, String>?,
sEpisode: SEpisode
): List<VideoServer> {
return listOf(
VideoServer(
episodeLink,
offline = true,
extraData = extra
)
)
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
returnTitles.add(title)
}
}
val returnList: MutableList<ShowResponse> = mutableListOf()
for (title in returnTitles) {
returnList.add(ShowResponse(title, title, title))
}
return returnList
}
override suspend fun loadByVideoServers(
episodeUrl: String,
extra: Map<String, String>?,
sEpisode: SEpisode,
callback: (VideoExtractor) -> Unit
) {
val server = loadVideoServers(episodeUrl, extra, sEpisode).first()
OfflineVideoExtractor(server).apply {
tryWithSuspend {
load()
}
callback.invoke(this)
}
}
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor {
return OfflineVideoExtractor(server)
}
}
class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
override val server: VideoServer
get() = videoServer
override suspend fun extract(): VideoContainer {
val sublist = getSubtitle(
videoServer.extraData?.get("title") ?: "",
videoServer.extraData?.get("episode") ?: ""
)?: emptyList()
//we need to return a "fake" video so that the app doesn't crash
val video = Video(
null,
VideoType.CONTAINER,
"",
)
return VideoContainer(listOf(video), sublist)
}
private fun getSubtitle(title: String, episode: String): List<Subtitle>? {
currContext()?.let {
DownloadsManager.getDirectory(
it,
ani.dantotsu.download.DownloadedType.Type.ANIME,
title,
episode
).listFiles()?.forEach {
if (it.name.contains("subtitle")) {
return listOf(
Subtitle(
"Downloaded Subtitle",
Uri.fromFile(it).toString(),
determineSubtitletype(it.absolutePath)
)
)
}
}
}
return null
}
fun determineSubtitletype(url: String): SubtitleType {
return when {
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
else -> SubtitleType.SRT
}
}
}

View file

@ -76,7 +76,7 @@ class OfflineMangaParser : MangaParser() {
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {

View file

@ -3,10 +3,7 @@ package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.manga.MangaNameAdapter
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() {
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.novelDownloads.map { it.title }.distinct()
val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
@ -75,7 +72,8 @@ class OfflineNovelParser: NovelParser() {
}
}
}
val cover = currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
val cover =
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
names.forEach {
returnList.add(ShowResponse(it, it, cover))
}

View file

@ -57,10 +57,13 @@ data class VideoServer(
val name: String,
val embed: FileUrl,
val extraData: Map<String, String>? = null,
val video: eu.kanade.tachiyomi.animesource.model.Video? = null
val video: eu.kanade.tachiyomi.animesource.model.Video? = null,
val offline: Boolean = false
) : Serializable {
constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null)
: this(name, FileUrl(embedUrl), extraData)
constructor(name: String, offline: Boolean, extraData: Map<String, String>?)
: this(name, FileUrl(""), extraData, null, offline)
constructor(
name: String,

View file

@ -22,10 +22,10 @@ class DynamicNovelParser(extension: NovelExtension.Installed) : NovelParser() {
override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.firstOrNull()
if (source is NovelInterface) {
return source.search(query, client)
return if (source is NovelInterface) {
source.search(query, client)
} else {
return emptyList()
emptyList()
}
}

View file

@ -111,7 +111,8 @@ internal object NovelExtensionLoader {
@Suppress("DEPRECATION")
private fun getSignatureHash(pkgInfo: PackageInfo): List<String>? {
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
val signatures =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
pkgInfo.signingInfo.apkContentsSigners
} else {
pkgInfo.signatures

View file

@ -11,6 +11,7 @@ data class CurrentNovelReaderSettings(
var justify: Boolean = true,
var hyphenation: Boolean = true,
var useDarkTheme: Boolean = false,
var useOledTheme: Boolean = false,
var invert: Boolean = false,
var maxInlineSize: Int = 720,
var maxBlockSize: Int = 1440,

View file

@ -6,11 +6,10 @@ import android.os.Build.VERSION.*
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.ViewGroup
import android.widget.AutoCompleteTextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
@ -23,12 +22,8 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
class ExtensionsActivity : AppCompatActivity() {
private val restartMainActivity = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity)
}
lateinit var binding: ActivityExtensionsBinding
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -119,6 +114,7 @@ class ExtensionsActivity : AppCompatActivity() {
initActivity(this)
binding.languageselect.visibility = View.GONE
/* TODO
binding.languageselect.setOnClickListener {
val popup = PopupMenu(this, it)

View file

@ -18,88 +18,83 @@ class FAQActivity : AppCompatActivity() {
Triple(
R.drawable.ic_round_help_24,
currContext()!!.getString(R.string.question_1),
currContext()!!.getString(R.string.answer_1)
currContext()?.getString(R.string.question_1) ?: "",
currContext()?.getString(R.string.answer_1) ?: ""
),
Triple(
R.drawable.ic_round_auto_awesome_24,
currContext()!!.getString(R.string.question_2),
currContext()!!.getString(R.string.answer_2)
currContext()?.getString(R.string.question_2) ?: "",
currContext()?.getString(R.string.answer_2) ?: ""
),
Triple(
R.drawable.ic_round_auto_awesome_24,
currContext()!!.getString(R.string.question_17),
currContext()!!.getString(R.string.answer_17)
currContext()?.getString(R.string.question_17) ?: "",
currContext()?.getString(R.string.answer_17) ?: ""
),
Triple(
R.drawable.ic_round_download_24,
currContext()!!.getString(R.string.question_3),
currContext()!!.getString(R.string.answer_3)
currContext()?.getString(R.string.question_3) ?: "",
currContext()?.getString(R.string.answer_3) ?: ""
),
Triple(
R.drawable.ic_round_help_24,
currContext()!!.getString(R.string.question_16),
currContext()!!.getString(R.string.answer_16)
currContext()?.getString(R.string.question_16) ?: "",
currContext()?.getString(R.string.answer_16) ?: ""
),
Triple(
R.drawable.ic_round_dns_24,
currContext()!!.getString(R.string.question_4),
currContext()!!.getString(R.string.answer_4)
currContext()?.getString(R.string.question_4) ?: "",
currContext()?.getString(R.string.answer_4) ?: ""
),
Triple(
R.drawable.ic_baseline_screen_lock_portrait_24,
currContext()!!.getString(R.string.question_5),
currContext()!!.getString(R.string.answer_5)
currContext()?.getString(R.string.question_5) ?: "",
currContext()?.getString(R.string.answer_5) ?: ""
),
Triple(
R.drawable.ic_anilist,
currContext()!!.getString(R.string.question_6),
currContext()!!.getString(R.string.answer_6)
currContext()?.getString(R.string.question_6) ?: "",
currContext()?.getString(R.string.answer_6) ?: ""
),
Triple(
R.drawable.ic_round_movie_filter_24,
currContext()!!.getString(R.string.question_7),
currContext()!!.getString(R.string.answer_7)
),
Triple(
R.drawable.ic_round_menu_book_24,
currContext()!!.getString(R.string.question_8),
currContext()!!.getString(R.string.answer_8)
currContext()?.getString(R.string.question_7) ?: "",
currContext()?.getString(R.string.answer_7) ?: ""
),
Triple(
R.drawable.ic_round_lock_open_24,
currContext()!!.getString(R.string.question_9),
currContext()!!.getString(R.string.answer_9)
currContext()?.getString(R.string.question_9) ?: "",
currContext()?.getString(R.string.answer_9) ?: ""
),
Triple(
R.drawable.ic_round_smart_button_24,
currContext()!!.getString(R.string.question_10),
currContext()!!.getString(R.string.answer_10)
currContext()?.getString(R.string.question_10) ?: "",
currContext()?.getString(R.string.answer_10) ?: ""
),
Triple(
R.drawable.ic_round_smart_button_24,
currContext()!!.getString(R.string.question_11),
currContext()!!.getString(R.string.answer_11)
currContext()?.getString(R.string.question_11) ?: "",
currContext()?.getString(R.string.answer_11) ?: ""
),
Triple(
R.drawable.ic_round_info_24,
currContext()!!.getString(R.string.question_12),
currContext()!!.getString(R.string.answer_12)
currContext()?.getString(R.string.question_12) ?: "",
currContext()?.getString(R.string.answer_12) ?: ""
),
Triple(
R.drawable.ic_round_help_24,
currContext()!!.getString(R.string.question_13),
currContext()!!.getString(R.string.answer_13)
currContext()?.getString(R.string.question_13) ?: "",
currContext()?.getString(R.string.answer_13) ?: ""
),
Triple(
R.drawable.ic_round_art_track_24,
currContext()!!.getString(R.string.question_14),
currContext()!!.getString(R.string.answer_14)
currContext()?.getString(R.string.question_14) ?: "",
currContext()?.getString(R.string.answer_14) ?: ""
),
Triple(
R.drawable.ic_round_video_settings_24,
currContext()!!.getString(R.string.question_15),
currContext()!!.getString(R.string.answer_15)
currContext()?.getString(R.string.question_15) ?: "",
currContext()?.getString(R.string.answer_15) ?: ""
)
)
}

View file

@ -1,5 +1,6 @@
package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.Context
@ -45,37 +46,42 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentAnimeExtensionsBinding? = null
private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false
private val skipIcons = loadData("skip_extension_icons") ?: false
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
private val extensionsAdapter = AnimeExtensionsAdapter(
{ pkg ->
val name = pkg.name
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = requireActivity() as ExtensionsActivity
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
activity.findViewById<ImageView>(R.id.languageselect).visibility = visibility
activity.findViewById<TextView>(R.id.extensions).text =
if (show) getString(R.string.extensions) else name
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
.toTypedArray()
var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
itemSelected = true
selectedIndex = which
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
// Move the fragment transaction here
val eActivity = requireActivity() as ExtensionsActivity
eActivity.runOnUiThread {
val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
eActivity.findViewById<ViewPager2>(R.id.viewPager).visibility =
View.VISIBLE
eActivity.findViewById<TabLayout>(R.id.tabLayout).visibility =
View.VISIBLE
eActivity.findViewById<TextInputLayout>(R.id.searchView).visibility =
View.VISIBLE
eActivity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
View.GONE
changeUIVisibility(true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
@ -83,52 +89,41 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
.addToBackStack(null)
.commit()
}
.setOnDismissListener {
if (!itemSelected) {
changeUIVisibility(true)
}
}
.show()
dialog.window?.setDimAmount(0.8f)
} else {
// If there's only one setting, proceed with the fragment transaction
val eActivity = requireActivity() as ExtensionsActivity
eActivity.runOnUiThread {
val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
eActivity.findViewById<ViewPager2>(R.id.viewPager).visibility =
View.VISIBLE
eActivity.findViewById<TabLayout>(R.id.tabLayout).visibility =
View.VISIBLE
eActivity.findViewById<TextInputLayout>(R.id.searchView).visibility =
View.VISIBLE
eActivity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
View.GONE
changeUIVisibility(true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
}
// Hide ViewPager2 and TabLayout
val activity = requireActivity() as ExtensionsActivity
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.GONE
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.GONE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.GONE
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
View.VISIBLE
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
},
{ pkg ->
{ pkg, forceDelete ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
if (pkg.hasUpdate) {
if (pkg.hasUpdate && !forceDelete) {
animeExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
@ -209,7 +204,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private class AnimeExtensionsAdapter(
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
private val onUninstallClicked: (AnimeExtension.Installed, Boolean) -> Unit,
val skipIcons: Boolean
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED
@ -225,6 +220,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
return ViewHolder(view)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) "(18+)" else ""
@ -240,11 +236,15 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
onUninstallClicked(extension)
onUninstallClicked(extension, false)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
holder.card.setOnLongClickListener {
onUninstallClicked(extension, true)
true
}
}
fun filter(query: String, currentList: List<AnimeExtension.Installed>) {
@ -264,6 +264,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
val card = view.findViewById<View>(R.id.extensionCardView)
}
companion object {

View file

@ -1,6 +1,7 @@
package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.Context
@ -44,27 +45,35 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentMangaExtensionsBinding? = null
private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false
private val skipIcons = loadData("skip_extension_icons") ?: false
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
private val extensionsAdapter = MangaExtensionsAdapter({ pkg ->
private val extensionsAdapter = MangaExtensionsAdapter(
{ pkg ->
val name = pkg.name
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = requireActivity() as ExtensionsActivity
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
activity.findViewById<ImageView>(R.id.languageselect).visibility = visibility
activity.findViewById<TextView>(R.id.extensions).text =
if (show) getString(R.string.extensions) else name
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
.toTypedArray()
var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
itemSelected = true
selectedIndex = which
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
@ -80,11 +89,17 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
.addToBackStack(null)
.commit()
}
.setOnDismissListener {
if (!itemSelected) {
changeUIVisibility(true)
}
}
.show()
dialog.window?.setDimAmount(0.8f)
} else {
// If there's only one setting, proceed with the fragment transaction
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
val fragment =
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true)
}
parentFragmentManager.beginTransaction()
@ -101,13 +116,13 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
.show()
}
},
{ pkg ->
{ pkg: MangaExtension.Installed, forceDelete: Boolean ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
if (pkg.hasUpdate) {
if (pkg.hasUpdate && !forceDelete) {
mangaExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
@ -188,7 +203,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private class MangaExtensionsAdapter(
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
private val onUninstallClicked: (MangaExtension.Installed, Boolean) -> Unit,
skipIcons: Boolean
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED
@ -206,6 +221,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
return ViewHolder(view)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) "(18+)" else ""
@ -221,11 +237,16 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
onUninstallClicked(extension)
onUninstallClicked(extension, false)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
holder.card.setOnLongClickListener {
onUninstallClicked(extension, true)
true
}
}
fun filter(query: String, currentList: List<MangaExtension.Installed>) {
@ -245,6 +266,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
val card: View = view.findViewById(R.id.extensionCardView)
}
companion object {

View file

@ -39,17 +39,18 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false
private val novelExtensionManager: NovelExtensionManager = Injekt.get()
private val extensionsAdapter = NovelExtensionsAdapter({ pkg ->
private val extensionsAdapter = NovelExtensionsAdapter(
{ pkg ->
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
},
{ pkg ->
{ pkg, forceDelete ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
if (pkg.hasUpdate) {
if (pkg.hasUpdate && !forceDelete) {
novelExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
@ -130,7 +131,7 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
private class NovelExtensionsAdapter(
private val onSettingsClicked: (NovelExtension.Installed) -> Unit,
private val onUninstallClicked: (NovelExtension.Installed) -> Unit,
private val onUninstallClicked: (NovelExtension.Installed, Boolean) -> Unit,
skipIcons: Boolean
) : ListAdapter<NovelExtension.Installed, NovelExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED
@ -165,11 +166,15 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
onUninstallClicked(extension)
onUninstallClicked(extension, false)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
holder.card.setOnLongClickListener {
onUninstallClicked(extension, true)
true
}
}
fun filter(query: String, currentList: List<NovelExtension.Installed>) {
@ -189,6 +194,7 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
val card = view.findViewById<View>(R.id.extensionCardView)
}
companion object {

View file

@ -1,10 +0,0 @@
package ani.dantotsu.settings
import java.io.Serializable
data class NovelReaderSettings(
var showSource: Boolean = true,
var showSystemBars: Boolean = false,
var default: CurrentNovelReaderSettings = CurrentNovelReaderSettings(),
var askIndividual: Boolean = true,
) : Serializable

View file

@ -22,7 +22,7 @@ data class PlayerSettings(
//TimeStamps
var timeStampsEnabled: Boolean = true,
var useProxyForTimeStamps: Boolean = true,
var useProxyForTimeStamps: Boolean = false,
var showTimeStampButton: Boolean = true,
//Auto
@ -45,6 +45,6 @@ data class PlayerSettings(
var skipTime: Int = 85,
//Other
var cast: Boolean = false,
var cast: Boolean = true,
var pip: Boolean = true
) : Serializable

View file

@ -97,7 +97,21 @@ class PlayerSettingsActivity : AppCompatActivity() {
val speeds =
arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f)
arrayOf(
0.25f,
0.33f,
0.5f,
0.66f,
0.75f,
1f,
1.15f,
1.25f,
1.33f,
1.5f,
1.66f,
1.75f,
2f
)
val cursedSpeeds = arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
var curSpeedArr = if (settings.cursedSpeeds) cursedSpeeds else speeds
var speedsName = curSpeedArr.map { "${it}x" }.toTypedArray()
@ -106,7 +120,8 @@ class PlayerSettingsActivity : AppCompatActivity() {
val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.default_speed))
binding.playerSettingsSpeed.setOnClickListener {
val dialog = speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i ->
val dialog =
speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i ->
settings.defaultSpeed = i
binding.playerSettingsSpeed.text =
getString(R.string.default_playback_speed, speedsName[i])
@ -256,7 +271,8 @@ class PlayerSettingsActivity : AppCompatActivity() {
val resizeDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.default_resize_mode))
binding.playerResizeMode.setOnClickListener {
val dialog = resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count ->
val dialog =
resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count ->
settings.resize = count
saveData(player, settings)
dialog.dismiss()
@ -382,7 +398,10 @@ class PlayerSettingsActivity : AppCompatActivity() {
val outlineDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.outline_type))
binding.videoSubOutline.setOnClickListener {
val dialog = outlineDialog.setSingleChoiceItems(typesOutline, settings.outline) { dialog, count ->
val dialog = outlineDialog.setSingleChoiceItems(
typesOutline,
settings.outline
) { dialog, count ->
settings.outline = count
saveData(player, settings)
dialog.dismiss()

View file

@ -8,6 +8,7 @@ data class ReaderSettings(
var autoDetectWebtoon: Boolean = true,
var default: CurrentReaderSettings = CurrentReaderSettings(),
var defaultLN: CurrentNovelReaderSettings = CurrentNovelReaderSettings(),
var askIndividual: Boolean = true,
var updateForH: Boolean = false

View file

@ -42,7 +42,7 @@ class ReaderSettingsActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed()
}
//General
//Manga Settings
binding.readerSettingsSourceName.isChecked = settings.showSource
binding.readerSettingsSourceName.setOnCheckedChangeListener { _, isChecked ->
settings.showSource = isChecked
@ -54,14 +54,14 @@ class ReaderSettingsActivity : AppCompatActivity() {
settings.showSystemBars = isChecked
saveData(reader, settings)
}
//Default Manga
binding.readerSettingsAutoWebToon.isChecked = settings.autoDetectWebtoon
binding.readerSettingsAutoWebToon.setOnCheckedChangeListener { _, isChecked ->
settings.autoDetectWebtoon = isChecked
saveData(reader, settings)
}
//Default
val layoutList = listOf(
binding.readerSettingsPaged,
binding.readerSettingsContinuousPaged,
@ -185,6 +185,169 @@ class ReaderSettingsActivity : AppCompatActivity() {
saveData(reader, settings)
}
//LN settings
val layoutListLN = listOf(
binding.LNpaged,
binding.LNcontinuous
)
binding.LNlayoutText.text = settings.defaultLN.layout.string
var selectedLN = layoutListLN[settings.defaultLN.layout.ordinal]
selectedLN.alpha = 1f
layoutListLN.forEachIndexed { index, imageButton ->
imageButton.setOnClickListener {
selectedLN.alpha = 0.33f
selectedLN = imageButton
selectedLN.alpha = 1f
settings.defaultLN.layout = CurrentNovelReaderSettings.Layouts[index]
?: CurrentNovelReaderSettings.Layouts.PAGED
binding.LNlayoutText.text = settings.defaultLN.layout.string
saveData(reader, settings)
}
}
val dualListLN = listOf(
binding.LNdualNo,
binding.LNdualAuto,
binding.LNdualForce
)
binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString()
var selectedDualLN = dualListLN[settings.defaultLN.dualPageMode.ordinal]
selectedDualLN.alpha = 1f
dualListLN.forEachIndexed { index, imageButton ->
imageButton.setOnClickListener {
selectedDualLN.alpha = 0.33f
selectedDualLN = imageButton
selectedDualLN.alpha = 1f
settings.defaultLN.dualPageMode = CurrentReaderSettings.DualPageModes[index]
?: CurrentReaderSettings.DualPageModes.Automatic
binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString()
saveData(reader, settings)
}
}
binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
binding.LNlineHeight.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
settings.defaultLN.lineHeight = value
binding.LNlineHeight.setText(value.toString())
saveData(reader, settings)
}
}
binding.LNincrementLineHeight.setOnClickListener {
val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
settings.defaultLN.lineHeight = value + 0.1f
binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
saveData(reader, settings)
}
binding.LNdecrementLineHeight.setOnClickListener {
val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
settings.defaultLN.lineHeight = value - 0.1f
binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
saveData(reader, settings)
}
binding.LNmargin.setText(settings.defaultLN.margin.toString())
binding.LNmargin.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
settings.defaultLN.margin = value
binding.LNmargin.setText(value.toString())
saveData(reader, settings)
}
}
binding.LNincrementMargin.setOnClickListener {
val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
settings.defaultLN.margin = value + 0.01f
binding.LNmargin.setText(settings.defaultLN.margin.toString())
saveData(reader, settings)
}
binding.LNdecrementMargin.setOnClickListener {
val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
settings.defaultLN.margin = value - 0.01f
binding.LNmargin.setText(settings.defaultLN.margin.toString())
saveData(reader, settings)
}
binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
binding.LNmaxInlineSize.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
settings.defaultLN.maxInlineSize = value
binding.LNmaxInlineSize.setText(value.toString())
saveData(reader, settings)
}
}
binding.LNincrementMaxInlineSize.setOnClickListener {
val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
settings.defaultLN.maxInlineSize = value + 10
binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
saveData(reader, settings)
}
binding.LNdecrementMaxInlineSize.setOnClickListener {
val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
settings.defaultLN.maxInlineSize = value - 10
binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
saveData(reader, settings)
}
binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString())
binding.LNmaxBlockSize.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
settings.defaultLN.maxBlockSize = value
binding.LNmaxBlockSize.setText(value.toString())
saveData(reader, settings)
}
}
binding.LNincrementMaxBlockSize.setOnClickListener {
val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
settings.defaultLN.maxInlineSize = value + 10
binding.LNmaxBlockSize.setText(settings.defaultLN.maxInlineSize.toString())
saveData(reader, settings)
}
binding.LNdecrementMaxBlockSize.setOnClickListener {
val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
settings.defaultLN.maxBlockSize = value - 10
binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString())
saveData(reader, settings)
}
binding.LNuseDarkTheme.isChecked = settings.defaultLN.useDarkTheme
binding.LNuseDarkTheme.setOnCheckedChangeListener { _, isChecked ->
settings.defaultLN.useDarkTheme = isChecked
saveData(reader, settings)
}
binding.LNuseOledTheme.isChecked = settings.defaultLN.useOledTheme
binding.LNuseOledTheme.setOnCheckedChangeListener { _, isChecked ->
settings.defaultLN.useOledTheme = isChecked
saveData(reader, settings)
}
binding.LNkeepScreenOn.isChecked = settings.defaultLN.keepScreenOn
binding.LNkeepScreenOn.setOnCheckedChangeListener { _, isChecked ->
settings.defaultLN.keepScreenOn = isChecked
saveData(reader, settings)
}
binding.LNvolumeButton.isChecked = settings.defaultLN.volumeButtons
binding.LNvolumeButton.setOnCheckedChangeListener { _, isChecked ->
settings.defaultLN.volumeButtons = isChecked
saveData(reader, settings)
}
//Update Progress
binding.readerSettingsAskUpdateProgress.isChecked = settings.askIndividual
binding.readerSettingsAskUpdateProgress.setOnCheckedChangeListener { _, isChecked ->

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.os.Build.*
import android.os.Build.VERSION.*
@ -11,19 +12,24 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.ActivitySettingsBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.others.AppUpdater
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.LangSet
@ -37,7 +43,9 @@ import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.skydoves.colorpickerview.listeners.ColorListener
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.network.NetworkPreferences
import io.noties.markwon.Markwon
@ -50,7 +58,7 @@ import uy.kohesive.injekt.api.get
import kotlin.random.Random
class SettingsActivity : AppCompatActivity() {
class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListener {
private val restartMainActivity = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
}
@ -59,6 +67,7 @@ class SettingsActivity : AppCompatActivity() {
private val networkPreferences = Injekt.get<NetworkPreferences>()
private var cursedCounter = 0
@OptIn(UnstableApi::class)
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -71,26 +80,7 @@ class SettingsActivity : AppCompatActivity() {
binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
binding.settingsVersion.setOnLongClickListener {
fun getArch(): String {
SUPPORTED_ABIS.forEach {
when (it) {
"arm64-v8a" -> return "aarch64"
"armeabi-v7a" -> return "arm"
"x86_64" -> return "x86_64"
"x86" -> return "i686"
}
}
return System.getProperty("os.arch") ?: System.getProperty("os.product.cpu.abi")
?: "Unknown Architecture"
}
val info = """
dantotsu Version: ${BuildConfig.VERSION_NAME}
Device: $BRAND $DEVICE
Architecture: ${getArch()}
OS Version: $CODENAME $RELEASE ($SDK_INT)
""".trimIndent()
copyToClipboard(info, false)
copyToClipboard(getDeviceInfo(), false)
toast(getString(R.string.copied_device_info))
return@setOnLongClickListener true
}
@ -176,34 +166,30 @@ class SettingsActivity : AppCompatActivity() {
binding.customTheme.setOnClickListener {
var passedColor: Int = 0
val dialogView = layoutInflater.inflate(R.layout.dialog_color_picker, null)
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Custom Theme")
.setView(dialogView)
.setPositiveButton("OK") { dialog, _ ->
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("custom_theme_int", passedColor).apply()
logger("Custom Theme: $passedColor")
dialog.dismiss()
val originalColor = getSharedPreferences("Dantotsu", MODE_PRIVATE).getInt(
"custom_theme_int",
Color.parseColor("#6200EE")
)
class CustomColorDialog : SimpleColorDialog() { //idk where to put it
override fun onPositiveButtonClick() {
restartApp()
super.onPositiveButtonClick()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.create()
val colorPickerView =
dialogView.findViewById<com.skydoves.colorpickerview.ColorPickerView>(R.id.colorPickerView)
colorPickerView.setColorListener(ColorListener { color, fromUser ->
val linearLayout = dialogView.findViewById<LinearLayout>(R.id.linear)
passedColor = color
linearLayout.setBackgroundColor(color)
})
alertDialog.show()
alertDialog.window?.setDimAmount(0.8f)
}
//val animeSource = loadData<Int>("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
val tag = "colorPicker"
CustomColorDialog().title("Custom Theme")
.colorPreset(originalColor)
.colors(this, SimpleColorDialog.BEIGE_COLOR_PALLET)
.allowCustom(true)
.showOutline(0x46000000)
.gridNumColumn(5)
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
.neg()
.show(this, tag)
}
val animeSource = getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
@ -228,6 +214,47 @@ class SettingsActivity : AppCompatActivity() {
binding.animeSource.clearFocus()
}
binding.settingsPinnedAnimeSources.setOnClickListener {
val animeSourcesWithoutDownloadsSource = AnimeSources.list.filter { it.name != "Downloaded" }
val names = animeSourcesWithoutDownloadsSource.map { it.name }
val pinnedSourcesBoolean = animeSourcesWithoutDownloadsSource.map { it.name in AnimeSources.pinnedAnimeSources }
val pinnedSourcesOriginal = getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
).getStringSet("pinned_anime_sources", null)
val pinnedSources = pinnedSourcesOriginal?.toMutableSet() ?: mutableSetOf()
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Pinned Anime Sources")
.setMultiChoiceItems(
names.toTypedArray(),
pinnedSourcesBoolean.toBooleanArray()
) { _, which, isChecked ->
if (isChecked) {
pinnedSources.add(AnimeSources.names[which])
} else {
pinnedSources.remove(AnimeSources.names[which])
}
}
.setPositiveButton("OK") { dialog, _ ->
val oldDefaultSourceIndex = getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
).getInt("settings_def_anime_source_s_r", 0)
val oldName = AnimeSources.names[oldDefaultSourceIndex]
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putStringSet("pinned_anime_sources", pinnedSources).apply()
AnimeSources.pinnedAnimeSources = pinnedSources
AnimeSources.performReorderAnimeSources()
val newDefaultSourceIndex = AnimeSources.names.indexOf(oldName)
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("settings_def_anime_source_s_r", newDefaultSourceIndex).apply()
dialog.dismiss()
}
.create()
alertDialog.show()
alertDialog.window?.setDimAmount(0.8f)
}
binding.settingsPlayer.setOnClickListener {
startActivity(Intent(this, PlayerSettingsActivity::class.java))
}
@ -237,7 +264,10 @@ class SettingsActivity : AppCompatActivity() {
AlertDialog.Builder(this, R.style.DialogTheme).setTitle("Download Manager")
var downloadManager = loadData<Int>("settings_download_manager") ?: 0
binding.settingsDownloadManager.setOnClickListener {
val dialog = downloadManagerDialog.setSingleChoiceItems(managers, downloadManager) { dialog, count ->
val dialog = downloadManagerDialog.setSingleChoiceItems(
managers,
downloadManager
) { dialog, count ->
downloadManager = count
saveData("settings_download_manager", downloadManager)
dialog.dismiss()
@ -245,6 +275,62 @@ class SettingsActivity : AppCompatActivity() {
dialog.window?.setDimAmount(0.8f)
}
binding.purgeAnimeDownloads.setOnClickListener {
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Purge Anime Downloads")
.setMessage("Are you sure you want to purge all anime downloads?")
.setPositiveButton("Yes") { dialog, _ ->
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.purgeDownloads(DownloadedType.Type.ANIME)
DownloadService.sendRemoveAllDownloads(
this,
ExoplayerDownloadService::class.java,
false
)
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
}
binding.purgeMangaDownloads.setOnClickListener {
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Purge Manga Downloads")
.setMessage("Are you sure you want to purge all manga downloads?")
.setPositiveButton("Yes") { dialog, _ ->
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.purgeDownloads(DownloadedType.Type.MANGA)
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
}
binding.purgeNovelDownloads.setOnClickListener {
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Purge Novel Downloads")
.setMessage("Are you sure you want to purge all novel downloads?")
.setPositiveButton("Yes") { dialog, _ ->
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.purgeDownloads(DownloadedType.Type.NOVEL)
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
}
binding.settingsForceLegacyInstall.isChecked =
extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY
binding.settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked ->
@ -259,9 +345,9 @@ class SettingsActivity : AppCompatActivity() {
binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked ->
saveData("skip_extension_icons", isChecked)
}
binding.NSFWExtension.isChecked = loadData("NFSWExtension") ?: true
binding.NSFWExtension.isChecked = loadData("NSFWExtension") ?: true
binding.NSFWExtension.setOnCheckedChangeListener { _, isChecked ->
saveData("NFSWExtension", isChecked)
saveData("NSFWExtension", isChecked)
}
@ -278,7 +364,7 @@ class SettingsActivity : AppCompatActivity() {
}
.setNeutralButton("Reset") { dialog, _ ->
networkPreferences.defaultUserAgent()
.set("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0") // Reset to default or empty
.set("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")
editText.setText("")
dialog.dismiss()
}
@ -339,6 +425,17 @@ class SettingsActivity : AppCompatActivity() {
binding.settingsRecentlyListOnly.setOnCheckedChangeListener { _, isChecked ->
saveData("recently_list_only", isChecked)
}
binding.settingsShareUsername.isChecked = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getBoolean("shared_user_id", true)
binding.settingsShareUsername.setOnCheckedChangeListener { _, isChecked ->
getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).edit().putBoolean("shared_user_id", isChecked).apply()
}
binding.settingsPreferDub.isChecked = loadData("settings_prefer_dub") ?: false
binding.settingsPreferDub.setOnCheckedChangeListener { _, isChecked ->
saveData("settings_prefer_dub", isChecked)
@ -370,6 +467,47 @@ class SettingsActivity : AppCompatActivity() {
binding.mangaSource.clearFocus()
}
binding.settingsPinnedMangaSources.setOnClickListener {
val mangaSourcesWithoutDownloadsSource = MangaSources.list.filter { it.name != "Downloaded" }
val names = mangaSourcesWithoutDownloadsSource.map { it.name }
val pinnedSourcesBoolean = mangaSourcesWithoutDownloadsSource.map { it.name in MangaSources.pinnedMangaSources }
val pinnedSourcesOriginal = getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
).getStringSet("pinned_manga_sources", null)
val pinnedSources = pinnedSourcesOriginal?.toMutableSet() ?: mutableSetOf()
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Pinned Manga Sources")
.setMultiChoiceItems(
names.toTypedArray(),
pinnedSourcesBoolean.toBooleanArray()
) { _, which, isChecked ->
if (isChecked) {
pinnedSources.add(MangaSources.names[which])
} else {
pinnedSources.remove(MangaSources.names[which])
}
}
.setPositiveButton("OK") { dialog, _ ->
val oldDefaultSourceIndex = getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
).getInt("settings_def_manga_source_s_r", 0)
val oldName = MangaSources.names[oldDefaultSourceIndex]
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putStringSet("pinned_manga_sources", pinnedSources).apply()
MangaSources.pinnedMangaSources = pinnedSources
MangaSources.performReorderMangaSources()
val newDefaultSourceIndex = MangaSources.names.indexOf(oldName)
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("settings_def_manga_source_s_r", newDefaultSourceIndex).apply()
dialog.dismiss()
}
.create()
alertDialog.show()
alertDialog.window?.setDimAmount(0.8f)
}
binding.settingsReader.setOnClickListener {
startActivity(Intent(this, ReaderSettingsActivity::class.java))
}
@ -408,16 +546,6 @@ class SettingsActivity : AppCompatActivity() {
uiTheme(true, it)
}
binding.settingsIncognito.isChecked =
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean(
"incognito",
false
)
binding.settingsIncognito.setOnCheckedChangeListener { _, isChecked ->
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putBoolean("incognito", isChecked).apply()
}
var previousStart: View = when (uiSettings.defaultStartUpTab) {
0 -> binding.uiSettingsAnime
1 -> binding.uiSettingsHome
@ -434,6 +562,7 @@ class SettingsActivity : AppCompatActivity() {
initActivity(this)
}
binding.uiSettingsAnime.setOnClickListener {
uiTheme(0, it)
}
@ -510,10 +639,6 @@ class SettingsActivity : AppCompatActivity() {
lifecycleScope.launch {
binding.settingBuyMeCoffee.pop()
}
binding.settingUPI.visibility = if (checkCountry(this)) View.VISIBLE else View.GONE
lifecycleScope.launch {
binding.settingUPI.pop()
}
binding.loginDiscord.setOnClickListener {
openLinkInBrowser(getString(R.string.discord))
@ -521,7 +646,9 @@ class SettingsActivity : AppCompatActivity() {
binding.loginGithub.setOnClickListener {
openLinkInBrowser(getString(R.string.github))
}
binding.loginTelegram.setOnClickListener {
openLinkInBrowser(getString(R.string.telegram))
}
binding.settingsUi.setOnClickListener {
startActivity(Intent(this, UserInterfaceSettingsActivity::class.java))
}
@ -759,8 +886,7 @@ class SettingsActivity : AppCompatActivity() {
}
setPositiveButton("denote :)") {
if (binding.settingUPI.visibility == View.VISIBLE) binding.settingUPI.performClick()
else binding.settingBuyMeCoffee.performClick()
binding.settingBuyMeCoffee.performClick()
dismiss()
}
show(supportFragmentManager, "dialog")
@ -770,6 +896,18 @@ class SettingsActivity : AppCompatActivity() {
}
}
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
if (which == BUTTON_POSITIVE) {
if (dialogTag == "colorPicker") {
val color = extras.getInt(SimpleColorDialog.COLOR)
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("custom_theme_int", color).apply()
logger("Custom Theme: $color")
}
}
return true
}
private fun restartApp() {
Snackbar.make(
binding.root,
@ -788,4 +926,28 @@ class SettingsActivity : AppCompatActivity() {
show()
}
}
companion object {
fun getDeviceInfo(): String {
return """
dantotsu Version: ${BuildConfig.VERSION_NAME}
Device: $BRAND $DEVICE
Architecture: ${getArch()}
OS Version: $CODENAME $RELEASE ($SDK_INT)
""".trimIndent()
}
private fun getArch(): String {
SUPPORTED_ABIS.forEach {
when (it) {
"arm64-v8a" -> return "aarch64"
"armeabi-v7a" -> return "arm"
"x86_64" -> return "x86_64"
"x86" -> return "i686"
}
}
return System.getProperty("os.arch") ?: System.getProperty("os.product.cpu.abi")
?: "Unknown Architecture"
}
}
}

View file

@ -1,41 +1,43 @@
package ani.dantotsu.settings
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSettingsBinding
import ani.dantotsu.download.DownloadContainerActivity
import ani.dantotsu.download.anime.OfflineAnimeFragment
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.loadData
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment
import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet
import ani.dantotsu.incognitoNotification
import ani.dantotsu.loadImage
import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.startMainActivity
import ani.dantotsu.toast
class SettingsDialogFragment() : BottomSheetDialogFragment() {
class SettingsDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSettingsBinding? = null
private val binding get() = _binding!!
private lateinit var pageType: PageType
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pageType = arguments?.getSerializable("pageType") as? PageType ?: PageType.HOME
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -72,6 +74,17 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
}
}
binding.settingsIncognito.isChecked =
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean(
"incognito",
false
) ?: false
binding.settingsIncognito.setOnCheckedChangeListener { _, isChecked ->
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putBoolean("incognito", isChecked)?.apply()
incognitoNotification(requireContext())
}
binding.settingsExtensionSettings.setSafeOnClickListener {
startActivity(Intent(activity, ExtensionsActivity::class.java))
dismiss()
@ -88,48 +101,61 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
startActivity(Intent(activity, ImageSearchActivity::class.java))
dismiss()
}
binding.settingsDownloads.setSafeOnClickListener {
binding.settingsDownloads.isChecked =
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("offlineMode", false) ?: false
binding.settingsDownloads.setOnCheckedChangeListener { _, isChecked ->
when (pageType) {
PageType.MANGA -> {
val intent = Intent(activity, DownloadContainerActivity::class.java)
intent.putExtra("FRAGMENT_CLASS_NAME", OfflineMangaFragment::class.java.name)
val intent = Intent(activity, NoInternet::class.java)
intent.putExtra(
"FRAGMENT_CLASS_NAME",
OfflineMangaFragment::class.java.name
)
startActivity(intent)
}
PageType.ANIME -> {
try {
val arrayOfFiles =
ContextCompat.getExternalFilesDirs(requireContext(), null)
startActivity(
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString()
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
val intent = Intent(activity, NoInternet::class.java)
intent.putExtra(
"FRAGMENT_CLASS_NAME",
OfflineAnimeFragment::class.java.name
)
} catch (e: ActivityNotFoundException) {
toast(getString(R.string.file_manager_not_found))
}
startActivity(intent)
}
PageType.HOME -> {
try {
val arrayOfFiles =
ContextCompat.getExternalFilesDirs(requireContext(), null)
startActivity(
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString()
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
)
} catch (e: ActivityNotFoundException) {
toast(getString(R.string.file_manager_not_found))
val intent = Intent(activity, NoInternet::class.java)
intent.putExtra("FRAGMENT_CLASS_NAME", OfflineFragment::class.java.name)
startActivity(intent)
}
PageType.OfflineMANGA -> {
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra("FRAGMENT_CLASS_NAME", MangaFragment::class.java.name)
startActivity(intent)
}
PageType.OfflineHOME -> {
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra(
"FRAGMENT_CLASS_NAME",
if (Anilist.token != null) HomeFragment::class.java.name else LoginFragment::class.java.name
)
startActivity(intent)
}
PageType.OfflineANIME -> {
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra("FRAGMENT_CLASS_NAME", AnimeFragment::class.java.name)
startActivity(intent)
}
}
dismiss()
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putBoolean("offlineMode", isChecked)?.apply()
}
}
@ -140,7 +166,7 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
companion object {
enum class PageType {
MANGA, ANIME, HOME
MANGA, ANIME, HOME, OfflineMANGA, OfflineANIME, OfflineHOME
}
fun newInstance(pageType: PageType): SettingsDialogFragment {

View file

@ -68,7 +68,6 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
saveData(ui, settings)
restartApp()
}
binding.uiSettingsBannerAnimation.isChecked = settings.bannerAnimations
binding.uiSettingsBannerAnimation.setOnCheckedChangeListener { _, isChecked ->
settings.bannerAnimations = isChecked

View file

@ -10,6 +10,7 @@ import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.forEach
import androidx.preference.getOnBindEditTextListener
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
@ -21,7 +22,12 @@ import uy.kohesive.injekt.api.get
class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = populateAnimePreferenceScreen()
preferenceScreen = try {
populateAnimePreferenceScreen()
} catch (e: Exception) {
snackString(e.message ?: "Unknown error")
preferenceManager.createPreferenceScreen(requireContext())
}
//set background color
val color = TypedValue()
requireContext().theme.resolveAttribute(
@ -42,8 +48,8 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
private fun populateAnimePreferenceScreen(): PreferenceScreen {
val sourceId = requireArguments().getLong(SOURCE_ID)
val source = Injekt.get<AnimeSourceManager>().get(sourceId)!!
check(source is ConfigurableAnimeSource)
val source = Injekt.get<AnimeSourceManager>().get(sourceId) as? ConfigurableAnimeSource
?: error("Source with id: $sourceId not found!")
val sharedPreferences =
requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
val dataStore = SharedPreferencesDataStore(sharedPreferences)

View file

@ -1,5 +1,6 @@
package ani.dantotsu.settings.paging
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
@ -91,18 +92,15 @@ class AnimeExtensionPagingSource(
val availableExtensions =
availableExtensionsFlow.filterNot { it.pkgName in installedExtensions }
val query = searchQuery
val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true
val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true
val filteredExtensions = if (query.isEmpty()) {
availableExtensions
} else {
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
}
val filternfsw = if (isNsfwEnabled) {
filteredExtensions
} else {
filteredExtensions.filterNot { it.isNsfw }
}
val filternfsw =
if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw }
return try {
val sublist = filternfsw.subList(
fromIndex = position,
@ -176,6 +174,7 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
init {
binding.closeTextView.setOnClickListener {
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
val extension = getItem(bindingAdapterPosition)
if (extension != null) {
clickListener.onInstallClick(extension)
@ -198,6 +197,7 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
val extensionIconImageView: ImageView = binding.extensionIconImageView
@SuppressLint("SetTextI18n")
fun bind(extension: AnimeExtension.Available) {
val nsfw = if (extension.isNsfw) "(18+)" else ""
val lang = LanguageMapper.mapLanguageCodeToName(extension.lang)

View file

@ -1,5 +1,6 @@
package ani.dantotsu.settings.paging
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
@ -91,17 +92,14 @@ class MangaExtensionPagingSource(
val availableExtensions =
availableExtensionsFlow.filterNot { it.pkgName in installedExtensions }
val query = searchQuery
val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true
val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true
val filteredExtensions = if (query.isEmpty()) {
availableExtensions
} else {
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
}
val filternfsw = if (isNsfwEnabled) {
filteredExtensions
} else {
filteredExtensions.filterNot { it.isNsfw }
}
val filternfsw =
if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw }
return try {
val sublist = filternfsw.subList(
fromIndex = position,
@ -173,6 +171,7 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
init {
binding.closeTextView.setOnClickListener {
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
val extension = getItem(bindingAdapterPosition)
if (extension != null) {
clickListener.onInstallClick(extension)
@ -194,6 +193,8 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
}
val extensionIconImageView: ImageView = binding.extensionIconImageView
@SuppressLint("SetTextI18n")
fun bind(extension: MangaExtension.Available) {
val nsfw = if (extension.isNsfw) "(18+)" else ""
val lang = LanguageMapper.mapLanguageCodeToName(extension.lang)

View file

@ -176,6 +176,7 @@ class NovelExtensionAdapter(private val clickListener: OnNovelInstallClickListen
init {
binding.closeTextView.setOnClickListener {
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
val extension = getItem(bindingAdapterPosition)
if (extension != null) {
clickListener.onInstallClick(extension)

View file

@ -0,0 +1,21 @@
package ani.dantotsu.subcriptions
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ani.dantotsu.INCOGNITO_CHANNEL_ID
class NotificationClickReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putBoolean("incognito", false)
.apply()
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(INCOGNITO_CHANNEL_ID)
}
}

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