Compare commits

...

58 Commits

Author SHA1 Message Date
Finnley Somdahl
8d9254140d quickfix 2023-11-21 03:01:11 -06:00
rebelonion
533aa9f56e Merge pull request #54 from rebelonion/dev
Dev
2023-11-21 02:52:46 -06:00
rebelonion
c310bea0e9 Merge branch 'main' into dev 2023-11-21 02:52:31 -06:00
Finnley Somdahl
813f7a0992 Update AniyomiAdapter.kt 2023-11-21 02:49:52 -06:00
Finnley Somdahl
4c0f56d3e3 Squashed commit of the following:
commit 187262a266
Author: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com>
Date:   Mon Nov 20 23:38:24 2023 -0600

    work
2023-11-21 02:46:56 -06:00
Finnley Somdahl
1f44d32f35 :bocchi_overload_animated: 2023-11-21 02:38:18 -06:00
Finnley Somdahl
d937f447ef work(ing) 2023-11-20 23:51:59 -06:00
Finnley Somdahl
187262a266 work 2023-11-20 23:38:24 -06:00
Finnley Somdahl
f40ebc9d09 on second thought 2023-11-20 19:53:09 -06:00
Finnley Somdahl
3998d88297 oh no 2023-11-20 19:45:33 -06:00
Finnley Somdahl
4db301ca7a more offline stuff/bugfixes 2023-11-20 00:39:14 -06:00
Finnley Somdahl
3dfcc9fc31 Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2023-11-19 20:49:12 -06:00
Asvin Ragunathan
df63586c02 Updated Support For Android 14 (API 34) (WIP) (#47)
* Updated Support For Android 14 (API 34)

* Updated To Not Require Greater than API 26

* Fixed Github Stash Issue With Commit
2023-11-19 20:48:59 -06:00
Finnley Somdahl
d7372d4dbb move CoroutineScopes for clarity 2023-11-19 20:47:43 -06:00
aayush262
f4266d0da3 got rid of both companion object (#50) 2023-11-18 00:33:09 -06:00
aayush262
736b06bdbe Added a option to toggle fast forward / Added NSFW extension toggle to extension settings (#48)
* Remove 18+ extension if Anilist 18+ is off
 ~requested by @arif

* Translation filter for extension(WIP)

* Added a option to toggle fast forward
suggested by arif

* Added NFSW toggle to extension settings
now it will be more easy rather then going to anilist to toggle it
 ~suggested by arif

* Forgot to undo this

* changed icons in extension setting

* get rid of companion object (todo)

* get rid of companion object (todo)

---------

Co-authored-by: rebelonion <87634197+rebelonion@users.noreply.github.com>
2023-11-17 15:10:58 -06:00
aayush262
5543d29317 Better extension page(maybe) (#45)
* Fixed Mono icon not loading/Extension page customization

* Better extension page(maybe)
2023-11-14 19:41:07 -06:00
aayush262
2fc351f57a Fixed Mono icon not loading/Extension page customization (#44) 2023-11-12 23:08:14 -06:00
Wai What
eee1242964 Added Saikou theme (#40)
* Update colors.xml

* Update themes.xml

* Update themes.xml

* Update ThemeManager.kt

* Update ThemeManager.kt

* Update ThemeManager.kt

* Update ThemeManager.kt

* Update DevelopersDialogFragment.kt

* Update activity_main.xml

* Update item_anime_page.xml

* Update item_manga_page.xml

* Update fragment_login.xml

* Update activity_media.xml

* Update activity_media.xml

* Update item_anime_page.xml

* Update item_manga_page.xml

* Update themes.xml

* Update themes.xml

* Update exo_player_control_view.xml

* Update activity_author.xml

* Update activity_studio.xml

* Update activity_manga_reader.xml

* Update activity_novel_reader.xml

* Update activity_media.xml

Fix

* Update tab_layout_icon.xml

* Update activity_media.xml

* Update activity_media.xml

* Update tab_layout_icon.xml

Changed selected layout icon from primary to secondary

* Update activity_list.xml

* Update ListActivity.kt

Unbound listTabLayout, listAppBar and listTitle because it stopped color reallocation

* Update CalendarActivity.kt

Unbound listTabLayout, listAppBar and listTitle because it stopped color reallocation

* Update button_switch_track.xml

* Update CalendarActivity.kt

Undo

* Update ListActivity.kt

Undo

* Update CalendarActivity.kt

* Update ListActivity.kt

* Update ListActivity.kt

* Update CalendarActivity.kt (Saikou theme complete!)

I'll just need to check for bugs and request to merge

* Update ThemeManager.kt

Took Sakiou theme out of beta

* Update tab_layout_icon.xml

Changes to media tabs (less accurate to Saikou but selected menu is more vibrant and supports other themes better)

* Update activity_media.xml

Changes to media tabs (less accurate to Saikou but selected menu is more vibrant and supports other themes better)

* Update activity_media.xml

Changes to media tabs (less accurate to Saikou but selected menu is more vibrant and supports other themes better)

* Update control_background_40dp.xml

* Update build.gradle

Changed version number
2023-11-12 12:55:16 -06:00
rebelonion
a58e9a523a Update README.md 2023-11-07 11:00:37 -06:00
Finnley Somdahl
cd3aad1c33 double error and crash fix 2023-11-05 02:40:40 -06:00
Finnley Somdahl
5a482d8307 Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2023-11-05 02:17:57 -06:00
Finnley Somdahl
91d869005c basic offline manga fragment 2023-11-05 02:17:49 -06:00
rebelonion
05e73269d3 Merge pull request #35 from aayush2622/main
Changed "skip loading extension icons" icon
2023-11-05 01:33:10 -06:00
aayush262
3dac48ced8 Changed "skip loading extension icons" icon 2023-11-05 12:45:38 +05:30
rebelonion
8c5726ab8a Merge pull request #31 from elucubro/zero-mb-displaying
when video size is 0mb, display unknown size instead
2023-11-04 13:39:58 -05:00
rebelonion
076516be23 Merge pull request #27 from aayush2622/main
Added some screenshots
2023-11-04 02:13:08 -05:00
elucubro
1059a3c17e when video size is 0mb, display unknown size instead 2023-11-04 15:12:09 +08:00
Finnley Somdahl
c75df942f2 beta updater fix 2023-11-04 02:05:21 -05:00
rebelonion
cfe7be5cdb Merge pull request #30 from elucubro/double-tap-zoom
making double tap zoom lower in manga reader
2023-11-04 02:04:23 -05:00
Finnley Somdahl
390fc18c4c queueing system for manga downloads 2023-11-04 01:40:40 -05:00
elucubro
4c82c56828 making double tap zoom lower in manga reader 2023-11-04 13:13:23 +08:00
aayush262
da5c480ba7 added website link 2023-11-03 20:07:36 +05:30
Finnley Somdahl
20acd71b1a parent acb0225699
author Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992132 -0500
committer Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992691 -0500

manga downloading base

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md
2023-11-03 01:29:24 -05:00
aayush262
231c9c5b98 fixed typo 2023-11-02 21:34:00 +05:30
aayush262
4cfdcdb23c Added some screenshots 2023-11-01 21:46:43 +05:30
Finnley Somdahl
acb0225699 Merge pull request #24 from aayush2622/main
little customizations on readme page
2023-10-31 12:03:30 -05:00
aayush262
ebffaaa742 a lil better README.md 2023-10-31 22:21:28 +05:30
aayush262
1760064555 Update README.md 2023-10-31 17:58:41 +05:30
aayush262
44a6db3fc2 Update README.md 2023-10-31 17:52:27 +05:30
aayush262
aab25d157e Update README.md 2023-10-31 17:51:19 +05:30
aayush262
8a4be86ddc Update README.md 2023-10-31 17:50:00 +05:30
aayush262
31baf729be Update README.md 2023-10-31 17:48:29 +05:30
aayush262
b98e3dc780 Update README.md 2023-10-31 17:41:50 +05:30
aayush262
878d58679e Update README.md 2023-10-31 17:34:50 +05:30
aayush262
f500ba6cf0 Update README.md 2023-10-31 17:25:30 +05:30
aayush262
d124736556 Update README.md 2023-10-31 17:24:15 +05:30
aayush262
1a825e2509 Update README.md 2023-10-31 17:19:59 +05:30
Finnley Somdahl
6e14c2221d Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2023-10-31 01:03:00 -05:00
Finnley Somdahl
5b6e351a56 extension fixes 2023-10-31 01:02:54 -05:00
Finnley Somdahl
c310708401 Merge pull request #23 from Sadwhy/patch-1
Update README.md
2023-10-30 22:03:01 -05:00
Sadwhy
f0093b903a Update README.md
Just made some titles uppercase. Consistency
2023-10-31 09:00:47 +06:00
Finnley Somdahl
d33568f0ad icon and logging 2023-10-29 22:37:28 -05:00
Finnley Somdahl
26f9f40042 ui tweaks 2023-10-29 20:33:13 -05:00
Finnley Somdahl
7545870f38 ui tweaks 2023-10-29 20:06:16 -05:00
Finnley Somdahl
3368a1bc8d extension settings 2023-10-29 19:45:11 -05:00
Finnley Somdahl
9c0ef7a788 bugfixes and themes 2023-10-27 00:47:44 -05:00
Finnley Somdahl
960c2b4113 Update stable.md 2023-10-26 02:11:43 -05:00
201 changed files with 6376 additions and 1045 deletions

3
.gitignore vendored
View File

@@ -28,3 +28,6 @@ google-services.json
# Android Profiling
*.hprof
#other
scripts/

113
README.md
View File

@@ -1,109 +1,36 @@
# **Dantotsu** (🚧 ALPHA 🚧)
> ⚠️ **WARNING**: This project is in alpha stage. Things may not work as expected.
<p align="center">
<img src="https://pbxt.replicate.delivery/2PX94viD6lJSDVayQrGyDH7CGu7IjQ6e8HEtOGDeelefXRdOC/out.png" alt="Dantotsu Banner" width=100% >
</p>
<p align="center">
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
</p>
Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an <a href="https://anilist.co/">Anilist</a> only client, which also lets you stream-download Anime & Manga through extensions.
<br><br>
<i>Dantotsu (断トツ; Dan-totsu) literally means the best of the best in Japanese. Well, we would like to say this is the best open source app for anime and manga on Android, but hey, try it out yourself & judge!
</i>
<br>
<br>
# **Dantotsu** 🌟
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!
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
<br>
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
> **Warning**
>
> Please do not attempt to upload Dantotsu or any of it's forks on Playstore or any other Android appstores on the internet. Doing so, may infringe their terms and conditions. This may result to legal action or immediate take-down of the app.
## WANT TO CONTRIBUTE? 🤝
## Extension Status
All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content - whatever you have to offer, we can use!
| Type | Status |
| ---------------- | ------- |
| Anime Extensions | Working |
| Manga Extensions | "Working" |
| Light Novel Extensions | Not Working |
You can come hang out with our awesome community, request new features, and report any bugs or issues at our Discord server too. 📣
### OFFICIAL DISCORD SERVER 🚀
## APP FEATURES
- Easy and functional way to both, watch anime and read manga, ad-free.
- A completely open source app with a nice UI & Animations :)
- Aniyomi extension support built right into the app.
- Synchronize anime and manga real-time with AniList and MyAnimeList. Easily categorise anime and manga based on your current status. (Powered by AniList)
- Find all shows using thoroughly and frequently updated list of all trending, popular and ongoing anime based on scores.
- View extensive details about anime shows, movies and manga titles. It also features ability to countdown to the next episode of airing anime. (Powered by AniList & MyAnimeList)
- Get notified when new episodes/chapters are released!
* **Available Anime sources:-**
NONE BUILT IN!
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
* **Available Manga sources:-**
NONE BUILT IN!
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
## Planned Stuff
- get app out of alpha
- Accent Color Change (RIP Hot Pink Supremacy.)
## Rejected Stuff (still rejected)
- Sources of any language except English
- News Section in the App
- Comment Section
## WANT TO CONTRIBUTE?
- All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content- whatever you have to offer, we can use it!
- You can come hang out with our awesome community and request new features and report any bugs or issue at our discord server too.
### Official Discord Server
<p align="center">
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
<a href="https://discord.gg/4HPZ5nAWwM">
<img src="https://invidget.switchblade.xyz/4HPZ5nAWwM">
</a>
</p>
### VISIT FOR MORE INFORMATION:-
no website yet :(
## DISCLAIMER
* Dantotsu by itself only provides an anime and manga tracker and does not provide any anime or manga streaming or downloading capabilities.
* Dantotsu or any of its developer/staff don't host any of the content found inside Dantotsu. Any and all images and anime/manga information found in the app are taken from various public APIs (AniList, MyAnimeList, Kitsu).
* Furthermore, all of the anime/manga links found in Dantotsu are taken from various 3rd party plugins and have no affiliation with Dantotsu or its staff.
* Dantotsu or it's owners aren't liable for any misuse of any of the contents found inside or outside of the app and cannot be held accountable for the distribution of any of the contents found inside the app.
* By using Dantotsu, you comply to the fact that the developer of the app is not responsible for any of the contents found in the app. You also agree to the fact that you may not use Dantotsu to download or stream any copyrighted content.
* If the internet infringement issues are involved, please contact the source website. The developer does not assume any legal responsibility.
## License
## LICENSE 📜
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)

View File

@@ -21,15 +21,14 @@ android {
minSdk 23
targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "0.1.5"
versionName "1.0.0-beta03"
signingConfig signingConfigs.debug
}
buildTypes {
debug {
//applicationIdSuffix ".beta"
applicationIdSuffix ".beta"
debuggable true
versionNameSuffix "." + gitCommitHash
}
release {
debuggable false
@@ -62,9 +61,10 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:2.8.1"
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.github.Blatzar:NiceHttp:0.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'androidx.preference:preference:1.2.1'
// Glide
ext.glide_version = '4.16.0'

View File

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

View File

@@ -221,6 +221,7 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".download.DownloadContainerActivity" />
<activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
@@ -263,10 +264,16 @@
</intent-filter>
</service>
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<service android:name=".download.manga.MangaDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -11,16 +11,28 @@ import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import eu.kanade.tachiyomi.data.notification.Notifications
import tachiyomi.core.util.system.logcat
import ani.dantotsu.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
import com.google.android.material.color.DynamicColors
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
@SuppressLint("StaticFieldLeak")
class App : MultiDexApplication() {
private lateinit var animeExtensionManager: AnimeExtensionManager
private lateinit var mangaExtensionManager: MangaExtensionManager
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
@@ -52,6 +64,22 @@ class App : MultiDexApplication() {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}
animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get()
val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch {
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
}
private fun setupNotificationChannels() {

View File

@@ -188,6 +188,9 @@ fun Activity.hideStatusBar() {
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() {
super.onStart()
val window = dialog?.window
val decorView: View = window?.decorView ?: return
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
@@ -204,23 +207,21 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
fun isOnline(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false
}
} else false
} else true
else -> false
}
} else false
} ?: false
}
@@ -419,7 +420,13 @@ fun String.findBetween(a: String, b: String): String? {
fun ImageView.loadImage(url: String?, size: Int = 0) {
if (!url.isNullOrEmpty()) {
loadImage(FileUrl(url), size)
val localFile = File(url)
if (localFile.exists()) {
loadLocalImage(localFile, size)
}
else {
loadImage(FileUrl(url), size)
}
}
}
@@ -432,6 +439,14 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
}
}
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
if (file?.exists() == true) {
tryWith {
Glide.with(this.context).load(file).transition(withCrossFade()).override(size).into(this)
}
}
}
class SafeClickListener(
private var defaultInterval: Int = 1000,
private val onSafeCLick: (View) -> Unit
@@ -729,7 +744,7 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
if (s != null) {
(activity ?: currActivity())?.apply {
runOnUiThread {
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG)
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_SHORT)
snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)

View File

@@ -21,6 +21,7 @@ import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.FrameLayout
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.viewModels
@@ -40,7 +41,9 @@ import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.ItemNavbarBinding
import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment
@@ -54,6 +57,8 @@ import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
@@ -76,39 +81,32 @@ class MainActivity : AppCompatActivity() {
private var load = false
private var uiSettings = UserInterfaceSettings()
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
val backgroundDrawable = bottomBar.background as GradientDrawable
val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
bottomBar.background = backgroundDrawable
_bottomBar.background = backgroundDrawable
}
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
}
val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch {
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) {
if (doubleBackToExitPressedOnce) {
@@ -125,24 +123,40 @@ class MainActivity : AppCompatActivity() {
binding.root.isMotionEventSplittingEnabled = false
lifecycleScope.launch {
val splash = SplashScreenBinding.inflate(layoutInflater)
binding.root.addView(splash.root)
(splash.splashImage.drawable as Animatable).start()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
val splash = SplashScreenBinding.inflate(layoutInflater)
binding.root.addView(splash.root)
(splash.splashImage.drawable as Animatable).start()
// Wait for 2 seconds (2000 milliseconds)
delay(2000)
delay(1200)
// Now perform the animation
ObjectAnimator.ofFloat(
splash.root,
View.TRANSLATION_Y,
0f,
-splash.root.height.toFloat()
).apply {
interpolator = AnticipateInterpolator()
duration = 200L
doOnEnd { binding.root.removeView(splash.root) }
start()
ObjectAnimator.ofFloat(
splash.root,
View.TRANSLATION_Y,
0f,
-splash.root.height.toFloat()
).apply {
interpolator = AnticipateInterpolator()
duration = 200L
doOnEnd { binding.root.removeView(splash.root) }
start()
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { splashScreenView ->
ObjectAnimator.ofFloat(
splashScreenView,
View.TRANSLATION_Y,
0f,
-splashScreenView.height.toFloat()
).apply {
interpolator = AnticipateInterpolator()
duration = 200L
doOnEnd { splashScreenView.remove() }
start()
}
}
}
@@ -151,7 +165,7 @@ class MainActivity : AppCompatActivity() {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab
binding.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
@@ -164,7 +178,7 @@ class MainActivity : AppCompatActivity() {
model.genres.observe(this) {
if (it != null) {
if (it) {
val navbar = binding.navbar
val navbar = binding.includedNavbar.navbar
bottomBar = navbar
navbar.visibility = View.VISIBLE
binding.mainProgressBar.visibility = View.GONE

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application
import android.content.Context
import androidx.core.content.ContextCompat
import ani.dantotsu.media.manga.MangaCache
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import tachiyomi.core.preference.PreferenceStore
@@ -12,23 +13,32 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
import kotlinx.serialization.json.Json
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.source.manga.service.MangaSourceManager
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
import ani.dantotsu.download.DownloadsManager
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) }
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
addSingleton(sharedPreferences)
@@ -40,6 +50,11 @@ class AppModule(val app: Application) : InjektModule {
}
addSingletonFactory { MangaCache() }
ContextCompat.getMainExecutor(app).execute {
get<AnimeSourceManager>()
get<MangaSourceManager>()
}
}
}

View File

@@ -8,11 +8,13 @@ import ani.dantotsu.logError
import ani.dantotsu.logger
import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val data: Uri? = intent?.data
logger(data.toString())
try {

View File

@@ -7,11 +7,13 @@ import androidx.core.os.bundleOf
import ani.dantotsu.loadMedia
import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class UrlMedia : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
var isMAL = false
var continueMedia = true

View File

@@ -12,13 +12,15 @@ import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord.saveToken
import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class Login : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)

View File

@@ -8,13 +8,15 @@ import ani.dantotsu.*
import ani.dantotsu.connections.mal.MAL.clientId
import ani.dantotsu.connections.mal.MAL.saveResponse
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
try {
val data: Uri = intent?.data
?: throw Exception(getString(R.string.mal_login_uri_not_found))

View File

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

View File

@@ -0,0 +1,115 @@
package ani.dantotsu.download
import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
import android.widget.Toast
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.File
import java.io.Serializable
class DownloadsManager(private val context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList()
val mangaDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.MANGA }
val animeDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.ANIME }
private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply()
}
private fun loadDownloads(): List<Download> {
val jsonString = prefs.getString("downloads_key", null)
return if (jsonString != null) {
val type = object : TypeToken<List<Download>>() {}.type
gson.fromJson(jsonString, type)
} else {
emptyList()
}
}
fun addDownload(download: Download) {
downloadsList.add(download)
saveDownloads()
}
fun removeDownload(download: Download) {
downloadsList.remove(download)
removeDirectory(download)
saveDownloads()
}
private fun removeDirectory(download: Download) {
val directory = if (download.type == Download.Type.MANGA){
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
}
// Check if the directory exists and delete it recursively
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
val directory = if (download.type == Download.Type.MANGA){
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
}
val destination = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/${download.title}/${download.chapter}")
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
if (copied) {
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun purgeDownloads(type: Download.Type){
val directory = if (type == Download.Type.MANGA){
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
}
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
downloadsList.removeAll { it.type == type }
saveDownloads()
}
}
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type {
MANGA,
ANIME
}
}

View File

@@ -0,0 +1,384 @@
package ani.dantotsu.download.manga
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.graphics.Bitmap
import android.net.Uri
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 ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.Media
import ani.dantotsu.media.manga.ImageData
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
import java.net.HttpURLConnection
import java.net.URL
import androidx.core.content.ContextCompat
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class MangaDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var builder: NotificationCompat.Builder
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
private val downloadJobs = mutableMapOf<String, Job>()
private val mutex = Mutex()
private var isCurrentlyProcessing = false
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, CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Manga Download Progress")
setSmallIcon(R.drawable.ic_round_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(0, 0, false)
}
startForeground(NOTIFICATION_ID, builder.build())
ContextCompat.registerReceiver(this, cancelReceiver, IntentFilter(ACTION_CANCEL_DOWNLOAD), ContextCompat.RECEIVER_NOT_EXPORTED)
}
override fun onDestroy() {
super.onDestroy()
ServiceDataSingleton.downloadQueue.clear()
downloadJobs.clear()
ServiceDataSingleton.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 (ServiceDataSingleton.downloadQueue.isNotEmpty()) {
val task = ServiceDataSingleton.downloadQueue.poll()
if (task != null) {
val job = launch { download(task) }
mutex.withLock {
downloadJobs[task.chapter] = job
}
job.join() // Wait for the job to complete before continuing to the next task
mutex.withLock {
downloadJobs.remove(task.chapter)
}
updateNotification() // Update the notification after each task is completed
}
if (ServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty
}
}
}
}
}
fun cancelDownload(chapter: String) {
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[chapter]?.cancel()
downloadJobs.remove(chapter)
ServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
updateNotification() // Update the notification after cancellation
}
}
}
private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = ServiceDataSingleton.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())
}
suspend fun download(task: DownloadTask) {
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@MangaDownloaderService,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
val deferredList = mutableListOf<Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
// 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()
}
// Download the image and add to deferred list
val deferred = async(Dispatchers.IO) {
var bitmap: Bitmap? = null
var retryCount = 0
while (bitmap == null && retryCount < task.retries) {
bitmap = image.fetchAndProcessImage(
image.page,
image.source,
this@MangaDownloaderService
)
retryCount++
}
// Cache the image if successful
if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
}
farthest++
builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress(task.chapter, farthest * 100 / task.imageData.size)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
bitmap
}
deferredList.add(deferred)
}
// Wait for any remaining deferred to complete
deferredList.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
notificationManager.notify(NOTIFICATION_ID, builder.build())
saveMediaInfo(task)
downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA))
broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished")
}
}
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try {
// Define the directory within the private external storage space
val directory = File(
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$title/$chapter"
)
if (!directory.exists()) {
directory.mkdirs()
}
// Create a file reference within that directory for your image
val file = File(directory, fileName)
// Use a FileOutputStream to write the bitmap to the file
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
} catch (e: Exception) {
println("Exception while saving image: ${e.message}")
snackString("Exception while saving image: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
}
}
private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.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") }
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@MangaDownloaderService, "Exception while saving ${name}: ${e.message}", Toast.LENGTH_LONG).show()
}
null
} finally {
connection?.disconnect()
}
}
private fun broadcastDownloadStarted(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFinished(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_FINISHED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFailed(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_FAILED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadProgress(chapterNumber: String, progress: Int) {
val intent = Intent(ACTION_DOWNLOAD_PROGRESS).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
putExtra("progress", progress)
}
sendBroadcast(intent)
}
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
chapter?.let {
cancelDownload(it)
}
}
}
}
data class DownloadTask(
val title: String,
val chapter: String,
val imageData: List<ImageData>,
val sourceMedia: Media? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
)
companion object {
private const val NOTIFICATION_ID = 1103
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
const val EXTRA_CHAPTER = "extra_chapter"
}
}
object ServiceDataSingleton {
var imageData: List<ImageData> = listOf()
var sourceMedia: Media? = null
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile
var isServiceRunning: Boolean = false
}

View File

@@ -0,0 +1,52 @@
package ani.dantotsu.download.manga
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.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
class OfflineMangaAdapter(private val context: Context, private val items: List<OfflineMangaModel>) : BaseAdapter() {
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
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()
}
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 item = getItem(position) as OfflineMangaModel
val imageView = view!!.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
// Bind item data to the views
// For example:
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
}
}

View File

@@ -0,0 +1,226 @@
package ani.dantotsu.download.manga
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.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import android.widget.GridView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment: Fragment() {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val animeUserAvatar= view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
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())
}
gridView = view.findViewById(R.id.gridView)
getDownloads()
adapter = OfflineMangaAdapter(requireContext(), downloads)
gridView.adapter = adapter
gridView.setOnItemClickListener { parent, view, position, id ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media = downloadManager.mangaDownloads.filter { it.title == item.title }.first()
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(media))
.putExtra("download", true)
)
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
var 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 {
//TODO: scroll to top
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
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() {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val newDownloads = mutableListOf<OfflineMangaModel>()
for (title in titles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newDownloads += offlineMangaModel
}
downloads = newDownloads
}
private fun getMedia(download: Download): Media? {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}"
)
//load media.json and convert to media class with gson
try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
return gson.fromJson(mediaJson, Media::class.java)
}
catch (e: Exception){
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return null
}
}
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel{
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.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 cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else {
null
}
val title = mediaModel.nameMAL?:"unknown"
val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else
if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString()
val isOngoing = false
val isUserScored = mediaModel.userScore != 0
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
}
catch (e: Exception){
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel("unknown", "0", false, false, null)
}
}
}

View File

@@ -0,0 +1,6 @@
package ani.dantotsu.download.manga
import android.net.Uri
data class OfflineMangaModel(val title: String, val score: String, val isOngoing: Boolean, val isUserScored: Boolean, val image: Uri?) {
}

View File

@@ -28,6 +28,9 @@ import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.IOException
import java.util.concurrent.*
@@ -118,6 +121,9 @@ object Helper {
val database = StandaloneDatabaseProvider(context)
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
val networkHelper = Injekt.get<NetworkHelper>()
val okHttpClient = networkHelper.client
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)

View File

@@ -24,7 +24,7 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this,
R.drawable.monochrome,
R.drawable.mono,
null,
null,
downloads,

View File

@@ -1,8 +1,11 @@
package ani.dantotsu.home
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -19,6 +22,7 @@ import ani.dantotsu.media.GenreActivity
import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.loadData
import ani.dantotsu.loadImage
@@ -54,10 +58,20 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
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())
}
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
@@ -81,7 +95,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
binding.animeUserAvatar.setSafeOnClickListener {
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.ANIME).show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
listOf(

View File

@@ -107,7 +107,7 @@ class HomeFragment : Fragment() {
}
binding.homeUserAvatarContainer.setSafeOnClickListener {
SettingsDialogFragment().show(parentFragmentManager, "dialog")
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(parentFragmentManager, "dialog")
}
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {

View File

@@ -1,8 +1,10 @@
package ani.dantotsu.home
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -19,6 +21,7 @@ import ani.dantotsu.media.GenreActivity
import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemMangaPageBinding
import ani.dantotsu.loadData
import ani.dantotsu.loadImage
@@ -53,10 +56,20 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
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())
}
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
@@ -76,7 +89,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
}
binding.mangaUserAvatar.setSafeOnClickListener {
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.MANGA).show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
binding.mangaSearchBar.setEndIconOnClickListener {

View File

@@ -1,32 +1,111 @@
package ani.dantotsu.home
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R
import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ActivityNoInternetBinding
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity
import ani.dantotsu.isOnline
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight
import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import nl.joery.animatedbottombar.AnimatedBottomBar
class NoInternet : AppCompatActivity() {
private lateinit var binding: ActivityNoInternetBinding
lateinit var bottomBar: AnimatedBottomBar
private var uiSettings = UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val binding = ActivityNoInternetBinding.inflate(layoutInflater)
binding = ActivityNoInternetBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable
}
binding.refreshButton.setOnClickListener {
if (isOnline(this)) {
startMainActivity(this)
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
}
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
val navbar = binding.includedNavbar.navbar
ani.dantotsu.bottomBar = navbar
navbar.visibility = View.VISIBLE
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)
//supportFragmentManager.beginTransaction().replace(binding.fragmentContainer.id, OfflineFragment()).commit()
}
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
when (position) {
0 -> return OfflineFragment()
1 -> return OfflineFragment()
2 -> return OfflineMangaFragment()
}
return LoginFragment()
}
}
}

View File

@@ -16,6 +16,7 @@ import ani.dantotsu.*
import ani.dantotsu.databinding.ActivityAuthorBinding
import ani.dantotsu.others.getSerialized
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -29,7 +30,8 @@ class AuthorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -18,6 +18,7 @@ import ani.dantotsu.loadData
import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
@@ -33,26 +34,30 @@ class CalendarActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater)
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue, true)
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
val primaryColor = typedValue.data
val typedValue2 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true)
val primaryTextColor = typedValue2.data
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue2, true)
val titleTextColor = typedValue2.data
val typedValue3 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue3, true)
val secondaryColor = typedValue3.data
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
val primaryTextColor = typedValue3.data
val typedValue4 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
val secondaryTextColor = typedValue4.data
window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor
binding.listTabLayout.setBackgroundColor(primaryColor)
binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor)
binding.listTabLayout.setTabTextColors(primaryTextColor, primaryTextColor)
binding.listTitle.setTextColor(titleTextColor)
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
if (!uiSettings.immersiveMode) {
@@ -103,4 +108,4 @@ class CalendarActivity : AppCompatActivity() {
}
}
}
}

View File

@@ -19,6 +19,7 @@ import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -34,7 +35,8 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -16,6 +16,7 @@ import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@@ -26,7 +27,8 @@ class GenreActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityGenreBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)

View File

@@ -22,7 +22,7 @@ data class Media(
val userPreferredName: String,
var cover: String? = null,
val banner: String? = null,
var banner: String? = null,
var relation: String? = null,
var popularity: Int? = null,

View File

@@ -48,6 +48,7 @@ import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView
@@ -72,7 +73,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat()
@@ -118,7 +120,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
var media: Media = intent.getSerialized("media") ?: return
media.selected = model.loadSelected(media)
val isDownload = intent.getBooleanExtra("download", false)
media.selected = model.loadSelected(media, isDownload)
binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener {
@@ -324,7 +327,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
tabLayout.setOnItemSelectedListener { item ->
selectFromID(item.itemId)
viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel, this)
true

View File

@@ -3,12 +3,14 @@ package ani.dantotsu.media
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
import android.os.Handler
import android.os.Looper
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import ani.dantotsu.FileUrl
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
@@ -30,6 +32,8 @@ import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.currContext
import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.AniyomiAdapter
import ani.dantotsu.parsers.DynamicMangaParser
@@ -44,6 +48,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class MediaDetailsViewModel : ViewModel() {
val scrolledToTop = MutableLiveData(true)
@@ -53,17 +58,23 @@ class MediaDetailsViewModel : ViewModel() {
}
fun loadSelected(media: Media): Selected {
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
val sharedPreferences = Injekt.get<SharedPreferences>()
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
true ->sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
else ->sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
}
it.preferDub = loadData("settings_prefer_dub") ?: false
saveSelected(media.id, it)
it
}
if (isDownload) {
data.sourceIndex = when (media.anime != null) {
true -> AnimeSources.list.size - 1
else -> MangaSources.list.size - 1
}
}
return data
}
@@ -120,8 +131,8 @@ class MediaDetailsViewModel : ViewModel() {
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
suspend fun loadEpisodes(media: Media, i: Int) {
if (!epsLoaded.containsKey(i)) {
suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
if (!epsLoaded.containsKey(i) || invalidate) {
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
}
episodes.postValue(epsLoaded)
@@ -240,9 +251,9 @@ class MediaDetailsViewModel : ViewModel() {
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int) {
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
logger("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i)) tryWithSuspend {
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
}
mangaChapters.postValue(mangaLoaded)
@@ -258,7 +269,28 @@ class MediaDetailsViewModel : ViewModel() {
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, series: String, post: Boolean = true): Boolean {
//check if the chapter has been downloaded already
val downloadsManager = Injekt.get<DownloadsManager>()
if(downloadsManager.mangaDownloads.contains(Download(series, chapter.title!!, Download.Type.MANGA))) {
val download = downloadsManager.mangaDownloads.find { it.title == series && it.chapter == chapter.title!! } ?: return false
//look in the downloads folder for the chapter and add all the numerically named images to the chapter
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$series/${chapter.title!!}"
)
val images = mutableListOf<MangaImage>()
directory.listFiles()?.forEach {
if (it.nameWithoutExtension.toIntOrNull() != null) {
images.add(MangaImage(FileUrl(it.absolutePath), false))
}
}
//sort the images by name
images.sortBy { it.url.url }
chapter.addImages(images)
if (post) mangaChapter.postValue(chapter)
return true
}
return tryWithSuspend(true) {
chapter.addImages(
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false

View File

@@ -16,6 +16,7 @@ import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.others.getSerialized
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

View File

@@ -17,6 +17,7 @@ import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
@@ -38,7 +39,8 @@ class SearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)

View File

@@ -9,8 +9,10 @@ data class Selected(
var chip: Int = 0,
//var source: String = "",
var sourceIndex: Int = 0,
var langIndex: Int = 0,
var preferDub: Boolean = false,
var server: String? = null,
var video: Int = 0,
var latest: Float = 0f,
var scanlators: List<String>? = null,
) : Serializable

View File

@@ -16,6 +16,7 @@ import ani.dantotsu.*
import ani.dantotsu.databinding.ActivityStudioBinding
import ani.dantotsu.others.getSerialized
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -29,7 +30,8 @@ class StudioActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityStudioBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.TypedValue
@@ -8,22 +10,38 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
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.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.IndexOutOfBoundsException
class AnimeWatchAdapter(
private val media: Media,
@@ -69,7 +87,8 @@ class AnimeWatchAdapter(
}
//Source Selection
val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
var source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex,source)
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source])
watchSources[source].apply {
@@ -91,11 +110,41 @@ class AnimeWatchAdapter(
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
source = i
setLanguageList(0,i)
}
subscribeButton(false)
fragment.loadEpisodes(i)
fragment.loadEpisodes(i, false)
}
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
ext.sourceLanguage = i
fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
setLanguageList(i, source)
}
subscribeButton(false)
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
} ?: run {
}
}
//settings
binding.animeSourceSettings.setOnClickListener {
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
fragment.openSettings(ext.extension)
}
}
//Subscription
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
@@ -177,6 +226,7 @@ class AnimeWatchAdapter(
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
}
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
chip.setOnClickListener {
selected()
@@ -261,6 +311,25 @@ class AnimeWatchAdapter(
}
}
fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (watchSources is AnimeSources) {
val parser = watchSources[source] as? DynamicAnimeParser
if (parser != null) {
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
ext.sourceLanguage = lang
}
try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
}catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown")
}
binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang }))
}
}
}
override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {

View File

@@ -1,11 +1,17 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.math.MathUtils
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
@@ -13,24 +19,38 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
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.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
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
@@ -214,6 +234,13 @@ class AnimeWatchFragment : Fragment() {
return model.watchSources?.get(i)!!
}
fun onLangChange(i: Int) {
val selected = model.loadSelected(media)
selected.langIndex = i
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
fun onDubClicked(checked: Boolean) {
val selected = model.loadSelected(media)
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
@@ -223,8 +250,8 @@ class AnimeWatchFragment : Fragment() {
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
}
fun loadEpisodes(i: Int) {
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) }
fun loadEpisodes(i: Int, invalidate: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
}
fun onIconPressed(viewType: Int, rev: Boolean) {
@@ -262,45 +289,115 @@ class AnimeWatchFragment : Fragment() {
else getString(R.string.unsubscribed_notification)
)
}
fun onEpisodeClick(i: String) {
model.continueMedia = false
model.saveSelected(media.id, media.selected!!, requireActivity())
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
}
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
//Find latest episode for subscription
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
arr.addAll(
media.anime!!.episodes!!.values.toList()
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
fun openSettings(pkg: AnimeExtension.Installed){
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = requireActivity() as MediaDetailsActivity
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try{
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
}catch (e: ClassCastException){
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
var selectedIndex = 0
AlertDialog.Builder(requireContext())
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
// Move the fragment transaction here
val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
changeUIVisibility(true)
return@setNegativeButton
}
.show()
} else {
// If there's only one setting, proceed with the fragment transaction
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
}
override fun onDestroy() {
model.watchSources?.flushText()
super.onDestroy()
}
fun onEpisodeClick(i: String) {
model.continueMedia = false
model.saveSelected(media.id, media.selected!!, requireActivity())
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
}
var state: Parcelable? = null
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
//Find latest episode for subscription
selected.latest =
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
arr.addAll(
media.anime!!.episodes!!.values.toList()
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
}
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
}
override fun onDestroy() {
model.watchSources?.flushText()
super.onDestroy()
}
var state: Parcelable? = null
override fun onResume() {
super.onResume()
binding.mediaInfoProgressBar.visibility = progress

View File

@@ -17,6 +17,7 @@ import android.graphics.drawable.Animatable
import android.hardware.SensorManager
import android.media.AudioManager
import android.media.AudioManager.*
import android.media.PlaybackParams
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -77,8 +78,10 @@ import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.bumptech.glide.Glide
import com.google.android.material.slider.Slider
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.lagradost.nicehttp.ignoreAllSSLErrors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -317,7 +320,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityExoplayerBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -806,23 +810,24 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
fun fastForward() {
val newSpeed = playbackParameters.speed * 2f
exoPlayer.playbackParameters = playbackParameters.withSpeed(newSpeed)
isFastForwarding = true
snackString("Playing at ${newSpeed}x speed")
exoPlayer.setPlaybackSpeed(2f)
snackString("Playing at 2x speed")
}
fun stopFastForward() {
if (isFastForwarding) {
exoPlayer.playbackParameters = playbackParameters
isFastForwarding = false
exoPlayer.setPlaybackSpeed(1f)
snackString("Playing at normal speed")
}
}
//FastRewind (Left Panel)
val fastRewindDetector = GestureDetector(this, object : GesturesListener() {
override fun onLongClick(event: MotionEvent) = fastForward()
override fun onLongClick(event: MotionEvent) {
if (settings.fastforward) fastForward()
}
override fun onDoubleClick(event: MotionEvent) {
doubleTap(false, event)
@@ -852,8 +857,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
//FastForward (Right Panel)
val fastForwardDetector = GestureDetector(this, object : GesturesListener() {
override fun onLongClick(event: MotionEvent) = fastForward()
override fun onLongClick(event: MotionEvent) {
if (settings.fastforward) fastForward()
}
override fun onDoubleClick(event: MotionEvent) {
doubleTap(true, event)
}
@@ -1614,7 +1620,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
else
-> toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
-> {
toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
FirebaseCrashlytics.getInstance().recordException(error)
}
}
}

View File

@@ -3,8 +3,10 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
@@ -24,6 +26,7 @@ import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -53,6 +56,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
val window = dialog?.window
window?.statusBarColor = Color.TRANSPARENT
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
window?.navigationBarColor = typedValue.data
return binding.root
}
@@ -236,7 +245,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text =
(if (video.extraNote != null) " : " else "") + DecimalFormat("#.##").format(video.size ?: 0).toString() + " MB"
// if video size is null or 0, show "Unknown Size" else show the size in MB
(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"

View File

@@ -10,6 +10,9 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.LruCache
import android.widget.Toast
import ani.dantotsu.logger
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers
@@ -26,22 +29,23 @@ data class ImageData(
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()
val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
inputStream.close()
//saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
return@withContext bitmap
} catch (e: Exception) {
// Handle any exceptions
println("An error occurred: ${e.message}")
logger("An error occurred: ${e.message}")
snackString("An error occurred: ${e.message}")
return@withContext null
}
}
@@ -57,7 +61,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga")
}
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
val uri: Uri? = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
uri?.let {
contentResolver.openOutputStream(it)?.use { os ->
@@ -65,7 +69,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
}
}
} else {
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
if (!directory.exists()) {
directory.mkdirs()
}

View File

@@ -11,9 +11,11 @@ data class MangaChapter(
var link: String,
var title: String? = null,
var description: String? = null,
var sChapter: SChapter
var sChapter: SChapter,
val scanlator: String? = null,
var progress: String? = ""
) : Serializable {
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter, chapter.scanlator)
private val images = mutableListOf<MangaImage>()
fun images(): List<MangaImage> = images

View File

@@ -4,6 +4,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media
@@ -28,7 +29,15 @@ class MangaChapterAdapter(
false
)
)
0 -> ChapterListViewHolder(ItemChapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
0 -> ChapterListViewHolder(
ItemChapterListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException()
}
}
@@ -39,7 +48,8 @@ class MangaChapterAdapter(
override fun getItemCount(): Int = arr.size
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
@@ -48,12 +58,102 @@ class MangaChapterAdapter(
}
}
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
private val activeDownloads = mutableSetOf<String>()
private val downloadedChapters = mutableSetOf<String>()
fun startDownload(chapterNumber: String) {
activeDownloads.add(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
fun stopDownload(chapterNumber: String) {
activeDownloads.remove(chapterNumber)
downloadedChapters.add(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
arr[position].progress = "Downloaded"
notifyItemChanged(position)
}
}
fun deleteDownload(chapterNumber: String) {
downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
fun removeDownload(chapterNumber: String) {
activeDownloads.remove(chapterNumber)
downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
fun updateDownloadProgress(chapterNumber: String, progress: Int) {
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
arr[position].progress = "Downloading: ${progress}%"
notifyItemChanged(position)
}
}
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(chapterNumber: String, progress: String?) {
if (progress != null) {
binding.itemChapterTitle.visibility = View.VISIBLE
binding.itemChapterTitle.text = "$progress"
}else{
binding.itemChapterTitle.visibility = View.GONE
binding.itemChapterTitle.text = ""
}
if (activeDownloads.contains(chapterNumber)) {
// Show spinner
binding.itemDownload.setImageResource(R.drawable.ic_round_refresh_24)
} else if (downloadedChapters.contains(chapterNumber)) {
// Show checkmark
binding.itemDownload.setImageResource(R.drawable.ic_check)
} else {
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_round_download_24)
}
}
init {
itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
}
binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val chapterNumber = arr[bindingAdapterPosition].number
if (activeDownloads.contains(chapterNumber)) {
fragment.onMangaChapterStopDownloadClick(chapterNumber)
return@setOnClickListener
} else if (downloadedChapters.contains(chapterNumber)) {
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
return@setOnClickListener
} else {
fragment.onMangaChapterDownloadClick(chapterNumber)
startDownload(chapterNumber)
}
}
}
}
}
@@ -63,44 +163,50 @@ class MangaChapterAdapter(
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val ep = arr[position]
binding.itemEpisodeNumber.text = ep.number
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
if (media.userProgress != null) {
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat())
if ((MangaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
)
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true
}
}
}
}
is ChapterListViewHolder -> {
is ChapterListViewHolder -> {
val binding = holder.binding
val ep = arr[position]
holder.bind(ep.number, ep.progress)
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemChapterNumber.text = ep.number
if (!ep.title.isNullOrEmpty()) {
binding.itemChapterTitle.text = ep.title
binding.itemChapterTitle.setOnLongClickListener {
binding.itemChapterTitle.maxLines.apply {
binding.itemChapterTitle.maxLines = if (this == 1) 3 else 1
}
true
}
binding.itemChapterTitle.visibility = View.VISIBLE
} else binding.itemChapterTitle.visibility = View.GONE
if (ep.progress.isNullOrEmpty()) {
binding.itemChapterTitle.visibility = View.GONE
} else binding.itemChapterTitle.visibility = View.VISIBLE
if (media.userProgress != null) {
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat()) {
if ((MangaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
) {
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
binding.itemEpisodeViewed.visibility = View.VISIBLE
} else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE
binding.root.setOnLongClickListener {
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true
}
}
@@ -117,4 +223,4 @@ class MangaChapterAdapter(
}
}
}

View File

@@ -1,27 +1,36 @@
package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.App.Companion.context
import ani.dantotsu.media.anime.handleProgress
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.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.lang.IndexOutOfBoundsException
class MangaReadAdapter(
private val media: Media,
@@ -31,6 +40,9 @@ class MangaReadAdapter(
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
val hiddenScanlators = mutableListOf<String>()
var scanlatorSelectionListener: ScanlatorSelectionListener? = null
var options = listOf<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -49,10 +61,10 @@ class MangaReadAdapter(
}
//Source Selection
val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
var source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex,source)
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
@@ -64,9 +76,36 @@ class MangaReadAdapter(
fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
source = i
setLanguageList(0,i)
}
subscribeButton(false)
fragment.loadChapters(i)
//invalidate if it's the last source
val invalidate = i == mangaReadSources.names.size - 1
fragment.loadChapters(i, invalidate)
}
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
ext.sourceLanguage = i
fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
setLanguageList(i, source)
}
subscribeButton(false)
fragment.loadChapters(media.selected!!.sourceIndex, true)
} ?: run {
}
}
//settings
binding.animeSourceSettings.setOnClickListener {
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
fragment.openSettings(ext.extension)
}
}
//Subscription
@@ -98,6 +137,46 @@ class MangaReadAdapter(
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed)
}
binding.animeScanlatorTop.setOnClickListener {
val dialogView = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
// Dynamically add checkboxes
options.forEach { option ->
val checkBox = CheckBox(currContext()).apply {
text = option
}
//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)
}
// Create AlertDialog
AlertDialog.Builder(currContext())
.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())
}
}
media.selected!!.scanlators = hiddenScanlators
scanlatorSelectionListener?.onScanlatorsSelected()
}
.setNegativeButton("Cancel", null)
.show()
}
var selected = when (style) {
0 -> binding.animeSourceList
1 -> binding.animeSourceCompact
@@ -145,6 +224,7 @@ class MangaReadAdapter(
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
}
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
chip.setOnClickListener {
selected()
@@ -167,6 +247,7 @@ class MangaReadAdapter(
@SuppressLint("SetTextI18n")
fun handleChapters() {
val binding = _binding
if (binding != null) {
if (media.manga?.chapters != null) {
@@ -174,7 +255,11 @@ class MangaReadAdapter(
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
val formattedChapters = chapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() }
val filteredChapters = chapters.filter { chapterKey ->
val chapter = media.manga.chapters!![chapterKey]!!
chapter.scanlator !in hiddenScanlators
}
val formattedChapters = filteredChapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() }
if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)]
binding.animeSourceContinue.visibility = View.VISIBLE
@@ -222,7 +307,30 @@ class MangaReadAdapter(
}
}
fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (mangaReadSources is MangaSources) {
val parser = mangaReadSources[source] as? DynamicMangaParser
if (parser != null) {
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
ext.sourceLanguage = lang
}
try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
}catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown")
}
binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang }))
}
}
}
override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root)
}
}
interface ScanlatorSelectionListener {
fun onScanlatorsSelected()
}

View File

@@ -1,11 +1,24 @@
package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
@@ -13,27 +26,48 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
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.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.ServiceDataSingleton
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
import android.Manifest
import androidx.core.app.ActivityCompat
open class MangaReadFragment : Fragment() {
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels()
@@ -48,6 +82,8 @@ open class MangaReadFragment : Fragment() {
private lateinit var headerAdapter: MangaReadAdapter
private lateinit var chapterAdapter: MangaChapterAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f
private var progress = View.VISIBLE
@@ -67,10 +103,18 @@ open class MangaReadFragment : 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_NOT_EXPORTED)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt()
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
@@ -116,8 +160,13 @@ open class MangaReadFragment : Fragment() {
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
headerAdapter.scanlatorSelectionListener = this
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
for (download in downloadManager.mangaDownloads){
chapterAdapter.stopDownload(download.chapter)
}
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) {
@@ -134,45 +183,70 @@ open class MangaReadFragment : Fragment() {
}
}
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
if (loadedChapters != null) {
val chapters = loadedChapters[media.selected!!.sourceIndex]
if (chapters != null) {
media.manga?.chapters = chapters
model.getMangaChapters().observe(viewLifecycleOwner) { _ ->
updateChapters()
}
}
//CHIP GROUP
val total = chapters.size
val divisions = total.toDouble() / 10
start = 0
end = null
val limit = when {
(divisions < 25) -> 25
(divisions < 50) -> 50
else -> 100
}
headerAdapter.clearChips()
if (total > limit) {
val arr = chapters.keys.toTypedArray()
val stored = ceil((total).toDouble() / limit).toInt()
val position = clamp(media.selected!!.chip, 0, stored - 1)
val last = if (position + 1 == stored) total else (limit * (position + 1))
start = limit * (position)
end = last - 1
headerAdapter.updateChips(
limit,
arr,
(1..stored).toList().toTypedArray(),
position
)
}
override fun onScanlatorsSelected() {
updateChapters()
}
headerAdapter.subscribeButton(true)
reload()
private fun updateChapters() {
val loadedChapters = model.getMangaChapters().value
if (loadedChapters != null) {
val chapters = loadedChapters[media.selected!!.sourceIndex]
if (chapters != null) {
headerAdapter.options = getScanlators(chapters)
val filteredChapters = chapters.filterNot { (_, chapter) ->
chapter.scanlator in headerAdapter.hiddenScanlators
}
media.manga?.chapters = filteredChapters.toMutableMap()
//CHIP GROUP
val total = filteredChapters.size
val divisions = total.toDouble() / 10
start = 0
end = null
val limit = when {
(divisions < 25) -> 25
(divisions < 50) -> 50
else -> 100
}
headerAdapter.clearChips()
if (total > limit) {
val arr = filteredChapters.keys.toTypedArray()
val stored = ceil((total).toDouble() / limit).toInt()
val position = clamp(media.selected!!.chip, 0, stored - 1)
val last = if (position + 1 == stored) total else (limit * (position + 1))
start = limit * (position)
end = last - 1
headerAdapter.updateChips(
limit,
arr,
(1..stored).toList().toTypedArray(),
position
)
}
headerAdapter.subscribeButton(true)
reload()
}
}
}
fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
val scanlators = mutableListOf<String>()
if (chap != null) {
val chapters = chap.values
for (chapter in chapters) {
scanlators.add(chapter.scanlator ?: "Unknown")
}
}
return scanlators.distinct()
}
fun onSourceChange(i: Int): MangaParser {
media.manga?.chapters = null
reload()
@@ -185,8 +259,16 @@ open class MangaReadFragment : Fragment() {
return model.mangaReadSources?.get(i)!!
}
fun loadChapters(i: Int) {
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) }
fun onLangChange(i: Int) {
val selected = model.loadSelected(media)
selected.langIndex = i
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
fun loadChapters(i: Int, invalidate: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i, invalidate) }
}
fun onIconPressed(viewType: Int, rev: Boolean) {
@@ -225,6 +307,75 @@ open class MangaReadFragment : Fragment() {
)
}
fun openSettings(pkg: MangaExtension.Installed){
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = requireActivity() as MediaDetailsActivity
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try{
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
}catch (e: ClassCastException){
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
var selectedIndex = 0
AlertDialog.Builder(requireContext())
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
// Move the fragment transaction here
val fragment =
MangaSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadChapters(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
changeUIVisibility(true)
return@setNegativeButton
}
.show()
} else {
// If there's only one setting, proceed with the fragment transaction
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadChapters(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
}
fun onMangaChapterClick(i: String) {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let {
@@ -234,6 +385,112 @@ open class MangaReadFragment : Fragment() {
}
}
fun onMangaChapterDownloadClick(i: String) {
if (!isNotificationPermissionGranted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
val parser = model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
CoroutineScope(Dispatchers.IO).launch {
val images = parser.imageList("", chapter.sChapter)
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.nameMAL ?: "",
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
retries = 2,
simultaneousDownloads = 2
)
ServiceDataSingleton.downloadQueue.offer(downloadTask)
// If the service is not already running, start it
if (!ServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
ServiceDataSingleton.isServiceRunning = true
}
// Inform the adapter that the download has started
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
}
}
}
}
}
private fun isNotificationPermissionGranted(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
}
return true
}
fun onMangaChapterRemoveDownloadClick(i: String){
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
chapterAdapter.deleteDownload(i)
}
fun onMangaChapterStopDownloadClick(i: String) {
val cancelIntent = Intent().apply {
action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD
putExtra(MangaDownloaderService.EXTRA_CHAPTER, i)
}
requireContext().sendBroadcast(cancelIntent)
// Remove the download from the manager and update the UI
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
chapterAdapter.stopDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.startDownload(it) }
}
ACTION_DOWNLOAD_FINISHED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.stopDownload(it) }
}
ACTION_DOWNLOAD_FAILED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let {
chapterAdapter.removeDownload(it)
}
}
ACTION_DOWNLOAD_PROGRESS -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
val progress = intent.getIntExtra("progress", 0)
chapterNumber?.let {
chapterAdapter.updateDownloadProgress(it, progress)
}
}
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
@@ -262,6 +519,7 @@ open class MangaReadFragment : Fragment() {
override fun onDestroy() {
model.mangaReadSources?.flushText()
super.onDestroy()
requireContext().unregisterReceiver(downloadStatusReceiver)
}
private var state: Parcelable? = null
@@ -275,4 +533,12 @@ open class MangaReadFragment : Fragment() {
super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
}
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
}
}

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
@@ -29,6 +30,7 @@ import kotlinx.coroutines.withContext
import ani.dantotsu.media.manga.MangaCache
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
abstract class BaseImageAdapter(
val activity: MangaReaderActivity,
@@ -116,7 +118,7 @@ abstract class BaseImageAdapter(
abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object {
suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? { //still used in some places
return tryWithSuspend {
withContext(Dispatchers.IO) {
Glide.with(this@loadBitmap_old)
@@ -151,8 +153,10 @@ abstract class BaseImageAdapter(
Glide.with(this@loadBitmap)
.asBitmap()
.let {
if (link.url.startsWith("file://")) {
it.load(link.url)
val fileUri = Uri.fromFile(File(link.url)).toString()
val localFile = File(link.url)
if (localFile.exists()) {
it.load(localFile.absoluteFile)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else {

View File

@@ -1,7 +1,9 @@
package ani.dantotsu.media.manga.mangareader
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -45,7 +47,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
loaded = true
binding.selectorAutoText.text = chp.title
lifecycleScope.launch(Dispatchers.IO) {
if(model.loadMangaChapterImages(chp, m.selected!!)) {
if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL!!)) {
val activity = currActivity()
activity?.runOnUiThread {
tryWith { dismiss() }
@@ -63,6 +65,12 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
val window = dialog?.window
window?.statusBarColor = Color.TRANSPARENT
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
window?.navigationBarColor = typedValue.data
return binding.root
}

View File

@@ -46,6 +46,7 @@ import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@@ -126,7 +127,8 @@ class MangaReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -215,6 +217,10 @@ class MangaReaderActivity : AppCompatActivity() {
logError(e)
}
}
//check that index is not out of bounds (crash fix)
if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) {
media.selected!!.sourceIndex = 0
}
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex]
binding.mangaReaderTitle.text = media.userPreferredName
@@ -311,7 +317,7 @@ class MangaReaderActivity : AppCompatActivity() {
}
}
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) }
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!, media.nameMAL!!) }
}
private val snapHelper = PagerSnapHelper()
@@ -700,6 +706,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!,
media.nameMAL!!,
false
)
loading = false

View File

@@ -14,6 +14,7 @@ import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@@ -35,6 +35,7 @@ import ani.dantotsu.settings.NovelReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import ani.dantotsu.tryWith
import com.google.android.material.slider.Slider
import com.vipulog.ebookreader.Book
@@ -136,7 +137,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -18,6 +18,7 @@ import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
@@ -32,25 +33,29 @@ class ListActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater)
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue, true)
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
val primaryColor = typedValue.data
val typedValue2 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true)
val primaryTextColor = typedValue2.data
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue2, true)
val titleTextColor = typedValue2.data
val typedValue3 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue3, true)
val secondaryColor = typedValue3.data
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
val primaryTextColor = typedValue3.data
val typedValue4 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
val secondaryTextColor = typedValue4.data
window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor
binding.listTabLayout.setBackgroundColor(primaryColor)
binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor)
binding.listTabLayout.setTabTextColors(primaryTextColor, primaryTextColor)
binding.listTitle.setTextColor(titleTextColor)
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
if (!uiSettings.immersiveMode) {
@@ -126,4 +131,4 @@ class ListActivity : AppCompatActivity() {
popup.show()
}
}
}
}

View File

@@ -12,6 +12,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.OtherDetailsViewModel
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class ListFragment : Fragment() {
private var _binding: FragmentListBinding? = null

View File

@@ -0,0 +1,29 @@
package ani.dantotsu.offline
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.databinding.FragmentOfflineBinding
import ani.dantotsu.isOnline
import ani.dantotsu.navBarHeight
import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight
class OfflineFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentOfflineBinding.inflate(inflater, container, false)
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
binding.refreshButton.setOnClickListener {
if (isOnline(requireContext())) {
startMainActivity(requireActivity())
}
}
return binding.root
}
}

View File

@@ -10,6 +10,7 @@ import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
@@ -95,8 +96,9 @@ object AppUpdater {
private fun compareVersion(version: String): Boolean {
if(BuildConfig.DEBUG)
if(BuildConfig.DEBUG) {
return BuildConfig.VERSION_NAME != version
}
else {
fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String ->
@@ -141,7 +143,8 @@ object AppUpdater {
-1
}
if (id == -1L) return true
registerReceiver(
ContextCompat.registerReceiver(
this,
object : BroadcastReceiver() {
@SuppressLint("Range")
override fun onReceive(context: Context?, intent: Intent?) {
@@ -170,7 +173,8 @@ object AppUpdater {
logError(e)
}
}
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_NOT_EXPORTED
)
return true
}

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.others
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -46,6 +48,12 @@ open class CustomBottomDialog : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetCustomBinding.inflate(inflater, container, false)
val window = dialog?.window
window?.statusBarColor = Color.TRANSPARENT
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
window?.navigationBarColor = typedValue.data
return binding.root
}

View File

@@ -20,6 +20,7 @@ import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.shareImage
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import ani.dantotsu.toast
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.ImageSource
@@ -74,15 +75,16 @@ class ImageViewDialog : BottomSheetDialogFragment() {
if (image2 != null) openLinkInBrowser(image2.url)
true
}
val context = requireContext()
lifecycleScope.launch {
viewLifecycleOwner.lifecycleScope.launch {
val binding = _binding ?: return@launch
var bitmap = requireContext().loadBitmap_old(image, trans1 ?: listOf())
var bitmap2 = if (image2 != null) requireContext().loadBitmap_old(image2, trans2 ?: listOf()) else null
var bitmap = context.loadBitmap_old(image, trans1 ?: listOf())
var bitmap2 = if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null
if (bitmap == null) {
bitmap = requireContext().loadBitmap(image, trans1 ?: listOf())
bitmap2 = if (image2 != null) requireContext().loadBitmap(image2, trans2 ?: listOf()) else null
bitmap = context.loadBitmap(image, trans1 ?: listOf())
bitmap2 = if (image2 != null) context.loadBitmap(image2, trans2 ?: listOf()) else null
}
bitmap = if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2,) else bitmap

View File

@@ -0,0 +1,24 @@
package ani.dantotsu.others
import android.app.Activity
import android.content.res.Configuration
import android.content.res.Resources
import java.util.Locale
class LangSet {
companion object{
fun setLocale(activity: Activity) {
val useCursedLang = activity.getSharedPreferences("Dantotsu", Activity.MODE_PRIVATE).getBoolean("use_cursed_lang", false)
if(!useCursedLang) return
val locale = Locale("en", "rDW")
Locale.setDefault(locale)
val resources: Resources = activity.resources
val config: Configuration = resources.configuration
config.setLocale(locale)
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}

View File

@@ -16,6 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ActivityImageSearchBinding
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import ani.dantotsu.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -47,7 +48,8 @@ class ImageSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityImageSearchBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.parsers
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
@@ -10,14 +11,20 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import androidx.core.content.ContextCompat
import ani.dantotsu.FileUrl
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import ani.dantotsu.currContext
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.ServiceDataSingleton
import ani.dantotsu.logger
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@@ -62,22 +69,54 @@ class AniyomiAdapter {
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val extension: AnimeExtension.Installed
var sourceLanguage = 0
init {
this.extension = extension
}
override val name = extension.name
override val saveName = extension.name
override val hostUrl = extension.sources.first().name
override val isDubAvailableSeparately = false
override val isNSFW = extension.isNsfw
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
val source = extension.sources.first()
override suspend fun loadEpisodes(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
}
if (source is AnimeCatalogueSource) {
try {
val res = source.getEpisodeList(sAnime)
// Sort episodes by episode_number
val sortedEpisodes = res.sortedBy { it.episode_number }
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
}
// 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
}
} else {
// Sort by the episode_number field
res.sortedBy { it.episode_number }
}
// Transform SEpisode objects to Episode objects
@@ -90,8 +129,17 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
}
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
override suspend fun loadVideoServers(
episodeLink: String,
extra: Map<String, String>?,
sEpisode: SEpisode
): List<VideoServer> {
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList()
return try {
val videos = source.getVideoList(sEpisode)
@@ -103,20 +151,25 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
}
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor {
return VideoServerPassthrough(server)
}
override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList()
return try {
val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
convertAnimesPageToShowResponse(res)
} catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
.show()
}
emptyList()
} catch (e: Exception) {
@@ -150,7 +203,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
sEpisode.episode_number
}
return Episode(
episodeNumberInt.toString(),
if (episodeNumberInt.toInt() != -1) {
episodeNumberInt.toString()
} else {
sEpisode.name
},
sEpisode.url,
sEpisode.name,
null,
@@ -174,16 +231,28 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val mangaCache = Injekt.get<MangaCache>()
val extension: MangaExtension.Installed
var sourceLanguage = 0
init {
this.extension = extension
}
override val name = extension.name
override val saveName = extension.name
override val hostUrl = extension.sources.first().name
override val isNSFW = extension.isNsfw
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
val source = extension.sources.first() as? CatalogueSource ?: return emptyList()
override suspend fun loadChapters(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
return try {
val res = source.getChapterList(sManga)
@@ -201,32 +270,79 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val source = extension.sources.first() as? HttpSource ?: return emptyList()
return coroutineScope {
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
var imageDataList: List<ImageData> = listOf()
val ret = coroutineScope {
try {
println("source.name " + source.name)
println("source.name " + source.name)
val res = source.getPageList(sChapter)
val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) {
mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
imageDataList += ImageData(page, source)
logger("put page: ${page.imageUrl}")
pageToMangaImage(page)
}
}
deferreds.awaitAll()
} catch (e: Exception) {
logger("loadImages Exception: $e")
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT).show()
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
.show()
emptyList()
}
}
return ret
}
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData>{
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
var imageDataList: List<ImageData> = listOf()
coroutineScope {
try {
println("source.name " + source.name)
val res = source.getPageList(sChapter)
val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) {
imageDataList += ImageData(page, source)
}
}
deferreds.awaitAll()
} catch (e: Exception) {
logger("loadImages Exception: $e")
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
.show()
emptyList()
}
}
return imageDataList
}
suspend fun fetchAndProcessImage(
page: Page,
httpSource: HttpSource,
context: Context
): Bitmap? {
return withContext(Dispatchers.IO) {
try {
// Fetch the image
@@ -259,7 +375,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
CoroutineScope(Dispatchers.IO).launch {
try {
@@ -274,7 +389,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
withContext(Dispatchers.IO) {
// Save the Bitmap using MediaStore API
saveImage(bitmap, contentResolver, "image_${System.currentTimeMillis()}.jpg", Bitmap.CompressFormat.JPEG, 100)
saveImage(
bitmap,
contentResolver,
"image_${System.currentTimeMillis()}.jpg",
Bitmap.CompressFormat.JPEG,
100
)
}
inputStream?.close()
@@ -285,16 +406,28 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
}
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
fun saveImage(
bitmap: Bitmap,
contentResolver: ContentResolver,
filename: String,
format: Bitmap.CompressFormat,
quality: Int
) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime")
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime"
)
}
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
val uri: Uri? = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
uri?.let {
contentResolver.openOutputStream(it)?.use { os ->
@@ -302,7 +435,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
}
} else {
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
val directory =
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
if (!directory.exists()) {
directory.mkdirs()
}
@@ -319,9 +453,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first() as? HttpSource ?: return emptyList()
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
return try {
val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
@@ -330,7 +468,8 @@ 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()
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
.show()
}
emptyList()
} catch (e: Exception) {
@@ -390,30 +529,21 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
/*val parsedChapterTitle = parseChapterTitle(sChapter.name)
val number = if (sChapter.chapter_number.toInt() != -1){
sChapter.chapter_number.toString()
} else if(parsedChapterTitle.first != null || parsedChapterTitle.second != null){
(parsedChapterTitle.first ?: "") + "." + (parsedChapterTitle.second ?: "")
}else{
sChapter.name
}*/
return MangaChapter(
sChapter.name,
sChapter.url,
//if (parsedChapterTitle.first != null || parsedChapterTitle.second != null) {
// parsedChapterTitle.third
//} else {
sChapter.name,
//},
sChapter.name,
null,
sChapter.scanlator,
sChapter
)
}
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val volumePattern =
Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val chapterPattern =
Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val volumeMatcher = volumePattern.matcher(title)
val chapterMatcher = chapterPattern.matcher(title)
@@ -423,10 +553,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
var remainingTitle = title
if (volumeNumber != null) {
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
remainingTitle =
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
}
if (chapterNumber != null) {
remainingTitle = chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
remainingTitle =
chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
}
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
@@ -449,7 +581,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
}
}
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video): ani.dantotsu.parsers.Video {
// Find the number value from the .quality string
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
@@ -479,9 +611,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
// If the format is still undetermined, log an error or handle it appropriately
if (format == null) {
logger("Unknown video format: $videoUrl")
throw Exception("Unknown video format")
FirebaseCrashlytics.getInstance().recordException(Exception("Unknown video format: $videoUrl"))
format = VideoType.CONTAINER
}
val headersMap: Map<String, String> = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
val headersMap: Map<String, String> =
aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
return ani.dantotsu.parsers.Video(
@@ -494,7 +628,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
private fun getVideoType(fileName: String): VideoType? {
return when {
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
".mkv",
ignoreCase = true
) -> VideoType.CONTAINER
fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> null
@@ -507,12 +645,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
runBlocking {
type = findSubtitleType(track.url)
}
return Subtitle(track.lang, track.url, type?: SubtitleType.SRT)
return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
}
private fun findSubtitleType(url: String): SubtitleType? {
// First, try to determine the type based on the URL file extension
var type: SubtitleType? = when {
val type: SubtitleType? = when {
url.endsWith(".vtt", true) -> SubtitleType.VTT
url.endsWith(".ass", true) -> SubtitleType.ASS
url.endsWith(".srt", true) -> SubtitleType.SRT

View File

@@ -50,8 +50,8 @@ abstract class BaseParser {
* Isn't necessary to override, but recommended, if you want to improve auto search results
* **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id)
if (response != null) {
var response: ShowResponse? = null//loadSavedShowResponse(mediaObj.id)
if (response != null && this !is OfflineMangaParser) {
saveShowResponse(mediaObj.id, response, true)
} else {
setUserText("Searching : ${mediaObj.mainName()}")

View File

@@ -7,6 +7,7 @@ import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.Media
import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
abstract class WatchSources : BaseSources() {
@@ -23,14 +24,28 @@ abstract class WatchSources : BaseSources() {
} ?: mutableMapOf()
}
suspend fun loadEpisodes(i: Int, showLink: String, extra: Map<String, String>?, sAnime: SAnime?): MutableMap<String, Episode> {
suspend fun loadEpisodes(
i: Int,
showLink: String,
extra: Map<String, String>?,
sAnime: SAnime?
): MutableMap<String, Episode> {
println("finder333 $showLink")
val map = mutableMapOf<String, Episode>()
val parser = get(i)
tryWithSuspend(true) {
if (sAnime != null) {
parser.loadEpisodes(showLink,extra, sAnime).forEach {
map[it.number] = Episode(it.number, it.link, it.title, it.description, it.thumbnail, it.isFiller, extra = it.extra, sEpisode = it.sEpisode)
parser.loadEpisodes(showLink, extra, sAnime).forEach {
map[it.number] = Episode(
it.number,
it.link,
it.title,
it.description,
it.thumbnail,
it.isFiller,
extra = it.extra,
sEpisode = it.sEpisode
)
}
}
}
@@ -42,7 +57,7 @@ abstract class WatchSources : BaseSources() {
abstract class MangaReadSources : BaseSources() {
override operator fun get(i: Int): MangaParser {
return (list.getOrNull(i)?:list.firstOrNull())?.get?.value as? MangaParser
return (list.getOrNull(i) ?: list.firstOrNull())?.get?.value as? MangaParser
?: EmptyMangaParser()
}
@@ -56,6 +71,7 @@ abstract class MangaReadSources : BaseSources() {
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
val map = mutableMapOf<String, MangaChapter>()
val parser = get(i)
show.sManga?.let { sManga ->
tryWithSuspend(true) {
parser.loadChapters(show.link, show.extra, sManga).forEach {
@@ -63,15 +79,28 @@ abstract class MangaReadSources : BaseSources() {
}
}
}
if(show.sManga == null) {
//must be downloaded
if (show.sManga == null) {
logger("sManga is null")
}
if (parser is OfflineMangaParser && show.sManga == null) {
tryWithSuspend(true) {
// Since we've checked, we can safely cast parser to OfflineMangaParser and call its methods
parser.loadChapters(show.link, show.extra, SManga.create()).forEach {
map[it.number] = MangaChapter(it)
}
}
} else {
logger("Parser is not an instance of OfflineMangaParser")
}
logger("map size ${map.size}")
return map
}
}
abstract class NovelReadSources : BaseSources(){
abstract class NovelReadSources : BaseSources() {
override operator fun get(i: Int): NovelParser? {
return if (list.isNotEmpty()) {
(list.getOrNull(i) ?: list[0]).get.value as NovelParser
@@ -87,7 +116,7 @@ class EmptyNovelParser : NovelParser() {
override val volumeRegex: Regex = Regex("")
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
return Book("","", null, emptyList()) // Return an empty Book object or some default value
return Book("", "", null, emptyList()) // Return an empty Book object or some default value
}
override suspend fun search(query: String): List<ShowResponse> {

View File

@@ -67,7 +67,7 @@ data class MangaChapter(
//Self-Descriptive
val title: String? = null,
val description: String? = null,
val scanlator: String? = null,
val sChapter: SChapter,
)
@@ -81,8 +81,8 @@ data class MangaImage(
val useTransformation: Boolean = false,
val page: Page
val page: Page? = null,
) : Serializable{
constructor(url: String,useTransformation: Boolean=false, page: Page)
constructor(url: String,useTransformation: Boolean=false, page: Page? = null)
: this(FileUrl(url),useTransformation, page)
}

View File

@@ -7,16 +7,19 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions)
list = createParsersFromExtensions(initialExtensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions)
list = createParsersFromExtensions(extensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
}
}

View File

@@ -0,0 +1,77 @@
package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
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 OfflineMangaParser: MangaParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
override val saveName: String = "Offline"
override suspend fun loadChapters(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$mangaLink"
)
//get all of the folder names and add them to the list
val chapters = mutableListOf<MangaChapter>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = MangaChapter(it.name, "$mangaLink/${it.name}", it.name, null, null, SChapter.create())
chapters.add(chapter)
}
}
chapters.sortBy { MangaNameAdapter.findChapterNumber(it.number) }
return chapters
}
return emptyList()
}
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$chapterLink"
)
val images = mutableListOf<MangaImage>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isFile) {
val image = MangaImage(it.absolutePath, false, null)
images.add(image)
}
}
return images
}
return emptyList()
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
returnTitles.add(title)
}
}
val returnList: MutableList<ShowResponse> = mutableListOf()
for (title in returnTitles) {
returnList.add(ShowResponse(title, title, title))
}
return returnList
}
}

View File

@@ -49,10 +49,10 @@ class AnimeExtensionsFragment : Fragment(),
): View {
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = true
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = false
binding.allAnimeExtensionsRecyclerView.adapter = adapter
binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true
lifecycleScope.launch {
viewModel.pagerFlow.collectLatest {
@@ -60,6 +60,8 @@ class AnimeExtensionsFragment : Fragment(),
}
}
viewModel.invalidatePager() // Force a refresh of the pager
return binding.root
}

View File

@@ -13,26 +13,9 @@ class DevelopersDialogFragment : BottomSheetDialogFragment() {
private val binding get() = _binding!!
private val developers = arrayOf(
Developer("vorobyovgabriel","https://avatars.githubusercontent.com/u/99561687?s=120&v=4","Owner","https://github.com/vorobyovgabriel"),
Developer("brahmkshtriya","https://avatars.githubusercontent.com/u/69040506?s=120&v=4","Maintainer","https://github.com/brahmkshatriya"),
Developer("jeelpatel231","https://avatars.githubusercontent.com/u/33726155?s=120&v=4","Contributor","https://github.com/jeelpatel231"),
Developer("blatzar","https://avatars.githubusercontent.com/u/46196380?s=120&v=4","Contributor","https://github.com/Blatzar"),
Developer("bilibox","https://avatars.githubusercontent.com/u/1800580?s=120&v=4","Contributor","https://github.com/Bilibox"),
Developer("sutslec","https://avatars.githubusercontent.com/u/27722281?s=120&v=4","Contributor","https://github.com/Sutslec"),
Developer("4jx","https://avatars.githubusercontent.com/u/79868816?s=120&v=4","Contributor","https://github.com/4JX"),
Developer("xtrm-en","https://avatars.githubusercontent.com/u/26600206?s=120&v=4","Contributor","https://github.com/xtrm-en"),
Developer("scrazzz","https://avatars.githubusercontent.com/u/70033559?s=120&v=4","Contributor","https://github.com/scrazzz"),
Developer("defcoding","https://avatars.githubusercontent.com/u/39608887?s=120&v=4","Contributor","https://github.com/defcoding"),
Developer("adolar0042","https://avatars.githubusercontent.com/u/39769465?s=120&v=4","Contributor","https://github.com/adolar0042"),
Developer("diegopyl1209","https://avatars.githubusercontent.com/u/80992641?s=120&v=4","Contributor","https://github.com/diegopyl1209"),
Developer("sreekrishna2001","https://avatars.githubusercontent.com/u/67505103?s=120&v=4","Contributor","https://github.com/Sreekrishna2001"),
Developer("riimuru","https://avatars.githubusercontent.com/u/57333995?s=120&v=4","Contributor","https://github.com/riimuru"),
Developer("vu nguyen","https://avatars.githubusercontent.com/u/68330291?s=120&v=4","Contributor","https://github.com/hoangvu12"),
Developer("animejeff","https://avatars.githubusercontent.com/u/101831300?s=120&v=4","Contributor","https://github.com/AnimeJeff"),
Developer("antonydp","https://avatars.githubusercontent.com/u/38143733?s=120&v=4","Contributor","https://github.com/antonydp"),
Developer("tobybridle","https://avatars.githubusercontent.com/u/52335751?s=120&v=4","Contributor","https://github.com/TobyBridle"),
Developer("enimax","https://avatars.githubusercontent.com/u/107899019?s=120&v=4","Contributor","https://github.com/enimax-anime"),
Developer("vipulog","https://avatars.githubusercontent.com/u/90324465?s=120&v=4","Contributor","https://github.com/VipulOG")
Developer("rebelonion","https://avatars.githubusercontent.com/u/87634197?v=4","Owner and Maintainer","https://github.com/rebelonion"),
Developer("Wai What", "https://avatars.githubusercontent.com/u/149729762?v=4", "Icon Designer", "https://github.com/WaiWhat"),
Developer("Aayush262", "https://avatars.githubusercontent.com/u/99584765?v=4", "Contributor", "https://github.com/aayush2622"),
)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {

View File

@@ -1,55 +1,32 @@
package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Build.*
import android.os.Build.VERSION.*
import android.os.Bundle
import android.view.LayoutInflater
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.AutoCompleteTextView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.SearchView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.databinding.ActivityExtensionsBinding
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.MangaFragment
import ani.dantotsu.themes.ThemeManager
import com.bumptech.glide.Glide
import ani.dantotsu.others.LangSet
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import javax.inject.Inject
class ExtensionsActivity : AppCompatActivity() {
private val restartMainActivity = object : OnBackPressedCallback(false) {
@@ -61,7 +38,8 @@ class ExtensionsActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityExtensionsBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -94,21 +72,20 @@ class ExtensionsActivity : AppCompatActivity() {
}.attach()
val searchView: SearchView = findViewById(R.id.searchView)
val searchView: AutoCompleteTextView = findViewById(R.id.searchViewText)
val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
searchView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun onQueryTextChange(newText: String?): Boolean {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
if (currentFragment is SearchQueryHandler) {
currentFragment.updateContentBasedOnQuery(newText)
currentFragment.updateContentBasedOnQuery(s?.toString()?.trim())
}
return true
}
})
@@ -116,18 +93,18 @@ class ExtensionsActivity : AppCompatActivity() {
initActivity(this)
binding.languageselect.setOnClickListener {
val popup = PopupMenu(this, it)
popup.inflate(R.menu.launguage_selector_menu)
popup.show()
}
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
onBackPressedDispatcher.addCallback(this, restartMainActivity)
binding.settingsBack.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}

View File

@@ -8,6 +8,7 @@ import ani.dantotsu.currContext
import ani.dantotsu.databinding.ActivityFaqBinding
import ani.dantotsu.initActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class FAQActivity : AppCompatActivity() {
private lateinit var binding: ActivityFaqBinding
@@ -105,7 +106,8 @@ class FAQActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityFaqBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.settings
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.Context
import android.os.Bundle
@@ -7,8 +8,10 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
@@ -17,10 +20,15 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
import ani.dantotsu.loadData
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@@ -30,62 +38,129 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class InstalledAnimeExtensionsFragment : Fragment() {
private var _binding: FragmentAnimeExtensionsBinding? = null
private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
private val extensionsAdapter = AnimeExtensionsAdapter({ pkg ->
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
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
var selectedIndex = 0
AlertDialog.Builder(requireContext())
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
if (pkg.hasUpdate) {
animeExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
{ installStep ->
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Updating extension")
.setContentText("Step: $installStep")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
},
{ error ->
FirebaseCrashlytics.getInstance().recordException(error)
Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_ERROR
)
.setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Update failed: ${error.message}")
.setContentText("Error: ${error.message}")
.setPriority(NotificationCompat.PRIORITY_HIGH)
notificationManager.notify(1, builder.build())
},
{
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
.setContentTitle("Update complete")
.setContentText("The extension has been successfully updated.")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
// Move the fragment transaction here
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
val activity = requireActivity() as ExtensionsActivity
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.VISIBLE
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.VISIBLE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.VISIBLE
activity.findViewById<FrameLayout>(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()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
return@setNegativeButton
}
.show()
} else {
animeExtensionManager.uninstallExtension(pkg.pkgName)
// If there's only one setting, proceed with the fragment transaction
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
val activity = requireActivity() as ExtensionsActivity
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.VISIBLE
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.VISIBLE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.VISIBLE
activity.findViewById<FrameLayout>(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<ViewPager2>(R.id.viewPager).visibility = View.GONE
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.GONE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.GONE
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = View.VISIBLE
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
}, skipIcons)
},
{ pkg ->
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) {
animeExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
{ installStep ->
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Updating extension")
.setContentText("Step: $installStep")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
},
{ error ->
FirebaseCrashlytics.getInstance().recordException(error)
Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_ERROR
)
.setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Update failed: ${error.message}")
.setContentText("Error: ${error.message}")
.setPriority(NotificationCompat.PRIORITY_HIGH)
notificationManager.notify(1, builder.build())
},
{
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
.setContentTitle("Update complete")
.setContentText("The extension has been successfully updated.")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
}
)
} else {
animeExtensionManager.uninstallExtension(pkg.pkgName)
}
}
}, skipIcons
)
override fun onCreateView(
inflater: LayoutInflater,
@@ -114,6 +189,7 @@ class InstalledAnimeExtensionsFragment : Fragment() {
private class AnimeExtensionsAdapter(
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
skipIcons: Boolean
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
@@ -133,31 +209,37 @@ class InstalledAnimeExtensionsFragment : Fragment() {
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter
val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) {
"(18+)"
} else {
""
}
holder.extensionNameTextView.text = extension.name
holder.extensionVersionTextView.text = "${extension.versionName} $nsfw"
if (!skipIcons) {
holder.extensionIconImageView.setImageDrawable(extension.icon)
}
if (extension.hasUpdate) {
holder.closeTextView.text = "Update"
holder.closeTextView.setTextColor(
ContextCompat.getColor(
holder.itemView.context,
R.color.warning
)
)
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
} else {
holder.closeTextView.text = "Uninstall"
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
onUninstallClicked(extension)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
val extensionVersionTextView: TextView = view.findViewById(R.id.extensionVersionTextView)
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
}
companion object {
@@ -180,5 +262,4 @@ class InstalledAnimeExtensionsFragment : Fragment() {
}
}
}

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.settings
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.Context
import android.os.Bundle
@@ -8,8 +9,10 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
@@ -18,13 +21,18 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
import ani.dantotsu.loadData
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
import kotlinx.coroutines.launch
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
@@ -37,6 +45,66 @@ class InstalledMangaExtensionsFragment : Fragment() {
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<ViewPager2>(R.id.viewPager).visibility = visibility
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
var selectedIndex = 0
AlertDialog.Builder(requireContext())
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
// 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()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
changeUIVisibility(true)
return@setNegativeButton
}
.show()
} 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()
}
// Hide ViewPager2 and TabLayout
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
},
{ pkg ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
@@ -115,6 +183,7 @@ class InstalledMangaExtensionsFragment : Fragment() {
private class MangaExtensionsAdapter(
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
skipIcons: Boolean
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
@@ -135,30 +204,35 @@ class InstalledMangaExtensionsFragment : Fragment() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) {
"(18+)"
} else {
""
}
holder.extensionNameTextView.text = extension.name
holder.extensionVersionTextView.text = "${extension.versionName} $nsfw"
if (!skipIcons) {
holder.extensionIconImageView.setImageDrawable(extension.icon)
}
if (extension.hasUpdate) {
holder.closeTextView.text = "Update"
holder.closeTextView.setTextColor(
ContextCompat.getColor(
holder.itemView.context,
R.color.warning
)
)
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
} else {
holder.closeTextView.text = "Uninstall"
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
onUninstallClicked(extension)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
val extensionVersionTextView: TextView = view.findViewById(R.id.extensionVersionTextView)
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
}
companion object {

View File

@@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagingData
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
@@ -25,6 +26,7 @@ import ani.dantotsu.settings.paging.MangaExtensionAdapter
import ani.dantotsu.settings.paging.MangaExtensionsViewModel
import ani.dantotsu.settings.paging.MangaExtensionsViewModelFactory
import ani.dantotsu.settings.paging.OnMangaInstallClickListener
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
class MangaExtensionsFragment : Fragment(),
@@ -50,17 +52,19 @@ class MangaExtensionsFragment : Fragment(),
): View {
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = true
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = false
binding.allMangaExtensionsRecyclerView.adapter = adapter
binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true
lifecycleScope.launch {
viewModel.pagerFlow.collectLatest {
adapter.submitData(it)
viewModel.pagerFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
viewModel.invalidatePager() // Force a refresh of the pager
return binding.root
}

View File

@@ -40,6 +40,7 @@ data class PlayerSettings(
var focusPause: Boolean = true,
var gestures: Boolean = true,
var doubleTap: Boolean = true,
var fastforward: Boolean = true,
var seekTime: Int = 10,
var skipTime: Int = 85,

View File

@@ -17,6 +17,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.snackbar.Snackbar
import kotlin.math.roundToInt
@@ -31,7 +32,8 @@ class PlayerSettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityPlayerSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -183,7 +185,11 @@ class PlayerSettingsActivity : AppCompatActivity() {
settings.doubleTap = isChecked
saveData(player, settings)
}
binding.playerSettingsFastForward.isChecked = settings.fastforward
binding.playerSettingsFastForward.setOnCheckedChangeListener { _, isChecked ->
settings.fastforward = isChecked
saveData(player, settings)
}
binding.playerSettingsSeekTime.value = settings.seekTime.toFloat()
binding.playerSettingsSeekTime.addOnChangeListener { _, value, _ ->
settings.seekTime = value.toInt()

View File

@@ -13,13 +13,15 @@ import ani.dantotsu.saveData
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class ReaderSettingsActivity : AppCompatActivity() {
lateinit var binding: ActivityReaderSettingsBinding
private val reader = "reader_settings"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityReaderSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -33,6 +33,7 @@ import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.snackbar.Snackbar
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.network.NetworkPreferences
@@ -53,11 +54,13 @@ class SettingsActivity : AppCompatActivity() {
lateinit var binding: ActivitySettingsBinding
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
private val networkPreferences = Injekt.get<NetworkPreferences>()
private var cursedCounter = 0
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -105,6 +108,12 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
restartApp()
}
binding.settingsUseOLED.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_oled", false)
binding.settingsUseOLED.setOnCheckedChangeListener { _, isChecked ->
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_oled", isChecked).apply()
restartApp()
}
val themeString = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!
binding.themeSwitcher.setText(themeString.substring(0, 1) + themeString.substring(1).lowercase())
@@ -161,6 +170,11 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked ->
saveData("skip_extension_icons", isChecked)
}
binding.NSFWExtension.isChecked = loadData("NFSWExtension") ?: true
binding.NSFWExtension.setOnCheckedChangeListener { _, isChecked ->
saveData("NFSWExtension", isChecked)
}
binding.userAgent.setText(networkPreferences.defaultUserAgent().get())
binding.userAgent.setOnEditorActionListener { _, _, _ ->
@@ -168,7 +182,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
true
}
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan")
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan", "Libre")
binding.settingsExtensionDns.setText(exDns[networkPreferences.dohProvider().get()], false)
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
@@ -383,8 +397,15 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
val array = resources.getStringArray(R.array.tips)
binding.settingsLogo.setSafeOnClickListener {
cursedCounter++
(binding.settingsLogo.drawable as Animatable).start()
snackString(array[(Math.random() * array.size).toInt()], this)
if (cursedCounter % 7 == 0){
snackString("youwu have been cuwsed :pwayge:")
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_cursed_lang", true).apply()
} else{
snackString(array[(Math.random() * array.size).toInt()], this)
}
}
binding.settingsDev.setOnClickListener {
@@ -408,7 +429,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
}
}
var curTime = loadData<Int>("subscriptions_time_r") ?: defaultTime
var curTime = loadData<Int>("subscriptions_time_s") ?: defaultTime
val timeNames = timeMinutes.map {
val mins = it % 60
val hours = it / 60
@@ -421,7 +442,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i ->
curTime = i
binding.settingsSubscriptionsTime.text = getString(R.string.subscriptions_checking_time_s, timeNames[i])
saveData("subscriptions_time_r", curTime)
saveData("subscriptions_time_s", curTime)
dialog.dismiss()
startSubscription(true)
}.show()

View File

@@ -3,19 +3,24 @@ package ani.dantotsu.settings
import android.app.DownloadManager
import android.content.ActivityNotFoundException
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 android.widget.Switch
import androidx.core.content.ContextCompat
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.databinding.BottomSheetSettingsBinding
import ani.dantotsu.download.DownloadContainerActivity
import ani.dantotsu.download.manga.OfflineMangaFragment
class SettingsDialogFragment : BottomSheetDialogFragment() {
class SettingsDialogFragment(val pageType: PageType) : BottomSheetDialogFragment() {
private var _binding: BottomSheetSettingsBinding? = null
private val binding get() = _binding!!
@@ -26,6 +31,12 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val window = dialog?.window
window?.statusBarColor = Color.CYAN
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
window?.navigationBarColor = typedValue.data
if (Anilist.token != null) {
binding.settingsLogin.setText(R.string.logout)
@@ -62,18 +73,42 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
dismiss()
}
binding.settingsDownloads.setSafeOnClickListener {
try {
val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null)
startActivity(
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString()
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
)
} catch (e: ActivityNotFoundException) {
toast(getString(R.string.file_manager_not_found))
when(pageType) {
PageType.MANGA -> {
val intent = Intent(activity, DownloadContainerActivity::class.java)
intent.putExtra("FRAGMENT_CLASS_NAME", OfflineMangaFragment::class.java.name)
startActivity(intent)
}
PageType.ANIME -> {
try {
val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null)
startActivity(
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString()
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
)
} catch (e: ActivityNotFoundException) {
toast(getString(R.string.file_manager_not_found))
}
}
PageType.HOME -> {
try {
val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null)
startActivity(
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString()
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
)
} catch (e: ActivityNotFoundException) {
toast(getString(R.string.file_manager_not_found))
}
}
}
dismiss()
}
}
@@ -82,4 +117,10 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
super.onDestroyView()
_binding = null
}
companion object{
enum class PageType{
MANGA, ANIME, HOME
}
}
}

View File

@@ -9,6 +9,7 @@ import androidx.core.view.updateLayoutParams
import ani.dantotsu.*
import ani.dantotsu.databinding.ActivityUserInterfaceSettingsBinding
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.snackbar.Snackbar
class UserInterfaceSettingsActivity : AppCompatActivity() {
@@ -16,7 +17,8 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
private val ui = "ui_settings"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityUserInterfaceSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -0,0 +1,88 @@
package ani.dantotsu.settings.extensionprefs
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.widget.FrameLayout
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.preference.DialogPreference
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.forEach
import androidx.preference.getOnBindEditTextListener
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.settings.ExtensionsActivity
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
import eu.kanade.tachiyomi.source.anime.getPreferenceKey
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = populateAnimePreferenceScreen()
//set background color
val color = TypedValue()
requireContext().theme.resolveAttribute(com.google.android.material.R.attr.backgroundColor, color, true)
view?.setBackgroundColor(color.data)
}
private var onCloseAction: (() -> Unit)? = null
override fun onDestroyView() {
super.onDestroyView()
onCloseAction?.invoke()
}
fun populateAnimePreferenceScreen(): PreferenceScreen {
val sourceId = requireArguments().getLong(SOURCE_ID)
val source = Injekt.get<AnimeSourceManager>().get(sourceId)!!
check(source is ConfigurableAnimeSource)
val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
val dataStore = SharedPreferencesDataStore(sharedPreferences)
preferenceManager.preferenceDataStore = dataStore
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
source.setupPreferenceScreen(sourceScreen)
sourceScreen.forEach { pref ->
pref.isIconSpaceReserved = false
if (pref is DialogPreference) {
pref.dialogTitle = pref.title
println("pref.dialogTitle: ${pref.dialogTitle}")
}
for (entry in sharedPreferences.all.entries) {
Log.d("Preferences", "Key: ${entry.key}, Value: ${entry.value}")
}
// Apply incognito IME for EditTextPreference
if (pref is EditTextPreference) {
val setListener = pref.getOnBindEditTextListener()
pref.setOnBindEditTextListener {
setListener?.onBindEditText(it)
it.setIncognito(lifecycleScope)
}
}
}
return sourceScreen
}
fun getInstance(sourceId: Long, onCloseAction: (() -> Unit)? = null): AnimeSourcePreferencesFragment {
val fragment = AnimeSourcePreferencesFragment()
fragment.arguments = bundleOf(SOURCE_ID to sourceId)
fragment.onCloseAction = onCloseAction
return fragment
}
companion object { //idk why it needs both
private const val SOURCE_ID = "source_id"
}
}

View File

@@ -0,0 +1,78 @@
package ani.dantotsu.settings.extensionprefs
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.preference.DialogPreference
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.forEach
import androidx.preference.getOnBindEditTextListener
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.settings.ExtensionsActivity
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.PreferenceScreen
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.manga.getPreferenceKey
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
import tachiyomi.domain.source.manga.service.MangaSourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaSourcePreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = populateMangaPreferenceScreen()
}
private var onCloseAction: (() -> Unit)? = null
override fun onDestroyView() {
super.onDestroyView()
onCloseAction?.invoke()
}
fun populateMangaPreferenceScreen(): PreferenceScreen {
val sourceId = requireArguments().getLong(SOURCE_ID)
val source = Injekt.get<MangaSourceManager>().get(sourceId)!!
check(source is ConfigurableSource)
val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
val dataStore = SharedPreferencesDataStore(sharedPreferences)
preferenceManager.preferenceDataStore = dataStore
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
source.setupPreferenceScreen(sourceScreen)
sourceScreen.forEach { pref ->
pref.isIconSpaceReserved = false
if (pref is DialogPreference) {
pref.dialogTitle = pref.title
println("pref.dialogTitle: ${pref.dialogTitle}")
}
// Apply incognito IME for EditTextPreference
if (pref is EditTextPreference) {
val setListener = pref.getOnBindEditTextListener()
pref.setOnBindEditTextListener {
setListener?.onBindEditText(it)
it.setIncognito(lifecycleScope)
}
}
}
return sourceScreen
}
fun getInstance(sourceId: Long, onCloseAction: (() -> Unit)? = null): MangaSourcePreferencesFragment {
val fragment = MangaSourcePreferencesFragment()
fragment.arguments = bundleOf(SOURCE_ID to sourceId)
fragment.onCloseAction = onCloseAction
return fragment
}
companion object {
private const val SOURCE_ID = "source_id"
}
}

View File

@@ -15,6 +15,7 @@ import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.databinding.ItemExtensionAllBinding
import ani.dantotsu.loadData
import com.bumptech.glide.Glide
@@ -77,21 +78,26 @@ class AnimeExtensionPagingSource(
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
val query = searchQuery.first()
var isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: 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 }
}
return try {
val sublist = filteredExtensions.subList(
val sublist = filternfsw.subList(
fromIndex = position,
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size)
toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
)
LoadResult.Page(
data = sublist,
prevKey = if (position == 0) null else position - params.loadSize,
nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize
nextKey = if (position + params.loadSize >= filternfsw.size) null else position + params.loadSize
)
} catch (e: Exception) {
LoadResult.Error(e)
@@ -151,8 +157,14 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
}
}
val extensionIconImageView: ImageView = binding.extensionIconImageView
fun bind(extension: AnimeExtension.Available) {
fun bind(extension: AnimeExtension.Available) {
val nsfw = if (extension.isNsfw) {
"(18+)"
} else {
""
}
binding.extensionNameTextView.text = extension.name
binding.extensionVersionTextView.text = "${extension.versionName} $nsfw"
}
}
}

View File

@@ -16,6 +16,7 @@ import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.databinding.ItemExtensionAllBinding
import ani.dantotsu.loadData
import com.bumptech.glide.Glide
@@ -81,21 +82,26 @@ class MangaExtensionPagingSource(
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
val query = searchQuery.first()
var isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: false
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 }
}
return try {
val sublist = filteredExtensions.subList(
val sublist = filternfsw.subList(
fromIndex = position,
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size)
toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
)
LoadResult.Page(
data = sublist,
prevKey = if (position == 0) null else position - params.loadSize,
nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize
nextKey = if (position + params.loadSize >= filternfsw.size) null else position + params.loadSize
)
} catch (e: Exception) {
LoadResult.Error(e)
@@ -117,12 +123,10 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MangaExtension.Available>() {
override fun areItemsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
// Your logic here
return oldItem.pkgName == newItem.pkgName
}
override fun areContentsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
// Your logic here
return oldItem == newItem
}
}
@@ -156,7 +160,13 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
}
val extensionIconImageView: ImageView = binding.extensionIconImageView
fun bind(extension: MangaExtension.Available) {
val nsfw = if (extension.isNsfw) {
"(18+)"
} else {
""
}
binding.extensionNameTextView.text = extension.name
binding.extensionVersionTextView.text = "${extension.versionName} $nsfw"
}
}
}

View File

@@ -40,7 +40,7 @@ class AlarmReceiver : BroadcastReceiver() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val curTime = loadData<Int>("subscriptions_time_r", context) ?: defaultTime
val curTime = loadData<Int>("subscriptions_time_s", context) ?: defaultTime
if (timeMinutes[curTime] > 0)
alarmManager.setRepeating(

View File

@@ -16,8 +16,8 @@ import kotlinx.coroutines.launch
@SuppressLint("MissingPermission")
class Subscription {
companion object {
const val defaultTime = 3
val timeMinutes = arrayOf(0L, 120, 180, 240, 360, 480, 720, 1440)
const val defaultTime = 1
val timeMinutes = arrayOf(0L, 720, 1440)
private var alreadyStarted = false
fun Context.startSubscription(force: Boolean = false) {

View File

@@ -22,7 +22,7 @@ class SubscriptionWorker(val context: Context, params: WorkerParameters) : Corou
private const val SUBSCRIPTION_WORK_NAME = "work_subscription"
fun enqueue(context: Context) {
val curTime = loadData<Int>("subscriptions_time_r") ?: defaultTime
val curTime = loadData<Int>("subscriptions_time_s") ?: defaultTime
if(timeMinutes[curTime]>0L) {
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val periodicSyncDataWork = PeriodicWorkRequest.Builder(

View File

@@ -1,41 +1,42 @@
package ani.dantotsu.themes
import android.content.Context
import android.content.res.Configuration
import ani.dantotsu.R
class ThemeManager(private val context: Context) {
fun applyTheme() {
val useOLED = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_oled", false) && isDarkThemeActive(context)
if(context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false)){
return
}
when (context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!) {
"PURPLE" -> {
context.setTheme(R.style.Theme_Dantotsu_Purple)
}
"BLUE" -> {
context.setTheme(R.style.Theme_Dantotsu_Blue)
}
"GREEN" -> {
context.setTheme(R.style.Theme_Dantotsu_Green)
}
"PINK" -> {
context.setTheme(R.style.Theme_Dantotsu_Pink)
}
"RED" -> {
context.setTheme(R.style.Theme_Dantotsu_Red)
}
"LAVENDER" -> {
context.setTheme(R.style.Theme_Dantotsu_Lavender)
}
"MONOCHROME (BETA)" -> {
context.setTheme(R.style.Theme_Dantotsu_Monochrome)
}
else -> {
context.setTheme(R.style.Theme_Dantotsu_Purple)
}
val theme = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).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
"PINK" -> if (useOLED) R.style.Theme_Dantotsu_PinkOLED else R.style.Theme_Dantotsu_Pink
"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
"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
}
context.setTheme(themeToApply)
}
private fun isDarkThemeActive(context: Context): Boolean {
return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> true
Configuration.UI_MODE_NIGHT_NO -> false
Configuration.UI_MODE_NIGHT_UNDEFINED -> false
else -> false
}
}
companion object{
enum class Theme(val theme: String) {
PURPLE("PURPLE"),
@@ -44,7 +45,8 @@ class ThemeManager(private val context: Context) {
PINK("PINK"),
RED("RED"),
LAVENDER("LAVENDER"),
MONOCHROME("MONOCHROME (BETA)");
MONOCHROME("MONOCHROME (BETA)"),
SAIKOU("SAIKOU");
companion object {
fun fromString(value: String): Theme {
@@ -53,4 +55,4 @@ class ThemeManager(private val context: Context) {
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.animesource
/**
* A source that explicitly doesn't require traffic considerations.
*
* This typically applies for self-hosted sources.
*/
interface UnmeteredSource

View File

@@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.data.preference
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceDataStore
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return prefs.getBoolean(key, defValue)
}
override fun putBoolean(key: String?, value: Boolean) {
prefs.edit {
putBoolean(key, value)
}
}
override fun getInt(key: String?, defValue: Int): Int {
return prefs.getInt(key, defValue)
}
override fun putInt(key: String?, value: Int) {
prefs.edit {
putInt(key, value)
}
}
override fun getLong(key: String?, defValue: Long): Long {
return prefs.getLong(key, defValue)
}
override fun putLong(key: String?, value: Long) {
prefs.edit {
putLong(key, value)
}
}
override fun getFloat(key: String?, defValue: Float): Float {
return prefs.getFloat(key, defValue)
}
override fun putFloat(key: String?, value: Float) {
prefs.edit {
putFloat(key, value)
}
}
override fun getString(key: String?, defValue: String?): String? {
return prefs.getString(key, defValue)
}
override fun putString(key: String?, value: String?) {
prefs.edit {
putString(key, value)
}
}
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
return prefs.getStringSet(key, defValues)
}
override fun putStringSet(key: String?, values: MutableSet<String>?) {
prefs.edit {
putStringSet(key, values)
}
}
}

View File

@@ -8,6 +8,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
@@ -100,7 +101,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
}
init {
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED)
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
@@ -25,7 +26,8 @@ class AnimeExtensionInstallActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type)

View File

@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import kotlinx.coroutines.CoroutineStart
@@ -27,7 +28,7 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
* Registers this broadcast receiver
*/
fun register(context: Context) {
context.registerReceiver(this, filter)
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
/**

View File

@@ -216,7 +216,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
isRegistered = true
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
context.registerReceiver(this, filter)
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
/**

View File

@@ -8,6 +8,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
@@ -100,7 +101,7 @@ class PackageInstallerInstallerManga(private val service: Service) : InstallerMa
}
init {
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED)
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
@@ -25,7 +26,8 @@ class MangaExtensionInstallActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type)

View File

@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import kotlinx.coroutines.CoroutineStart
@@ -27,7 +28,7 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
* Registers this broadcast receiver
*/
fun register(context: Context) {
context.registerReceiver(this, filter)
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
/**

View File

@@ -213,7 +213,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
isRegistered = true
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
context.registerReceiver(this, filter)
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
/**

View File

@@ -21,6 +21,7 @@ const val PREF_DOH_MULLVAD = 9
const val PREF_DOH_CONTROLD = 10
const val PREF_DOH_NJALLA = 11
const val PREF_DOH_SHECAN = 12
const val PREF_DOH_LIBREDNS = 13
fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build())
@@ -184,3 +185,13 @@ fun OkHttpClient.Builder.dohShecan() = dns(
)
.build(),
)
fun OkHttpClient.Builder.dohLibreDNS() = dns(
DnsOverHttps.Builder().client(build())
.url("https://doh.libredns.gr/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("116.202.176.26"), // IPv4 address for LibreDNS
InetAddress.getByName("192.71.166.92") // Fallback IPv4 address
)
.build()
)

View File

@@ -62,6 +62,7 @@ class NetworkHelper(
PREF_DOH_CONTROLD -> builder.dohControlD()
PREF_DOH_NJALLA -> builder.dohNajalla()
PREF_DOH_SHECAN -> builder.dohShecan()
PREF_DOH_LIBREDNS -> builder.dohLibreDNS()
}
return builder

View File

@@ -0,0 +1,85 @@
package eu.kanade.tachiyomi.source.anime
import android.content.Context
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.source.local.entries.anime.LocalAnimeSource
import java.util.concurrent.ConcurrentHashMap
class AndroidAnimeSourceManager(
private val context: Context,
private val extensionManager: AnimeExtensionManager,
) : AnimeSourceManager {
private val scope = CoroutineScope(Job() + Dispatchers.IO)
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, AnimeSource>())
private val stubSourcesMap = ConcurrentHashMap<Long, StubAnimeSource>()
override val catalogueSources: Flow<List<AnimeCatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<AnimeCatalogueSource>() }
init {
scope.launch {
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, AnimeSource>(
mapOf(
LocalAnimeSource.ID to LocalAnimeSource(
context,
),
),
)
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it
registerStubSource(it.toSourceData())
}
}
sourcesMapFlow.value = mutableMap
}
}
}
override fun get(sourceKey: Long): AnimeSource? {
return sourcesMapFlow.value[sourceKey]
}
override fun getOrStub(sourceKey: Long): AnimeSource {
return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
runBlocking { createStubSource(sourceKey) }
}
}
override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<AnimeHttpSource>()
override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<AnimeCatalogueSource>()
override fun getStubSources(): List<StubAnimeSource> {
val onlineSourceIds = getOnlineSources().map { it.id }
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
}
private fun registerStubSource(sourceData: AnimeSourceData) {
}
private suspend fun createStubSource(id: Long): StubAnimeSource {
return StubAnimeSource(AnimeSourceData(id, "", ""))
}
}

View File

@@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.source.anime
import android.graphics.drawable.Drawable
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource
import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this.id)
fun AnimeSource.getPreferenceKey(): String = "source_$id"
fun AnimeSource.toSourceData(): AnimeSourceData = AnimeSourceData(id = id, lang = lang, name = name)
fun AnimeSource.getNameForAnimeInfo(): String {
val preferences = Injekt.get<SourcePreferences>()
val enabledLanguages = preferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
val isInEnabledLanguages = lang in enabledLanguages
return when {
// For edge cases where user disables a source they got manga of in their library.
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
// Hide the language tag when only one language is used.
hasOneActiveLanguages && isInEnabledLanguages -> name
else -> toString()
}
}
fun AnimeSource.isLocalOrStub(): Boolean = isLocal() || this is StubAnimeSource

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