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 # Android Profiling
*.hprof *.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"> <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://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> <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> </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> # **Dantotsu** 🌟
<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> Dantotsu is an [Anilist](https://anilist.co/) only client.
<br>
<br> > **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> <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** ## WANT TO CONTRIBUTE? 🤝
>
> 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.
## 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 | You can come hang out with our awesome community, request new features, and report any bugs or issues at our Discord server too. 📣
| ---------------- | ------- |
| Anime Extensions | Working |
| Manga Extensions | "Working" |
| Light Novel Extensions | Not Working |
### 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"> <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> </p>
## LICENSE 📜
### 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
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md) Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)

View File

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

View File

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

View File

@@ -221,6 +221,7 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/> <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".download.DownloadContainerActivity" />
<activity <activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
@@ -263,10 +264,16 @@
</intent-filter> </intent-filter>
</service> </service>
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService" <service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService" <service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<service android:name=".download.manga.MangaDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application> </application>
</manifest> </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 eu.kanade.tachiyomi.data.notification.Notifications
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import ani.dantotsu.others.DisabledReports 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.android.material.color.DynamicColors
import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase 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.AndroidLogcatLogger
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
class App : MultiDexApplication() { class App : MultiDexApplication() {
private lateinit var animeExtensionManager: AnimeExtensionManager
private lateinit var mangaExtensionManager: MangaExtensionManager
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
MultiDex.install(this) MultiDex.install(this)
@@ -52,6 +64,22 @@ class App : MultiDexApplication() {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) 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() { private fun setupNotificationChannels() {

View File

@@ -188,6 +188,9 @@ fun Activity.hideStatusBar() {
open class BottomSheetDialogFragment : BottomSheetDialogFragment() { open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() { override fun onStart() {
super.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) { if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View) val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
@@ -204,23 +207,21 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
fun isOnline(context: Context): Boolean { fun isOnline(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith { return tryWith {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) return@tryWith if (cap != null) {
return@tryWith if (cap != null) { when {
when { cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_BLUETOOTH) || cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_CELLULAR) || cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_ETHERNET) || cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_LOWPAN) || cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_USB) || cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_VPN) || cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI) || cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false else -> false
} }
} else false } else false
} else true
} ?: false } ?: false
} }
@@ -419,7 +420,13 @@ fun String.findBetween(a: String, b: String): String? {
fun ImageView.loadImage(url: String?, size: Int = 0) { fun ImageView.loadImage(url: String?, size: Int = 0) {
if (!url.isNullOrEmpty()) { 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( class SafeClickListener(
private var defaultInterval: Int = 1000, private var defaultInterval: Int = 1000,
private val onSafeCLick: (View) -> Unit private val onSafeCLick: (View) -> Unit
@@ -729,7 +744,7 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
if (s != null) { if (s != null) {
(activity ?: currActivity())?.apply { (activity ?: currActivity())?.apply {
runOnUiThread { 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 { snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> { updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)

View File

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

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import tachiyomi.core.preference.PreferenceStore 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.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences 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 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.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import ani.dantotsu.download.DownloadsManager
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app, get()) } addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) } addSingletonFactory { MangaExtensionManager(app) }
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
addSingleton(sharedPreferences) addSingleton(sharedPreferences)
@@ -40,6 +50,11 @@ class AppModule(val app: Application) : InjektModule {
} }
addSingletonFactory { MangaCache() } 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.logger
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val data: Uri? = intent?.data val data: Uri? = intent?.data
logger(data.toString()) logger(data.toString())
try { try {

View File

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

View File

@@ -12,13 +12,15 @@ import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord.saveToken import ani.dantotsu.connections.discord.Discord.saveToken
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName() val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process) 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.clientId
import ani.dantotsu.connections.mal.MAL.saveResponse import ani.dantotsu.connections.mal.MAL.saveResponse
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
try { try {
val data: Uri = intent?.data val data: Uri = intent?.data
?: throw Exception(getString(R.string.mal_login_uri_not_found)) ?: 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.SubtitleType
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType 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.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.* import java.util.concurrent.*
@@ -118,6 +121,9 @@ object Helper {
val database = StandaloneDatabaseProvider(context) val database = StandaloneDatabaseProvider(context)
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory { 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() val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach { defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value) 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 = override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification( DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this, this,
R.drawable.monochrome, R.drawable.mono,
null, null,
null, null,
downloads, downloads,

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,111 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams 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.databinding.ActivityNoInternetBinding
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight 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.startMainActivity
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import nl.joery.animatedbottombar.AnimatedBottomBar
class NoInternet : AppCompatActivity() { class NoInternet : AppCompatActivity() {
private lateinit var binding: ActivityNoInternetBinding
lateinit var bottomBar: AnimatedBottomBar
private var uiSettings = UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val binding = ActivityNoInternetBinding.inflate(layoutInflater) binding = ActivityNoInternetBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
topMargin = statusBarHeight if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
bottomMargin = navBarHeight
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 { val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
if (isOnline(this)) { .getBoolean("colorOverflow", false)
startMainActivity(this) 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.databinding.ActivityAuthorBinding
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -29,7 +30,8 @@ class AuthorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater) binding = ActivityAuthorBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,14 @@ package ani.dantotsu.media
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.FileUrl
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment import ani.dantotsu.media.anime.SelectorDialogFragment
@@ -30,6 +32,8 @@ import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.AniyomiAdapter import ani.dantotsu.parsers.AniyomiAdapter
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
@@ -44,6 +48,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
class MediaDetailsViewModel : ViewModel() { class MediaDetailsViewModel : ViewModel() {
val scrolledToTop = MutableLiveData(true) 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 sharedPreferences = Injekt.get<SharedPreferences>()
val data = loadData<Selected>("${media.id}-select") ?: Selected().let { val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) { it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0) true ->sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0) else ->sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
} }
it.preferDub = loadData("settings_prefer_dub") ?: false it.preferDub = loadData("settings_prefer_dub") ?: false
saveSelected(media.id, it) saveSelected(media.id, it)
it it
} }
if (isDownload) {
data.sourceIndex = when (media.anime != null) {
true -> AnimeSources.list.size - 1
else -> MangaSources.list.size - 1
}
}
return data return data
} }
@@ -120,8 +131,8 @@ class MediaDetailsViewModel : ViewModel() {
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null) private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>() private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
suspend fun loadEpisodes(media: Media, i: Int) { suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
if (!epsLoaded.containsKey(i)) { if (!epsLoaded.containsKey(i) || invalidate) {
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
} }
episodes.postValue(epsLoaded) episodes.postValue(epsLoaded)
@@ -240,9 +251,9 @@ class MediaDetailsViewModel : ViewModel() {
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null) private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>() private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters 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") logger("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i)) tryWithSuspend { if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
} }
mangaChapters.postValue(mangaLoaded) mangaChapters.postValue(mangaLoaded)
@@ -258,7 +269,28 @@ class MediaDetailsViewModel : ViewModel() {
private val mangaChapter = MutableLiveData<MangaChapter?>(null) private val mangaChapter = MutableLiveData<MangaChapter?>(null)
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter 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) { return tryWithSuspend(true) {
chapter.addImages( chapter.addImages(
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false 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.connections.mal.MAL
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
@@ -38,7 +39,8 @@ class SearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivitySearchBinding.inflate(layoutInflater) binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.TypedValue import android.util.TypedValue
@@ -8,22 +10,38 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources 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.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip 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.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.IndexOutOfBoundsException
class AnimeWatchAdapter( class AnimeWatchAdapter(
private val media: Media, private val media: Media,
@@ -69,7 +87,8 @@ class AnimeWatchAdapter(
} }
//Source Selection //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) { if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source]) binding.animeSource.setText(watchSources.names[source])
watchSources[source].apply { watchSources[source].apply {
@@ -91,11 +110,41 @@ class AnimeWatchAdapter(
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
source = i
setLanguageList(0,i)
} }
subscribeButton(false) 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 //Subscription
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
@@ -177,6 +226,7 @@ class AnimeWatchAdapter(
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
} }
chip.text = "${names[limit * (position)]} - ${names[last - 1]}" chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
chip.setOnClickListener { chip.setOnClickListener {
selected() 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 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) { inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {

View File

@@ -1,11 +1,17 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.math.MathUtils
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -13,24 +19,38 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription 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.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -214,6 +234,13 @@ class AnimeWatchFragment : Fragment() {
return model.watchSources?.get(i)!! 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) { fun onDubClicked(checked: Boolean) {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
@@ -223,8 +250,8 @@ class AnimeWatchFragment : Fragment() {
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) } lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
} }
fun loadEpisodes(i: Int) { fun loadEpisodes(i: Int, invalidate: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) } lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
} }
fun onIconPressed(viewType: Int, rev: Boolean) { fun onIconPressed(viewType: Int, rev: Boolean) {
@@ -262,45 +289,115 @@ class AnimeWatchFragment : Fragment() {
else getString(R.string.unsubscribed_notification) else getString(R.string.unsubscribed_notification)
) )
} }
fun openSettings(pkg: AnimeExtension.Installed){
fun onEpisodeClick(i: String) { val changeUIVisibility: (Boolean) -> Unit = { show ->
model.continueMedia = false val activity = requireActivity() as MediaDetailsActivity
model.saveSelected(media.id, media.selected!!, requireActivity()) val visibility = if (show) View.VISIBLE else View.GONE
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
} activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
@SuppressLint("NotifyDataSetChanged") activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
private fun reload() { try{
val selected = model.loadSelected(media) activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
}catch (e: ClassCastException){
//Find latest episode for subscription activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f }
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
model.saveSelected(media.id, selected, requireActivity()) }
headerAdapter.handleEpisodes() val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) if (allSettings.isNotEmpty()) {
var arr: ArrayList<Episode> = arrayListOf() var selectedSetting = allSettings[0]
if (media.anime!!.episodes != null) { if (allSettings.size > 1) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null val names = allSettings.map { it.lang }.toTypedArray()
arr.addAll( var selectedIndex = 0
media.anime!!.episodes!!.values.toList() AlertDialog.Builder(requireContext())
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1))) .setTitle("Select a Source")
) .setSingleChoiceItems(names, selectedIndex) { _, which ->
if (reverse) selectedIndex = which
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr }
.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() { fun onEpisodeClick(i: String) {
model.watchSources?.flushText() model.continueMedia = false
super.onDestroy() 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() { override fun onResume() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress

View File

@@ -17,6 +17,7 @@ import android.graphics.drawable.Animatable
import android.hardware.SensorManager import android.hardware.SensorManager
import android.media.AudioManager import android.media.AudioManager
import android.media.AudioManager.* import android.media.AudioManager.*
import android.media.PlaybackParams
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -77,8 +78,10 @@ import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.PlayerSettingsActivity import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.lagradost.nicehttp.ignoreAllSSLErrors import com.lagradost.nicehttp.ignoreAllSSLErrors
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -317,7 +320,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityExoplayerBinding.inflate(layoutInflater) binding = ActivityExoplayerBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -806,23 +810,24 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
fun fastForward() { fun fastForward() {
val newSpeed = playbackParameters.speed * 2f
exoPlayer.playbackParameters = playbackParameters.withSpeed(newSpeed)
isFastForwarding = true isFastForwarding = true
snackString("Playing at ${newSpeed}x speed") exoPlayer.setPlaybackSpeed(2f)
snackString("Playing at 2x speed")
} }
fun stopFastForward() { fun stopFastForward() {
if (isFastForwarding) { if (isFastForwarding) {
exoPlayer.playbackParameters = playbackParameters
isFastForwarding = false isFastForwarding = false
exoPlayer.setPlaybackSpeed(1f)
snackString("Playing at normal speed") snackString("Playing at normal speed")
} }
} }
//FastRewind (Left Panel) //FastRewind (Left Panel)
val fastRewindDetector = GestureDetector(this, object : GesturesListener() { 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) { override fun onDoubleClick(event: MotionEvent) {
doubleTap(false, event) doubleTap(false, event)
@@ -852,8 +857,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
//FastForward (Right Panel) //FastForward (Right Panel)
val fastForwardDetector = GestureDetector(this, object : GesturesListener() { 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) { override fun onDoubleClick(event: MotionEvent) {
doubleTap(true, event) doubleTap(true, event)
} }
@@ -1614,7 +1620,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
else 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.annotation.SuppressLint
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -24,6 +26,7 @@ import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -53,6 +56,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false) _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 return binding.root
} }
@@ -236,7 +245,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (video.format == VideoType.CONTAINER) { if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text = 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 { else {
binding.urlQuality.text = "Multi Quality" binding.urlQuality.text = "Multi Quality"

View File

@@ -10,6 +10,9 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.LruCache 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.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -26,22 +29,23 @@ data class ImageData(
try { try {
// Fetch the image // Fetch the image
val response = httpSource.getImage(page) val response = httpSource.getImage(page)
println("Response: ${response.code}") logger("Response: ${response.code}")
println("Response: ${response.message}") logger("Response: ${response.message}")
// Convert the Response to an InputStream // Convert the Response to an InputStream
val inputStream = response.body?.byteStream() val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap // Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream) val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close() inputStream.close()
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100) //saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
return@withContext bitmap return@withContext bitmap
} catch (e: Exception) { } catch (e: Exception) {
// Handle any exceptions // Handle any exceptions
println("An error occurred: ${e.message}") logger("An error occurred: ${e.message}")
snackString("An error occurred: ${e.message}")
return@withContext null 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") 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 { uri?.let {
contentResolver.openOutputStream(it)?.use { os -> contentResolver.openOutputStream(it)?.use { os ->
@@ -65,7 +69,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
} }
} }
} else { } 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()) { if (!directory.exists()) {
directory.mkdirs() directory.mkdirs()
} }

View File

@@ -11,9 +11,11 @@ data class MangaChapter(
var link: String, var link: String,
var title: String? = null, var title: String? = null,
var description: String? = null, var description: String? = null,
var sChapter: SChapter var sChapter: SChapter,
val scanlator: String? = null,
var progress: String? = ""
) : Serializable { ) : 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>() private val images = mutableListOf<MangaImage>()
fun images(): List<MangaImage> = images fun images(): List<MangaImage> = images

View File

@@ -4,6 +4,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemChapterListBinding import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@@ -28,7 +29,15 @@ class MangaChapterAdapter(
false false
) )
) )
0 -> ChapterListViewHolder(ItemChapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
0 -> ChapterListViewHolder(
ItemChapterListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }
@@ -39,7 +48,8 @@ class MangaChapterAdapter(
override fun getItemCount(): Int = arr.size 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 { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) 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 { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) 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 val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val ep = arr[position] 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 (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.itemEpisodeViewedCover.visibility = View.VISIBLE
else { else {
binding.itemEpisodeViewedCover.visibility = View.GONE binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeCont.setOnLongClickListener { binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true true
} }
} }
} }
} }
is ChapterListViewHolder -> {
is ChapterListViewHolder -> {
val binding = holder.binding val binding = holder.binding
val ep = arr[position] val ep = arr[position]
holder.bind(ep.number, ep.progress)
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemChapterNumber.text = ep.number binding.itemChapterNumber.text = ep.number
if (!ep.title.isNullOrEmpty()) { if (ep.progress.isNullOrEmpty()) {
binding.itemChapterTitle.text = ep.title binding.itemChapterTitle.visibility = View.GONE
binding.itemChapterTitle.setOnLongClickListener { } else binding.itemChapterTitle.visibility = View.VISIBLE
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 (media.userProgress != null) { 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.itemEpisodeViewedCover.visibility = View.VISIBLE
binding.itemEpisodeViewed.visibility = View.VISIBLE binding.itemEpisodeViewed.visibility = View.VISIBLE
} else { } else {
binding.itemEpisodeViewedCover.visibility = View.GONE binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE binding.itemEpisodeViewed.visibility = View.GONE
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true true
} }
} }
@@ -117,4 +223,4 @@ class MangaChapterAdapter(
} }
} }

View File

@@ -1,27 +1,36 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.App.Companion.context
import ani.dantotsu.media.anime.handleProgress import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment 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.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.lang.IndexOutOfBoundsException
class MangaReadAdapter( class MangaReadAdapter(
private val media: Media, private val media: Media,
@@ -31,6 +40,9 @@ class MangaReadAdapter(
var subscribe: MediaDetailsActivity.PopImageButton? = null var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false) val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -49,10 +61,10 @@ class MangaReadAdapter(
} }
//Source Selection //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) { if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source]) binding.animeSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply { mangaReadSources[source].apply {
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
@@ -64,9 +76,36 @@ class MangaReadAdapter(
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
source = i
setLanguageList(0,i)
} }
subscribeButton(false) 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 //Subscription
@@ -98,6 +137,46 @@ class MangaReadAdapter(
binding.animeSourceTop.rotation = if (reversed) -90f else 90f binding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed) 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) { var selected = when (style) {
0 -> binding.animeSourceList 0 -> binding.animeSourceList
1 -> binding.animeSourceCompact 1 -> binding.animeSourceCompact
@@ -145,6 +224,7 @@ class MangaReadAdapter(
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
} }
chip.text = "${names[limit * (position)]} - ${names[last - 1]}" chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
chip.setOnClickListener { chip.setOnClickListener {
selected() selected()
@@ -167,6 +247,7 @@ class MangaReadAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun handleChapters() { fun handleChapters() {
val binding = _binding val binding = _binding
if (binding != null) { if (binding != null) {
if (media.manga?.chapters != null) { if (media.manga?.chapters != null) {
@@ -174,7 +255,11 @@ class MangaReadAdapter(
val anilistEp = (media.userProgress ?: 0).plus(1) val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1 val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() 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)) { if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)] continueEp = chapters[formattedChapters.indexOf(continueEp)]
binding.animeSourceContinue.visibility = View.VISIBLE 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 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) 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 package ani.dantotsu.media.manga
import android.annotation.SuppressLint 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.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.math.MathUtils.clamp
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -13,27 +26,48 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding 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.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.UserInterfaceSettings 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
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription 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.Dispatchers
import kotlinx.coroutines.launch 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.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt 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 var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels() private val model: MediaDetailsViewModel by activityViewModels()
@@ -48,6 +82,8 @@ open class MangaReadFragment : Fragment() {
private lateinit var headerAdapter: MangaReadAdapter private lateinit var headerAdapter: MangaReadAdapter
private lateinit var chapterAdapter: MangaChapterAdapter private lateinit var chapterAdapter: MangaChapterAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f var screenWidth = 0f
private var progress = View.VISIBLE private var progress = View.VISIBLE
@@ -67,10 +103,18 @@ open class MangaReadFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt() var maxGridSize = (screenWidth / 100f).roundToInt()
maxGridSize = max(4, maxGridSize - (maxGridSize % 2)) maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
@@ -116,8 +160,13 @@ open class MangaReadFragment : Fragment() {
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!) headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
headerAdapter.scanlatorSelectionListener = this
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
for (download in downloadManager.mangaDownloads){
chapterAdapter.stopDownload(download.chapter)
}
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter) binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -134,45 +183,70 @@ open class MangaReadFragment : Fragment() {
} }
} }
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters -> model.getMangaChapters().observe(viewLifecycleOwner) { _ ->
if (loadedChapters != null) { updateChapters()
val chapters = loadedChapters[media.selected!!.sourceIndex] }
if (chapters != null) { }
media.manga?.chapters = chapters
//CHIP GROUP override fun onScanlatorsSelected() {
val total = chapters.size updateChapters()
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
)
}
headerAdapter.subscribeButton(true) private fun updateChapters() {
reload() 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 { fun onSourceChange(i: Int): MangaParser {
media.manga?.chapters = null media.manga?.chapters = null
reload() reload()
@@ -185,8 +259,16 @@ open class MangaReadFragment : Fragment() {
return model.mangaReadSources?.get(i)!! return model.mangaReadSources?.get(i)!!
} }
fun loadChapters(i: Int) { fun onLangChange(i: Int) {
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) } 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) { 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) { fun onMangaChapterClick(i: String) {
model.continueMedia = false model.continueMedia = false
media.manga?.chapters?.get(i)?.let { 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") @SuppressLint("NotifyDataSetChanged")
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
@@ -262,6 +519,7 @@ open class MangaReadFragment : Fragment() {
override fun onDestroy() { override fun onDestroy() {
model.mangaReadSources?.flushText() model.mangaReadSources?.flushText()
super.onDestroy() super.onDestroy()
requireContext().unregisterReceiver(downloadStatusReceiver)
} }
private var state: Parcelable? = null private var state: Parcelable? = null
@@ -275,4 +533,12 @@ open class MangaReadFragment : Fragment() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() 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.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.net.Uri
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -29,6 +30,7 @@ import kotlinx.coroutines.withContext
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
abstract class BaseImageAdapter( abstract class BaseImageAdapter(
val activity: MangaReaderActivity, val activity: MangaReaderActivity,
@@ -116,7 +118,7 @@ abstract class BaseImageAdapter(
abstract suspend fun loadImage(position: Int, parent: View): Boolean abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object { 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 { return tryWithSuspend {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Glide.with(this@loadBitmap_old) Glide.with(this@loadBitmap_old)
@@ -151,8 +153,10 @@ abstract class BaseImageAdapter(
Glide.with(this@loadBitmap) Glide.with(this@loadBitmap)
.asBitmap() .asBitmap()
.let { .let {
if (link.url.startsWith("file://")) { val fileUri = Uri.fromFile(File(link.url)).toString()
it.load(link.url) val localFile = File(link.url)
if (localFile.exists()) {
it.load(localFile.absoluteFile)
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
} else { } else {

View File

@@ -1,7 +1,9 @@
package ani.dantotsu.media.manga.mangareader package ani.dantotsu.media.manga.mangareader
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -45,7 +47,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
loaded = true loaded = true
binding.selectorAutoText.text = chp.title binding.selectorAutoText.text = chp.title
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if(model.loadMangaChapterImages(chp, m.selected!!)) { if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL!!)) {
val activity = currActivity() val activity = currActivity()
activity?.runOnUiThread { activity?.runOnUiThread {
tryWith { dismiss() } tryWith { dismiss() }
@@ -63,6 +65,12 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false) _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 return binding.root
} }

View File

@@ -46,6 +46,7 @@ import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
import ani.dantotsu.settings.ReaderSettings import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.alexvasilkov.gestures.views.GestureFrameLayout import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@@ -126,7 +127,8 @@ class MangaReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityMangaReaderBinding.inflate(layoutInflater) binding = ActivityMangaReaderBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -215,6 +217,10 @@ class MangaReaderActivity : AppCompatActivity() {
logError(e) 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.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex]
binding.mangaReaderTitle.text = media.userPreferredName 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() private val snapHelper = PagerSnapHelper()
@@ -700,6 +706,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages( model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!, chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!, media.selected!!,
media.nameMAL!!,
false false
) )
loading = false loading = false

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.OtherDetailsViewModel import ani.dantotsu.media.OtherDetailsViewModel
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class ListFragment : Fragment() { class ListFragment : Fragment() {
private var _binding: FragmentListBinding? = null 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.net.Uri
import android.os.Environment import android.os.Environment
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@@ -95,8 +96,9 @@ object AppUpdater {
private fun compareVersion(version: String): Boolean { private fun compareVersion(version: String): Boolean {
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG) {
return BuildConfig.VERSION_NAME != version return BuildConfig.VERSION_NAME != version
}
else { else {
fun toDouble(list: List<String>): Double { fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String -> return list.mapIndexed { i: Int, s: String ->
@@ -141,7 +143,8 @@ object AppUpdater {
-1 -1
} }
if (id == -1L) return true if (id == -1L) return true
registerReceiver( ContextCompat.registerReceiver(
this,
object : BroadcastReceiver() { object : BroadcastReceiver() {
@SuppressLint("Range") @SuppressLint("Range")
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
@@ -170,7 +173,8 @@ object AppUpdater {
logError(e) logError(e)
} }
} }
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) }, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_NOT_EXPORTED
) )
return true return true
} }

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.others package ani.dantotsu.others
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -46,6 +48,12 @@ open class CustomBottomDialog : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetCustomBinding.inflate(inflater, container, false) _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 return binding.root
} }

View File

@@ -20,6 +20,7 @@ import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.shareImage import ani.dantotsu.shareImage
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import ani.dantotsu.toast import ani.dantotsu.toast
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
@@ -74,15 +75,16 @@ class ImageViewDialog : BottomSheetDialogFragment() {
if (image2 != null) openLinkInBrowser(image2.url) if (image2 != null) openLinkInBrowser(image2.url)
true true
} }
val context = requireContext()
lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
val binding = _binding ?: return@launch val binding = _binding ?: return@launch
var bitmap = requireContext().loadBitmap_old(image, trans1 ?: listOf()) var bitmap = context.loadBitmap_old(image, trans1 ?: listOf())
var bitmap2 = if (image2 != null) requireContext().loadBitmap_old(image2, trans2 ?: listOf()) else null var bitmap2 = if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null
if (bitmap == null) { if (bitmap == null) {
bitmap = requireContext().loadBitmap(image, trans1 ?: listOf()) bitmap = context.loadBitmap(image, trans1 ?: listOf())
bitmap2 = if (image2 != null) requireContext().loadBitmap(image2, trans2 ?: listOf()) else null bitmap2 = if (image2 != null) context.loadBitmap(image2, trans2 ?: listOf()) else null
} }
bitmap = if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2,) else bitmap 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.databinding.ActivityImageSearchBinding
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import ani.dantotsu.toast import ani.dantotsu.toast
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -47,7 +48,8 @@ class ImageSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityImageSearchBinding.inflate(layoutInflater) binding = ActivityImageSearchBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.parsers
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
@@ -10,14 +11,20 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.ServiceDataSingleton
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.manga.ImageData import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache 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.SEpisode
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
@@ -62,22 +69,54 @@ class AniyomiAdapter {
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val extension: AnimeExtension.Installed val extension: AnimeExtension.Installed
var sourceLanguage = 0
init { init {
this.extension = extension this.extension = extension
} }
override val name = extension.name override val name = extension.name
override val saveName = extension.name override val saveName = extension.name
override val hostUrl = extension.sources.first().name override val hostUrl = extension.sources.first().name
override val isDubAvailableSeparately = false override val isDubAvailableSeparately = false
override val isNSFW = extension.isNsfw override val isNSFW = extension.isNsfw
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> { override suspend fun loadEpisodes(
val source = extension.sources.first() 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) { if (source is AnimeCatalogueSource) {
try { try {
val res = source.getEpisodeList(sAnime) val res = source.getEpisodeList(sAnime)
// Sort episodes by episode_number val sortedEpisodes = if (res[0].episode_number == -1f) {
val sortedEpisodes = res.sortedBy { it.episode_number } // 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 // 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 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> { override suspend fun loadVideoServers(
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList() 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 { return try {
val videos = source.getVideoList(sEpisode) 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) return VideoServerPassthrough(server)
} }
override suspend fun search(query: String): List<ShowResponse> { 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 { return try {
val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first() val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
convertAnimesPageToShowResponse(res) convertAnimesPageToShowResponse(res)
} catch (e: CloudflareBypassException) { } catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
withContext(Dispatchers.Main) { 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() emptyList()
} catch (e: Exception) { } catch (e: Exception) {
@@ -150,7 +203,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
sEpisode.episode_number sEpisode.episode_number
} }
return Episode( return Episode(
episodeNumberInt.toString(), if (episodeNumberInt.toInt() != -1) {
episodeNumberInt.toString()
} else {
sEpisode.name
},
sEpisode.url, sEpisode.url,
sEpisode.name, sEpisode.name,
null, null,
@@ -174,16 +231,28 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val mangaCache = Injekt.get<MangaCache>() val mangaCache = Injekt.get<MangaCache>()
val extension: MangaExtension.Installed val extension: MangaExtension.Installed
var sourceLanguage = 0
init { init {
this.extension = extension this.extension = extension
} }
override val name = extension.name override val name = extension.name
override val saveName = extension.name override val saveName = extension.name
override val hostUrl = extension.sources.first().name override val hostUrl = extension.sources.first().name
override val isNSFW = extension.isNsfw override val isNSFW = extension.isNsfw
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> { override suspend fun loadChapters(
val source = extension.sources.first() as? CatalogueSource ?: return emptyList() 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 { return try {
val res = source.getChapterList(sManga) 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> { override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val source = extension.sources.first() as? HttpSource ?: return emptyList() val source = try {
extension.sources[sourceLanguage]
return coroutineScope { } catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
var imageDataList: List<ImageData> = listOf()
val ret = coroutineScope {
try { try {
println("source.name " + source.name) println("source.name " + source.name)
val res = source.getPageList(sChapter) 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 -> val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) { async(Dispatchers.IO) {
mangaCache.put(page.imageUrl ?: "", ImageData(page, source)) mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
imageDataList += ImageData(page, source)
logger("put page: ${page.imageUrl}") logger("put page: ${page.imageUrl}")
pageToMangaImage(page) pageToMangaImage(page)
} }
} }
deferreds.awaitAll() deferreds.awaitAll()
} catch (e: Exception) { } catch (e: Exception) {
logger("loadImages Exception: $e") 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() 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) { return withContext(Dispatchers.IO) {
try { try {
// Fetch the image // Fetch the image
@@ -259,7 +375,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) { fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
@@ -274,7 +389,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// Save the Bitmap using MediaStore API // 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() 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 { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply { val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename) put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}") 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 { uri?.let {
contentResolver.openOutputStream(it)?.use { os -> contentResolver.openOutputStream(it)?.use { os ->
@@ -302,7 +435,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
} }
} else { } 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()) { if (!directory.exists()) {
directory.mkdirs() directory.mkdirs()
} }
@@ -319,9 +453,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
override suspend fun search(query: String): List<ShowResponse> { 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 { return try {
val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first() val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
@@ -330,7 +468,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} catch (e: CloudflareBypassException) { } catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
withContext(Dispatchers.Main) { 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() emptyList()
} catch (e: Exception) { } catch (e: Exception) {
@@ -390,30 +529,21 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter { 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( return MangaChapter(
sChapter.name, sChapter.name,
sChapter.url, sChapter.url,
//if (parsedChapterTitle.first != null || parsedChapterTitle.second != null) { sChapter.name,
// parsedChapterTitle.third
//} else {
sChapter.name,
//},
null, null,
sChapter.scanlator,
sChapter sChapter
) )
} }
fun parseChapterTitle(title: String): Triple<String?, String?, String> { fun parseChapterTitle(title: String): Triple<String?, String?, String> {
val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE) val volumePattern =
val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE) 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 volumeMatcher = volumePattern.matcher(title)
val chapterMatcher = chapterPattern.matcher(title) val chapterMatcher = chapterPattern.matcher(title)
@@ -423,10 +553,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
var remainingTitle = title var remainingTitle = title
if (volumeNumber != null) { if (volumeNumber != null) {
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString() remainingTitle =
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
} }
if (chapterNumber != null) { 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()) 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 // Find the number value from the .quality string
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0 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 the format is still undetermined, log an error or handle it appropriately
if (format == null) { if (format == null) {
logger("Unknown video format: $videoUrl") 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( return ani.dantotsu.parsers.Video(
@@ -494,7 +628,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
private fun getVideoType(fileName: String): VideoType? { private fun getVideoType(fileName: String): VideoType? {
return when { 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(".m3u8", ignoreCase = true) -> VideoType.M3U8
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> null else -> null
@@ -507,12 +645,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
runBlocking { runBlocking {
type = findSubtitleType(track.url) 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? { private fun findSubtitleType(url: String): SubtitleType? {
// First, try to determine the type based on the URL file extension // 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(".vtt", true) -> SubtitleType.VTT
url.endsWith(".ass", true) -> SubtitleType.ASS url.endsWith(".ass", true) -> SubtitleType.ASS
url.endsWith(".srt", true) -> SubtitleType.SRT 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 * Isn't necessary to override, but recommended, if you want to improve auto search results
* **/ * **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? { open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id) var response: ShowResponse? = null//loadSavedShowResponse(mediaObj.id)
if (response != null) { if (response != null && this !is OfflineMangaParser) {
saveShowResponse(mediaObj.id, response, true) saveShowResponse(mediaObj.id, response, true)
} else { } else {
setUserText("Searching : ${mediaObj.mainName()}") setUserText("Searching : ${mediaObj.mainName()}")

View File

@@ -7,6 +7,7 @@ import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
abstract class WatchSources : BaseSources() { abstract class WatchSources : BaseSources() {
@@ -23,14 +24,28 @@ abstract class WatchSources : BaseSources() {
} ?: mutableMapOf() } ?: 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") println("finder333 $showLink")
val map = mutableMapOf<String, Episode>() val map = mutableMapOf<String, Episode>()
val parser = get(i) val parser = get(i)
tryWithSuspend(true) { tryWithSuspend(true) {
if (sAnime != null) { if (sAnime != null) {
parser.loadEpisodes(showLink,extra, sAnime).forEach { 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) 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() { abstract class MangaReadSources : BaseSources() {
override operator fun get(i: Int): MangaParser { 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() ?: EmptyMangaParser()
} }
@@ -56,6 +71,7 @@ abstract class MangaReadSources : BaseSources() {
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> { suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
val map = mutableMapOf<String, MangaChapter>() val map = mutableMapOf<String, MangaChapter>()
val parser = get(i) val parser = get(i)
show.sManga?.let { sManga -> show.sManga?.let { sManga ->
tryWithSuspend(true) { tryWithSuspend(true) {
parser.loadChapters(show.link, show.extra, sManga).forEach { 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") 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}") logger("map size ${map.size}")
return map return map
} }
} }
abstract class NovelReadSources : BaseSources(){ abstract class NovelReadSources : BaseSources() {
override operator fun get(i: Int): NovelParser? { override operator fun get(i: Int): NovelParser? {
return if (list.isNotEmpty()) { return if (list.isNotEmpty()) {
(list.getOrNull(i) ?: list[0]).get.value as NovelParser (list.getOrNull(i) ?: list[0]).get.value as NovelParser
@@ -87,7 +116,7 @@ class EmptyNovelParser : NovelParser() {
override val volumeRegex: Regex = Regex("") override val volumeRegex: Regex = Regex("")
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book { 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> { override suspend fun search(query: String): List<ShowResponse> {

View File

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

View File

@@ -7,16 +7,19 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() { object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList() override var list: List<Lazier<BaseParser>> = emptyList()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) { suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
// Initialize with the first value from StateFlow // Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first() val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) list = createParsersFromExtensions(initialExtensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
// Update as StateFlow emits new values // Update as StateFlow emits new values
fromExtensions.collect { extensions -> 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 { ): View {
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false) _binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = true binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = false
binding.allAnimeExtensionsRecyclerView.adapter = adapter binding.allAnimeExtensionsRecyclerView.adapter = adapter
binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context) binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false (binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true
lifecycleScope.launch { lifecycleScope.launch {
viewModel.pagerFlow.collectLatest { viewModel.pagerFlow.collectLatest {
@@ -60,6 +60,8 @@ class AnimeExtensionsFragment : Fragment(),
} }
} }
viewModel.invalidatePager() // Force a refresh of the pager
return binding.root return binding.root
} }

View File

@@ -13,26 +13,9 @@ class DevelopersDialogFragment : BottomSheetDialogFragment() {
private val binding get() = _binding!! private val binding get() = _binding!!
private val developers = arrayOf( private val developers = arrayOf(
Developer("vorobyovgabriel","https://avatars.githubusercontent.com/u/99561687?s=120&v=4","Owner","https://github.com/vorobyovgabriel"), Developer("rebelonion","https://avatars.githubusercontent.com/u/87634197?v=4","Owner and Maintainer","https://github.com/rebelonion"),
Developer("brahmkshtriya","https://avatars.githubusercontent.com/u/69040506?s=120&v=4","Maintainer","https://github.com/brahmkshatriya"), Developer("Wai What", "https://avatars.githubusercontent.com/u/149729762?v=4", "Icon Designer", "https://github.com/WaiWhat"),
Developer("jeelpatel231","https://avatars.githubusercontent.com/u/33726155?s=120&v=4","Contributor","https://github.com/jeelpatel231"), Developer("Aayush262", "https://avatars.githubusercontent.com/u/99584765?v=4", "Contributor", "https://github.com/aayush2622"),
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")
) )
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {

View File

@@ -1,55 +1,32 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.annotation.SuppressLint 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.*
import android.os.Build.VERSION.* import android.os.Build.VERSION.*
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.text.Editable
import android.text.TextWatcher
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.AutoCompleteTextView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.SearchView import android.widget.SearchView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.appcompat.widget.PopupMenu
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment 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.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* 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.databinding.ActivityExtensionsBinding
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.MangaFragment
import ani.dantotsu.themes.ThemeManager 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.TabLayout
import com.google.android.material.tabs.TabLayoutMediator 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.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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() { class ExtensionsActivity : AppCompatActivity() {
private val restartMainActivity = object : OnBackPressedCallback(false) { private val restartMainActivity = object : OnBackPressedCallback(false) {
@@ -61,7 +38,8 @@ class ExtensionsActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityExtensionsBinding.inflate(layoutInflater) binding = ActivityExtensionsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -94,21 +72,20 @@ class ExtensionsActivity : AppCompatActivity() {
}.attach() }.attach()
val searchView: SearchView = findViewById(R.id.searchView) val searchView: AutoCompleteTextView = findViewById(R.id.searchViewText)
val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader) searchView.addTextChangedListener(object : TextWatcher {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun afterTextChanged(s: Editable?) {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
} }
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}") val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
if (currentFragment is SearchQueryHandler) { if (currentFragment is SearchQueryHandler) {
currentFragment.updateContentBasedOnQuery(newText) currentFragment.updateContentBasedOnQuery(s?.toString()?.trim())
} }
return true
} }
}) })
@@ -116,18 +93,18 @@ class ExtensionsActivity : AppCompatActivity() {
initActivity(this) initActivity(this)
binding.languageselect.setOnClickListener {
val popup = PopupMenu(this, it)
popup.inflate(R.menu.launguage_selector_menu)
popup.show()
}
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight
bottomMargin = navBarHeight 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.databinding.ActivityFaqBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class FAQActivity : AppCompatActivity() { class FAQActivity : AppCompatActivity() {
private lateinit var binding: ActivityFaqBinding private lateinit var binding: ActivityFaqBinding
@@ -105,7 +106,8 @@ class FAQActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityFaqBinding.inflate(layoutInflater) binding = ActivityFaqBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.app.AlertDialog
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
@@ -7,8 +8,10 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -17,10 +20,15 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
import ani.dantotsu.loadData 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 com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@@ -30,62 +38,129 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class InstalledAnimeExtensionsFragment : Fragment() { class InstalledAnimeExtensionsFragment : Fragment() {
private var _binding: FragmentAnimeExtensionsBinding? = null private var _binding: FragmentAnimeExtensionsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false val skipIcons = loadData("skip_extension_icons") ?: false
private val animeExtensionManager: AnimeExtensionManager = Injekt.get() private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
private val extensionsAdapter = AnimeExtensionsAdapter({ pkg -> private val extensionsAdapter = AnimeExtensionsAdapter({ pkg ->
if (isAdded) { // Check if the fragment is currently added to its activity val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
val context = requireContext() // Store context in a variable if (allSettings.isNotEmpty()) {
val notificationManager = var selectedSetting = allSettings[0]
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once 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) { // Move the fragment transaction here
animeExtensionManager.updateExtension(pkg) val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread val activity = requireActivity() as ExtensionsActivity
.subscribe( activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.VISIBLE
{ installStep -> activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.VISIBLE
val builder = NotificationCompat.Builder( activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.VISIBLE
context, activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
Notifications.CHANNEL_DOWNLOADER_PROGRESS View.GONE
)
.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())
} }
) 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 { } 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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -114,6 +189,7 @@ class InstalledAnimeExtensionsFragment : Fragment() {
private class AnimeExtensionsAdapter( private class AnimeExtensionsAdapter(
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit, private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
skipIcons: Boolean skipIcons: Boolean
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>( ) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
@@ -133,31 +209,37 @@ class InstalledAnimeExtensionsFragment : Fragment() {
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { 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.extensionNameTextView.text = extension.name
holder.extensionVersionTextView.text = "${extension.versionName} $nsfw"
if (!skipIcons) { if (!skipIcons) {
holder.extensionIconImageView.setImageDrawable(extension.icon) holder.extensionIconImageView.setImageDrawable(extension.icon)
} }
if (extension.hasUpdate) { if (extension.hasUpdate) {
holder.closeTextView.text = "Update" holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
holder.closeTextView.setTextColor(
ContextCompat.getColor(
holder.itemView.context,
R.color.warning
)
)
} else { } else {
holder.closeTextView.text = "Uninstall" holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
} }
holder.closeTextView.setOnClickListener { holder.closeTextView.setOnClickListener {
onUninstallClicked(extension) onUninstallClicked(extension)
} }
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
} }
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) 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 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 { companion object {
@@ -180,5 +262,4 @@ class InstalledAnimeExtensionsFragment : Fragment() {
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.app.AlertDialog
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
@@ -8,8 +9,10 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -18,13 +21,18 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
import ani.dantotsu.loadData 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 com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -37,6 +45,66 @@ class InstalledMangaExtensionsFragment : Fragment() {
val skipIcons = loadData("skip_extension_icons") ?: false val skipIcons = loadData("skip_extension_icons") ?: false
private val mangaExtensionManager: MangaExtensionManager = Injekt.get() private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
private val extensionsAdapter = MangaExtensionsAdapter({ pkg -> 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 if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable val context = requireContext() // Store context in a variable
val notificationManager = val notificationManager =
@@ -115,6 +183,7 @@ class InstalledMangaExtensionsFragment : Fragment() {
private class MangaExtensionsAdapter( private class MangaExtensionsAdapter(
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
private val onUninstallClicked: (MangaExtension.Installed) -> Unit, private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
skipIcons: Boolean skipIcons: Boolean
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>( ) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
@@ -135,30 +204,35 @@ class InstalledMangaExtensionsFragment : Fragment() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) { 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.extensionNameTextView.text = extension.name
holder.extensionVersionTextView.text = "${extension.versionName} $nsfw"
if (!skipIcons) { if (!skipIcons) {
holder.extensionIconImageView.setImageDrawable(extension.icon) holder.extensionIconImageView.setImageDrawable(extension.icon)
} }
if (extension.hasUpdate) { if (extension.hasUpdate) {
holder.closeTextView.text = "Update" holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
holder.closeTextView.setTextColor(
ContextCompat.getColor(
holder.itemView.context,
R.color.warning
)
)
} else { } else {
holder.closeTextView.text = "Uninstall" holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
} }
holder.closeTextView.setOnClickListener { holder.closeTextView.setOnClickListener {
onUninstallClicked(extension) onUninstallClicked(extension)
} }
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
} }
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) 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 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 { companion object {

View File

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

View File

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

View File

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

View File

@@ -13,13 +13,15 @@ import ani.dantotsu.saveData
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class ReaderSettingsActivity : AppCompatActivity() { class ReaderSettingsActivity : AppCompatActivity() {
lateinit var binding: ActivityReaderSettingsBinding lateinit var binding: ActivityReaderSettingsBinding
private val reader = "reader_settings" private val reader = "reader_settings"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityReaderSettingsBinding.inflate(layoutInflater) binding = ActivityReaderSettingsBinding.inflate(layoutInflater)
setContentView(binding.root) 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.startSubscription
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
@@ -53,11 +54,13 @@ class SettingsActivity : AppCompatActivity() {
lateinit var binding: ActivitySettingsBinding lateinit var binding: ActivitySettingsBinding
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller() private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
private val networkPreferences = Injekt.get<NetworkPreferences>() private val networkPreferences = Injekt.get<NetworkPreferences>()
private var cursedCounter = 0
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -105,6 +108,12 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
restartApp() 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")!! val themeString = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!
binding.themeSwitcher.setText(themeString.substring(0, 1) + themeString.substring(1).lowercase()) 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 -> binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked ->
saveData("skip_extension_icons", 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.setText(networkPreferences.defaultUserAgent().get())
binding.userAgent.setOnEditorActionListener { _, _, _ -> binding.userAgent.setOnEditorActionListener { _, _, _ ->
@@ -168,7 +182,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
true 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.setText(exDns[networkPreferences.dohProvider().get()], false)
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns)) binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ -> binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
@@ -383,8 +397,15 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
val array = resources.getStringArray(R.array.tips) val array = resources.getStringArray(R.array.tips)
binding.settingsLogo.setSafeOnClickListener { binding.settingsLogo.setSafeOnClickListener {
cursedCounter++
(binding.settingsLogo.drawable as Animatable).start() (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 { 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 timeNames = timeMinutes.map {
val mins = it % 60 val mins = it % 60
val hours = it / 60 val hours = it / 60
@@ -421,7 +442,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i -> speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i ->
curTime = i curTime = i
binding.settingsSubscriptionsTime.text = getString(R.string.subscriptions_checking_time_s, timeNames[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() dialog.dismiss()
startSubscription(true) startSubscription(true)
}.show() }.show()

View File

@@ -3,19 +3,24 @@ package ani.dantotsu.settings
import android.app.DownloadManager import android.app.DownloadManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Switch
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.databinding.BottomSheetSettingsBinding 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 var _binding: BottomSheetSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@@ -26,6 +31,12 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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) { if (Anilist.token != null) {
binding.settingsLogin.setText(R.string.logout) binding.settingsLogin.setText(R.string.logout)
@@ -62,18 +73,42 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
dismiss() dismiss()
} }
binding.settingsDownloads.setSafeOnClickListener { binding.settingsDownloads.setSafeOnClickListener {
try { when(pageType) {
val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null) PageType.MANGA -> {
startActivity( val intent = Intent(activity, DownloadContainerActivity::class.java)
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) { intent.putExtra("FRAGMENT_CLASS_NAME", OfflineMangaFragment::class.java.name)
val parentDirectory = arrayOfFiles[1].toString() startActivity(intent)
val intent = Intent(Intent.ACTION_VIEW) }
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder") PageType.ANIME -> {
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) try {
) val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null)
} catch (e: ActivityNotFoundException) { startActivity(
toast(getString(R.string.file_manager_not_found)) 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() dismiss()
} }
} }
@@ -82,4 +117,10 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
super.onDestroyView() super.onDestroyView()
_binding = null _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.*
import ani.dantotsu.databinding.ActivityUserInterfaceSettingsBinding import ani.dantotsu.databinding.ActivityUserInterfaceSettingsBinding
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
class UserInterfaceSettingsActivity : AppCompatActivity() { class UserInterfaceSettingsActivity : AppCompatActivity() {
@@ -16,7 +17,8 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
private val ui = "ui_settings" private val ui = "ui_settings"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityUserInterfaceSettingsBinding.inflate(layoutInflater) binding = ActivityUserInterfaceSettingsBinding.inflate(layoutInflater)
setContentView(binding.root) 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.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.databinding.ItemExtensionAllBinding import ani.dantotsu.databinding.ItemExtensionAllBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -77,21 +78,26 @@ class AnimeExtensionPagingSource(
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet() val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions } val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
val query = searchQuery.first() val query = searchQuery.first()
var isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true
val filteredExtensions = if (query.isEmpty()) { val filteredExtensions = if (query.isEmpty()) {
availableExtensions availableExtensions
} else { } else {
availableExtensions.filter { it.name.contains(query, ignoreCase = true) } availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
} }
val filternfsw = if(isNsfwEnabled) {
filteredExtensions
} else {
filteredExtensions.filterNot { it.isNsfw }
}
return try { return try {
val sublist = filteredExtensions.subList( val sublist = filternfsw.subList(
fromIndex = position, fromIndex = position,
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size) toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
) )
LoadResult.Page( LoadResult.Page(
data = sublist, data = sublist,
prevKey = if (position == 0) null else position - params.loadSize, 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) { } catch (e: Exception) {
LoadResult.Error(e) LoadResult.Error(e)
@@ -151,8 +157,14 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
} }
} }
val extensionIconImageView: ImageView = binding.extensionIconImageView 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.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.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.databinding.ItemExtensionAllBinding import ani.dantotsu.databinding.ItemExtensionAllBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -81,21 +82,26 @@ class MangaExtensionPagingSource(
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet() val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions } val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
val query = searchQuery.first() val query = searchQuery.first()
var isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: false
val filteredExtensions = if (query.isEmpty()) { val filteredExtensions = if (query.isEmpty()) {
availableExtensions availableExtensions
} else { } else {
availableExtensions.filter { it.name.contains(query, ignoreCase = true) } availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
} }
val filternfsw = if(isNsfwEnabled) {
filteredExtensions
} else {
filteredExtensions.filterNot { it.isNsfw }
}
return try { return try {
val sublist = filteredExtensions.subList( val sublist = filternfsw.subList(
fromIndex = position, fromIndex = position,
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size) toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
) )
LoadResult.Page( LoadResult.Page(
data = sublist, data = sublist,
prevKey = if (position == 0) null else position - params.loadSize, 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) { } catch (e: Exception) {
LoadResult.Error(e) LoadResult.Error(e)
@@ -117,12 +123,10 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
companion object { companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MangaExtension.Available>() { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MangaExtension.Available>() {
override fun areItemsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean { override fun areItemsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
// Your logic here
return oldItem.pkgName == newItem.pkgName return oldItem.pkgName == newItem.pkgName
} }
override fun areContentsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean { override fun areContentsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
// Your logic here
return oldItem == newItem return oldItem == newItem
} }
} }
@@ -156,7 +160,13 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
} }
val extensionIconImageView: ImageView = binding.extensionIconImageView val extensionIconImageView: ImageView = binding.extensionIconImageView
fun bind(extension: MangaExtension.Available) { fun bind(extension: MangaExtension.Available) {
val nsfw = if (extension.isNsfw) {
"(18+)"
} else {
""
}
binding.extensionNameTextView.text = extension.name 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 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 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) if (timeMinutes[curTime] > 0)
alarmManager.setRepeating( alarmManager.setRepeating(

View File

@@ -16,8 +16,8 @@ import kotlinx.coroutines.launch
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class Subscription { class Subscription {
companion object { companion object {
const val defaultTime = 3 const val defaultTime = 1
val timeMinutes = arrayOf(0L, 120, 180, 240, 360, 480, 720, 1440) val timeMinutes = arrayOf(0L, 720, 1440)
private var alreadyStarted = false private var alreadyStarted = false
fun Context.startSubscription(force: Boolean = 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" private const val SUBSCRIPTION_WORK_NAME = "work_subscription"
fun enqueue(context: Context) { fun enqueue(context: Context) {
val curTime = loadData<Int>("subscriptions_time_r") ?: defaultTime val curTime = loadData<Int>("subscriptions_time_s") ?: defaultTime
if(timeMinutes[curTime]>0L) { if(timeMinutes[curTime]>0L) {
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val periodicSyncDataWork = PeriodicWorkRequest.Builder( val periodicSyncDataWork = PeriodicWorkRequest.Builder(

View File

@@ -1,41 +1,42 @@
package ani.dantotsu.themes package ani.dantotsu.themes
import android.content.Context import android.content.Context
import android.content.res.Configuration
import ani.dantotsu.R import ani.dantotsu.R
class ThemeManager(private val context: Context) { class ThemeManager(private val context: Context) {
fun applyTheme() { 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)){ if(context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false)){
return return
} }
when (context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!) { val theme = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!
"PURPLE" -> {
context.setTheme(R.style.Theme_Dantotsu_Purple) val themeToApply = when (theme) {
} "PURPLE" -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple
"BLUE" -> { "BLUE" -> if (useOLED) R.style.Theme_Dantotsu_BlueOLED else R.style.Theme_Dantotsu_Blue
context.setTheme(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
"GREEN" -> { "RED" -> if (useOLED) R.style.Theme_Dantotsu_RedOLED else R.style.Theme_Dantotsu_Red
context.setTheme(R.style.Theme_Dantotsu_Green) "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
"PINK" -> { "SAIKOU" -> if (useOLED) R.style.Theme_Dantotsu_SaikouOLED else R.style.Theme_Dantotsu_Saikou
context.setTheme(R.style.Theme_Dantotsu_Pink) else -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple
} }
"RED" -> {
context.setTheme(R.style.Theme_Dantotsu_Red) context.setTheme(themeToApply)
} }
"LAVENDER" -> {
context.setTheme(R.style.Theme_Dantotsu_Lavender) private fun isDarkThemeActive(context: Context): Boolean {
} return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
"MONOCHROME (BETA)" -> { Configuration.UI_MODE_NIGHT_YES -> true
context.setTheme(R.style.Theme_Dantotsu_Monochrome) Configuration.UI_MODE_NIGHT_NO -> false
} Configuration.UI_MODE_NIGHT_UNDEFINED -> false
else -> { else -> false
context.setTheme(R.style.Theme_Dantotsu_Purple)
}
} }
} }
companion object{ companion object{
enum class Theme(val theme: String) { enum class Theme(val theme: String) {
PURPLE("PURPLE"), PURPLE("PURPLE"),
@@ -44,7 +45,8 @@ class ThemeManager(private val context: Context) {
PINK("PINK"), PINK("PINK"),
RED("RED"), RED("RED"),
LAVENDER("LAVENDER"), LAVENDER("LAVENDER"),
MONOCHROME("MONOCHROME (BETA)"); MONOCHROME("MONOCHROME (BETA)"),
SAIKOU("SAIKOU");
companion object { companion object {
fun fromString(value: String): Theme { 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.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.util.lang.use import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
@@ -100,7 +101,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
} }
init { 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.content.Intent
import android.os.Bundle import android.os.Bundle
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
@@ -25,7 +26,8 @@ class AnimeExtensionInstallActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE) val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type) .setDataAndType(intent.data, intent.type)

View File

@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
@@ -27,7 +28,7 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
* Registers this broadcast receiver * Registers this broadcast receiver
*/ */
fun register(context: Context) { 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 isRegistered = true
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) 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.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.util.lang.use import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
@@ -100,7 +101,7 @@ class PackageInstallerInstallerManga(private val service: Service) : InstallerMa
} }
init { 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.content.Intent
import android.os.Bundle import android.os.Bundle
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
@@ -25,7 +26,8 @@ class MangaExtensionInstallActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE) val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type) .setDataAndType(intent.data, intent.type)

View File

@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
@@ -27,7 +28,7 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
* Registers this broadcast receiver * Registers this broadcast receiver
*/ */
fun register(context: Context) { 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 isRegistered = true
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) 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_CONTROLD = 10
const val PREF_DOH_NJALLA = 11 const val PREF_DOH_NJALLA = 11
const val PREF_DOH_SHECAN = 12 const val PREF_DOH_SHECAN = 12
const val PREF_DOH_LIBREDNS = 13
fun OkHttpClient.Builder.dohCloudflare() = dns( fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build()) DnsOverHttps.Builder().client(build())
@@ -184,3 +185,13 @@ fun OkHttpClient.Builder.dohShecan() = dns(
) )
.build(), .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_CONTROLD -> builder.dohControlD()
PREF_DOH_NJALLA -> builder.dohNajalla() PREF_DOH_NJALLA -> builder.dohNajalla()
PREF_DOH_SHECAN -> builder.dohShecan() PREF_DOH_SHECAN -> builder.dohShecan()
PREF_DOH_LIBREDNS -> builder.dohLibreDNS()
} }
return builder 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