diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index ba36e72be61..14e28c5e6a4 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -3,12 +3,15 @@ name: Build APK and Notify Discord on: push: branches: - - main - dev + paths-ignore: + - '**/README.md' jobs: build: runs-on: ubuntu-latest + env: + CI: true steps: - name: Checkout repo @@ -29,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 @@ -45,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 diff --git a/README.md b/README.md index a5605a6c5cc..adc78f62296 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Dantotsu is an [Anilist](https://anilist.co/) only client. > **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge! - + ### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION! @@ -31,6 +31,10 @@ You can come hang out with our awesome community, request new features, and repo

+## VISITORS + +:rebeloniondantotsu + ## LICENSE 📜 Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md) diff --git a/app/build.gradle b/app/build.gradle index 1cde5bdba29..df3a7889326 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index ef58c36a6eb..92f0ffec672 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,4 +1,4 @@ - Dantotsu ß + Dantotsu β \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a77c0d318bb..c61e37dd073 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,8 @@ android:required="false" /> + @@ -17,25 +19,22 @@ - - - + - - - + - - - + - - @@ -48,6 +47,7 @@ + tools:ignore="AllowBackup"> + + + + + + + + + + + android:exported="true"> + @@ -71,13 +85,11 @@ - - @@ -103,9 +115,9 @@ - + - - - + - + + + + + + + + + + + + + + - - + + + - - + android:exported="false" + android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + android:exported="false" + android:theme="@android:style/Theme.Translucent.NoTitleBar" /> - + @@ -258,32 +287,49 @@ android:resource="@xml/provider_paths" /> - - - - + + + + + + - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/App.kt b/app/src/main/java/ani/dantotsu/App.kt index 89a5d4415c1..5b6869ea99c 100644 --- a/app/src/main/java/ani/dantotsu/App.kt +++ b/app/src/main/java/ani/dantotsu/App.kt @@ -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) } - } diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index 6a789743f16..da5b5e1fbe9 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -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 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,35 +799,40 @@ fun toast(string: String?) { } fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) { - if (s != null) { - (activity ?: currActivity())?.apply { - runOnUiThread { - val snackBar = Snackbar.make( - window.decorView.findViewById(android.R.id.content), - s, - Snackbar.LENGTH_SHORT - ) - snackBar.view.apply { - updateLayoutParams { - gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) - width = WRAP_CONTENT - } - translationY = -(navBarHeight.dp + 32f) - translationZ = 32f - updatePadding(16f.px, right = 16f.px) - setOnClickListener { - snackBar.dismiss() - } - setOnLongClickListener { - copyToClipboard(clipboard ?: s, false) - toast(getString(R.string.copied_to_clipboard)) - true + try { //I have no idea why this sometimes crashes for some people... + if (s != null) { + (activity ?: currActivity())?.apply { + runOnUiThread { + val snackBar = Snackbar.make( + window.decorView.findViewById(android.R.id.content), + s, + Snackbar.LENGTH_SHORT + ) + snackBar.view.apply { + updateLayoutParams { + gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) + width = WRAP_CONTENT + } + translationY = -(navBarHeight.dp + 32f) + translationZ = 32f + updatePadding(16f.px, right = 16f.px) + setOnClickListener { + snackBar.dismiss() + } + setOnLongClickListener { + copyToClipboard(clipboard ?: s, false) + toast(getString(R.string.copied_to_clipboard)) + true + } } + snackBar.show() } - snackBar.show() } + logger(s) } - logger(s) + } catch (e: Exception) { + logger(e.stackTraceToString()) + FirebaseCrashlytics.getInstance().recordException(e) } } @@ -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() diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index b27ee17dada..2be1a87d04d 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -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,106 +198,146 @@ 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 { 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 { - val model: AnilistHomeViewModel by viewModels() - model.genres.observe(this) { - if (it != null) { - if (it) { - val navbar = binding.includedNavbar.navbar - bottomBar = navbar - navbar.visibility = View.VISIBLE - binding.mainProgressBar.visibility = View.GONE - val mainViewPager = binding.viewpager - mainViewPager.isUserInputEnabled = false - mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle) - mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) - navbar.setOnTabSelectListener(object : - AnimatedBottomBar.OnTabSelectListener { - override fun onTabSelected( - lastIndex: Int, - lastTab: AnimatedBottomBar.Tab?, - newIndex: Int, - newTab: AnimatedBottomBar.Tab - ) { - navbar.animate().translationZ(12f).setDuration(200).start() - selectedOption = newIndex - mainViewPager.setCurrentItem(newIndex, false) + 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) { it -> + if (it != null) { + if (it) { + val navbar = binding.includedNavbar.navbar + bottomBar = navbar + navbar.visibility = View.VISIBLE + binding.mainProgressBar.visibility = View.GONE + val mainViewPager = binding.viewpager + mainViewPager.isUserInputEnabled = false + mainViewPager.adapter = + ViewPagerAdapter(supportFragmentManager, lifecycle) + mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) + navbar.setOnTabSelectListener(object : + AnimatedBottomBar.OnTabSelectListener { + override fun onTabSelected( + lastIndex: Int, + lastTab: AnimatedBottomBar.Tab?, + newIndex: Int, + newTab: AnimatedBottomBar.Tab + ) { + navbar.animate().translationZ(12f).setDuration(200).start() + selectedOption = newIndex + mainViewPager.setCurrentItem(newIndex, false) + } + }) + navbar.selectTabAt(selectedOption) + mainViewPager.post { + mainViewPager.setCurrentItem( + selectedOption, + false + ) } - }) - navbar.selectTabAt(selectedOption) - mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) } - } else { - binding.mainProgressBar.visibility = View.GONE + } else { + binding.mainProgressBar.visibility = View.GONE + } } } - } - //Load Data - if (!load) { - scope.launch(Dispatchers.IO) { - model.loadMain(this@MainActivity) - val id = intent.extras?.getInt("mediaId", 0) - val isMAL = intent.extras?.getBoolean("mal") ?: false - val cont = intent.extras?.getBoolean("continue") ?: false - if (id != null && id != 0) { - val media = withContext(Dispatchers.IO) { - Anilist.query.getMedia(id, isMAL) - } - if (media != null) { - media.cameFromContinue = cont - startActivity( - Intent(this@MainActivity, MediaDetailsActivity::class.java) - .putExtra("media", media as Serializable) - ) - } else { - snackString(this@MainActivity.getString(R.string.anilist_not_found)) + //Load Data + if (!load) { + scope.launch(Dispatchers.IO) { + model.loadMain(this@MainActivity) + val id = intent.extras?.getInt("mediaId", 0) + val isMAL = intent.extras?.getBoolean("mal") ?: false + val cont = intent.extras?.getBoolean("continue") ?: false + if (id != null && id != 0) { + val media = withContext(Dispatchers.IO) { + Anilist.query.getMedia(id, isMAL) + } + if (media != null) { + media.cameFromContinue = cont + startActivity( + Intent(this@MainActivity, MediaDetailsActivity::class.java) + .putExtra("media", media as Serializable) + ) + } else { + snackString(this@MainActivity.getString(R.string.anilist_not_found)) + } } + delay(500) + startSubscription() } - delay(500) - startSubscription() + load = true } - load = true - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (loadData("allow_opening_links", this) != true) { - CustomBottomDialog.newInstance().apply { - title = "Allow Dantotsu to automatically open Anilist & MAL Links?" - val md = "Open settings & click +Add Links & select Anilist & Mal urls" - addView(TextView(this@MainActivity).apply { - val markWon = - Markwon.builder(this@MainActivity) - .usePlugin(SoftBreakAddsNewLinePlugin.create()).build() - markWon.setMarkdown(this, md) - }) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (loadData("allow_opening_links", this) != true) { + CustomBottomDialog.newInstance().apply { + title = "Allow Dantotsu to automatically open Anilist & MAL Links?" + val md = "Open settings & click +Add Links & select Anilist & Mal urls" + addView(TextView(this@MainActivity).apply { + val markWon = + Markwon.builder(this@MainActivity) + .usePlugin(SoftBreakAddsNewLinePlugin.create()).build() + markWon.setMarkdown(this, md) + }) - setNegativeButton(this@MainActivity.getString(R.string.no)) { - saveData("allow_opening_links", true, this@MainActivity) - dismiss() - } + setNegativeButton(this@MainActivity.getString(R.string.no)) { + saveData("allow_opening_links", true, this@MainActivity) + dismiss() + } - setPositiveButton(this@MainActivity.getString(R.string.yes)) { - saveData("allow_opening_links", true, this@MainActivity) - tryWith(true) { - startActivity( - Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS) - .setData(Uri.parse("package:$packageName")) - ) + setPositiveButton(this@MainActivity.getString(R.string.yes)) { + saveData("allow_opening_links", true, this@MainActivity) + tryWith(true) { + startActivity( + Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS) + .setData(Uri.parse("package:$packageName")) + ) + } } - } - }.show(supportFragmentManager, "dialog") + }.show(supportFragmentManager, "dialog") + } } } } + //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) + } + } + } } @@ -261,4 +357,4 @@ class MainActivity : AppCompatActivity() { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt index 38b9fc4cb41..19e45a589f4 100644 --- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt +++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt @@ -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 { diff --git a/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt b/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt index f7084645265..fb08c59d7e5 100644 --- a/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt +++ b/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt @@ -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) diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index a0c1c270f09..5c344b59372 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -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,7 +248,9 @@ class AnilistQueries { else snackString(currContext()?.getString(R.string.what_did_you_open)) } } else { - snackString(currContext()?.getString(R.string.error_getting_data)) + if (currContext()?.let { isOnline(it) } == true) { + snackString(currContext()?.getString(R.string.error_getting_data)) + } } } val mal = async { @@ -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 -> diff --git a/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt b/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt deleted file mode 100644 index 21f0a9100ce..00000000000 --- a/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index b3536ea4616..ef7765628cb 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -15,43 +15,43 @@ class DownloadsManager(private val context: Context) { private val gson = Gson() private val downloadsList = loadDownloads().toMutableList() - val mangaDownloads: List - get() = downloadsList.filter { it.type == Download.Type.MANGA } - val animeDownloads: List - get() = downloadsList.filter { it.type == Download.Type.ANIME } - val novelDownloads: List - get() = downloadsList.filter { it.type == Download.Type.NOVEL } + val mangaDownloadedTypes: List + get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA } + val animeDownloadedTypes: List + get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME } + val novelDownloadedTypes: List + 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 { + private fun loadDownloads(): List { val jsonString = prefs.getString("downloads_key", null) return if (jsonString != null) { - val type = object : TypeToken>() {}.type + val type = object : TypeToken>() {}.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) //for debugging + fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List) //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) + } + + 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(download: Download) { - val directory = if (download.type == Download.Type.MANGA) { + 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, diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt new file mode 100644 index 00000000000..a69ef446d15 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt @@ -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() + + private val downloadJobs = mutableMapOf() + private val mutex = Mutex() + private var isCurrentlyProcessing = false + private var currentTasks: MutableList = 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 { + SChapterImpl() // Provide an instance of SChapterImpl + }) + .registerTypeAdapter(SAnime::class.java, InstanceCreator { + SAnimeImpl() // Provide an instance of SAnimeImpl + }) + .registerTypeAdapter(SEpisode::class.java, InstanceCreator { + 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 = ConcurrentLinkedQueue() + + @Volatile + var isServiceRunning: Boolean = false +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeAdapter.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeAdapter.kt new file mode 100644 index 00000000000..4cd57f050ff --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeAdapter.kt @@ -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, + private val searchListener: OfflineAnimeSearchListener +) : BaseAdapter() { + private val inflater: LayoutInflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + private var originalItems: List = 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(R.id.itemCompactImage) + val titleTextView = view.findViewById(R.id.itemCompactTitle) + val itemScore = view.findViewById(R.id.itemCompactScore) + val itemScoreBG = view.findViewById(R.id.itemCompactScoreBG) + val ongoing = view.findViewById(R.id.itemCompactOngoing) + val totalepisodes = view.findViewById(R.id.itemCompactTotal) + val typeimage = view.findViewById(R.id.itemCompactTypeImage) + val type = view.findViewById(R.id.itemCompactRelation) + val typeView = view.findViewById(R.id.itemCompactType) + + if (style == 0) { + val bannerView = view.findViewById(R.id.itemCompactBanner) // for large view + val episodes = view.findViewById(R.id.itemTotal) + episodes.text = " Episodes" + bannerView.setImageURI(item.banner) + totalepisodes.text = item.totalEpisodeList + } else if (style == 1) { + val watchedEpisodes = + view.findViewById(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) { + this.items = items + this.originalItems = items + notifyDataSetChanged() + } + + fun notifyNewGrid() { + style = + context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt new file mode 100644 index 00000000000..99888856e6b --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt @@ -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() + private var downloads: List = 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(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(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(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(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(R.id.downloadedList) + val layoutcompact = view.findViewById(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(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() + 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 { + SChapterImpl() // Provide an instance of SChapterImpl + }) + .registerTypeAdapter(SAnime::class.java, InstanceCreator { + SAnimeImpl() // Provide an instance of SAnimeImpl + }) + .registerTypeAdapter(SEpisode::class.java, InstanceCreator { + 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) +} diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeModel.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeModel.kt new file mode 100644 index 00000000000..4823e2afc91 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeModel.kt @@ -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?, +) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt index fce89d3ee2b..0a1337dfebc 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt @@ -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>() + //val deferredList = mutableListOf>() + val deferredMap = mutableMapOf>() 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) { - file.writeText(jsonString) + try { + file.writeText(jsonString) + } catch (e: android.system.ErrnoException) { + e.printStackTrace() + Toast.makeText( + this@MangaDownloaderService, + "Error while saving: ${e.localizedMessage}", + Toast.LENGTH_LONG + ).show() + } } } } diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt index b91bb55abec..3bacbcb2afd 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt @@ -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 = 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(R.id.itemCompactImage) + val imageView = view.findViewById(R.id.itemCompactImage) val titleTextView = view.findViewById(R.id.itemCompactTitle) val itemScore = view.findViewById(R.id.itemCompactScore) val itemScoreBG = view.findViewById(R.id.itemCompactScoreBG) val ongoing = view.findViewById(R.id.itemCompactOngoing) + val totalchapter = view.findViewById(R.id.itemCompactTotal) + val typeimage = view.findViewById(R.id.itemCompactTypeImage) + val type = view.findViewById(R.id.itemCompactRelation) + val typeView = view.findViewById(R.id.itemCompactType) + + if (style == 0) { + val bannerView = view.findViewById(R.id.itemCompactBanner) // for large view + val chapters = view.findViewById(R.id.itemTotal) + chapters.text = " Chapters" + bannerView.setImageURI(item.banner) + totalchapter.text = item.totalChapter + } else if (style == 1) { + val readchapter = + view.findViewById(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() + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt index e81d2817cf1..8f1d2b06b4a 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -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() private var downloads: List = 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(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(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(R.id.downloadedList) + val layoutcompact = view.findViewById(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 + } - gridView = view.findViewById(R.id.gridView) + 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,32 +182,33 @@ 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 - } else { - Download.Type.NOVEL - } + val type: DownloadedType.Type = + if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) { + DownloadedType.Type.MANGA + } else { + 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 } - val dialog = builder.show() + val dialog = builder.show() 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(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() 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() 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,44 +354,72 @@ 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 + ) } } } interface OfflineMangaSearchListener { fun onSearchQuery(query: String) -} \ No newline at end of file +} diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt index 568081ee0d2..e9693a5b83f 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt @@ -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? ) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt index e987cd253f2..d729ef5789c 100644 --- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt @@ -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) diff --git a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt similarity index 91% rename from app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt rename to app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt index 3a26ac1d867..4add998c8c7 100644 --- a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt +++ b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt @@ -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 diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index 2dcf31c641b..25d0d476697 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -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() 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() + 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() + 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 + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt index 1a08934d82c..8d0cd6aba47 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt index 1f3255ddada..a2ee6593310 100644 --- a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt @@ -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) diff --git a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt index d3144237311..58bb2fc9055 100644 --- a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt @@ -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)) } } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt index 14870b7fad5..d4b5095ec9c 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt @@ -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() } diff --git a/app/src/main/java/ani/dantotsu/home/NoInternet.kt b/app/src/main/java/ani/dantotsu/home/NoInternet.kt index 524aa825477..147cf8874b1 100644 --- a/app/src/main/java/ani/dantotsu/home/NoInternet.kt +++ b/app/src/main/java/ani/dantotsu/home/NoInternet.kt @@ -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 { 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 when (position) { + 0 -> OfflineAnimeFragment() + 2 -> OfflineMangaFragment() + else -> OfflineFragment() } - return LoginFragment() } } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt b/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt index d6428a38e38..77035416c47 100644 --- a/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt @@ -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 { + topMargin = statusBarHeight + bottomMargin = navBarHeight + } } setContentView(binding.root) diff --git a/app/src/main/java/ani/dantotsu/media/Media.kt b/app/src/main/java/ani/dantotsu/media/Media.kt index 3360a94a673..ebc4d4f7cb9 100644 --- a/app/src/main/java/ani/dantotsu/media/Media.kt +++ b/app/src/main/java/ani/dantotsu/media/Media.kt @@ -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 diff --git a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt index fd04eb58cd7..6d19fdf47fe 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt @@ -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() ) } } diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt index a902a1cf43d..66c250291f7 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt @@ -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("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 { topMargin += statusBarHeight } + binding.incognito.updateLayoutParams { topMargin += statusBarHeight } binding.mediaCollapsing.minimumHeight = statusBarHeight if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams { @@ -154,7 +159,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi } }) banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true } - binding.mediaTitle.text = media.userPreferredName + 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 ) - tabLayout.inflateMenu(R.menu.manga_menu_detail) + 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 + } } diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index 35013e02d50..cb5304726a7 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -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>(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)) diff --git a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt index a7b78f8e5a5..c3c290aa60c 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt @@ -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 { bottomMargin += 128f.px + navBarHeight } @@ -101,29 +103,33 @@ class MediaInfoFragment : Fragment() { if (media.anime.mainStudio != null) { binding.mediaInfoStudioContainer.visibility = View.VISIBLE binding.mediaInfoStudio.text = media.anime.mainStudio!!.name - binding.mediaInfoStudioContainer.setOnClickListener { - ContextCompat.startActivity( - requireActivity(), - Intent(activity, StudioActivity::class.java).putExtra( - "studio", - media.anime.mainStudio!! as Serializable - ), - null - ) + if (!offline) { + binding.mediaInfoStudioContainer.setOnClickListener { + ContextCompat.startActivity( + requireActivity(), + Intent(activity, StudioActivity::class.java).putExtra( + "studio", + media.anime.mainStudio!! as Serializable + ), + null + ) + } } } if (media.anime.author != null) { binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthor.text = media.anime.author!!.name - binding.mediaInfoAuthorContainer.setOnClickListener { - ContextCompat.startActivity( - requireActivity(), - Intent(activity, AuthorActivity::class.java).putExtra( - "author", - media.anime.author!! as Serializable - ), - null - ) + if (!offline) { + binding.mediaInfoAuthorContainer.setOnClickListener { + ContextCompat.startActivity( + requireActivity(), + Intent(activity, AuthorActivity::class.java).putExtra( + "author", + media.anime.author!! as Serializable + ), + null + ) + } } } binding.mediaInfoTotalTitle.setText(R.string.total_eps) @@ -137,15 +143,17 @@ class MediaInfoFragment : Fragment() { if (media.manga.author != null) { binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthor.text = media.manga.author!!.name - binding.mediaInfoAuthorContainer.setOnClickListener { - ContextCompat.startActivity( - requireActivity(), - Intent(activity, AuthorActivity::class.java).putExtra( - "author", - media.manga.author!! as Serializable - ), - null - ) + if (!offline) { + binding.mediaInfoAuthorContainer.setOnClickListener { + ContextCompat.startActivity( + requireActivity(), + Intent(activity, AuthorActivity::class.java).putExtra( + "author", + media.manga.author!! as Serializable + ), + null + ) + } } } } @@ -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, diff --git a/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt index b1576e40ab5..fe5c0c93a04 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt @@ -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 -> diff --git a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt index b3cf36f543f..4c71664b5d1 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt @@ -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) } diff --git a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt index 67c523e6784..18fe0a4c2c9 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt @@ -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 diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt index 17759141cb8..d74a89294d6 100644 --- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt +++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt @@ -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() @@ -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().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 + } + + } } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt index 17d20bc6770..f072a549b51 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt @@ -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 = + "(? + mr.value.replaceFirst(mr.groupValues[1], "") + } + } else { + removedNumber + } + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt index ea30fa58685..2f70a2a9c85 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -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 - var reversed = media.selected!!.recyclerReversed - var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - binding.animeSourceTop.setOnClickListener { - reversed = !reversed - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - fragment.onIconPressed(style, reversed) - } - var selected = when (style) { - 0 -> binding.animeSourceList - 1 -> binding.animeSourceGrid - 2 -> binding.animeSourceCompact - else -> binding.animeSourceList - } - selected.alpha = 1f - fun selected(it: ImageView) { - selected.alpha = 0.33f - selected = it + //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 + 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 + 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 -> 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: 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.animeSourceGrid.setOnClickListener { + selected(it as ImageButton) + style = 1 + dialogBinding.layoutText.text = "Grid" + run = true + } + dialogBinding.animeSourceCompact.setOnClickListener { + selected(it as ImageButton) + style = 2 + 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.animeSourceList.setOnClickListener { - selected(it as ImageView) - style = 0 - fragment.onIconPressed(style, reversed) - } - binding.animeSourceGrid.setOnClickListener { - selected(it as ImageView) - style = 1 - fragment.onIconPressed(style, reversed) - } - binding.animeSourceCompact.setOnClickListener { - selected(it as ImageView) - style = 2 - fragment.onIconPressed(style, reversed) - } - 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( - fragment.requireContext(), - R.layout.item_dropdown, - parser.extension.sources.map { it.lang }) + val adapter = ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + 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) } } @@ -371,4 +458,4 @@ class AnimeWatchAdapter( countDown(media, binding.animeSourceContainer) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt index df3522200a8..487bf207185 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -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() + 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,12 +209,15 @@ 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 - ?: FileUrl[media.cover] + 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() 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() - changeUIVisibility(true) - return@setNegativeButton + .setOnDismissListener { + if (!itemSelected) { + changeUIVisibility(true) + } } .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 = 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() } -} \ No newline at end of file + 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" + } + +} diff --git a/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt b/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt new file mode 100644 index 00000000000..611f378a34d --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt index 1461524648e..535174cd475 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt @@ -14,7 +14,8 @@ data class Episode( var selectedExtractor: String? = null, var selectedVideo: Int = 0, var selectedSubtitle: Int? = -1, - var extractors: MutableList? = null, + var downloadProgress: String? = null, + @Transient var extractors: MutableList? = null, @Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null, var allStreams: Boolean = false, var watched: Long? = null, diff --git a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt index 1a85b9bf19b..54e851edb1c 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt @@ -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("${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 = arrayListOf() + var arr: List = arrayListOf(), + var offlineMode: Boolean ) : RecyclerView.Adapter() { + 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() + private val downloadedEpisodes = mutableSetOf() + + 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() + 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) + } +} diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index f16f47efb20..ee5baedff82 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -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,12 +487,18 @@ 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 { - Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay) - exoPlayer.play() + 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() + } } } } @@ -812,18 +839,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { audioManager.setStreamVolume(STREAM_MUSIC, volume, 0) volumeHide() } - + val fastForward = playerView.findViewById(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() 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(R.id.exo_cast).apply { + playerView.findViewById(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("${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(R.id.exo_download) - if (video?.format == VideoType.CONTAINER || (loadData("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,9 +1341,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { } dataSource } + val dafuckDataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString())) cacheFactory = CacheDataSource.Factory().apply { - setCache(simpleCache) - setUpstreamDataSourceFactory(dataSourceFactory) + setCache(Helper.getSimpleCache(this@ExoplayerView)) + if (ext.server.offline) { + setUpstreamDataSourceFactory(dafuckDataSourceFactory) + } else { + setUpstreamDataSourceFactory(dataSourceFactory) + } + setCacheWriteDataSinkFactory(null) } val mimeType = when (video?.format) { @@ -1309,15 +1358,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { else -> MimeTypes.APPLICATION_MP4 } - val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) - logger("url: ${video!!.file.url}") - logger("mimeType: $mimeType") + 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") - if (sub != null) { - val listofnotnullsubs = immutableListOf(sub).filterNotNull() - builder.setSubtitleConfigurations(listofnotnullsubs) + if (sub != null) { + val listofnotnullsubs = immutableListOf(sub).filterNotNull() + builder.setSubtitleConfigurations(listofnotnullsubs) + } + 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() } - mediaItem = builder.build() + //Source exoSource.setOnClickListener { @@ -1439,7 +1513,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { exoPlayer.release() VideoCache.release() mediaSession?.release() - if(DiscordServiceRunningSingleton.running) { + if (DiscordServiceRunningSingleton.running) { val stopIntent = Intent(this, DiscordService::class.java) DiscordServiceRunningSingleton.running = false stopService(stopIntent) @@ -1479,7 +1553,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { super.onPause() orientationListener?.disable() if (isInitialized) { - playerView.player?.pause() + 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() { - playerView.player?.pause() + 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, diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt index d42bcd948c9..688222c5859 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -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 = - ep.extractors?.find { it.server.name == selected }?.videos?.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,24 +290,107 @@ 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 - binding.urlDownload.visibility = View.VISIBLE + 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) - download( - requireActivity(), - media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!, - media!!.userPreferredName - ) + 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("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 + ) + } else { + snackString("No Download Manager Selected") + } + true + } if (video.format == VideoType.CONTAINER) { binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE binding.urlSize.text = @@ -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("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) } } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt index 406efb3cb2e..6cc971574fa 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt @@ -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) { diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt index fb921a9fd3e..9eb14a6ca23 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt @@ -141,8 +141,8 @@ class MangaChapterAdapter( inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) { private val activeCoroutines = mutableSetOf() - 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() } diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt index 05e9b360da2..ae755faa874 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt @@ -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 = "(?= mangaReadSources.names.size) 0 else it } @@ -117,7 +136,7 @@ class MangaReadAdapter( } } - //Subscription + //Grids subscribe = MediaDetailsActivity.PopImageButton( fragment.lifecycleScope, binding.animeSourceSubscribe, @@ -136,98 +155,156 @@ class MangaReadAdapter( openSettings(fragment.requireContext(), getChannelId(true, media.id)) } - //Icons - binding.animeSourceGrid.visibility = View.GONE - var reversed = media.selected!!.recyclerReversed - var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - binding.animeSourceTop.setOnClickListener { - reversed = !reversed - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - fragment.onIconPressed(style, reversed) - } + binding.animeNestedButton.setOnClickListener { - binding.animeScanlatorTop.setOnClickListener { val dialogView = - LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) - val checkboxContainer = dialogView.findViewById(R.id.checkboxContainer) - - // Dynamically add checkboxes + 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 + 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 + dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f + dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" + run = true + } - options.forEach { option -> - val checkBox = CheckBox(currContext()).apply { - text = option + //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") } - //set checked if it's already selected - if (media.selected!!.scanlators != null) { - checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true - scanlatorSelectionListener?.onScanlatorsSelected() - } else { - checkBox.isChecked = true + //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) + } } - checkboxContainer.addView(checkBox) } - // Create AlertDialog - val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup) - .setView(dialogView) - .setPositiveButton("OK") { dialog, which -> - //add unchecked to hidden - hiddenScanlators.clear() - for (i in 0 until checkboxContainer.childCount) { - val checkBox = checkboxContainer.getChildAt(i) as CheckBox - if (!checkBox.isChecked) { - hiddenScanlators.add(checkBox.text.toString()) - } + //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 = + dialogView2.findViewById(R.id.checkboxContainer) + + // Dynamically add checkboxes + options.forEach { option -> + val checkBox = CheckBox(currContext()).apply { + text = option } - fragment.onScanlatorChange(hiddenScanlators) - scanlatorSelectionListener?.onScanlatorsSelected() + //set checked if it's already selected + if (media.selected!!.scanlators != null) { + checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true + scanlatorSelectionListener?.onScanlatorsSelected() + } else { + checkBox.isChecked = true + } + checkboxContainer.addView(checkBox) } - .setNegativeButton("Cancel", null) - .show() - 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) + // Create AlertDialog + val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup) + .setView(dialogView2) + .setPositiveButton("OK") { _, _ -> + //add unchecked to hidden + hiddenScanlators.clear() + for (i in 0 until checkboxContainer.childCount) { + val checkBox = checkboxContainer.getChildAt(i) as CheckBox + if (!checkBox.isChecked) { + hiddenScanlators.add(checkBox.text.toString()) + } + } + fragment.onScanlatorChange(hiddenScanlators) + scanlatorSelectionListener?.onScanlatorsSelected() + } + .setNegativeButton("Cancel", null) + .show() + dialog.window?.setDimAmount(0.8f) } - alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } - val dialog = alertDialog.show() - dialog.window?.setDimAmount(0.8f) - } - var selected = when (style) { - 0 -> binding.animeSourceList - 1 -> binding.animeSourceCompact - else -> binding.animeSourceList - } - selected.alpha = 1f - fun selected(it: ImageView) { - selected.alpha = 0.33f - selected = it - selected.alpha = 1f - } - binding.animeSourceList.setOnClickListener { - selected(it as ImageView) - style = 0 - fragment.onIconPressed(style, reversed) - } - binding.animeSourceCompact.setOnClickListener { - selected(it as ImageView) - style = 1 - fragment.onIconPressed(style, reversed) + 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()) + } + if (refresh) fragment.loadChapters(source, true) + } + .setNegativeButton("Cancel") { _, _ -> + if (refresh) fragment.loadChapters(source, true) + } + .setOnCancelListener { + if (refresh) fragment.loadChapters(source, true) + } + .create() + nestedDialog?.show() } - //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( - fragment.requireContext(), - R.layout.item_dropdown, - parser.extension.sources.map { it.lang }) + val adapter = ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + 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) } } diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index 25a87c9bc16..3153bb6a72e 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -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,20 +203,25 @@ 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) { - for (chapter in chaptersToDownload) { - onMangaChapterDownloadClick(chapter.title!!) - } - } + 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 if (loadedChapters != null) { @@ -260,7 +266,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } } - fun getScanlators(chap: MutableMap?): List { + private fun getScanlators(chap: MutableMap?): List { val scanlators = mutableListOf() 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() 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() - changeUIVisibility(true) - return@setNegativeButton + .setOnDismissListener { + if (!itemSelected) { + changeUIVisibility(true) + } } .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) diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt index 6a18b9e8625..952db6195c8 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt @@ -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(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 { diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt index 8d31e9d1846..e1bebb83ba8 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt @@ -50,7 +50,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() { if (model.loadMangaChapterImages( chp, m.selected!!, - m.nameMAL ?: m.nameRomaji + m.mainName() ) ) { val activity = currActivity() diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt index 9765149959b..58b48038a82 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt @@ -91,4 +91,16 @@ open class ImageAdapter( } override fun getItemCount(): Int = images.size + + override fun isZoomed(): Boolean { + val imageView = + activity.findViewById(R.id.imgProgImageNoGestures) + return imageView.scale > imageView.minScale + } + + override fun setZoom(zoom: Float) { + val imageView = + activity.findViewById(R.id.imgProgImageNoGestures) + imageView.setScaleAndCenter(zoom, imageView.center) + } } diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt index 30fefb26524..c396088de2d 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt @@ -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().installedExtensionsFlow) + mangaSources.init(Injekt.get().installedExtensionsFlow, this@MangaReaderActivity) } model.mangaReadSources = mangaSources } else { @@ -253,7 +255,8 @@ class MangaReaderActivity : AppCompatActivity() { } showProgressDialog = - if (settings.askIndividual) loadData("${media.id}_progressDialog") != true else false + if (settings.askIndividual) loadData("${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).. 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("${media.id}_progressDialog") + ?: true else false if (showProgressDialog) { + val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null) val checkbox = dialogView.findViewById(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, _ -> diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt index 7154412ab27..0c654c16c49 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt @@ -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() 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() 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.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) { diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt index 5f99681f27b..6d57037456c 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt @@ -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 = "" } } diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt index 069fef2820c..b62032fae18 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt @@ -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) } diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt index 27a9caaae93..760c7eb3d82 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt @@ -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 diff --git a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt index ada753a478e..a6d044e18f0 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt @@ -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 { + 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 { diff --git a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt index 3eccf001a22..48573e5064d 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt @@ -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 } diff --git a/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt b/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt index fb780651a1c..02779ab28d5 100644 --- a/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt +++ b/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/others/DisabledReports.kt b/app/src/main/java/ani/dantotsu/others/DisabledReports.kt index 8dc4a0e46a9..ed8f5813f94 100644 --- a/app/src/main/java/ani/dantotsu/others/DisabledReports.kt +++ b/app/src/main/java/ani/dantotsu/others/DisabledReports.kt @@ -1,4 +1,5 @@ package ani.dantotsu.others const val DisabledReports = false -//Setting this to false, will allow sending crash reports to Dantotsu's Firebase Crashlytics \ No newline at end of file +//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 \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/others/Kitsu.kt b/app/src/main/java/ani/dantotsu/others/Kitsu.kt index e815572909b..bee4beeea78 100644 --- a/app/src/main/java/ani/dantotsu/others/Kitsu.kt +++ b/app/src/main/java/ani/dantotsu/others/Kitsu.kt @@ -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? { @@ -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 diff --git a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt index 25f8f6a4816..3d9c439bb5c 100644 --- a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt +++ b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt @@ -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 } } diff --git a/app/src/main/java/ani/dantotsu/others/MalScraper.kt b/app/src/main/java/ani/dantotsu/others/MalScraper.kt index 8b39ac64401..648f3a4a312 100644 --- a/app/src/main/java/ani/dantotsu/others/MalScraper.kt +++ b/app/src/main/java/ani/dantotsu/others/MalScraper.kt @@ -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)) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ani/dantotsu/others/SharedPreferenceLiveData.kt b/app/src/main/java/ani/dantotsu/others/SharedPreferenceLiveData.kt new file mode 100644 index 00000000000..70c9b4f75f4 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/others/SharedPreferenceLiveData.kt @@ -0,0 +1,112 @@ +package ani.dantotsu.others + +import android.content.SharedPreferences +import androidx.lifecycle.LiveData + +abstract class SharedPreferenceLiveData( + val sharedPrefs: SharedPreferences, + val key: String, + val defValue: T +) : LiveData() { + + 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(sharedPrefs, key, defValue) { + override fun getValueFromPreferences(key: String, defValue: Int): Int = + sharedPrefs.getInt(key, defValue) +} + +class SharedPreferenceStringLiveData( + sharedPrefs: SharedPreferences, + key: String, + defValue: String +) : + SharedPreferenceLiveData(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(sharedPrefs, key, defValue) { + override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean = + sharedPrefs.getBoolean(key, defValue) +} + +class SharedPreferenceFloatLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Float) : + SharedPreferenceLiveData(sharedPrefs, key, defValue) { + override fun getValueFromPreferences(key: String, defValue: Float): Float = + sharedPrefs.getFloat(key, defValue) +} + +class SharedPreferenceLongLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Long) : + SharedPreferenceLiveData(sharedPrefs, key, defValue) { + override fun getValueFromPreferences(key: String, defValue: Long): Long = + sharedPrefs.getLong(key, defValue) +} + +class SharedPreferenceStringSetLiveData( + sharedPrefs: SharedPreferences, + key: String, + defValue: Set +) : + SharedPreferenceLiveData>(sharedPrefs, key, defValue) { + override fun getValueFromPreferences(key: String, defValue: Set): Set = + sharedPrefs.getStringSet(key, defValue)?.toSet() ?: defValue +} + +fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData { + return SharedPreferenceIntLiveData(this, key, defValue) +} + +fun SharedPreferences.stringLiveData( + key: String, + defValue: String +): SharedPreferenceLiveData { + return SharedPreferenceStringLiveData(this, key, defValue) +} + +fun SharedPreferences.booleanLiveData( + key: String, + defValue: Boolean +): SharedPreferenceLiveData { + return SharedPreferenceBooleanLiveData(this, key, defValue) +} + +fun SharedPreferences.floatLiveData(key: String, defValue: Float): SharedPreferenceLiveData { + return SharedPreferenceFloatLiveData(this, key, defValue) +} + +fun SharedPreferences.longLiveData(key: String, defValue: Long): SharedPreferenceLiveData { + return SharedPreferenceLongLiveData(this, key, defValue) +} + +fun SharedPreferences.stringSetLiveData( + key: String, + defValue: Set +): SharedPreferenceLiveData> { + return SharedPreferenceStringSetLiveData(this, key, defValue) +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt index 1e38fc0e8d5..282c17fc40d 100644 --- a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt @@ -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 { + bottomMargin = navBarHeight + } binding.uploadImage.setOnClickListener { viewModel.clearResults() imageSelectionLauncher.launch("image/*") diff --git a/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt new file mode 100644 index 00000000000..d1f3635e1e2 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt @@ -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(R.id.discordWebview) + + val cookies: CookieManager = Injekt.get().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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt b/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt index 257a703cc38..489c68fc801 100644 --- a/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt +++ b/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt @@ -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().cookieJar.manager + //CookieManager.getInstance() override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt index c48a4b3e1ed..11a69c3101d 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt @@ -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> = emptyList() + var pinnedAnimeSources: Set = emptySet() + + suspend fun init(fromExtensions: StateFlow>, context: Context) { + val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + pinnedAnimeSources = sharedPrefs.getStringSet("pinned_anime_sources", emptySet()) ?: emptySet() - suspend fun init(fromExtensions: StateFlow>) { // 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): List> { return extensions.map { extension -> val name = extension.name Lazier({ DynamicAnimeParser(extension) }, name) } } + + private fun sortPinnedAnimeSources(Sources: List>, pinnedAnimeSources: Set): List> { + //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 + } } diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 03b40919051..4ef6aee20cf 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -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,50 +83,56 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { } catch (e: Exception) { sourceLanguage = 0 extension.sources[sourceLanguage] - } - if (source is AnimeCatalogueSource) { - 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 - it.episode_number = number // Store the found number in episode_number - number - } + } 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 = AnimeNameAdapter.findEpisodeNumber(it.name) + val number = matchResult ?: Float.MAX_VALUE + it.episode_number = number // Store the found number in episode_number + number + } - // If there is no number, reverse the order and give them an incrementing number - var incrementingNumber = 1f - sortedByStringNumber.map { - if (it.episode_number == Float.MAX_VALUE) { - it.episode_number = - incrementingNumber++ // Update episode_number with the incrementing number - } - it + // If there is no number, reverse the order and give them an incrementing number + var incrementingNumber = 1f + sortedByStringNumber.map { + if (it.episode_number == Float.MAX_VALUE) { + it.episode_number = + incrementingNumber++ // Update episode_number with the incrementing number } - } else { - var episodeCounter = 1f - // Group by season, sort within each season, and then renumber while keeping episode number 0 as is - val seasonGroups = - res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 } - seasonGroups.keys.sorted().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++ + it + } + } else { + var episodeCounter = 1f + // Group by season, sort within each season, and then renumber while keeping episode number 0 as is + val seasonGroups = + res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 } + 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 + val potentialNumber = + AnimeNameAdapter.findEpisodeNumber(episode.name) + if (potentialNumber != null) { + episode.episode_number = potentialNumber + } else { + episode.episode_number = episodeCounter } - episode - } ?: emptyList() - } + episodeCounter++ + } + episode + } ?: emptyList() } - return sortedEpisodes.map { SEpisodeToEpisode(it) } - } catch (e: Exception) { - println("Exception: $e") } - return emptyList() + return sortedEpisodes.map { SEpisodeToEpisode(it) } + } catch (e: Exception) { + logger("Exception: $e") } - return emptyList() // Return an empty list if source is not an AnimeCatalogueSource + return emptyList() } override suspend fun loadVideoServers( @@ -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 = 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 = 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() + // 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 = @@ -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 diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt index 03562f68a2e..40ce4601ea8 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt @@ -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 = "" diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt index f8535772cf1..5aa6ffc24e2 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt @@ -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 { 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 diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt index 0f8a5642e81..a6da1540bc1 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt @@ -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> = emptyList() + var pinnedMangaSources: Set = emptySet() + + suspend fun init(fromExtensions: StateFlow>, context: Context) { + val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + pinnedMangaSources = sharedPrefs.getStringSet("pinned_manga_sources", emptySet()) ?: emptySet() - suspend fun init(fromExtensions: StateFlow>) { // 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): List> { return extensions.map { extension -> val name = extension.name Lazier({ DynamicMangaParser(extension) }, name) } } + + private fun sortPinnedMangaSources(Sources: List>, pinnedMangaSources: Set): List> { + //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() { diff --git a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt index 9a625600d21..f2bc0703257 100644 --- a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt @@ -34,7 +34,7 @@ abstract class NovelParser : BaseParser() { //val query = mediaObj.name ?: mediaObj.nameRomaji //return search(query).sortByVolume(query) val results: List - return if(mediaObj.name != null) { + return if (mediaObj.name != null) { val query = mediaObj.name results = search(query).sortByVolume(query) results.ifEmpty { diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt new file mode 100644 index 00000000000..0f8c79243ee --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt @@ -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() + + 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?, + sAnime: SAnime + ): List { + 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() + if (directory.exists()) { + directory.listFiles()?.forEach { + //put the title and episdode number in the extra data + val extraData = mutableMapOf() + 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?, + sEpisode: SEpisode + ): List { + return listOf( + VideoServer( + episodeLink, + offline = true, + extraData = extra + ) + ) + } + + + override suspend fun search(query: String): List { + val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() + val returnTitles: MutableList = mutableListOf() + for (title in titles) { + if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { + returnTitles.add(title) + } + } + val returnList: MutableList = mutableListOf() + for (title in returnTitles) { + returnList.add(ShowResponse(title, title, title)) + } + return returnList + } + + override suspend fun loadByVideoServers( + episodeUrl: String, + extra: Map?, + 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? { + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt index 218a0f9aada..deffb420b88 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt @@ -76,7 +76,7 @@ class OfflineMangaParser : MangaParser() { } override suspend fun search(query: String): List { - val titles = downloadManager.mangaDownloads.map { it.title }.distinct() + val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() val returnTitles: MutableList = mutableListOf() for (title in titles) { if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt index ca47b50ab6e..534c3ac5e12 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt @@ -3,16 +3,13 @@ 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 import java.io.File -class OfflineNovelParser: NovelParser() { +class OfflineNovelParser : NovelParser() { private val downloadManager = Injekt.get() override val hostUrl: String = "Offline" @@ -34,7 +31,7 @@ class OfflineNovelParser: NovelParser() { if (it.isDirectory) { val chapter = Book( it.name, - it.absolutePath + "/cover.jpg", + it.absolutePath + "/cover.jpg", null, listOf(it.absolutePath + "/0.epub") ) @@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() { } override suspend fun search(query: String): List { - val titles = downloadManager.novelDownloads.map { it.title }.distinct() + val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() val returnTitles: MutableList = 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)) } diff --git a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt index ce56182c70c..582a7cb4bff 100644 --- a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt +++ b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt @@ -57,10 +57,13 @@ data class VideoServer( val name: String, val embed: FileUrl, val extraData: Map? = 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? = null) : this(name, FileUrl(embedUrl), extraData) + constructor(name: String, offline: Boolean, extraData: Map?) + : this(name, FileUrl(""), extraData, null, offline) constructor( name: String, diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt index 13da2ce2620..e23ec62fb21 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt @@ -22,10 +22,10 @@ class DynamicNovelParser(extension: NovelExtension.Installed) : NovelParser() { override suspend fun search(query: String): List { 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() } } diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt index b0f6693b29f..3c26530cb59 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt @@ -111,11 +111,12 @@ internal object NovelExtensionLoader { @Suppress("DEPRECATION") private fun getSignatureHash(pkgInfo: PackageInfo): List? { - val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) { - pkgInfo.signingInfo.apkContentsSigners - } else { - pkgInfo.signatures - } + val signatures = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) { + pkgInfo.signingInfo.apkContentsSigners + } else { + pkgInfo.signatures + } return if (!signatures.isNullOrEmpty()) { signatures.map { Hash.sha256(it.toByteArray()) } } else { diff --git a/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt index bbe3fcd32f7..b1cd429bda5 100644 --- a/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt @@ -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, diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index 32e3a7e7f53..04701603c98 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -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,17 +114,18 @@ class ExtensionsActivity : AppCompatActivity() { initActivity(this) -/* TODO - binding.languageselect.setOnClickListener { - val popup = PopupMenu(this, it) - popup.inflate(R.menu.launguage_selector_menu) - popup.setOnMenuItemClickListener { menuItem -> - true - } - popup.setOnDismissListener { - } - popup.show() - }*/ + binding.languageselect.visibility = View.GONE + /* TODO + binding.languageselect.setOnClickListener { + val popup = PopupMenu(this, it) + popup.inflate(R.menu.launguage_selector_menu) + popup.setOnMenuItemClickListener { menuItem -> + true + } + popup.setOnDismissListener { + } + popup.show() + }*/ binding.settingsContainer.updateLayoutParams { topMargin = statusBarHeight bottomMargin = navBarHeight diff --git a/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt b/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt index c03ba9e1517..74d9525d8f2 100644 --- a/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt @@ -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) ?: "" ) ) } diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt index bdbcb7caf3d..6f83009a7fb 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt @@ -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,90 +46,84 @@ 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(R.id.viewPager).visibility = visibility + activity.findViewById(R.id.tabLayout).visibility = visibility + activity.findViewById(R.id.searchView).visibility = visibility + activity.findViewById(R.id.languageselect).visibility = visibility + activity.findViewById(R.id.extensions).text = + if (show) getString(R.string.extensions) else name + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + if (show) View.GONE else View.VISIBLE + } + var itemSelected = false val allSettings = pkg.sources.filterIsInstance() 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(R.id.viewPager).visibility = - View.VISIBLE - eActivity.findViewById(R.id.tabLayout).visibility = - View.VISIBLE - eActivity.findViewById(R.id.searchView).visibility = - View.VISIBLE - eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility = - View.GONE - } - parentFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) - .replace(R.id.fragmentExtensionsContainer, fragment) - .addToBackStack(null) - .commit() + val fragment = + AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .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) { + val fragment = + AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() - eActivity.findViewById(R.id.viewPager).visibility = - View.VISIBLE - eActivity.findViewById(R.id.tabLayout).visibility = - View.VISIBLE - eActivity.findViewById(R.id.searchView).visibility = - View.VISIBLE - eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility = - View.GONE - } - 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(R.id.viewPager).visibility = View.GONE - activity.findViewById(R.id.tabLayout).visibility = View.GONE - activity.findViewById(R.id.searchView).visibility = View.GONE - activity.findViewById(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( 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) { @@ -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(R.id.extensionCardView) } companion object { diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt index ecef2e58eb7..1ad5228a547 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt @@ -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,70 +45,84 @@ 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 -> - val changeUIVisibility: (Boolean) -> Unit = { show -> - val activity = requireActivity() as ExtensionsActivity - val visibility = if (show) View.VISIBLE else View.GONE - activity.findViewById(R.id.viewPager).visibility = visibility - activity.findViewById(R.id.tabLayout).visibility = visibility - activity.findViewById(R.id.searchView).visibility = visibility - activity.findViewById(R.id.fragmentExtensionsContainer).visibility = - if (show) View.GONE else View.VISIBLE - } - val allSettings = pkg.sources.filterIsInstance() - if (allSettings.isNotEmpty()) { - var selectedSetting = allSettings[0] - if (allSettings.size > 1) { - val names = allSettings.map { it.lang }.toTypedArray() - var selectedIndex = 0 - val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) - .setTitle("Select a Source") - .setSingleChoiceItems(names, selectedIndex) { dialog, which -> - selectedIndex = which - selectedSetting = allSettings[selectedIndex] - dialog.dismiss() + 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(R.id.viewPager).visibility = visibility + activity.findViewById(R.id.tabLayout).visibility = visibility + activity.findViewById(R.id.searchView).visibility = visibility + activity.findViewById(R.id.languageselect).visibility = visibility + activity.findViewById(R.id.extensions).text = + if (show) getString(R.string.extensions) else name + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + if (show) View.GONE else View.VISIBLE + } + var itemSelected = false + val allSettings = pkg.sources.filterIsInstance() + if (allSettings.isNotEmpty()) { + var selectedSetting = allSettings[0] + if (allSettings.size > 1) { + 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 fragment = - MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { + // Move the fragment transaction here + val fragment = + MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + .setOnDismissListener { + if (!itemSelected) { changeUIVisibility(true) } - parentFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) - .replace(R.id.fragmentExtensionsContainer, fragment) - .addToBackStack(null) - .commit() - } - .show() - dialog.window?.setDimAmount(0.8f) - } else { - // If there's only one setting, proceed with the fragment transaction - val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { - 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) { + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() } - parentFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) - .replace(R.id.fragmentExtensionsContainer, fragment) - .addToBackStack(null) - .commit() - } - // Hide ViewPager2 and TabLayout - changeUIVisibility(false) - } else { - Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) - .show() - } - }, - { pkg -> + // Hide ViewPager2 and TabLayout + changeUIVisibility(false) + } else { + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() + } + }, + { 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( 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) { @@ -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 { diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt index c0a61dd5aa8..76b84bca55a 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt @@ -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 -> - Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) - .show() - }, + private val extensionsAdapter = NovelExtensionsAdapter( { pkg -> + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() + }, + { 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( 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) { @@ -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(R.id.extensionCardView) } companion object { diff --git a/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt deleted file mode 100644 index 7c72c7e9bdb..00000000000 --- a/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt index 1589d66da63..83037c4c420 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt @@ -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 diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt index ff3d704dc8d..88167079093 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt @@ -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,13 +120,14 @@ 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 -> - settings.defaultSpeed = i - binding.playerSettingsSpeed.text = - getString(R.string.default_playback_speed, speedsName[i]) - saveData(player, settings) - dialog.dismiss() - }.show() + val dialog = + speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i -> + settings.defaultSpeed = i + binding.playerSettingsSpeed.text = + getString(R.string.default_playback_speed, speedsName[i]) + saveData(player, settings) + dialog.dismiss() + }.show() dialog.window?.setDimAmount(0.8f) } @@ -256,11 +271,12 @@ 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 -> - settings.resize = count - saveData(player, settings) - dialog.dismiss() - }.show() + val dialog = + resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count -> + settings.resize = count + saveData(player, settings) + dialog.dismiss() + }.show() dialog.window?.setDimAmount(0.8f) } fun restartApp() { @@ -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() diff --git a/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt index 8a249dd7538..e0a91af8bb0 100644 --- a/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt @@ -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 diff --git a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt index 61c87afe596..33a6ddf20e9 100644 --- a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt @@ -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 -> diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index 6d1e70df0bb..d140224b8e7 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -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() 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(R.id.colorPickerView) - colorPickerView.setColorListener(ColorListener { color, fromUser -> - val linearLayout = dialogView.findViewById(R.id.linear) - passedColor = color - linearLayout.setBackgroundColor(color) - }) - alertDialog.show() - alertDialog.window?.setDimAmount(0.8f) + } + + 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 = loadData("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0 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("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.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.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.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() } } -} \ No newline at end of file + + 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" + } + } +} diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt index 72e621b16b5..4834125935a 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt @@ -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("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", + OfflineAnimeFragment::class.java.name + ) + startActivity(intent) } PageType.HOME -> { - try { - val arrayOfFiles = - ContextCompat.getExternalFilesDirs(requireContext(), null) - startActivity( - if (loadData("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 { diff --git a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt index 0012e6e8c32..d3499d5c832 100644 --- a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt @@ -44,14 +44,14 @@ class UserInterfaceSettingsActivity : AppCompatActivity() { binding.uiSettingsHomeLayout.setOnClickListener { val dialog = AlertDialog.Builder(this, R.style.DialogTheme) .setTitle(getString(R.string.home_layout_show)).apply { - setMultiChoiceItems( - views, - settings.homeLayoutShow.toBooleanArray() - ) { _, i, value -> - settings.homeLayoutShow[i] = value - saveData(ui, settings) - } - }.show() + setMultiChoiceItems( + views, + settings.homeLayoutShow.toBooleanArray() + ) { _, i, value -> + settings.homeLayoutShow[i] = value + saveData(ui, settings) + } + }.show() dialog.window?.setDimAmount(0.8f) } @@ -68,7 +68,6 @@ class UserInterfaceSettingsActivity : AppCompatActivity() { saveData(ui, settings) restartApp() } - binding.uiSettingsBannerAnimation.isChecked = settings.bannerAnimations binding.uiSettingsBannerAnimation.setOnCheckedChangeListener { _, isChecked -> settings.bannerAnimations = isChecked diff --git a/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt index c3e3b4f4b0b..4caf9e21d02 100644 --- a/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt +++ b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt @@ -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().get(sourceId)!! - check(source is ConfigurableAnimeSource) + val source = Injekt.get().get(sourceId) as? ConfigurableAnimeSource + ?: error("Source with id: $sourceId not found!") val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) val dataStore = SharedPreferencesDataStore(sharedPreferences) diff --git a/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt index 045ca1400ef..46efaf5a9dc 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt @@ -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) diff --git a/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt index 0ae7e01df7f..ed95c2be640 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt @@ -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) diff --git a/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt index 82585ce0031..91dcffd6002 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt @@ -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) diff --git a/app/src/main/java/ani/dantotsu/subcriptions/NotificationClickReceiver.kt b/app/src/main/java/ani/dantotsu/subcriptions/NotificationClickReceiver.kt new file mode 100644 index 00000000000..6b6e20b2a62 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/subcriptions/NotificationClickReceiver.kt @@ -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) + + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt b/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt index c28b5a497a0..83b3bd70593 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt @@ -87,9 +87,7 @@ class Subscription { progress(index[it.first]!!, parser.name, media.name) val ep: MangaChapter? = SubscriptionHelper.getChapter(context, parser, media.id, media.isAdult) - if (ep != null) currActivity()!!.getString(R.string.chapter) + "${ep.number}${ - if (ep.title != null) " : ${ep.title}" else "" - } " + currActivity()!!.getString(R.string.just_released) to null + if (ep != null) ep.number + " " + currActivity()!!.getString(R.string.just_released) to null else null } ?: return@map createNotification(context.applicationContext, media, text.first, text.second) diff --git a/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt b/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt index 2029177b9ec..4dc1da4f1df 100644 --- a/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt +++ b/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt @@ -4,12 +4,14 @@ import android.app.Activity import android.content.Context import android.content.res.Configuration import android.graphics.Bitmap +import android.view.Window +import android.view.WindowManager import ani.dantotsu.R import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColorsOptions -class ThemeManager(private val context: Context) { +class ThemeManager(private val context: Activity) { fun applyTheme(fromImage: Bitmap? = null) { val useOLED = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) .getBoolean("use_oled", false) && isDarkThemeActive(context) @@ -42,20 +44,36 @@ class ThemeManager(private val context: Context) { .getString("theme", "PURPLE")!! val themeToApply = when (theme) { - "PURPLE" -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple "BLUE" -> if (useOLED) R.style.Theme_Dantotsu_BlueOLED else R.style.Theme_Dantotsu_Blue "GREEN" -> if (useOLED) R.style.Theme_Dantotsu_GreenOLED else R.style.Theme_Dantotsu_Green + "PURPLE" -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple "PINK" -> if (useOLED) R.style.Theme_Dantotsu_PinkOLED else R.style.Theme_Dantotsu_Pink + "SAIKOU" -> if (useOLED) R.style.Theme_Dantotsu_SaikouOLED else R.style.Theme_Dantotsu_Saikou "RED" -> if (useOLED) R.style.Theme_Dantotsu_RedOLED else R.style.Theme_Dantotsu_Red "LAVENDER" -> if (useOLED) R.style.Theme_Dantotsu_LavenderOLED else R.style.Theme_Dantotsu_Lavender + "OCEAN" -> if (useOLED) R.style.Theme_Dantotsu_OceanOLED else R.style.Theme_Dantotsu_Ocean "MONOCHROME (BETA)" -> if (useOLED) R.style.Theme_Dantotsu_MonochromeOLED else R.style.Theme_Dantotsu_Monochrome - "SAIKOU" -> if (useOLED) R.style.Theme_Dantotsu_SaikouOLED else R.style.Theme_Dantotsu_Saikou else -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple } + val window = context.window + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + window.statusBarColor = 0x00000000 context.setTheme(themeToApply) } + fun setWindowFlag(activity: Activity, bits: Int, on: Boolean) { + val win: Window = activity.window + val winParams: WindowManager.LayoutParams = win.attributes + if (on) { + winParams.flags = winParams.flags or bits + } else { + winParams.flags = winParams.flags and bits.inv() + } + win.attributes = winParams + } + private fun applyDynamicColors( useMaterialYou: Boolean, context: Context, @@ -109,14 +127,15 @@ class ThemeManager(private val context: Context) { companion object { enum class Theme(val theme: String) { - PURPLE("PURPLE"), BLUE("BLUE"), GREEN("GREEN"), + PURPLE("PURPLE"), PINK("PINK"), + SAIKOU("SAIKOU"), RED("RED"), LAVENDER("LAVENDER"), - MONOCHROME("MONOCHROME (BETA)"), - SAIKOU("SAIKOU"); + OCEAN("OCEAN"), + MONOCHROME("MONOCHROME (BETA)"); companion object { fun fromString(value: String): Theme { diff --git a/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringRemoteViewsFactory.kt b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringRemoteViewsFactory.kt new file mode 100644 index 00000000000..3fd890bbb4e --- /dev/null +++ b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringRemoteViewsFactory.kt @@ -0,0 +1,139 @@ +package ani.dantotsu.widgets + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.widget.RemoteViews +import android.widget.RemoteViewsService +import ani.dantotsu.R +import ani.dantotsu.logger +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL + +class CurrentlyAiringRemoteViewsFactory(private val context: Context, intent: Intent) : + RemoteViewsService.RemoteViewsFactory { + private var widgetItems = mutableListOf() + + override fun onCreate() { + // 4 items for testing + widgetItems.clear() + logger("CurrentlyAiringRemoteViewsFactory onCreate") + widgetItems = List(4) { + WidgetItem( + "Show $it", + "$it days $it hours $it minutes", + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx14741-alxqoP4yx6WF.jpg" + ) + }.toMutableList() + } + + override fun onDataSetChanged() { + // 4 items for testing + logger("CurrentlyAiringRemoteViewsFactory onDataSetChanged") + widgetItems.clear() + widgetItems.add( + WidgetItem( + "Show 1", + "1 day 2 hours 3 minutes", + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx14741-alxqoP4yx6WF.jpg" + ) + ) + widgetItems.add( + WidgetItem( + "Show 2", + "2 days 3 hours 4 minutes", + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx14741-alxqoP4yx6WF.jpg" + ) + ) + widgetItems.add( + WidgetItem( + "Show 3", + "3 days 4 hours 5 minutes", + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx14741-alxqoP4yx6WF.jpg" + ) + ) + widgetItems.add( + WidgetItem( + "Show 4", + "4 days 5 hours 6 minutes", + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx14741-alxqoP4yx6WF.jpg" + ) + ) + widgetItems.add( + WidgetItem( + "Show 5", + "5 days 6 hours 7 minutes", + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx14741-alxqoP4yx6WF.jpg" + ) + ) + } + + override fun onDestroy() { + widgetItems.clear() + } + + override fun getCount(): Int { + return widgetItems.size + } + + override fun getViewAt(position: Int): RemoteViews { + logger("CurrentlyAiringRemoteViewsFactory getViewAt") + val item = widgetItems[position] + val rv = RemoteViews(context.packageName, R.layout.item_currently_airing_widget).apply { + setTextViewText(R.id.text_show_title, item.title) + setTextViewText(R.id.text_show_countdown, item.countdown) + val bitmap = downloadImageAsBitmap(item.image) + //setImageViewUri(R.id.image_show_icon, Uri.parse(item.image)) + setImageViewBitmap(R.id.image_show_icon, bitmap) + } + + return rv + } + + private fun downloadImageAsBitmap(imageUrl: String): Bitmap? { + var bitmap: Bitmap? = null + var inputStream: InputStream? = null + var urlConnection: HttpURLConnection? = null + + try { + val url = URL(imageUrl) + urlConnection = url.openConnection() as HttpURLConnection + urlConnection.requestMethod = "GET" + urlConnection.connect() + + if (urlConnection.responseCode == HttpURLConnection.HTTP_OK) { + inputStream = urlConnection.inputStream + bitmap = BitmapFactory.decodeStream(inputStream) + } + } catch (e: Exception) { + e.printStackTrace() + // Handle the error according to your needs + } finally { + // Clean up resources + inputStream?.close() + urlConnection?.disconnect() + } + + return bitmap + } + + override fun getLoadingView(): RemoteViews { + return RemoteViews(context.packageName, R.layout.item_currently_airing_widget) + } + + override fun getViewTypeCount(): Int { + return 1 + } + + override fun getItemId(p0: Int): Long { + return p0.toLong() + } + + override fun hasStableIds(): Boolean { + return true + } +} + +data class WidgetItem(val title: String, val countdown: String, val image: String) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringRemoteViewsService.kt b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringRemoteViewsService.kt new file mode 100644 index 00000000000..9770ff8695f --- /dev/null +++ b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringRemoteViewsService.kt @@ -0,0 +1,12 @@ +package ani.dantotsu.widgets + +import android.content.Intent +import android.widget.RemoteViewsService +import ani.dantotsu.logger + +class CurrentlyAiringRemoteViewsService : RemoteViewsService() { + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { + logger("CurrentlyAiringRemoteViewsFactory onGetViewFactory") + return CurrentlyAiringRemoteViewsFactory(applicationContext, intent) + } +} diff --git a/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringWidget.kt b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringWidget.kt new file mode 100644 index 00000000000..b2c3948c81e --- /dev/null +++ b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringWidget.kt @@ -0,0 +1,112 @@ +package ani.dantotsu.widgets + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.net.Uri +import android.widget.RemoteViews +import androidx.core.content.res.ResourcesCompat +import ani.dantotsu.R + +/** + * Implementation of App Widget functionality. + * App Widget Configuration implemented in [CurrentlyAiringWidgetConfigureActivity] + */ +class CurrentlyAiringWidget : AppWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + appWidgetIds.forEach { appWidgetId -> + val intent = Intent(context, CurrentlyAiringRemoteViewsService::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + + val rv = RemoteViews(context.packageName, R.layout.currently_airing_widget).apply { + setRemoteAdapter(R.id.widgetListView, intent) + setEmptyView(R.id.widgetListView, R.id.empty_view) + } + + appWidgetManager.updateAppWidget(appWidgetId, rv) + } + super.onUpdate(context, appWidgetManager, appWidgetIds) + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + // When the user deletes the widget, delete the preference associated with it. + for (appWidgetId in appWidgetIds) { + deleteTitlePref(context, appWidgetId) + } + super.onDeleted(context, appWidgetIds) + } + + override fun onEnabled(context: Context) { + super.onEnabled(context) + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + } + + companion object { + fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + color: Int + ) { + // Create an intent to launch the configuration activity when the widget is clicked + val intent = Intent(context, CurrentlyAiringWidgetConfigureActivity::class.java) + val pendingIntent = + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + // Get the gradient drawable resource and update its start color with the user-selected color + val gradientDrawable = ResourcesCompat.getDrawable( + context.resources, + R.drawable.gradient_background, + null + ) as GradientDrawable + gradientDrawable.colors = intArrayOf(color, Color.GRAY) // End color is gray. + + // Create the RemoteViews object and set the background + val views = RemoteViews(context.packageName, R.layout.currently_airing_widget).apply { + //setImageViewBitmap(R.id.backgroundView, convertDrawableToBitmap(gradientDrawable)) + //setOnClickPendingIntent(R.id.backgroundView, pendingIntent) + } + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views) + } + + private fun convertDrawableToBitmap(drawable: Drawable): Bitmap { + val bitmap = Bitmap.createBitmap(100, 300, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } + } +} + +internal fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int +) { + val widgetText = loadTitlePref(context, appWidgetId) + // Construct the RemoteViews object + val views = RemoteViews(context.packageName, R.layout.currently_airing_widget) + views.setTextViewText(R.id.appwidget_text, widgetText) + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views) +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringWidgetConfigureActivity.kt b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringWidgetConfigureActivity.kt new file mode 100644 index 00000000000..79b928f547c --- /dev/null +++ b/app/src/main/java/ani/dantotsu/widgets/CurrentlyAiringWidgetConfigureActivity.kt @@ -0,0 +1,111 @@ +package ani.dantotsu.widgets + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.EditText +import ani.dantotsu.R +import ani.dantotsu.databinding.CurrentlyAiringWidgetConfigureBinding +import ani.dantotsu.others.LangSet +import ani.dantotsu.themes.ThemeManager + +/** + * The configuration screen for the [CurrentlyAiringWidget] AppWidget. + */ +class CurrentlyAiringWidgetConfigureActivity : Activity() { + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + private lateinit var appWidgetText: EditText + private var onClickListener = View.OnClickListener { + val context = this@CurrentlyAiringWidgetConfigureActivity + + // When the button is clicked, store the string locally + val widgetText = appWidgetText.text.toString() + saveTitlePref(context, appWidgetId, widgetText) + + // It is the responsibility of the configuration activity to update the app widget + val appWidgetManager = AppWidgetManager.getInstance(context) + //updateAppWidget(context, appWidgetManager, appWidgetId) + + + CurrentlyAiringWidget.updateAppWidget( + context, + appWidgetManager, + appWidgetId, + -1 + ) + + // Make sure we pass back the original appWidgetId + val resultValue = Intent() + resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + setResult(RESULT_OK, resultValue) + finish() + } + private lateinit var binding: CurrentlyAiringWidgetConfigureBinding + + public override fun onCreate(icicle: Bundle?) { + LangSet.setLocale(this) + ThemeManager(this).applyTheme() + super.onCreate(icicle) + + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if the user presses the back button. + setResult(RESULT_CANCELED) + + binding = CurrentlyAiringWidgetConfigureBinding.inflate(layoutInflater) + setContentView(binding.root) + + appWidgetText = binding.appwidgetText + binding.addButton.setOnClickListener(onClickListener) + + // Find the widget id from the intent. + val intent = intent + val extras = intent.extras + if (extras != null) { + appWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + + // If this activity was started with an intent without an app widget ID, finish with an error. + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + appWidgetText.setText( + loadTitlePref( + this@CurrentlyAiringWidgetConfigureActivity, + appWidgetId + ) + ) + + } + +} + +private const val PREFS_NAME = "ani.dantotsu.parsers.CurrentlyAiringWidget" +private const val PREF_PREFIX_KEY = "appwidget_" + +// Write the prefix to the SharedPreferences object for this widget +internal fun saveTitlePref(context: Context, appWidgetId: Int, text: String) { + val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit() + prefs.putString(PREF_PREFIX_KEY + appWidgetId, text) + prefs.apply() +} + +// Read the prefix from the SharedPreferences object for this widget. +// If there is no preference saved, get the default from a resource +internal fun loadTitlePref(context: Context, appWidgetId: Int): String { + val prefs = context.getSharedPreferences(PREFS_NAME, 0) + val titleValue = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null) + return titleValue ?: context.getString(R.string.appwidget_text) +} + +internal fun deleteTitlePref(context: Context, appWidgetId: Int) { + val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit() + prefs.remove(PREF_PREFIX_KEY + appWidgetId) + prefs.apply() +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt index 176322d4953..fd4e2a2bda8 100644 --- a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt @@ -20,7 +20,10 @@ class BasePreferences( fun acraEnabled() = preferenceStore.getBoolean("acra.enable", true) - fun deviceHasPip() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + fun deviceHasPip() = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && context.packageManager.hasSystemFeature( + PackageManager.FEATURE_PICTURE_IN_PICTURE + ) enum class ExtensionInstaller(val titleResId: String) { LEGACY("Legacy"), diff --git a/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt b/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt index 14dbe10eaee..e9a5c3a7adb 100644 --- a/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt +++ b/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt @@ -1,8 +1,8 @@ package eu.kanade.domain.base import android.content.Context -import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller import eu.kanade.domain.base.BasePreferences.ExtensionInstaller +import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller import eu.kanade.tachiyomi.util.system.isShizukuInstalled import kotlinx.coroutines.CoroutineScope import tachiyomi.core.preference.Preference @@ -19,14 +19,14 @@ class ExtensionInstallerPreference( override fun key() = "extension_installer" - - val entries get() = BasePreferences.ExtensionInstaller.values().run { - if (context.hasMiuiPackageInstaller) { - filter { it != BasePreferences.ExtensionInstaller.PACKAGEINSTALLER } - } else { - toList() + val entries + get() = ExtensionInstaller.values().run { + if (context.hasMiuiPackageInstaller) { + filter { it != ExtensionInstaller.PACKAGEINSTALLER } + } else { + toList() + } } - } override fun defaultValue() = if (context.hasMiuiPackageInstaller) { ExtensionInstaller.LEGACY @@ -39,9 +39,11 @@ class ExtensionInstallerPreference( ExtensionInstaller.PACKAGEINSTALLER -> { if (context.hasMiuiPackageInstaller) return ExtensionInstaller.LEGACY } + ExtensionInstaller.SHIZUKU -> { if (!context.isShizukuInstalled) return defaultValue() } + else -> {} } return value diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index 22a24479b2f..a22ee27b647 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -11,15 +11,23 @@ class SourcePreferences( // Common options - fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize) + fun sourceDisplayMode() = preferenceStore.getObject( + "pref_display_mode_catalogue", + LibraryDisplayMode.default, + LibraryDisplayMode.Serializer::serialize, + LibraryDisplayMode.Serializer::deserialize + ) - fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) + fun enabledLanguages() = + preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true) - fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) + fun migrationSortingMode() = + preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) - fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING) + fun migrationSortingDirection() = + preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING) fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet()) @@ -37,12 +45,17 @@ class SourcePreferences( fun animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0) fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) - fun searchPinnedAnimeSourcesOnly() = preferenceStore.getBoolean("search_pinned_anime_sources_only", false) - fun searchPinnedMangaSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false) + fun searchPinnedAnimeSourcesOnly() = + preferenceStore.getBoolean("search_pinned_anime_sources_only", false) - fun hideInAnimeLibraryItems() = preferenceStore.getBoolean("browse_hide_in_anime_library_items", false) + fun searchPinnedMangaSourcesOnly() = + preferenceStore.getBoolean("search_pinned_sources_only", false) - fun hideInMangaLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) + fun hideInAnimeLibraryItems() = + preferenceStore.getBoolean("browse_hide_in_anime_library_items", false) + + fun hideInMangaLibraryItems() = + preferenceStore.getBoolean("browse_hide_in_library_items", false) // SY --> @@ -62,7 +75,8 @@ class SourcePreferences( fun dataSaverImageQuality() = preferenceStore.getInt("data_saver_image_quality", 80) - fun dataSaverImageFormatJpeg() = preferenceStore.getBoolean("data_saver_image_format_jpeg", false) + fun dataSaverImageFormatJpeg() = + preferenceStore.getBoolean("data_saver_image_format_jpeg", false) fun dataSaverServer() = preferenceStore.getString("data_saver_server", "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/NOTICE.md b/app/src/main/java/eu/kanade/tachiyomi/NOTICE.md index 5a81820ba40..2669a7906fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/NOTICE.md +++ b/app/src/main/java/eu/kanade/tachiyomi/NOTICE.md @@ -1,3 +1,4 @@ NOTICE -This software includes code modified from Aniyomi, available at https://github.com/aniyomiorg/aniyomi/. \ No newline at end of file +This software includes code modified from Aniyomi, available +at https://github.com/aniyomiorg/aniyomi/. \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt index 210e04dff43..53da5305e6a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt @@ -25,41 +25,11 @@ interface AnimeSource { get() = "" /** - * Returns an observable with the updated details for a anime. + * Get the updated details for a anime. * + * @since extensions-lib 1.5 * @param anime the anime to update. - */ - @Deprecated( - "Use the 1.x API instead", - ReplaceWith("getAnimeDetails"), - ) - fun fetchAnimeDetails(anime: SAnime): Observable = throw IllegalStateException("Not used") - - /** - * Returns an observable with all the available episodes for a anime. - * - * @param anime the anime to update. - */ - @Deprecated( - "Use the 1.x API instead", - ReplaceWith("getEpisodeList"), - ) - fun fetchEpisodeList(anime: SAnime): Observable> = throw IllegalStateException("Not used") - - /** - * Returns an observable with the list of videos a episode has. Videos should be returned - * in the expected order; the index is ignored. - * - * @param episode the episode. - */ - @Deprecated( - "Use the 1.x API instead", - ReplaceWith("getVideoList"), - ) - fun fetchVideoList(episode: SEpisode): Observable> = Observable.empty() - - /** - * [1.x API] Get the updated details for a anime. + * @return the updated anime. */ @Suppress("DEPRECATION") suspend fun getAnimeDetails(anime: SAnime): SAnime { @@ -67,7 +37,11 @@ interface AnimeSource { } /** - * [1.x API] Get all the available episodes for a anime. + * Get all the available episodes for a anime. + * + * @since extensions-lib 1.5 + * @param anime the anime to update. + * @return the episodes for the anime. */ @Suppress("DEPRECATION") suspend fun getEpisodeList(anime: SAnime): List { @@ -75,11 +49,37 @@ interface AnimeSource { } /** - * [1.x API] Get the list of videos a episode has. Videos should be returned + * Get the list of videos a episode has. Pages should be returned * in the expected order; the index is ignored. + * + * @since extensions-lib 1.5 + * @param episode the episode. + * @return the videos for the episode. */ @Suppress("DEPRECATION") suspend fun getVideoList(episode: SEpisode): List diff --git a/app/src/main/res/layout/activity_media.xml b/app/src/main/res/layout/activity_media.xml index a34117280d0..d1e3a6471cb 100644 --- a/app/src/main/res/layout/activity_media.xml +++ b/app/src/main/res/layout/activity_media.xml @@ -156,6 +156,7 @@ android:layout_height="match_parent" android:layout_gravity="center_vertical" app:contentScrim="?android:colorBackground" + android:background="?attr/colorSurface" android:ellipsize="marquee" android:focusable="true" android:focusableInTouchMode="true" @@ -242,7 +243,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="0dp" - android:layout_gravity="bottom" + android:layout_gravity="center_horizontal|bottom" android:background="?attr/colorSurface" android:translationZ="1dp" app:itemActiveIndicatorStyle="@style/BottomNavBar" @@ -253,6 +254,27 @@ app:itemTextColor="@color/tab_layout_icon" app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" /> + + + + + - - + - - - - - - + @@ -643,7 +643,7 @@ android:paddingEnd="32dp" android:text="@string/auto" /> - - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + diff --git a/app/src/main/res/layout/activity_reader_settings.xml b/app/src/main/res/layout/activity_reader_settings.xml index 8eb5982266f..e8886066113 100644 --- a/app/src/main/res/layout/activity_reader_settings.xml +++ b/app/src/main/res/layout/activity_reader_settings.xml @@ -73,609 +73,1176 @@ android:clipToPadding="false" android:orientation="vertical"> - + android:orientation="vertical"> - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + + + + + + android:layout_marginTop="16dp" + android:layout_marginBottom="8dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingStart="32dp" + android:paddingEnd="32dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + android:text="Line Height" /> - - + app:cardBackgroundColor="@color/nav_bg_inv" + app:cardCornerRadius="16dp" + app:cardElevation="0dp"> + + + + + - - - - - - - - - - - - - - + + - - + + + + + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginBottom="10dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingStart="32dp" + android:paddingEnd="32dp"> + android:text="Margin" /> - + app:cardBackgroundColor="@color/nav_bg_inv" + app:cardCornerRadius="16dp" + app:cardElevation="0dp"> + + + + + + + + + + + + + - - - - + android:layout_marginTop="16dp" + android:layout_marginBottom="10dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingStart="32dp" + android:paddingEnd="32dp"> - + - + + + + + + + + + + + + + + + + + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginBottom="10dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingStart="32dp" + android:paddingEnd="32dp"> + android:text="Maximum Block Size" /> - + app:cardBackgroundColor="@color/nav_bg_inv" + app:cardCornerRadius="16dp" + app:cardElevation="0dp"> + + + + + + + + + + + + + - - - - - - + + - - - - - + + - - - - - - - - - - - - + android:checked="false" + android:drawableStart="@drawable/ic_round_brightness_4_24" + android:drawablePadding="16dp" + android:elegantTextHeight="true" + android:fontFamily="@font/poppins_bold" + android:minHeight="64dp" + android:paddingStart="32dp" + android:paddingEnd="32dp" + android:text="Use OLED Theme" + android:textAlignment="viewStart" + android:textColor="?attr/colorOnBackground" + app:cornerRadius="0dp" + app:drawableTint="?attr/colorPrimary" + app:showText="false" + app:thumbTint="@color/button_switch_track" + tools:ignore="VisualLintButtonSize" /> + + + + + - - - - - - - - - - - - - + app:thumbTint="@color/button_switch_track"> - + - - - - - + app:thumbTint="@color/button_switch_track"> + - - + - - + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 849098faef6..1c3d5f805b2 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -90,78 +90,71 @@ tools:ignore="TextContrastCheck" /> - + - - - - - - - - - - - - - - - + + + + + + + + + + + + - - + + - + android:text="@string/custom_theme" /> - - - + app:srcCompat="@drawable/ic_anilist" + app:tint="?attr/colorPrimary" /> + android:textColor="?attr/colorSecondary" + android:textSize="16sp" /> + app:srcCompat="@drawable/ic_myanimelist" + app:tint="?attr/colorPrimary" /> + android:textColor="?attr/colorSecondary" + android:textSize="16sp" /> + android:textColor="?attr/colorSecondary" + android:textSize="16sp" /> - - - + + + + +