mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-12 16:06:16 +00:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8797af0cbc | ||
|
|
97a4cba680 | ||
|
|
acc5069c83 | ||
|
|
fab978dba4 | ||
|
|
ad1734d640 | ||
|
|
d687911c85 | ||
|
|
1d4257b1b3 | ||
|
|
55521ab9fc | ||
|
|
17e53a54af | ||
|
|
dc1edc9a42 | ||
|
|
b8782b0507 | ||
|
|
0d422a57e7 | ||
|
|
1bbc98d350 | ||
|
|
7ae6831628 | ||
|
|
65e89398d9 | ||
|
|
2b77b7578c | ||
|
|
e77ab2800a | ||
|
|
c1a0eeb361 | ||
|
|
393ab1e513 | ||
|
|
e26a6c647f | ||
|
|
ea83b722a6 | ||
|
|
34a3e9e5a3 | ||
|
|
7f92ac686d | ||
|
|
b6c79dae40 | ||
|
|
8c957007ab | ||
|
|
c728eae2ba | ||
|
|
3ded6ba87a | ||
|
|
111fb16266 | ||
|
|
121be4bc6f | ||
|
|
afa960c808 | ||
|
|
1df528c0dc | ||
|
|
f792296f78 | ||
|
|
d512929387 | ||
|
|
c7bc1ffe9e | ||
|
|
32f918450a | ||
|
|
f01377f0b1 | ||
|
|
c2a07278fc | ||
|
|
0a17bed243 | ||
|
|
c5ed8acfa3 | ||
|
|
e5f2bb6566 | ||
|
|
b4093b0c47 | ||
|
|
d0fd62abf2 | ||
|
|
4d0c3e5849 | ||
|
|
d131562f34 | ||
|
|
cf2d9ad654 | ||
|
|
af326c8258 | ||
|
|
ba351df331 | ||
|
|
d4c2df37ae | ||
|
|
79d1c44e63 | ||
|
|
38faedb4b5 | ||
|
|
39b0f28127 | ||
|
|
a1913ed968 | ||
|
|
f7917df907 | ||
|
|
84c58fbe6c | ||
|
|
75895d851f | ||
|
|
6d05a42168 | ||
|
|
594fa4daa9 | ||
|
|
8d9254140d | ||
|
|
533aa9f56e | ||
|
|
c310bea0e9 | ||
|
|
813f7a0992 | ||
|
|
4c0f56d3e3 | ||
|
|
1f44d32f35 | ||
|
|
d937f447ef | ||
|
|
187262a266 | ||
|
|
f40ebc9d09 | ||
|
|
3998d88297 | ||
|
|
4db301ca7a | ||
|
|
3dfcc9fc31 | ||
|
|
df63586c02 | ||
|
|
d7372d4dbb | ||
|
|
f4266d0da3 | ||
|
|
736b06bdbe | ||
|
|
5543d29317 | ||
|
|
2fc351f57a | ||
|
|
eee1242964 | ||
|
|
a58e9a523a | ||
|
|
cd3aad1c33 | ||
|
|
5a482d8307 | ||
|
|
91d869005c | ||
|
|
05e73269d3 | ||
|
|
3dac48ced8 | ||
|
|
8c5726ab8a | ||
|
|
076516be23 | ||
|
|
1059a3c17e | ||
|
|
c75df942f2 | ||
|
|
cfe7be5cdb | ||
|
|
390fc18c4c | ||
|
|
4c82c56828 | ||
|
|
da5c480ba7 | ||
|
|
20acd71b1a | ||
|
|
231c9c5b98 | ||
|
|
4cfdcdb23c | ||
|
|
acb0225699 | ||
|
|
ebffaaa742 | ||
|
|
1760064555 | ||
|
|
44a6db3fc2 | ||
|
|
aab25d157e | ||
|
|
8a4be86ddc | ||
|
|
31baf729be | ||
|
|
b98e3dc780 | ||
|
|
878d58679e | ||
|
|
f500ba6cf0 | ||
|
|
d124736556 | ||
|
|
1a825e2509 | ||
|
|
6e14c2221d | ||
|
|
5b6e351a56 | ||
|
|
c310708401 | ||
|
|
f0093b903a | ||
|
|
d33568f0ad | ||
|
|
26f9f40042 | ||
|
|
7545870f38 | ||
|
|
3368a1bc8d | ||
|
|
9c0ef7a788 | ||
|
|
960c2b4113 | ||
|
|
1eb85d4419 | ||
|
|
20bea76e6c | ||
|
|
866bd3b3a9 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [rebelonion]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: ['https://www.buymeacoffee.com/rebelonion']
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,3 +28,6 @@ google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
#other
|
||||
scripts/
|
||||
|
||||
113
README.md
113
README.md
@@ -1,109 +1,36 @@
|
||||
# **Dantotsu** (🚧 ALPHA 🚧)
|
||||
|
||||
> ⚠️ **WARNING**: This project is in alpha stage. Things may not work as expected.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://pbxt.replicate.delivery/2PX94viD6lJSDVayQrGyDH7CGu7IjQ6e8HEtOGDeelefXRdOC/out.png" alt="Dantotsu Banner" width=100% >
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
|
||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
||||
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
|
||||
</p>
|
||||
Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an <a href="https://anilist.co/">Anilist</a> only client, which also lets you stream-download Anime & Manga through extensions.
|
||||
<br><br>
|
||||
<i>Dantotsu (断トツ; Dan-totsu) literally means the best of the best in Japanese. Well, we would like to say this is the best open source app for anime and manga on Android, but hey, try it out yourself & judge!
|
||||
</i>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
# **Dantotsu** 🌟
|
||||
|
||||
Dantotsu is an [Anilist](https://anilist.co/) only client.
|
||||
|
||||
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
<br>
|
||||
|
||||
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> Please do not attempt to upload Dantotsu or any of it's forks on Playstore or any other Android appstores on the internet. Doing so, may infringe their terms and conditions. This may result to legal action or immediate take-down of the app.
|
||||
## WANT TO CONTRIBUTE? 🤝
|
||||
|
||||
## Extension Status
|
||||
All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content - whatever you have to offer, we can use!
|
||||
|
||||
| Type | Status |
|
||||
| ---------------- | ------- |
|
||||
| Anime Extensions | Working |
|
||||
| Manga Extensions | "Working" |
|
||||
| Light Novel Extensions | Not Working |
|
||||
You can come hang out with our awesome community, request new features, and report any bugs or issues at our Discord server too. 📣
|
||||
|
||||
### OFFICIAL DISCORD SERVER 🚀
|
||||
|
||||
|
||||
## APP FEATURES
|
||||
|
||||
- Easy and functional way to both, watch anime and read manga, ad-free.
|
||||
|
||||
- A completely open source app with a nice UI & Animations :)
|
||||
|
||||
- Aniyomi extension support built right into the app.
|
||||
|
||||
- Synchronize anime and manga real-time with AniList and MyAnimeList. Easily categorise anime and manga based on your current status. (Powered by AniList)
|
||||
|
||||
- Find all shows using thoroughly and frequently updated list of all trending, popular and ongoing anime based on scores.
|
||||
|
||||
- View extensive details about anime shows, movies and manga titles. It also features ability to countdown to the next episode of airing anime. (Powered by AniList & MyAnimeList)
|
||||
|
||||
- Get notified when new episodes/chapters are released!
|
||||
|
||||
|
||||
* **Available Anime sources:-**
|
||||
NONE BUILT IN!
|
||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
||||
|
||||
|
||||
* **Available Manga sources:-**
|
||||
NONE BUILT IN!
|
||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
||||
|
||||
## Planned Stuff
|
||||
|
||||
- get app out of alpha
|
||||
|
||||
- Accent Color Change (RIP Hot Pink Supremacy.)
|
||||
|
||||
|
||||
## Rejected Stuff (still rejected)
|
||||
|
||||
- Sources of any language except English
|
||||
|
||||
- News Section in the App
|
||||
|
||||
- Comment Section
|
||||
|
||||
|
||||
## WANT TO CONTRIBUTE?
|
||||
|
||||
- All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content- whatever you have to offer, we can use it!
|
||||
|
||||
- You can come hang out with our awesome community and request new features and report any bugs or issue at our discord server too.
|
||||
|
||||
### Official Discord Server
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
||||
<a href="https://discord.gg/4HPZ5nAWwM">
|
||||
<img src="https://invidget.switchblade.xyz/4HPZ5nAWwM">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
### VISIT FOR MORE INFORMATION:-
|
||||
|
||||
no website yet :(
|
||||
|
||||
## DISCLAIMER
|
||||
|
||||
* Dantotsu by itself only provides an anime and manga tracker and does not provide any anime or manga streaming or downloading capabilities.
|
||||
|
||||
* Dantotsu or any of its developer/staff don't host any of the content found inside Dantotsu. Any and all images and anime/manga information found in the app are taken from various public APIs (AniList, MyAnimeList, Kitsu).
|
||||
|
||||
* Furthermore, all of the anime/manga links found in Dantotsu are taken from various 3rd party plugins and have no affiliation with Dantotsu or its staff.
|
||||
|
||||
* Dantotsu or it's owners aren't liable for any misuse of any of the contents found inside or outside of the app and cannot be held accountable for the distribution of any of the contents found inside the app.
|
||||
|
||||
* By using Dantotsu, you comply to the fact that the developer of the app is not responsible for any of the contents found in the app. You also agree to the fact that you may not use Dantotsu to download or stream any copyrighted content.
|
||||
|
||||
* If the internet infringement issues are involved, please contact the source website. The developer does not assume any legal responsibility.
|
||||
|
||||
## License
|
||||
## LICENSE 📜
|
||||
|
||||
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
||||
|
||||
@@ -21,17 +21,18 @@ android {
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "0.1.3"
|
||||
versionName "2.0.0"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
//applicationIdSuffix ".beta"
|
||||
applicationIdSuffix ".beta"
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||
debuggable true
|
||||
versionNameSuffix "." + gitCommitHash
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||
debuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
@@ -62,9 +63,11 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.9.0'
|
||||
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
@@ -76,11 +79,11 @@ dependencies {
|
||||
|
||||
// FireBase
|
||||
implementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx:21.3.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.4.3'
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.1.1'
|
||||
ext.exo_version = '1.2.0'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
@@ -96,6 +99,8 @@ dependencies {
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation "com.github.skydoves:colorpickerview:2.3.0"
|
||||
|
||||
// string matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dantotsu α</string>
|
||||
<string name="app_name">Dantotsu ß</string>
|
||||
</resources>
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
@@ -46,11 +48,11 @@
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="${icon_placeholder}"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="${icon_placeholder_round}"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
@@ -221,6 +223,7 @@
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".download.DownloadContainerActivity" />
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
@@ -263,10 +266,24 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".download.manga.MangaDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".download.novel.NovelDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".connections.discord.DiscordService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/ic_launcher_beta-playstore.png
Normal file
BIN
app/src/main/ic_launcher_beta-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -8,19 +8,34 @@ import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import ani.dantotsu.others.DisabledReports
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class App : MultiDexApplication() {
|
||||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
@@ -36,24 +51,50 @@ class App : MultiDexApplication() {
|
||||
super.onCreate()
|
||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
||||
if(useMaterialYou) {
|
||||
if (useMaterialYou) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
//TODO: HarmonizedColors
|
||||
}
|
||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||
|
||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
initializeNetwork(baseContext)
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
|
||||
initializeNetwork(baseContext)
|
||||
|
||||
setupNotificationChannels()
|
||||
if (!LogcatLogger.isInstalled) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}
|
||||
|
||||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
novelExtensionManager = 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)
|
||||
}
|
||||
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||
novelScope.launch {
|
||||
novelExtensionManager.findAvailableExtensions()
|
||||
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun setupNotificationChannels() {
|
||||
try {
|
||||
Notifications.createChannels(this)
|
||||
@@ -81,7 +122,7 @@ class App : MultiDexApplication() {
|
||||
|
||||
companion object {
|
||||
private var instance: App? = null
|
||||
var context : Context? = null
|
||||
var context: Context? = null
|
||||
fun currentContext(): Context? {
|
||||
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
||||
}
|
||||
|
||||
@@ -132,9 +132,10 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
|
||||
fun initActivity(a: Activity) {
|
||||
val window = a.window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false) ?: UserInterfaceSettings().apply {
|
||||
saveData("ui_settings", this)
|
||||
}
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false)
|
||||
?: UserInterfaceSettings().apply {
|
||||
saveData("ui_settings", this)
|
||||
}
|
||||
uiSettings.darkMode.apply {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (this) {
|
||||
@@ -146,9 +147,10 @@ fun initActivity(a: Activity) {
|
||||
}
|
||||
if (uiSettings.immersiveMode) {
|
||||
if (navBarHeight == 0) {
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))?.apply {
|
||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||
}
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
?.apply {
|
||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||
}
|
||||
}
|
||||
a.hideStatusBar()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
@@ -160,7 +162,8 @@ fun initActivity(a: Activity) {
|
||||
}
|
||||
} else
|
||||
if (statusBarHeight == 0) {
|
||||
val windowInsets = ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
val windowInsets =
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
if (windowInsets != null) {
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
statusBarHeight = insets.top
|
||||
@@ -188,6 +191,9 @@ fun Activity.hideStatusBar() {
|
||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val window = dialog?.window
|
||||
val decorView: View = window?.decorView ?: return
|
||||
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
@@ -202,25 +208,24 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
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 {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return@tryWith if (cap != null) {
|
||||
when {
|
||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||
cap.hasTransport(TRANSPORT_USB) ||
|
||||
cap.hasTransport(TRANSPORT_VPN) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return@tryWith if (cap != null) {
|
||||
when {
|
||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||
cap.hasTransport(TRANSPORT_USB) ||
|
||||
cap.hasTransport(TRANSPORT_VPN) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
} else true
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
} ?: false
|
||||
}
|
||||
|
||||
@@ -238,7 +243,8 @@ fun startMainActivity(activity: Activity, bundle: Bundle? = null) {
|
||||
}
|
||||
|
||||
|
||||
class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) : DialogFragment(),
|
||||
class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) :
|
||||
DialogFragment(),
|
||||
DatePickerDialog.OnDateSetListener {
|
||||
var dialog: DatePickerDialog
|
||||
|
||||
@@ -263,9 +269,20 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
|
||||
}
|
||||
}
|
||||
|
||||
class InputFilterMinMax(private val min: Double, private val max: Double, private val status: AutoCompleteTextView? = null) :
|
||||
class InputFilterMinMax(
|
||||
private val min: Double,
|
||||
private val max: Double,
|
||||
private val status: AutoCompleteTextView? = null
|
||||
) :
|
||||
InputFilter {
|
||||
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
|
||||
override fun filter(
|
||||
source: CharSequence,
|
||||
start: Int,
|
||||
end: Int,
|
||||
dest: Spanned,
|
||||
dstart: Int,
|
||||
dend: Int
|
||||
): CharSequence? {
|
||||
try {
|
||||
val input = (dest.toString() + source.toString()).toDouble()
|
||||
if (isInRange(min, max, input)) return null
|
||||
@@ -288,11 +305,20 @@ class InputFilterMinMax(private val min: Double, private val max: Double, privat
|
||||
}
|
||||
|
||||
|
||||
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : ViewPager2.PageTransformer {
|
||||
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) :
|
||||
ViewPager2.PageTransformer {
|
||||
override fun transformPage(view: View, position: Float) {
|
||||
if (position == 0.0f && uiSettings.layoutAnimations) {
|
||||
setAnimation(view.context, view, uiSettings, 300, floatArrayOf(1.3f, 1f, 1.3f, 1f), 0.5f to 0f)
|
||||
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f).setDuration((200 * uiSettings.animationSpeed).toLong()).start()
|
||||
setAnimation(
|
||||
view.context,
|
||||
view,
|
||||
uiSettings,
|
||||
300,
|
||||
floatArrayOf(1.3f, 1f, 1.3f, 1f),
|
||||
0.5f to 0f
|
||||
)
|
||||
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f)
|
||||
.setDuration((200 * uiSettings.animationSpeed).toLong()).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,7 +353,11 @@ class FadingEdgeRecyclerView : RecyclerView {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun isPaddingOffsetRequired(): Boolean {
|
||||
return !clipToPadding
|
||||
@@ -413,13 +443,18 @@ fun MutableList<ShowResponse>.sortByTitle(string: String) {
|
||||
}
|
||||
|
||||
fun String.findBetween(a: String, b: String): String? {
|
||||
val string = substringAfter(a, "").substringBefore(b,"")
|
||||
val string = substringAfter(a, "").substringBefore(b, "")
|
||||
return string.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun ImageView.loadImage(url: String?, size: Int = 0) {
|
||||
if (!url.isNullOrEmpty()) {
|
||||
loadImage(FileUrl(url), size)
|
||||
val localFile = File(url)
|
||||
if (localFile.exists()) {
|
||||
loadLocalImage(localFile, size)
|
||||
} else {
|
||||
loadImage(FileUrl(url), size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +462,17 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size).into(this)
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,7 +530,12 @@ abstract class GesturesListener : GestureDetector.SimpleOnGestureListener() {
|
||||
return super.onDoubleTap(e)
|
||||
}
|
||||
|
||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
onScrollYClick(distanceY)
|
||||
onScrollXClick(distanceX)
|
||||
return super.onScroll(e1, e2, distanceX, distanceY)
|
||||
@@ -627,9 +677,15 @@ fun countDown(media: Media, view: ViewGroup) {
|
||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
currActivity()?.getString(R.string.episode_release_countdown, media.anime.nextAiringEpisode!! + 1)
|
||||
currActivity()?.getString(
|
||||
R.string.episode_release_countdown,
|
||||
media.anime.nextAiringEpisode!! + 1
|
||||
)
|
||||
|
||||
object : CountDownTimer((media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(), 1000) {
|
||||
object : CountDownTimer(
|
||||
(media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(),
|
||||
1000
|
||||
) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
val a = millisUntilFinished / 1000
|
||||
v.mediaCountdown.text = currActivity()?.getString(
|
||||
@@ -720,7 +776,8 @@ fun toast(string: String?) {
|
||||
if (string != null) {
|
||||
logger(string)
|
||||
MainScope().launch {
|
||||
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -729,7 +786,11 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG)
|
||||
val snackBar = Snackbar.make(
|
||||
window.decorView.findViewById(android.R.id.content),
|
||||
s,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
@@ -754,7 +815,8 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
||||
}
|
||||
}
|
||||
|
||||
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : ArrayAdapter<T>(context, layoutId, items) {
|
||||
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
|
||||
ArrayAdapter<T>(context, layoutId, items) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||
@@ -775,16 +837,21 @@ class SpinnerNoSwipe : androidx.appcompat.widget.AppCompatSpinner {
|
||||
setup()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
) {
|
||||
setup()
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
return performClick()
|
||||
}
|
||||
})
|
||||
mGestureDetector =
|
||||
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
return performClick()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
@@ -828,7 +895,11 @@ fun getCurrentBrightnessValue(context: Context): Float {
|
||||
}
|
||||
|
||||
fun getCur(): Float {
|
||||
return Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 127).toFloat()
|
||||
return Settings.System.getInt(
|
||||
context.contentResolver,
|
||||
Settings.System.SCREEN_BRIGHTNESS,
|
||||
127
|
||||
).toFloat()
|
||||
}
|
||||
|
||||
return brightnessConverter(getCur() / getMax(), true)
|
||||
@@ -850,12 +921,12 @@ fun checkCountry(context: Context): Boolean {
|
||||
tz.equals("Asia/Kolkata", ignoreCase = true)
|
||||
}
|
||||
|
||||
TelephonyManager.SIM_STATE_READY -> {
|
||||
TelephonyManager.SIM_STATE_READY -> {
|
||||
val countryCodeValue = telMgr.networkCountryIso
|
||||
countryCodeValue.equals("in", ignoreCase = true)
|
||||
}
|
||||
|
||||
else -> false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
package ani.dantotsu
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
@@ -23,7 +19,6 @@ import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -32,7 +27,6 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
@@ -44,23 +38,17 @@ import ani.dantotsu.home.MangaFragment
|
||||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
@@ -70,29 +58,33 @@ class MainActivity : AppCompatActivity() {
|
||||
private var load = false
|
||||
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
||||
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||
mangaScope.launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
}
|
||||
|
||||
|
||||
var doubleBackToExitPressedOnce = false
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (doubleBackToExitPressedOnce) {
|
||||
@@ -109,24 +101,40 @@ class MainActivity : AppCompatActivity() {
|
||||
binding.root.isMotionEventSplittingEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||
binding.root.addView(splash.root)
|
||||
(splash.splashImage.drawable as Animatable).start()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||
binding.root.addView(splash.root)
|
||||
(splash.splashImage.drawable as Animatable).start()
|
||||
|
||||
// Wait for 2 seconds (2000 milliseconds)
|
||||
delay(2000)
|
||||
delay(1200)
|
||||
|
||||
// Now perform the animation
|
||||
ObjectAnimator.ofFloat(
|
||||
splash.root,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splash.root.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { binding.root.removeView(splash.root) }
|
||||
start()
|
||||
ObjectAnimator.ofFloat(
|
||||
splash.root,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splash.root.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { binding.root.removeView(splash.root) }
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
splashScreen.setOnExitAnimationListener { splashScreenView ->
|
||||
ObjectAnimator.ofFloat(
|
||||
splashScreenView,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splashScreenView.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { splashScreenView.remove() }
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +143,7 @@ class MainActivity : AppCompatActivity() {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
binding.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
@@ -148,7 +156,7 @@ class MainActivity : AppCompatActivity() {
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.navbar
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
@@ -236,17 +244,6 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (ActivityHelper.shouldRefreshMainActivity) {
|
||||
ActivityHelper.shouldRefreshMainActivity = false
|
||||
Refresh.all()
|
||||
finish()
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
initActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//ViewPager
|
||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
@@ -264,8 +261,4 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ActivityHelper {
|
||||
var shouldRefreshMainActivity: Boolean = false
|
||||
}
|
||||
}
|
||||
@@ -8,58 +8,51 @@ import ani.dantotsu.others.webview.WebViewBottomDialog
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.nicehttp.addGenericDns
|
||||
import kotlinx.coroutines.*
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.PrintWriter
|
||||
import java.io.Serializable
|
||||
import java.io.StringWriter
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
val defaultHeaders = mapOf(
|
||||
"User-Agent" to
|
||||
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
|
||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||
)
|
||||
lateinit var cache: Cache
|
||||
lateinit var defaultHeaders: Map<String, String>
|
||||
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
lateinit var client: Requests
|
||||
|
||||
fun initializeNetwork(context: Context) {
|
||||
val dns = loadData<Int>("settings_dns")
|
||||
cache = Cache(
|
||||
File(context.cacheDir, "http_cache"),
|
||||
5 * 1024L * 1024L // 5 MiB
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
defaultHeaders = mapOf(
|
||||
"User-Agent" to
|
||||
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||
)
|
||||
okHttpClient = OkHttpClient.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.cache(cache)
|
||||
.apply {
|
||||
when (dns) {
|
||||
1 -> addGoogleDns()
|
||||
2 -> addCloudFlareDns()
|
||||
3 -> addAdGuardDns()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
okHttpClient = networkHelper.client
|
||||
client = Requests(
|
||||
okHttpClient,
|
||||
networkHelper.client,
|
||||
defaultHeaders,
|
||||
defaultCacheTime = 6,
|
||||
defaultCacheTimeUnit = TimeUnit.HOURS,
|
||||
responseParser = Mapper
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
object Mapper : ResponseParser {
|
||||
@@ -122,7 +115,11 @@ fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T):
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> tryWithSuspend(post: Boolean = false, snackbar: Boolean = true, call: suspend () -> T): T? {
|
||||
suspend fun <T> tryWithSuspend(
|
||||
post: Boolean = false,
|
||||
snackbar: Boolean = true,
|
||||
call: suspend () -> T
|
||||
): T? {
|
||||
return try {
|
||||
call.invoke()
|
||||
} catch (e: Throwable) {
|
||||
@@ -202,28 +199,29 @@ fun OkHttpClient.Builder.addAdGuardDns() = (
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? {
|
||||
var map : Map<String,String>? = null
|
||||
var map: Map<String, String>? = null
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
webViewDialog.callback = {
|
||||
map = it
|
||||
latch.countDown()
|
||||
}
|
||||
val fragmentManager = (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
||||
val fragmentManager =
|
||||
(currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
||||
webViewDialog.show(fragmentManager, "web-view")
|
||||
delay(0)
|
||||
latch.await(2,TimeUnit.MINUTES)
|
||||
latch.await(2, TimeUnit.MINUTES)
|
||||
return map
|
||||
}
|
||||
|
||||
suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? {
|
||||
val webViewDialog: WebViewBottomDialog = when (type) {
|
||||
"Cloudflare" -> CloudFlare.newInstance(url)
|
||||
else -> return null
|
||||
else -> return null
|
||||
}
|
||||
return webViewInterface(webViewDialog)
|
||||
}
|
||||
|
||||
suspend fun webViewInterface(type: String, url: String): Map<String, String>? {
|
||||
return webViewInterface(type,FileUrl(url))
|
||||
return webViewInterface(type, FileUrl(url))
|
||||
}
|
||||
@@ -3,16 +3,23 @@ package ani.dantotsu.aniyomi.anime.custom
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
@@ -23,11 +30,16 @@ class AppModule(val app: Application) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app, get()) }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
addSingletonFactory { NovelExtensionManager(app) }
|
||||
|
||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||
|
||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
addSingleton(sharedPreferences)
|
||||
@@ -40,6 +52,11 @@ class AppModule(val app: Application) : InjektModule {
|
||||
}
|
||||
|
||||
addSingletonFactory { MangaCache() }
|
||||
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
get<AnimeSourceManager>()
|
||||
get<MangaSourceManager>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,27 +13,33 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun updateProgress(media: Media, number: String) {
|
||||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val a = number.toFloatOrNull()?.roundToInt()
|
||||
if (a != media.userProgress) {
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
a,
|
||||
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
||||
)
|
||||
MAL.query.editList(
|
||||
media.idMAL,
|
||||
media.anime != null,
|
||||
a, null,
|
||||
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
||||
)
|
||||
toast(currContext()?.getString(R.string.setting_progress, a))
|
||||
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
|
||||
?.getBoolean("incognito", false) ?: false
|
||||
if (!incognito) {
|
||||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val a = number.toFloatOrNull()?.toInt()
|
||||
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
a,
|
||||
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
||||
)
|
||||
MAL.query.editList(
|
||||
media.idMAL,
|
||||
media.anime != null,
|
||||
a, null,
|
||||
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
||||
)
|
||||
toast(currContext()?.getString(R.string.setting_progress, a))
|
||||
}
|
||||
media.userProgress = a
|
||||
Refresh.all()
|
||||
}
|
||||
media.userProgress = a
|
||||
Refresh.all()
|
||||
} else {
|
||||
toast(currContext()?.getString(R.string.login_anilist_account))
|
||||
}
|
||||
} else {
|
||||
toast(currContext()?.getString(R.string.login_anilist_account))
|
||||
toast("Sneaky sneaky :3")
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import ani.dantotsu.currContext
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
|
||||
object Anilist {
|
||||
val query: AnilistQueries = AnilistQueries()
|
||||
@@ -29,7 +29,12 @@ object Anilist {
|
||||
var tags: Map<Boolean, List<String>>? = null
|
||||
|
||||
val sortBy = listOf(
|
||||
"SCORE_DESC","POPULARITY_DESC","TRENDING_DESC","TITLE_ENGLISH","TITLE_ENGLISH_DESC","SCORE"
|
||||
"SCORE_DESC",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"SCORE"
|
||||
)
|
||||
|
||||
val seasons = listOf(
|
||||
@@ -51,11 +56,11 @@ object Anilist {
|
||||
private val cal: Calendar = Calendar.getInstance()
|
||||
private val currentYear = cal.get(Calendar.YEAR)
|
||||
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
||||
0, 1, 2 -> 0
|
||||
3, 4, 5 -> 1
|
||||
6, 7, 8 -> 2
|
||||
0, 1, 2 -> 0
|
||||
3, 4, 5 -> 1
|
||||
6, 7, 8 -> 2
|
||||
9, 10, 11 -> 3
|
||||
else -> 0
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun getSeason(next: Boolean): Pair<String, Int> {
|
||||
@@ -132,7 +137,12 @@ object Anilist {
|
||||
if (token != null || force) {
|
||||
if (token != null && useToken) headers["Authorization"] = "Bearer $token"
|
||||
|
||||
val json = client.post("https://graphql.anilist.co/", headers, data = data, cacheTime = cache ?: 10)
|
||||
val json = client.post(
|
||||
"https://graphql.anilist.co/",
|
||||
headers,
|
||||
data = data,
|
||||
cacheTime = cache ?: 10
|
||||
)
|
||||
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
if (show) println("Response : ${json.text}")
|
||||
json.parsed()
|
||||
|
||||
@@ -20,7 +20,7 @@ class AnilistMutations {
|
||||
repeat: Int? = null,
|
||||
notes: String? = null,
|
||||
status: String? = null,
|
||||
private:Boolean? = null,
|
||||
private: Boolean? = null,
|
||||
startedAt: FuzzyDate? = null,
|
||||
completedAt: FuzzyDate? = null,
|
||||
customList: List<String>? = null
|
||||
@@ -41,7 +41,7 @@ class AnilistMutations {
|
||||
${if (repeat != null) ""","repeat":$repeat""" else ""}
|
||||
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""}
|
||||
${if (status != null) ""","status":"$status"""" else ""}
|
||||
${if (customList !=null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${if (customList != null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""}
|
||||
}""".replace("\n", "").replace(""" """, "")
|
||||
println(variables)
|
||||
executeQuery<JsonObject>(query, variables, show = true)
|
||||
|
||||
@@ -2,13 +2,13 @@ package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.app.Activity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.checkGenreTime
|
||||
import ani.dantotsu.checkId
|
||||
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.Page
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.checkGenreTime
|
||||
import ani.dantotsu.checkId
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logError
|
||||
@@ -113,9 +113,13 @@ class AnilistQueries {
|
||||
name = i.node?.name?.userPreferred,
|
||||
image = i.node?.image?.medium,
|
||||
banner = media.banner ?: media.cover,
|
||||
role = when (i.role.toString()){
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN"
|
||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING"
|
||||
role = when (i.role.toString()) {
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||
?: "MAIN"
|
||||
|
||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
|
||||
?: "SUPPORTING"
|
||||
|
||||
else -> i.role.toString()
|
||||
}
|
||||
)
|
||||
@@ -129,11 +133,16 @@ class AnilistQueries {
|
||||
val m = Media(mediaEdge)
|
||||
media.relations?.add(m)
|
||||
if (m.relation == "SEQUEL") {
|
||||
media.sequel = if ((media.sequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.sequel
|
||||
media.sequel =
|
||||
if ((media.sequel?.popularity ?: 0) < (m.popularity
|
||||
?: 0)
|
||||
) m else media.sequel
|
||||
|
||||
} else if (m.relation == "PREQUEL") {
|
||||
media.prequel =
|
||||
if ((media.prequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.prequel
|
||||
if ((media.prequel?.popularity ?: 0) < (m.popularity
|
||||
?: 0)
|
||||
) m else media.prequel
|
||||
}
|
||||
}
|
||||
media.relations?.sortByDescending { it.popularity }
|
||||
@@ -199,17 +208,19 @@ class AnilistQueries {
|
||||
)
|
||||
}
|
||||
|
||||
media.anime.nextAiringEpisodeTime = fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
||||
media.anime.nextAiringEpisodeTime =
|
||||
fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
||||
|
||||
fetchedMedia.externalLinks?.forEach { i ->
|
||||
when (i.site.lowercase()) {
|
||||
"youtube" -> media.anime.youtube = i.url
|
||||
"crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3)
|
||||
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
||||
"youtube" -> media.anime.youtube = i.url
|
||||
"crunchyroll" -> media.crunchySlug =
|
||||
i.url?.split("/")?.getOrNull(3)
|
||||
|
||||
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (media.manga != null) {
|
||||
} else if (media.manga != null) {
|
||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||
media.manga.author = Author(
|
||||
it.id.toString(),
|
||||
@@ -241,10 +252,10 @@ class AnilistQueries {
|
||||
return media
|
||||
}
|
||||
|
||||
suspend fun continueMedia(type: String,planned:Boolean=false): ArrayList<Media> {
|
||||
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
val statuses = if(!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
||||
val statuses = if (!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
||||
suspend fun repeat(status: String) {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
||||
@@ -275,21 +286,21 @@ class AnilistQueries {
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
|
||||
suspend fun getNextPage(page:Int): List<Media> {
|
||||
suspend fun getNextPage(page: Int): List<Media> {
|
||||
val response =
|
||||
executeQuery<Query.User>("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""")
|
||||
val favourites = response?.data?.user?.favourites
|
||||
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||
return apiMediaList?.edges?.mapNotNull {
|
||||
it.node?.let { i->
|
||||
it.node?.let { i ->
|
||||
Media(i).apply { isFav = true }
|
||||
}
|
||||
} ?: return listOf()
|
||||
}
|
||||
|
||||
val responseArray = arrayListOf<Media>()
|
||||
while(hasNextPage){
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
responseArray.addAll(getNextPage(page))
|
||||
}
|
||||
@@ -361,7 +372,11 @@ class AnilistQueries {
|
||||
return default
|
||||
}
|
||||
|
||||
suspend fun getMediaLists(anime: Boolean, userId: Int, sortOrder: String? = null): MutableMap<String, ArrayList<Media>> {
|
||||
suspend fun getMediaLists(
|
||||
anime: Boolean,
|
||||
userId: Int,
|
||||
sortOrder: String? = null
|
||||
): MutableMap<String, ArrayList<Media>> {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
||||
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
||||
@@ -388,7 +403,7 @@ class AnilistQueries {
|
||||
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
|
||||
}
|
||||
unsorted.forEach {
|
||||
if(!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
||||
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
||||
}
|
||||
|
||||
sorted["Favourites"] = favMedia(anime)
|
||||
@@ -399,11 +414,18 @@ class AnilistQueries {
|
||||
val sort = sortOrder ?: options?.rowOrder
|
||||
for (i in sorted.keys) {
|
||||
when (sort) {
|
||||
"score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) }
|
||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
||||
"score" -> sorted[i]?.sortWith { b, a ->
|
||||
compareValuesBy(
|
||||
a,
|
||||
b,
|
||||
{ it.userScore },
|
||||
{ it.meanScore })
|
||||
}
|
||||
|
||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
||||
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
|
||||
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
||||
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
||||
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
||||
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
@@ -559,18 +581,36 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||
${if (season != null) ""","season":"$season"""" else ""}
|
||||
${if (search != null) ""","search":"$search"""" else ""}
|
||||
${if (sort!=null) ""","sort":"$sort"""" else ""}
|
||||
${if (sort != null) ""","sort":"$sort"""" else ""}
|
||||
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
||||
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedGenres?.isNotEmpty() == true)
|
||||
""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
||||
""","excludedGenres":[${
|
||||
excludedGenres.joinToString {
|
||||
"\"${
|
||||
it.replace(
|
||||
"Not ",
|
||||
""
|
||||
)
|
||||
}\""
|
||||
}
|
||||
}]"""
|
||||
else ""
|
||||
}
|
||||
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedTags?.isNotEmpty() == true)
|
||||
""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
||||
""","excludedTags":[${
|
||||
excludedTags.joinToString {
|
||||
"\"${
|
||||
it.replace(
|
||||
"Not ",
|
||||
""
|
||||
)
|
||||
}\""
|
||||
}
|
||||
}]"""
|
||||
else ""
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
@@ -622,7 +662,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
greater: Long = 0,
|
||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||
): MutableList<Media>? {
|
||||
suspend fun execute(page:Int = 1):Page?{
|
||||
suspend fun execute(page: Int = 1): Page? {
|
||||
val query = """{
|
||||
Page(page:$page,perPage:50) {
|
||||
pageInfo {
|
||||
@@ -668,7 +708,7 @@ Page(page:$page,perPage:50) {
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||
}
|
||||
if(smaller) {
|
||||
if (smaller) {
|
||||
val response = execute()?.airingSchedules ?: return null
|
||||
val idArr = mutableListOf<Int>()
|
||||
val listOnly = loadData("recently_list_only") ?: false
|
||||
@@ -682,11 +722,11 @@ Page(page:$page,perPage:50) {
|
||||
else null
|
||||
}
|
||||
}.toMutableList()
|
||||
}else{
|
||||
} else {
|
||||
var i = 1
|
||||
val list = mutableListOf<Media>()
|
||||
var res : Page? = null
|
||||
suspend fun next(){
|
||||
var res: Page? = null
|
||||
suspend fun next() {
|
||||
res = execute(i)
|
||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||
j.media?.let {
|
||||
@@ -694,10 +734,10 @@ Page(page:$page,perPage:50) {
|
||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||
} else null
|
||||
}
|
||||
}?: listOf())
|
||||
} ?: listOf())
|
||||
}
|
||||
next()
|
||||
while (res?.pageInfo?.hasNextPage == true){
|
||||
while (res?.pageInfo?.hasNextPage == true) {
|
||||
next()
|
||||
i++
|
||||
}
|
||||
@@ -822,19 +862,20 @@ Page(page:$page,perPage:50) {
|
||||
var page = 0
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
hasNextPage = executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
val year = startDate?.year?.toString() ?: "TBA"
|
||||
val title = if (status != "CANCELLED") year else status
|
||||
if (!yearMedia.containsKey(title))
|
||||
yearMedia[title] = arrayListOf()
|
||||
yearMedia[title]?.add(Media(this))
|
||||
hasNextPage =
|
||||
executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
val year = startDate?.year?.toString() ?: "TBA"
|
||||
val title = if (status != "CANCELLED") year else status
|
||||
if (!yearMedia.containsKey(title))
|
||||
yearMedia[title] = arrayListOf()
|
||||
yearMedia[title]?.add(Media(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: false
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: false
|
||||
}
|
||||
if (yearMedia.contains("CANCELLED")) {
|
||||
val a = yearMedia["CANCELLED"]!!
|
||||
@@ -896,7 +937,10 @@ Page(page:$page,perPage:50) {
|
||||
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
hasNextPage = executeQuery<Query.Author>(query(page), force = true)?.data?.author?.staffMedia?.let {
|
||||
hasNextPage = executeQuery<Query.Author>(
|
||||
query(page),
|
||||
force = true
|
||||
)?.data?.author?.staffMedia?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
|
||||
@@ -7,8 +7,8 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.snackString
|
||||
@@ -19,9 +19,16 @@ import kotlinx.coroutines.launch
|
||||
|
||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (Discord.userid == null && Discord.token != null) {
|
||||
if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val token = sharedPref.getString("discord_token", null)
|
||||
val userid = sharedPref.getString("discord_id", null)
|
||||
if (userid == null && token != null) {
|
||||
/*if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))*/
|
||||
//TODO: Discord.getUserData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,39 +45,57 @@ suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
}
|
||||
} else true
|
||||
|
||||
if(anilist) block.invoke()
|
||||
if (anilist) block.invoke()
|
||||
}
|
||||
|
||||
class AnilistHomeViewModel : ViewModel() {
|
||||
private val listImages: MutableLiveData<ArrayList<String?>> = MutableLiveData<ArrayList<String?>>(arrayListOf())
|
||||
private val listImages: MutableLiveData<ArrayList<String?>> =
|
||||
MutableLiveData<ArrayList<String?>>(arrayListOf())
|
||||
|
||||
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
||||
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
|
||||
|
||||
private val animeContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
private val animeContinue: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
||||
|
||||
private val animeFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||
suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||
suspend fun setAnimePlanned() =
|
||||
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||
|
||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
||||
|
||||
private val mangaFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||
suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||
suspend fun setMangaPlanned() =
|
||||
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||
|
||||
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
private val recommendation: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||
|
||||
@@ -93,7 +118,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
private val type = "ANIME"
|
||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
private val trending: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||
suspend fun loadTrending(i: Int) {
|
||||
val (season, year) = Anilist.currentSeasons[i]
|
||||
@@ -109,7 +136,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
||||
|
||||
@@ -157,15 +186,33 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
private val type = "MANGA"
|
||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
private val trending: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||
suspend fun loadTrending() =
|
||||
trending.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], hd = true)?.results)
|
||||
trending.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
hd = true
|
||||
)?.results
|
||||
)
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadTrendingNovel() =
|
||||
updated.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], format = "NOVEL")?.results)
|
||||
updated.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
format = "NOVEL"
|
||||
)?.results
|
||||
)
|
||||
|
||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
|
||||
@@ -6,17 +6,20 @@ import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
val data: Uri? = intent?.data
|
||||
logger(data.toString())
|
||||
try {
|
||||
Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
Anilist.token =
|
||||
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
val filename = "anilistToken"
|
||||
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
||||
it.write(Anilist.token!!.toByteArray())
|
||||
|
||||
@@ -27,7 +27,15 @@ data class SearchResults(
|
||||
val list = mutableListOf<SearchChip>()
|
||||
sort?.let {
|
||||
val c = currContext()!!
|
||||
list.add(SearchChip("SORT", c.getString(R.string.filter_sort, c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)])))
|
||||
list.add(
|
||||
SearchChip(
|
||||
"SORT",
|
||||
c.getString(
|
||||
R.string.filter_sort,
|
||||
c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
format?.let {
|
||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||
@@ -42,27 +50,37 @@ data class SearchResults(
|
||||
list.add(SearchChip("GENRE", it))
|
||||
}
|
||||
excludedGenres?.forEach {
|
||||
list.add(SearchChip("EXCLUDED_GENRE", currContext()!!.getString(R.string.filter_exclude, it)))
|
||||
list.add(
|
||||
SearchChip(
|
||||
"EXCLUDED_GENRE",
|
||||
currContext()!!.getString(R.string.filter_exclude, it)
|
||||
)
|
||||
)
|
||||
}
|
||||
tags?.forEach {
|
||||
list.add(SearchChip("TAG", it))
|
||||
}
|
||||
excludedTags?.forEach {
|
||||
list.add(SearchChip("EXCLUDED_TAG", currContext()!!.getString(R.string.filter_exclude, it)))
|
||||
list.add(
|
||||
SearchChip(
|
||||
"EXCLUDED_TAG",
|
||||
currContext()!!.getString(R.string.filter_exclude, it)
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun removeChip(chip: SearchChip) {
|
||||
when (chip.type) {
|
||||
"SORT" -> sort = null
|
||||
"FORMAT" -> format = null
|
||||
"SEASON" -> season = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"SORT" -> sort = null
|
||||
"FORMAT" -> format = null
|
||||
"SEASON" -> season = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||
"TAG" -> tags?.remove(chip.text)
|
||||
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
||||
"TAG" -> tags?.remove(chip.text)
|
||||
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import ani.dantotsu.loadMedia
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class UrlMedia : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
@@ -21,6 +23,9 @@ class UrlMedia : Activity() {
|
||||
isMAL = data?.host != "anilist.co"
|
||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||
} else loadMedia = id
|
||||
startMainActivity(this, bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia))
|
||||
startMainActivity(
|
||||
this,
|
||||
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,24 @@ package ani.dantotsu.connections.anilist.api
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Query{
|
||||
class Query {
|
||||
@Serializable
|
||||
data class Viewer(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Viewer")
|
||||
val user: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Media(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Media")
|
||||
@@ -30,12 +31,12 @@ class Query{
|
||||
@Serializable
|
||||
data class Page(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page : ani.dantotsu.connections.anilist.api.Page?
|
||||
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||
)
|
||||
}
|
||||
// data class AiringSchedule(
|
||||
@@ -49,8 +50,8 @@ class Query{
|
||||
@Serializable
|
||||
data class Character(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Data(
|
||||
@@ -63,7 +64,7 @@ class Query{
|
||||
data class Studio(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Studio")
|
||||
@@ -76,7 +77,7 @@ class Query{
|
||||
data class Author(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Staff")
|
||||
@@ -95,8 +96,8 @@ class Query{
|
||||
@Serializable
|
||||
data class MediaListCollection(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("MediaListCollection")
|
||||
@@ -108,7 +109,7 @@ class Query{
|
||||
data class GenreCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("GenreCollection")
|
||||
@@ -120,7 +121,7 @@ class Query{
|
||||
data class MediaTagCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("MediaTagCollection")
|
||||
@@ -132,7 +133,7 @@ class Query{
|
||||
data class User(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
|
||||
@@ -3,7 +3,7 @@ package ani.dantotsu.connections.anilist.api
|
||||
import kotlinx.serialization.SerialName
|
||||
import java.io.Serializable
|
||||
import java.text.DateFormatSymbols
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class FuzzyDate(
|
||||
@@ -16,9 +16,11 @@ data class FuzzyDate(
|
||||
fun isEmpty(): Boolean {
|
||||
return year == null && month == null && day == null
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return if ( isEmpty() ) "??" else toStringOrEmpty()
|
||||
return if (isEmpty()) "??" else toStringOrEmpty()
|
||||
}
|
||||
|
||||
fun toStringOrEmpty(): String {
|
||||
return listOfNotNull(
|
||||
day?.toString(),
|
||||
@@ -29,16 +31,21 @@ data class FuzzyDate(
|
||||
|
||||
fun getToday(): FuzzyDate {
|
||||
val cal = Calendar.getInstance()
|
||||
return FuzzyDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH))
|
||||
return FuzzyDate(
|
||||
cal.get(Calendar.YEAR),
|
||||
cal.get(Calendar.MONTH) + 1,
|
||||
cal.get(Calendar.DAY_OF_MONTH)
|
||||
)
|
||||
}
|
||||
|
||||
fun toVariableString(): String {
|
||||
return listOfNotNull(
|
||||
year?.let {"year:$it"},
|
||||
month?.let {"month:$it"},
|
||||
day?.let {"day:$it"}
|
||||
year?.let { "year:$it" },
|
||||
month?.let { "month:$it" },
|
||||
day?.let { "day:$it" }
|
||||
).joinToString(",", "{", "}")
|
||||
}
|
||||
|
||||
fun toMALString(): String {
|
||||
val padding = '0'
|
||||
val values = listOf(
|
||||
@@ -46,7 +53,7 @@ data class FuzzyDate(
|
||||
month?.toString()?.padStart(2, padding),
|
||||
day?.toString()?.padStart(2, padding)
|
||||
)
|
||||
return values.takeWhile {it is String}.joinToString("-")
|
||||
return values.takeWhile { it is String }.joinToString("-")
|
||||
}
|
||||
|
||||
// fun toInt(): Int {
|
||||
@@ -54,8 +61,8 @@ data class FuzzyDate(
|
||||
// }
|
||||
|
||||
override fun compareTo(other: FuzzyDate): Int = when {
|
||||
year != other.year -> (year ?: 0) - (other.year ?: 0)
|
||||
year != other.year -> (year ?: 0) - (other.year ?: 0)
|
||||
month != other.month -> (month ?: 0) - (other.month ?: 0)
|
||||
else -> (day ?: 0) - (other.day ?: 0)
|
||||
else -> (day ?: 0) - (other.day ?: 0)
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ data class Media(
|
||||
@SerialName("characters") var characters: CharacterConnection?,
|
||||
|
||||
// The staff who produced the media
|
||||
@SerialName("staffPreview") var staff: StaffConnection?,
|
||||
@SerialName("staffPreview") var staff: StaffConnection?,
|
||||
|
||||
// The companies who produced the media
|
||||
@SerialName("studios") var studios: StudioConnection?,
|
||||
@@ -292,7 +292,7 @@ data class MediaList(
|
||||
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
|
||||
|
||||
// Map of booleans for which custom lists the entry are in
|
||||
@SerialName("customLists") var customLists: Map<String,Boolean>?,
|
||||
@SerialName("customLists") var customLists: Map<String, Boolean>?,
|
||||
|
||||
// Map of advanced scores with name keys
|
||||
// @SerialName("advancedScores") var advancedScores: Json?,
|
||||
@@ -355,7 +355,7 @@ data class MediaTrailer(
|
||||
|
||||
@Serializable
|
||||
data class MediaTagCollection(
|
||||
@SerialName("tags") var tags : List<MediaTag>?
|
||||
@SerialName("tags") var tags: List<MediaTag>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Recommendation(
|
||||
// The id of the recommendation
|
||||
@@ -22,6 +23,7 @@ data class Recommendation(
|
||||
// The user that first created the recommendation
|
||||
@SerialName("user") var user: User?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RecommendationConnection(
|
||||
//@SerialName("edges") var edges: List<RecommendationEdge>?,
|
||||
|
||||
@@ -9,7 +9,7 @@ data class Staff(
|
||||
@SerialName("id") var id: Int,
|
||||
|
||||
// The names of the staff member
|
||||
@SerialName("name") var name: StaffName?,
|
||||
@SerialName("name") var name: StaffName?,
|
||||
|
||||
// The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu
|
||||
@SerialName("languageV2") var languageV2: String?,
|
||||
@@ -80,8 +80,8 @@ data class Staff(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaffName (
|
||||
var userPreferred:String?
|
||||
data class StaffName(
|
||||
var userPreferred: String?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -96,6 +96,6 @@ data class StaffConnection(
|
||||
|
||||
@Serializable
|
||||
data class StaffEdge(
|
||||
var role:String?,
|
||||
var role: String?,
|
||||
var node: Staff?
|
||||
)
|
||||
@@ -80,10 +80,10 @@ data class UserOptions(
|
||||
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
||||
|
||||
// Whether the user receives notifications when a show they are watching aires
|
||||
@SerialName("airingNotifications") var airingNotifications: Boolean?,
|
||||
@SerialName("airingNotifications") var airingNotifications: Boolean?,
|
||||
//
|
||||
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
||||
@SerialName("profileColor") var profileColor: String?,
|
||||
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
||||
@SerialName("profileColor") var profileColor: String?,
|
||||
//
|
||||
// // Notification options
|
||||
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
|
||||
|
||||
@@ -5,14 +5,11 @@ import android.content.Intent
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.edit
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.serializers.User
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWith
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.io.File
|
||||
|
||||
object Discord {
|
||||
@@ -21,7 +18,7 @@ object Discord {
|
||||
var userid: String? = null
|
||||
var avatar: String? = null
|
||||
|
||||
private const val TOKEN = "discord_token"
|
||||
const val TOKEN = "discord_token"
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
@@ -60,17 +57,7 @@ object Discord {
|
||||
}
|
||||
}
|
||||
|
||||
private var rpc : RPC? = null
|
||||
suspend fun getUserData() = tryWithSuspend(true) {
|
||||
if(rpc==null) {
|
||||
val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it }
|
||||
val user: User = rpc.getUserData()
|
||||
userid = user.username
|
||||
avatar = user.userAvatar()
|
||||
rpc.close()
|
||||
true
|
||||
} else true
|
||||
} ?: false
|
||||
private var rpc: RPC? = null
|
||||
|
||||
|
||||
fun warning(context: Context) = CustomBottomDialog().apply {
|
||||
@@ -97,16 +84,21 @@ object Discord {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun defaultRPC(): RPC? {
|
||||
const val application_Id = "1163925779692912771"
|
||||
const val small_Image: String =
|
||||
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
|
||||
/*fun defaultRPC(): RPC? {
|
||||
return token?.let {
|
||||
RPC(it, Dispatchers.IO).apply {
|
||||
applicationId = "1163925779692912771"
|
||||
applicationId = application_Id
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
"mp:attachments/1163940221063278672/1163940262423298141/bitmap1024.png"
|
||||
small_Image
|
||||
)
|
||||
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
package ani.dantotsu.connections.discord
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import ani.dantotsu.MainActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import ani.dantotsu.connections.discord.serializers.User
|
||||
import ani.dantotsu.isOnline
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.io.File
|
||||
import java.io.OutputStreamWriter
|
||||
|
||||
class DiscordService : Service() {
|
||||
private var heartbeat: Int = 0
|
||||
private var sequence: Int? = null
|
||||
private var sessionId: String = ""
|
||||
private var resume = false
|
||||
private lateinit var logFile: File
|
||||
private lateinit var webSocket: WebSocket
|
||||
private lateinit var heartbeatThread: Thread
|
||||
private lateinit var client: OkHttpClient
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
var presenceStore = ""
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
allowStructuredMapKeys = true
|
||||
ignoreUnknownKeys = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
var log = ""
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
log("Service onCreate()")
|
||||
val powerManager = baseContext.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
wakeLock = powerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"discordRPC:backgroundPresence"
|
||||
)
|
||||
wakeLock.acquire()
|
||||
log("WakeLock Acquired")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val serviceChannel = NotificationChannel(
|
||||
"discordPresence",
|
||||
"Discord Presence Service Channel",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
}
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_MAIN
|
||||
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
val builder = NotificationCompat.Builder(this, "discordPresence")
|
||||
.setSmallIcon(R.mipmap.ic_launcher_round)
|
||||
.setContentTitle("Discord Presence")
|
||||
.setContentText("Running in the background")
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
startForeground(1, builder.build())
|
||||
log("Foreground service started, notification shown")
|
||||
client = OkHttpClient()
|
||||
client.newWebSocket(
|
||||
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||
DiscordWebSocketListener()
|
||||
)
|
||||
client.dispatcher.executorService.shutdown()
|
||||
SERVICE_RUNNING = true
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
log("Service onStartCommand()")
|
||||
if (intent != null) {
|
||||
if (intent.hasExtra("presence")) {
|
||||
log("Service onStartCommand() setPresence")
|
||||
val lPresence = intent.getStringExtra("presence")
|
||||
if (this::webSocket.isInitialized) webSocket.send(lPresence!!)
|
||||
presenceStore = lPresence!!
|
||||
} else {
|
||||
log("Service onStartCommand() no presence")
|
||||
DiscordServiceRunningSingleton.running = false
|
||||
//kill the client
|
||||
client = OkHttpClient()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
log("Service Destroyed")
|
||||
if (DiscordServiceRunningSingleton.running) {
|
||||
log("Accidental Service Destruction, restarting service")
|
||||
val intent = Intent(baseContext, DiscordService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
baseContext.startForegroundService(intent)
|
||||
} else {
|
||||
baseContext.startService(intent)
|
||||
}
|
||||
} else {
|
||||
if (this::webSocket.isInitialized)
|
||||
setPresence(
|
||||
json.encodeToString(
|
||||
Presence.Response(
|
||||
3,
|
||||
Presence(status = "offline")
|
||||
)
|
||||
)
|
||||
)
|
||||
wakeLock.release()
|
||||
}
|
||||
SERVICE_RUNNING = false
|
||||
client = OkHttpClient()
|
||||
if (this::webSocket.isInitialized) webSocket.close(1000, "Closed by user")
|
||||
super.onDestroy()
|
||||
//saveLogToFile()
|
||||
}
|
||||
|
||||
fun saveProfile(response: String) {
|
||||
val sharedPref = baseContext.getSharedPreferences(
|
||||
baseContext.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val user = json.decodeFromString<User.Response>(response).d.user
|
||||
log("User data: $user")
|
||||
with(sharedPref.edit()) {
|
||||
putString("discord_username", user.username)
|
||||
putString("discord_id", user.id)
|
||||
putString("discord_avatar", user.avatar)
|
||||
apply()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
|
||||
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||
|
||||
var retryAttempts = 0
|
||||
val maxRetryAttempts = 10
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
super.onOpen(webSocket, response)
|
||||
this@DiscordService.webSocket = webSocket
|
||||
log("WebSocket: Opened")
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
super.onMessage(webSocket, text)
|
||||
val json = JsonParser.parseString(text).asJsonObject
|
||||
log("WebSocket: Received op code ${json.get("op")}")
|
||||
when (json.get("op").asInt) {
|
||||
0 -> {
|
||||
if (json.has("s")) {
|
||||
log("WebSocket: Sequence ${json.get("s")} Received")
|
||||
sequence = json.get("s").asInt
|
||||
}
|
||||
if (json.get("t").asString != "READY") return
|
||||
saveProfile(text)
|
||||
log(text)
|
||||
sessionId = json.get("d").asJsonObject.get("session_id").asString
|
||||
log("WebSocket: SessionID ${json.get("d").asJsonObject.get("session_id")} Received")
|
||||
if (presenceStore.isNotEmpty()) setPresence(presenceStore)
|
||||
sendBroadcast(Intent("ServiceToConnectButton"))
|
||||
}
|
||||
|
||||
1 -> {
|
||||
log("WebSocket: Received Heartbeat request, sending heartbeat")
|
||||
heartbeatThread.interrupt()
|
||||
heartbeatSend(webSocket, sequence)
|
||||
heartbeatThread = Thread(HeartbeatRunnable())
|
||||
heartbeatThread.start()
|
||||
}
|
||||
|
||||
7 -> {
|
||||
resume = true
|
||||
log("WebSocket: Requested to Restart, restarting")
|
||||
webSocket.close(1000, "Requested to Restart by the server")
|
||||
client = OkHttpClient()
|
||||
client.newWebSocket(
|
||||
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||
.build(),
|
||||
DiscordWebSocketListener()
|
||||
)
|
||||
client.dispatcher.executorService.shutdown()
|
||||
}
|
||||
|
||||
9 -> {
|
||||
log("WebSocket: Invalid Session, restarting")
|
||||
webSocket.close(1000, "Invalid Session")
|
||||
Thread.sleep(5000)
|
||||
client = OkHttpClient()
|
||||
client.newWebSocket(
|
||||
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||
.build(),
|
||||
DiscordWebSocketListener()
|
||||
)
|
||||
client.dispatcher.executorService.shutdown()
|
||||
}
|
||||
|
||||
10 -> {
|
||||
heartbeat = json.get("d").asJsonObject.get("heartbeat_interval").asInt
|
||||
heartbeatThread = Thread(HeartbeatRunnable())
|
||||
heartbeatThread.start()
|
||||
if (resume) {
|
||||
log("WebSocket: Resuming because server requested")
|
||||
resume()
|
||||
resume = false
|
||||
} else {
|
||||
identify(webSocket, baseContext)
|
||||
log("WebSocket: Identified")
|
||||
}
|
||||
}
|
||||
|
||||
11 -> {
|
||||
log("WebSocket: Heartbeat ACKed")
|
||||
heartbeatThread = Thread(HeartbeatRunnable())
|
||||
heartbeatThread.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun identify(webSocket: WebSocket, context: Context) {
|
||||
val properties = JsonObject()
|
||||
properties.addProperty("os", "linux")
|
||||
properties.addProperty("browser", "unknown")
|
||||
properties.addProperty("device", "unknown")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(context))
|
||||
d.addProperty("intents", 0)
|
||||
d.add("properties", properties)
|
||||
val payload = JsonObject()
|
||||
payload.addProperty("op", 2)
|
||||
payload.add("d", d)
|
||||
webSocket.send(payload.toString())
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
super.onFailure(webSocket, t, response)
|
||||
if (!isOnline(baseContext)) {
|
||||
log("WebSocket: Error, onFailure() reason: No Internet")
|
||||
errorNotification("Could not set the presence", "No Internet")
|
||||
return
|
||||
} else {
|
||||
retryAttempts++
|
||||
if (retryAttempts >= maxRetryAttempts) {
|
||||
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
|
||||
errorNotification("Could not set the presence", "Max Retry Attempts")
|
||||
return
|
||||
}
|
||||
}
|
||||
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
|
||||
log("WebSocket: Error, onFailure() reason: ${t.message}")
|
||||
client = OkHttpClient()
|
||||
client.newWebSocket(
|
||||
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||
DiscordWebSocketListener()
|
||||
)
|
||||
client.dispatcher.executorService.shutdown()
|
||||
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||
heartbeatThread.interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
super.onClosing(webSocket, code, reason)
|
||||
Log.d("WebSocket", "onClosing() $code $reason")
|
||||
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||
heartbeatThread.interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
super.onClosed(webSocket, code, reason)
|
||||
Log.d("WebSocket", "onClosed() $code $reason")
|
||||
if (code >= 4000) {
|
||||
log("WebSocket: Error, code: $code reason: $reason")
|
||||
client = OkHttpClient()
|
||||
client.newWebSocket(
|
||||
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||
DiscordWebSocketListener()
|
||||
)
|
||||
client.dispatcher.executorService.shutdown()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getToken(context: Context): String {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val token = sharedPref.getString(Discord.TOKEN, null)
|
||||
if (token == null) {
|
||||
log("WebSocket: Token not found")
|
||||
errorNotification("Could not set the presence", "token not found")
|
||||
return ""
|
||||
} else {
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
fun heartbeatSend(webSocket: WebSocket, seq: Int?) {
|
||||
val json = JsonObject()
|
||||
json.addProperty("op", 1)
|
||||
json.addProperty("d", seq)
|
||||
webSocket.send(json.toString())
|
||||
}
|
||||
|
||||
private fun errorNotification(title: String, text: String) {
|
||||
val intent = Intent(this@DiscordService, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_MAIN
|
||||
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
val builder = NotificationCompat.Builder(this@DiscordService, "discordPresence")
|
||||
.setSmallIcon(R.mipmap.ic_launcher_round)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
//TODO: Request permission
|
||||
return
|
||||
}
|
||||
notificationManager.notify(2, builder.build())
|
||||
log("Error Notified")
|
||||
}
|
||||
|
||||
fun saveSimpleTestPresence() {
|
||||
val file = File(baseContext.cacheDir, "payload")
|
||||
//fill with test payload
|
||||
val payload = JsonObject()
|
||||
payload.addProperty("op", 3)
|
||||
payload.add("d", JsonObject().apply {
|
||||
addProperty("status", "online")
|
||||
addProperty("afk", false)
|
||||
add("activities", JsonArray().apply {
|
||||
add(JsonObject().apply {
|
||||
addProperty("name", "Test")
|
||||
addProperty("type", 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
file.writeText(payload.toString())
|
||||
log("WebSocket: Simple Test Presence Saved")
|
||||
}
|
||||
|
||||
fun setPresence(String: String) {
|
||||
log("WebSocket: Sending Presence payload")
|
||||
log(String)
|
||||
webSocket.send(String)
|
||||
}
|
||||
|
||||
fun log(string: String) {
|
||||
Log.d("WebSocket_Discord", string)
|
||||
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
|
||||
}
|
||||
|
||||
fun saveLogToFile() {
|
||||
val fileName = "log_${System.currentTimeMillis()}.txt"
|
||||
|
||||
// ContentValues to store file metadata
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||
}
|
||||
}
|
||||
|
||||
// Inserting the file in the MediaStore
|
||||
val resolver = baseContext.contentResolver
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
} else {
|
||||
val directory =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(directory, fileName)
|
||||
|
||||
// Make sure the Downloads directory exists
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Use FileProvider to get the URI for the file
|
||||
val authority =
|
||||
"${baseContext.packageName}.provider" // Adjust with your app's package name
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
|
||||
// Writing to the file
|
||||
uri?.let {
|
||||
resolver.openOutputStream(it).use { outputStream ->
|
||||
OutputStreamWriter(outputStream).use { writer ->
|
||||
writer.write(log)
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
log("Error saving log file")
|
||||
}
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
log("Sending Resume payload")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(baseContext))
|
||||
d.addProperty("session_id", sessionId)
|
||||
d.addProperty("seq", sequence)
|
||||
val json = JsonObject()
|
||||
json.addProperty("op", 6)
|
||||
json.add("d", d)
|
||||
log(json.toString())
|
||||
webSocket.send(json.toString())
|
||||
}
|
||||
|
||||
inner class HeartbeatRunnable : Runnable {
|
||||
override fun run() {
|
||||
try {
|
||||
Thread.sleep(heartbeat.toLong())
|
||||
heartbeatSend(webSocket, sequence)
|
||||
log("WebSocket: Heartbeat Sent")
|
||||
} catch (e: InterruptedException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var SERVICE_RUNNING = false
|
||||
}
|
||||
}
|
||||
|
||||
object DiscordServiceRunningSingleton {
|
||||
var running = false
|
||||
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import android.annotation.SuppressLint
|
||||
import android.app.Application.getProcessName
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
@@ -18,6 +20,7 @@ class Login : AppCompatActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
@@ -32,28 +35,46 @@ class Login : AppCompatActivity() {
|
||||
settings.databaseEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
}
|
||||
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (url != null && url.endsWith("/app")) {
|
||||
webView.stopLoading()
|
||||
webView.evaluateJavascript("""
|
||||
(function() {
|
||||
const wreq = webpackChunkdiscord_app.push([[Symbol()], {}, w => w])
|
||||
webpackChunkdiscord_app.pop()
|
||||
const token = Object.values(wreq.c).find(m => m.exports?.Z?.getToken).exports.Z.getToken();
|
||||
return token;
|
||||
})()
|
||||
""".trimIndent()){
|
||||
login(it.trim('"'))
|
||||
}
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
// Check if the URL is the one expected after a successful login
|
||||
if (request?.url.toString() != "https://discord.com/login") {
|
||||
// Delay the script execution to ensure the page is fully loaded
|
||||
view?.postDelayed({
|
||||
view.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken();
|
||||
return wreq;
|
||||
})()
|
||||
""".trimIndent()
|
||||
) { result ->
|
||||
login(result.trim('"'))
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
}
|
||||
|
||||
webView.loadUrl("https://discord.com/login")
|
||||
}
|
||||
|
||||
private fun login(token: String) {
|
||||
if (token.isEmpty() || token == "null") {
|
||||
Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
saveToken(this, token)
|
||||
startMainActivity(this@Login)
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
package ani.dantotsu.connections.discord
|
||||
|
||||
import ani.dantotsu.connections.discord.serializers.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import ani.dantotsu.connections.discord.serializers.Activity
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.util.concurrent.TimeUnit.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import ani.dantotsu.client as app
|
||||
|
||||
@@ -31,205 +17,73 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, SECONDS)
|
||||
.readTimeout(10, SECONDS)
|
||||
.writeTimeout(10, SECONDS)
|
||||
.build()
|
||||
|
||||
private val request = Request.Builder()
|
||||
.url("wss://gateway.discord.gg/?encoding=json&v=10")
|
||||
.build()
|
||||
|
||||
private var webSocket = client.newWebSocket(request, Listener())
|
||||
|
||||
var applicationId: String? = null
|
||||
var type: Type? = null
|
||||
var activityName: String? = null
|
||||
var details: String? = null
|
||||
var state: String? = null
|
||||
var largeImage: Link? = null
|
||||
var smallImage: Link? = null
|
||||
var status: String? = null
|
||||
var startTimestamp: Long? = null
|
||||
var stopTimestamp: Long? = null
|
||||
|
||||
enum class Type {
|
||||
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
||||
}
|
||||
|
||||
var buttons = mutableListOf<Link>()
|
||||
|
||||
data class Link(val label: String, val url: String)
|
||||
|
||||
private suspend fun createPresence(): String {
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
Presence(
|
||||
activities = listOf(
|
||||
Activity(
|
||||
name = activityName,
|
||||
state = state,
|
||||
details = details,
|
||||
type = type?.ordinal,
|
||||
timestamps = if (startTimestamp != null)
|
||||
Activity.Timestamps(startTimestamp, stopTimestamp)
|
||||
else null,
|
||||
assets = Activity.Assets(
|
||||
largeImage = largeImage?.url?.discordUrl(),
|
||||
largeText = largeImage?.label,
|
||||
smallImage = smallImage?.url?.discordUrl(),
|
||||
smallText = smallImage?.label
|
||||
),
|
||||
buttons = buttons.map { it.label },
|
||||
metadata = Activity.Metadata(
|
||||
buttonUrls = buttons.map { it.url }
|
||||
),
|
||||
applicationId = applicationId,
|
||||
)
|
||||
),
|
||||
afk = true,
|
||||
since = startTimestamp,
|
||||
status = status
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class KizzyApi(val id: String)
|
||||
val api = "https://kizzy-api.vercel.app/image?url="
|
||||
private suspend fun String.discordUrl(): String? {
|
||||
if (startsWith("mp:")) return this
|
||||
val json = app.get("$api$this").parsedSafe<KizzyApi>()
|
||||
return json?.id
|
||||
}
|
||||
|
||||
private fun sendIdentify() {
|
||||
val response = Identity.Response(
|
||||
op = 2,
|
||||
d = Identity(
|
||||
token = token,
|
||||
properties = Identity.Properties(
|
||||
os = "windows",
|
||||
browser = "Chrome",
|
||||
device = "disco"
|
||||
),
|
||||
compress = false,
|
||||
intents = 0
|
||||
)
|
||||
companion object {
|
||||
data class RPCData(
|
||||
val applicationId: String? = null,
|
||||
val type: Type? = null,
|
||||
val activityName: String? = null,
|
||||
val details: String? = null,
|
||||
val state: String? = null,
|
||||
val largeImage: Link? = null,
|
||||
val smallImage: Link? = null,
|
||||
val status: String? = null,
|
||||
val startTimestamp: Long? = null,
|
||||
val stopTimestamp: Long? = null,
|
||||
val buttons: MutableList<Link> = mutableListOf()
|
||||
)
|
||||
webSocket.send(json.encodeToString(response))
|
||||
}
|
||||
|
||||
fun send(block: RPC.() -> Unit) {
|
||||
block.invoke(this)
|
||||
send()
|
||||
}
|
||||
@Serializable
|
||||
data class KizzyApi(val id: String)
|
||||
|
||||
var started = false
|
||||
var whenStarted: ((User) -> Unit)? = null
|
||||
val api = "https://kizzy-api.vercel.app/image?url="
|
||||
private suspend fun String.discordUrl(): String? {
|
||||
if (startsWith("mp:")) return this
|
||||
val json = app.get("$api$this").parsedSafe<KizzyApi>()
|
||||
return json?.id
|
||||
}
|
||||
|
||||
fun send() {
|
||||
val send = {
|
||||
CoroutineScope(coroutineContext).launch {
|
||||
webSocket.send(createPresence())
|
||||
suspend fun createPresence(data: RPCData): String {
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
allowStructuredMapKeys = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
if (!started) whenStarted = {
|
||||
send.invoke()
|
||||
whenStarted = null
|
||||
}
|
||||
else send.invoke()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
webSocket.send(
|
||||
json.encodeToString(
|
||||
Presence.Response(
|
||||
3,
|
||||
Presence(status = "offline")
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
Presence(
|
||||
activities = listOf(
|
||||
Activity(
|
||||
name = data.activityName,
|
||||
state = data.state,
|
||||
details = data.details,
|
||||
type = data.type?.ordinal,
|
||||
timestamps = if (data.startTimestamp != null)
|
||||
Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
|
||||
else null,
|
||||
assets = Activity.Assets(
|
||||
largeImage = data.largeImage?.url?.discordUrl(),
|
||||
largeText = data.largeImage?.label,
|
||||
smallImage = data.smallImage?.url?.discordUrl(),
|
||||
smallText = data.smallImage?.label
|
||||
),
|
||||
buttons = data.buttons.map { it.label },
|
||||
metadata = Activity.Metadata(
|
||||
buttonUrls = data.buttons.map { it.url }
|
||||
),
|
||||
applicationId = data.applicationId,
|
||||
)
|
||||
),
|
||||
afk = true,
|
||||
since = data.startTimestamp,
|
||||
status = data.status
|
||||
)
|
||||
)
|
||||
)
|
||||
webSocket.close(4000, "Interrupt")
|
||||
}
|
||||
|
||||
//I hate this, but couldn't find any better way to solve it
|
||||
suspend fun getUserData(): User {
|
||||
var user : User? = null
|
||||
whenStarted = {
|
||||
user = it
|
||||
whenStarted = null
|
||||
}
|
||||
while (user == null) {
|
||||
delay(100)
|
||||
}
|
||||
return user!!
|
||||
}
|
||||
|
||||
var onReceiveUserData: ((User) -> Deferred<Unit>)? = null
|
||||
|
||||
inner class Listener : WebSocketListener() {
|
||||
private var seq: Int? = null
|
||||
private var heartbeatInterval: Long? = null
|
||||
|
||||
var scope = CoroutineScope(coroutineContext)
|
||||
|
||||
private fun sendHeartBeat() {
|
||||
scope.cancel()
|
||||
scope = CoroutineScope(coroutineContext)
|
||||
scope.launch {
|
||||
delay(heartbeatInterval!!)
|
||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
println("Message : $text")
|
||||
|
||||
val map = json.decodeFromString<Res>(text)
|
||||
seq = map.s
|
||||
|
||||
when (map.op) {
|
||||
10 -> {
|
||||
map.d as JsonObject
|
||||
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
|
||||
sendHeartBeat()
|
||||
sendIdentify()
|
||||
}
|
||||
|
||||
0 -> if (map.t == "READY") {
|
||||
val user = json.decodeFromString<User.Response>(text).d.user
|
||||
started = true
|
||||
whenStarted?.invoke(user)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
if (scope.isActive) scope.cancel()
|
||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
||||
}
|
||||
|
||||
11 -> sendHeartBeat()
|
||||
7 -> webSocket.close(400, "Reconnect")
|
||||
9 -> {
|
||||
sendHeartBeat()
|
||||
sendIdentify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
println("Server Closed : $code $reason")
|
||||
if (code == 4000) {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
println("Failure : ${t.message}")
|
||||
if (t.message != "Interrupt") {
|
||||
this@RPC.webSocket = client.newWebSocket(request, Listener())
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Activity (
|
||||
data class Activity(
|
||||
@SerialName("application_id")
|
||||
val applicationId: String? = null,
|
||||
val name: String? = null,
|
||||
|
||||
@@ -12,13 +12,13 @@ data class Identity(
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Response (
|
||||
data class Response(
|
||||
val op: Long,
|
||||
val d: Identity
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Properties (
|
||||
data class Properties(
|
||||
@SerialName("\$os")
|
||||
val os: String,
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ package ani.dantotsu.connections.discord.serializers
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Presence (
|
||||
data class Presence(
|
||||
val activities: List<Activity> = listOf(),
|
||||
val afk: Boolean = true,
|
||||
val since: Long? = null,
|
||||
val status: String? = null
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Response (
|
||||
data class Response(
|
||||
val op: Long,
|
||||
val d: Presence
|
||||
)
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
|
||||
@Serializable
|
||||
data class User (
|
||||
val verified: Boolean,
|
||||
data class User(
|
||||
val verified: Boolean? = null,
|
||||
val username: String,
|
||||
|
||||
@SerialName("purchased_flags")
|
||||
val purchasedFlags: Long,
|
||||
val purchasedFlags: Long? = null,
|
||||
|
||||
@SerialName("public_flags")
|
||||
val publicFlags: Long,
|
||||
val publicFlags: Long? = null,
|
||||
|
||||
val pronouns: String,
|
||||
val pronouns: String? = null,
|
||||
|
||||
@SerialName("premium_type")
|
||||
val premiumType: Long,
|
||||
val premiumType: Long? = null,
|
||||
|
||||
val premium: Boolean,
|
||||
val phone: String,
|
||||
val premium: Boolean? = null,
|
||||
val phone: String? = null,
|
||||
|
||||
@SerialName("nsfw_allowed")
|
||||
val nsfwAllowed: Boolean,
|
||||
val nsfwAllowed: Boolean? = null,
|
||||
|
||||
val mobile: Boolean,
|
||||
val mobile: Boolean? = null,
|
||||
|
||||
@SerialName("mfa_enabled")
|
||||
val mfaEnabled: Boolean,
|
||||
val mfaEnabled: Boolean? = null,
|
||||
|
||||
val id: String,
|
||||
|
||||
@SerialName("global_name")
|
||||
val globalName: String,
|
||||
val globalName: String? = null,
|
||||
|
||||
val flags: Long,
|
||||
val email: String,
|
||||
val discriminator: String,
|
||||
val desktop: Boolean,
|
||||
val bio: String,
|
||||
val flags: Long? = null,
|
||||
val email: String? = null,
|
||||
val discriminator: String? = null,
|
||||
val desktop: Boolean? = null,
|
||||
val bio: String? = null,
|
||||
|
||||
@SerialName("banner_color")
|
||||
val bannerColor: String,
|
||||
val bannerColor: String? = null,
|
||||
|
||||
val banner: JsonElement? = null,
|
||||
|
||||
@SerialName("avatar_decoration")
|
||||
val avatarDecoration: JsonElement? = null,
|
||||
|
||||
val avatar: String,
|
||||
val avatar: String? = null,
|
||||
|
||||
@SerialName("accent_color")
|
||||
val accentColor: Long
|
||||
val accentColor: Long? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Response(
|
||||
@@ -70,7 +70,7 @@ data class User (
|
||||
)
|
||||
}
|
||||
|
||||
fun userAvatar():String{
|
||||
fun userAvatar(): String {
|
||||
return "https://cdn.discordapp.com/avatars/$id/$avatar.png"
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,24 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.mal.MAL.clientId
|
||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
try {
|
||||
val data: Uri = intent?.data
|
||||
@@ -44,9 +52,8 @@ class Login : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e:Exception){
|
||||
logError(e,snackbar = false)
|
||||
} catch (e: Exception) {
|
||||
logError(e, snackbar = false)
|
||||
startMainActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,13 @@ import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
@@ -94,6 +100,6 @@ object MAL {
|
||||
@SerialName("expires_in") var expiresIn: Long,
|
||||
@SerialName("access_token") val accessToken: String,
|
||||
@SerialName("refresh_token") val refreshToken: String,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ani.dantotsu.connections.mal
|
||||
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -43,18 +43,18 @@ class MALQueries {
|
||||
start: FuzzyDate? = null,
|
||||
end: FuzzyDate? = null
|
||||
) {
|
||||
if(idMAL==null) return
|
||||
if (idMAL == null) return
|
||||
val data = mutableMapOf("status" to convertStatus(isAnime, status))
|
||||
if (progress != null)
|
||||
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
|
||||
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
|
||||
if (score != null)
|
||||
data["score"] = score.div(10).toString()
|
||||
if(rewatch!=null)
|
||||
data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
||||
if(start!=null)
|
||||
if (rewatch != null)
|
||||
data[if (isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
||||
if (start != null)
|
||||
data["start_date"] = start.toMALString()
|
||||
if(end!=null)
|
||||
if (end != null)
|
||||
data["finish_date"] = end.toMALString()
|
||||
tryWithSuspend {
|
||||
client.put(
|
||||
@@ -65,8 +65,8 @@ class MALQueries {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteList(isAnime: Boolean, idMAL: Int?){
|
||||
if(idMAL==null) return
|
||||
suspend fun deleteList(isAnime: Boolean, idMAL: Int?) {
|
||||
if (idMAL == null) return
|
||||
tryWithSuspend {
|
||||
client.delete(
|
||||
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
||||
|
||||
@@ -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.others.LangSet
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class DownloadContainerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
setContentView(R.layout.activity_container)
|
||||
|
||||
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
val fragment = Class.forName(fragmentClassName).newInstance() as Fragment
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
246
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
246
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
@@ -0,0 +1,246 @@
|
||||
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 }
|
||||
val novelDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.NOVEL }
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: Download.Type) {
|
||||
val subDirectory = if (type == Download.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
)
|
||||
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()
|
||||
cleanDownloads()
|
||||
}
|
||||
downloadsList.removeAll { it.title == title }
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun cleanDownloads() {
|
||||
cleanDownload(Download.Type.MANGA)
|
||||
cleanDownload(Download.Type.ANIME)
|
||||
cleanDownload(Download.Type.NOVEL)
|
||||
}
|
||||
|
||||
private fun cleanDownload(type: Download.Type) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = if (type == Download.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory"
|
||||
)
|
||||
val downloadsSubList = if (type == Download.Type.MANGA) {
|
||||
mangaDownloads
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
animeDownloads
|
||||
} else {
|
||||
novelDownloads
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubList.any { it.title == file.name }) {
|
||||
val deleted = file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//now remove all downloads that do not have a folder
|
||||
val iterator = downloadsList.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val download = iterator.next()
|
||||
val downloadDir = File(directory, download.title)
|
||||
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<Download>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
file.parentFile?.mkdirs()
|
||||
}
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(download: Download): Boolean {
|
||||
return downloadsList.contains(download)
|
||||
}
|
||||
|
||||
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 if (download.type == Download.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${download.title}/${download.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${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 if (download.type == Download.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${download.title}/${download.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${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 if (type == Download.Type.ANIME) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
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()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val novelLocation = "Dantotsu/Novel"
|
||||
const val mangaLocation = "Dantotsu/Manga"
|
||||
const val animeLocation = "Dantotsu/Anime"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME,
|
||||
NOVEL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
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.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
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.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class 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)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
cancelReceiver,
|
||||
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
MangaServiceDataSingleton.downloadQueue.clear()
|
||||
downloadJobs.clear()
|
||||
MangaServiceDataSingleton.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 (MangaServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||
val task = MangaServiceDataSingleton.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 (MangaServiceDataSingleton.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)
|
||||
MangaServiceDataSingleton.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 = MangaServiceDataSingleton.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) {
|
||||
try {
|
||||
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")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
broadcastDownloadFailed(task.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 MangaServiceDataSingleton {
|
||||
var imageData: List<ImageData> = listOf()
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
var isServiceRunning: Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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 var items: List<OfflineMangaModel>,
|
||||
private val searchListener: OfflineMangaSearchListener
|
||||
) : BaseAdapter() {
|
||||
private val inflater: LayoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var originalItems: List<OfflineMangaModel> = items
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun onSearchQuery(query: String) {
|
||||
// Implement the filtering logic here, for example:
|
||||
items = if (query.isEmpty()) {
|
||||
// Return the original list if the query is empty
|
||||
originalItems
|
||||
} else {
|
||||
// Filter the list based on the query
|
||||
originalItems.filter { it.title.contains(query, ignoreCase = true) }
|
||||
}
|
||||
notifyDataSetChanged() // Notify the adapter that the data set has changed
|
||||
}
|
||||
|
||||
fun setItems(items: List<OfflineMangaModel>) {
|
||||
this.items = items
|
||||
this.originalItems = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
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.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.GridView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
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.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private var downloads: List<OfflineMangaModel> = listOf()
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineMangaAdapter
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||
searchView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
onSearchQuery(s.toString())
|
||||
}
|
||||
})
|
||||
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
getDownloads()
|
||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||
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.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title }
|
||||
media?.let {
|
||||
startActivity(
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true)
|
||||
)
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
}
|
||||
|
||||
gridView.setOnItemLongClickListener { parent, view, position, id ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) {
|
||||
Download.Type.MANGA
|
||||
} else {
|
||||
Download.Type.NOVEL
|
||||
}
|
||||
// Alert dialog to confirm deletion
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete ${item.title}?")
|
||||
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onSearchQuery(query: String) {
|
||||
adapter.onSearchQuery(query)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
var height = statusBarHeight
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
height = max(
|
||||
statusBarHeight,
|
||||
min(
|
||||
displayCutout.boundingRects[0].width(),
|
||||
displayCutout.boundingRects[0].height()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
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 onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val _downloads = downloadManager.novelDownloads.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
|
||||
}
|
||||
|
||||
private fun getMedia(download: Download): Media? {
|
||||
val type = if (download.type == Download.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${download.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
return try {
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel {
|
||||
val type = if (download.type == Download.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${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 ?: mediaModel.nameRomaji
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OfflineMangaSearchListener {
|
||||
fun onSearchQuery(query: String)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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?
|
||||
)
|
||||
@@ -0,0 +1,478 @@
|
||||
package ani.dantotsu.download.novel
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class NovelDownloaderService : 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
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder =
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Novel Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
cancelReceiver,
|
||||
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
NovelServiceDataSingleton.downloadQueue.clear()
|
||||
downloadJobs.clear()
|
||||
NovelServiceDataSingleton.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 (NovelServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||
val task = NovelServiceDataSingleton.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 (NovelServiceDataSingleton.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)
|
||||
NovelServiceDataSingleton.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 = NovelServiceDataSingleton.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())
|
||||
}
|
||||
|
||||
private suspend fun isEpubFile(urlString: String): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(urlString)
|
||||
.head()
|
||||
.build()
|
||||
|
||||
networkHelper.client.newCall(request).execute().use { response ->
|
||||
val contentType = response.header("Content-Type")
|
||||
val contentDisposition = response.header("Content-Disposition")
|
||||
|
||||
logger("Content-Type: $contentType")
|
||||
logger("Content-Disposition: $contentDisposition")
|
||||
|
||||
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
|
||||
contentType == "application/epub+zip" ||
|
||||
(contentDisposition?.contains(".epub") == true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Error checking file type: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAlreadyDownloaded(urlString: String): Boolean {
|
||||
return urlString.contains("file://")
|
||||
}
|
||||
|
||||
suspend fun download(task: DownloadTask) {
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this@NovelDownloaderService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
broadcastDownloadStarted(task.originalLink)
|
||||
|
||||
if (notifi) {
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
if (!isEpubFile(task.downloadLink)) {
|
||||
if (isAlreadyDownloaded(task.originalLink)) {
|
||||
logger("Already downloaded")
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
snackString("Already downloaded")
|
||||
return@withContext
|
||||
}
|
||||
logger("Download link is not an .epub file")
|
||||
broadcastDownloadFailed(task.originalLink)
|
||||
snackString("Download link is not an .epub file")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
// Start the download
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(task.downloadLink)
|
||||
.build()
|
||||
|
||||
networkHelper.downloadClient.newCall(request).execute().use { response ->
|
||||
// Ensure the response is successful and has a body
|
||||
if (!response.isSuccessful || response.body == null) {
|
||||
throw IOException("Failed to download file: ${response.message}")
|
||||
}
|
||||
|
||||
val file = File(
|
||||
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
|
||||
)
|
||||
|
||||
// Create directories if they don't exist
|
||||
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
|
||||
|
||||
// Overwrite existing file
|
||||
if (file.exists()) file.delete()
|
||||
|
||||
//download cover
|
||||
task.coverUrl?.let {
|
||||
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
||||
}
|
||||
|
||||
val sink = file.sink().buffer()
|
||||
val responseBody = response.body
|
||||
val totalBytes = responseBody.contentLength()
|
||||
var downloadedBytes = 0L
|
||||
|
||||
val notificationUpdateInterval = 1024 * 1024 // 1 MB
|
||||
val broadcastUpdateInterval = 1024 * 256 // 256 KB
|
||||
var lastNotificationUpdate = 0L
|
||||
var lastBroadcastUpdate = 0L
|
||||
|
||||
responseBody.source().use { source ->
|
||||
while (true) {
|
||||
val read = source.read(sink.buffer, 8192)
|
||||
if (read == -1L) break
|
||||
downloadedBytes += read
|
||||
sink.emit()
|
||||
|
||||
// Update progress at intervals
|
||||
if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val progress =
|
||||
(downloadedBytes * 100 / totalBytes).toInt()
|
||||
builder.setProgress(100, progress, false)
|
||||
if (notifi) {
|
||||
notificationManager.notify(
|
||||
NOTIFICATION_ID,
|
||||
builder.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
lastNotificationUpdate = downloadedBytes
|
||||
}
|
||||
if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val progress =
|
||||
(downloadedBytes * 100 / totalBytes).toInt()
|
||||
logger("Download progress: $progress")
|
||||
broadcastDownloadProgress(task.originalLink, progress)
|
||||
}
|
||||
lastBroadcastUpdate = downloadedBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sink.close()
|
||||
//if the file is smaller than 95% of totalBytes, it means the download was interrupted
|
||||
if (file.length() < totalBytes * 0.95) {
|
||||
throw IOException("Failed to download file: ${response.message}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading .epub inside request: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Update notification for download completion
|
||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
saveMediaInfo(task)
|
||||
downloadsManager.addDownload(
|
||||
Download(
|
||||
task.title,
|
||||
task.chapter,
|
||||
Download.Type.NOVEL
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading .epub: ${e.message}")
|
||||
snackString("Exception while downloading .epub: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
broadcastDownloadFailed(task.originalLink)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${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@NovelDownloaderService,
|
||||
"Exception while saving ${name}: ${e.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
null
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastDownloadStarted(link: String) {
|
||||
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_STARTED).apply {
|
||||
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFinished(link: String) {
|
||||
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FINISHED).apply {
|
||||
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFailed(link: String) {
|
||||
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FAILED).apply {
|
||||
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadProgress(link: String, progress: Int) {
|
||||
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_PROGRESS).apply {
|
||||
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||
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 downloadLink: String,
|
||||
val originalLink: String,
|
||||
val sourceMedia: Media? = null,
|
||||
val coverUrl: String? = null,
|
||||
val retries: 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 NovelServiceDataSingleton {
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
var isServiceRunning: Boolean = false
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.*
|
||||
@@ -35,9 +38,10 @@ import java.util.concurrent.*
|
||||
object Helper {
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun downloadVideo(context : Context, video: Video, subtitle: Subtitle?){
|
||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
@@ -49,7 +53,7 @@ object Helper {
|
||||
val mimeType = when (video.format) {
|
||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
||||
else -> MimeTypes.APPLICATION_MP4
|
||||
else -> MimeTypes.APPLICATION_MP4
|
||||
}
|
||||
|
||||
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
||||
@@ -76,12 +80,13 @@ object Helper {
|
||||
DefaultRenderersFactory(context),
|
||||
dataSourceFactory
|
||||
)
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback{
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||
override fun onPrepared(helper: DownloadHelper) {
|
||||
TrackSelectionDialogBuilder(context,"Select thingy",helper.getTracks(0).groups
|
||||
TrackSelectionDialogBuilder(
|
||||
context, "Select thingy", helper.getTracks(0).groups
|
||||
) { _, overrides ->
|
||||
val params = TrackSelectionParameters.Builder(context)
|
||||
overrides.forEach{
|
||||
overrides.forEach {
|
||||
params.addOverride(it.value)
|
||||
}
|
||||
helper.addTrackSelection(0, params.build())
|
||||
@@ -118,7 +123,11 @@ object Helper {
|
||||
val database = StandaloneDatabaseProvider(context)
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
@@ -131,7 +140,8 @@ object Helper {
|
||||
dataSourceFactory,
|
||||
Executor(Runnable::run)
|
||||
).apply {
|
||||
requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
maxParallelDownloads = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
|
||||
|
||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
||||
|
||||
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
|
||||
override fun getForegroundNotification(
|
||||
downloads: MutableList<Download>,
|
||||
notMetRequirements: Int
|
||||
): Notification =
|
||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||
this,
|
||||
R.drawable.monochrome,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
|
||||
@@ -48,7 +48,8 @@ class AnimeFragment : Fragment() {
|
||||
private var _binding: FragmentAnimeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
val model: AnilistAnimeViewModel by activityViewModels()
|
||||
|
||||
@@ -224,7 +225,8 @@ class AnimeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.animePageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
binding.animePageScrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -15,13 +17,15 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.CalendarActivity
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.px
|
||||
@@ -31,6 +35,8 @@ import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
||||
val ready = MutableLiveData(false)
|
||||
@@ -38,10 +44,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
||||
val binding = ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return AnimePageViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -49,6 +57,25 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
binding = holder.binding
|
||||
trendingViewPager = binding.animeTrendingViewPager
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -71,7 +98,10 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
binding.animeUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.ANIME).show(
|
||||
(it.context as AppCompatActivity).supportFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
|
||||
listOf(
|
||||
@@ -101,7 +131,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.animeIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE
|
||||
binding.animeIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
}
|
||||
@@ -109,9 +140,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
ready.postValue(true)
|
||||
}
|
||||
|
||||
lateinit var onSeasonClick : ((Int)->Unit)
|
||||
lateinit var onSeasonLongClick : ((Int)->Boolean)
|
||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
||||
lateinit var onSeasonClick: ((Int) -> Unit)
|
||||
lateinit var onSeasonLongClick: ((Int) -> Boolean)
|
||||
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
@@ -128,7 +159,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.animeTrendingViewPager.currentItem = binding.animeTrendingViewPager.currentItem + 1
|
||||
binding.animeTrendingViewPager.currentItem =
|
||||
binding.animeTrendingViewPager.currentItem + 1
|
||||
}
|
||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
@@ -140,22 +172,30 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
)
|
||||
|
||||
binding.animeTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeSeasonsCont.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeSeasonsCont.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||
binding.animeUpdatedRecyclerView.layoutManager =
|
||||
LinearLayoutManager(binding.animeUpdatedRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
LinearLayoutManager(
|
||||
binding.animeUpdatedRecyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeRecently.visibility = View.VISIBLE
|
||||
binding.animeRecently.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animePopular.visibility = View.VISIBLE
|
||||
binding.animePopular.startAnimation(setSlideUp(uiSettings))
|
||||
}
|
||||
@@ -167,5 +207,6 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
}
|
||||
|
||||
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
@@ -31,13 +31,13 @@ import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.media.user.ListActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -52,7 +52,11 @@ class HomeFragment : Fragment() {
|
||||
private var _binding: FragmentHomeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -96,10 +100,12 @@ class HomeFragment : Fragment() {
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeUserDataContainer.visibility = View.VISIBLE
|
||||
binding.homeUserDataContainer.layoutAnimation = LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
||||
binding.homeUserDataContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
||||
binding.homeAnimeList.visibility = View.VISIBLE
|
||||
binding.homeMangaList.visibility = View.VISIBLE
|
||||
binding.homeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.homeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
}
|
||||
else {
|
||||
snackString(currContext()?.getString(R.string.please_reload))
|
||||
@@ -107,7 +113,10 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show(parentFragmentManager, "dialog")
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(
|
||||
parentFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
|
||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -123,11 +132,13 @@ class HomeFragment : Fragment() {
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||
.start()
|
||||
} else {
|
||||
if (reached) {
|
||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +149,13 @@ class HomeFragment : Fragment() {
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
height =
|
||||
max(statusBarHeight, min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height()))
|
||||
max(
|
||||
statusBarHeight,
|
||||
min(
|
||||
displayCutout.boundingRects[0].width(),
|
||||
displayCutout.boundingRects[0].height()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +206,8 @@ class HomeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
recyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
|
||||
} else {
|
||||
empty.visibility = View.VISIBLE
|
||||
@@ -313,7 +331,8 @@ class HomeFragment : Fragment() {
|
||||
live.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
withContext(Dispatchers.IO) {
|
||||
//Get userData First
|
||||
getUserId(requireContext()) {
|
||||
|
||||
@@ -15,7 +15,11 @@ class LoginFragment : Fragment() {
|
||||
private var _binding: FragmentLoginBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -44,11 +44,16 @@ class MangaFragment : Fragment() {
|
||||
private var _binding: FragmentMangaBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
val model: AnilistMangaViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentMangaBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -100,7 +105,8 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
||||
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
||||
binding.mangaPageRecyclerView.adapter = ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
||||
binding.mangaPageRecyclerView.adapter =
|
||||
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
||||
val layout = LinearLayoutManager(requireContext())
|
||||
binding.mangaPageRecyclerView.layoutManager = layout
|
||||
|
||||
@@ -177,7 +183,8 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.mangaPageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
binding.mangaPageScrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -15,13 +17,14 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.px
|
||||
@@ -31,6 +34,8 @@ import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
||||
val ready = MutableLiveData(false)
|
||||
@@ -38,10 +43,12 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
||||
val binding = ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return MangaPageViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -49,6 +56,25 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
binding = holder.binding
|
||||
trendingViewPager = binding.mangaTrendingViewPager
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -67,7 +93,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
|
||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.MANGA).show(
|
||||
(it.context as AppCompatActivity).supportFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
|
||||
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||
@@ -95,7 +124,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.mangaIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE
|
||||
binding.mangaIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
}
|
||||
@@ -104,7 +134,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
ready.postValue(true)
|
||||
}
|
||||
|
||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
||||
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
@@ -120,7 +150,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem + 1
|
||||
binding.mangaTrendingViewPager.currentItem =
|
||||
binding.mangaTrendingViewPager.currentItem + 1
|
||||
}
|
||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
@@ -132,21 +163,28 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
)
|
||||
|
||||
binding.mangaTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
}
|
||||
|
||||
fun updateNovel(adaptor: MediaAdaptor) {
|
||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||
binding.mangaNovelRecyclerView.layoutManager =
|
||||
LinearLayoutManager(binding.mangaNovelRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
LinearLayoutManager(
|
||||
binding.mangaNovelRecyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.mangaNovel.visibility = View.VISIBLE
|
||||
binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaNovelRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaPopular.visibility = View.VISIBLE
|
||||
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
|
||||
}
|
||||
@@ -158,5 +196,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
}
|
||||
|
||||
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,107 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.offline.OfflineFragment
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.selectedOption
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
class NoInternet : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityNoInternetBinding
|
||||
lateinit var bottomBar: AnimatedBottomBar
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
val binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
binding.refreshButton.setOnClickListener {
|
||||
if (isOnline(this)) {
|
||||
startMainActivity(this)
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
}
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
ani.dantotsu.bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
|
||||
//supportFragmentManager.beginTransaction().replace(binding.fragmentContainer.id, OfflineFragment()).commit()
|
||||
|
||||
}
|
||||
|
||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
when (position) {
|
||||
0 -> return OfflineFragment()
|
||||
1 -> return OfflineFragment()
|
||||
2 -> return OfflineMangaFragment()
|
||||
}
|
||||
return LoginFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,16 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.EmptyAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityAuthorBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -29,6 +36,7 @@ class AuthorActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -12,7 +14,10 @@ import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
@@ -29,27 +34,51 @@ class CalendarActivity : AppCompatActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue, true)
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
val primaryColor = typedValue.data
|
||||
val typedValue2 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue2, true)
|
||||
val primaryTextColor = typedValue2.data
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue2,
|
||||
true
|
||||
)
|
||||
val titleTextColor = typedValue2.data
|
||||
val typedValue3 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimaryContainer, typedValue3, true)
|
||||
val secondaryColor = typedValue3.data
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||
val primaryTextColor = typedValue3.data
|
||||
val typedValue4 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
|
||||
val secondaryTextColor = typedValue4.data
|
||||
|
||||
window.statusBarColor = primaryColor
|
||||
window.navigationBarColor = primaryColor
|
||||
binding.listTabLayout.setBackgroundColor(primaryColor)
|
||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(primaryTextColor, secondaryColor)
|
||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) {
|
||||
this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
binding.root.fitsSystemWindows = true
|
||||
|
||||
} else {
|
||||
binding.root.fitsSystemWindows = false
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.listTitle.setText(R.string.release_calendar)
|
||||
binding.listSort.visibility = View.GONE
|
||||
|
||||
@@ -57,14 +86,15 @@ class CalendarActivity : AppCompatActivity() {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
||||
}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) { }
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) { }
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
|
||||
model.getCalendar().observe(this) {
|
||||
if (it != null) {
|
||||
binding.listProgressBar.visibility = View.GONE
|
||||
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true,this)
|
||||
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true, this)
|
||||
val keys = it.keys.toList()
|
||||
val values = it.values.toList()
|
||||
val savedTab = this.selectedTabIdx
|
||||
@@ -86,4 +116,4 @@ class CalendarActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ class CharacterAdapter(
|
||||
private val characterList: ArrayList<Character>
|
||||
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
|
||||
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return CharacterViewHolder(binding)
|
||||
}
|
||||
|
||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
private val uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||
@@ -38,16 +40,23 @@ class CharacterAdapter(
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = characterList.size
|
||||
inner class CharacterViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class CharacterViewHolder(val binding: ItemCharacterBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
val char = characterList[bindingAdapterPosition]
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
Intent(itemView.context, CharacterDetailsActivity::class.java).putExtra("character", char as Serializable),
|
||||
Intent(
|
||||
itemView.context,
|
||||
CharacterDetailsActivity::class.java
|
||||
).putExtra("character", char as Serializable),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
itemView.context as Activity,
|
||||
Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!),
|
||||
Pair.create(
|
||||
binding.itemCompactImage,
|
||||
ViewCompat.getTransitionName(binding.itemCompactImage)!!
|
||||
),
|
||||
).toBundle()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,11 +13,19 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityCharacterBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -34,15 +42,18 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
initActivity(this)
|
||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
|
||||
val banner = if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
|
||||
|
||||
banner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
@@ -59,7 +70,13 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
binding.characterTitle.text = character.name
|
||||
banner.loadImage(character.banner)
|
||||
binding.characterCoverImage.loadImage(character.image)
|
||||
binding.characterCoverImage.setOnLongClickListener { ImageViewDialog.newInstance(this, character.name, character.image) }
|
||||
binding.characterCoverImage.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
this,
|
||||
character.name,
|
||||
character.image
|
||||
)
|
||||
}
|
||||
|
||||
model.getCharacter().observe(this) {
|
||||
if (it != null && !loaded) {
|
||||
@@ -71,14 +88,15 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
val roles = character.roles
|
||||
if (roles != null) {
|
||||
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true)
|
||||
val concatAdaptor = ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
|
||||
val concatAdaptor =
|
||||
ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
|
||||
|
||||
val gridSize = (screenWidth / 124f).toInt()
|
||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (position) {
|
||||
0 -> gridSize
|
||||
0 -> gridSize
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
@@ -116,16 +134,19 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
binding.characterCover.scaleY = 1f * cap
|
||||
binding.characterCover.cardElevation = 32f * cap
|
||||
|
||||
binding.characterCover.visibility = if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
binding.characterCover.visibility =
|
||||
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
|
||||
if (percentage >= percent && !isCollapsed) {
|
||||
isCollapsed = true
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
|
||||
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
|
||||
val binding = ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return GenreViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -23,20 +24,22 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val desc =
|
||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
||||
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when(character.gender){
|
||||
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||
"Male" -> currActivity()!!.getString(R.string.male)
|
||||
"Female" -> currActivity()!!.getString(R.string.female)
|
||||
else -> character.gender
|
||||
} else "") + "\n" + character.description
|
||||
|
||||
binding.characterDesc.isTextSelectable
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(SpoilerPlugin()).build()
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SpoilerPlugin()).build()
|
||||
markWon.setMarkdown(binding.characterDesc, desc)
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -26,6 +27,7 @@ class GenreActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -48,7 +50,8 @@ class GenreActivity : AppCompatActivity() {
|
||||
model.doneListener?.invoke()
|
||||
}
|
||||
binding.mediaInfoGenresRecyclerView.adapter = adapter
|
||||
binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||
binding.mediaInfoGenresRecyclerView.layoutManager =
|
||||
GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
|
||||
|
||||
@@ -37,7 +37,8 @@ class GenreAdapter(
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = genres.size
|
||||
inner class GenreViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class GenreViewHolder(val binding: ItemGenreBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
@@ -48,15 +49,15 @@ class GenreAdapter(
|
||||
.putExtra("sortBy", Anilist.sortBy[2])
|
||||
.putExtra("search", true)
|
||||
.also {
|
||||
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
||||
if (!Anilist.adult) Toast.makeText(
|
||||
itemView.context,
|
||||
currActivity()?.getString(R.string.content_18),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
it.putExtra("hentai", true)
|
||||
}
|
||||
},
|
||||
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
||||
if (!Anilist.adult) Toast.makeText(
|
||||
itemView.context,
|
||||
currActivity()?.getString(R.string.content_18),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
it.putExtra("hentai", true)
|
||||
}
|
||||
},
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.MediaEdge
|
||||
import ani.dantotsu.connections.anilist.api.MediaList
|
||||
@@ -22,7 +23,7 @@ data class Media(
|
||||
val userPreferredName: String,
|
||||
|
||||
var cover: String? = null,
|
||||
val banner: String? = null,
|
||||
var banner: String? = null,
|
||||
var relation: String? = null,
|
||||
var popularity: Int? = null,
|
||||
|
||||
@@ -40,7 +41,7 @@ data class Media(
|
||||
var userUpdatedAt: Long? = null,
|
||||
var userStartedAt: FuzzyDate = FuzzyDate(),
|
||||
var userCompletedAt: FuzzyDate = FuzzyDate(),
|
||||
var inCustomListsOf: MutableMap<String, Boolean>?= null,
|
||||
var inCustomListsOf: MutableMap<String, Boolean>? = null,
|
||||
var userFavOrder: Int? = null,
|
||||
|
||||
val status: String? = null,
|
||||
@@ -69,7 +70,7 @@ data class Media(
|
||||
var shareLink: String? = null,
|
||||
var selected: Selected? = null,
|
||||
|
||||
var idKitsu: String?=null,
|
||||
var idKitsu: String? = null,
|
||||
|
||||
var cameFromContinue: Boolean = false
|
||||
) : Serializable {
|
||||
@@ -119,4 +120,5 @@ data class Media(
|
||||
|
||||
object MediaSingleton {
|
||||
var media: Media? = null
|
||||
var bitmap: Bitmap? = null
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -37,20 +41,43 @@ class MediaAdaptor(
|
||||
private val viewPager: ViewPager2? = null,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
private val uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (type) {
|
||||
0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
3 -> MediaPageSmallViewHolder(
|
||||
0 -> MediaViewHolder(
|
||||
ItemMediaCompactBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
1 -> MediaLargeViewHolder(
|
||||
ItemMediaLargeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
2 -> MediaPageViewHolder(
|
||||
ItemMediaPageBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
3 -> MediaPageSmallViewHolder(
|
||||
ItemMediaPageSmallBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
@@ -65,10 +92,12 @@ class MediaAdaptor(
|
||||
val media = mediaList?.getOrNull(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
?: 0) else media.userScore) / 10.0).toString()
|
||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||
b.root.context,
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
@@ -100,6 +129,7 @@ class MediaAdaptor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1 -> {
|
||||
val b = (holder as MediaLargeViewHolder).binding
|
||||
setAnimation(activity, b.root, uiSettings)
|
||||
@@ -107,22 +137,29 @@ class MediaAdaptor(
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
|
||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
?: 0) else media.userScore) / 10.0).toString()
|
||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||
b.root.context,
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
)
|
||||
if (media.anime != null) {
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
b.itemCompactTotal.text =
|
||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
||||
?: "??").toString()) else (media.anime.totalEpisodes
|
||||
?: "??").toString()
|
||||
} else if (media.manga != null) {
|
||||
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
||||
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.chapter_plural)
|
||||
else currActivity()!!.getString(R.string.chapter_singular)
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@@ -133,6 +170,7 @@ class MediaAdaptor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val b = (holder as MediaPageViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
@@ -145,7 +183,8 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
@@ -153,22 +192,29 @@ class MediaAdaptor(
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.into(banner)
|
||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
?: 0) else media.userScore) / 10.0).toString()
|
||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||
b.root.context,
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
)
|
||||
if (media.anime != null) {
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
b.itemCompactTotal.text =
|
||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
||||
?: "??").toString()) else (media.anime.totalEpisodes
|
||||
?: "??").toString()
|
||||
} else if (media.manga != null) {
|
||||
b.itemTotal.text =" " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
||||
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.chapter_plural)
|
||||
else currActivity()!!.getString(R.string.chapter_singular)
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@@ -180,6 +226,7 @@ class MediaAdaptor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3 -> {
|
||||
val b = (holder as MediaPageSmallViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
@@ -192,7 +239,8 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
@@ -200,10 +248,12 @@ class MediaAdaptor(
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.into(banner)
|
||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
?: 0) else media.userScore) / 10.0).toString()
|
||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||
b.root.context,
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
@@ -218,13 +268,18 @@ class MediaAdaptor(
|
||||
}
|
||||
b.itemCompactStatus.text = media.status ?: ""
|
||||
if (media.anime != null) {
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
b.itemCompactTotal.text =
|
||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
||||
?: "??").toString()) else (media.anime.totalEpisodes
|
||||
?: "??").toString()
|
||||
} else if (media.manga != null) {
|
||||
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
||||
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.chapter_plural)
|
||||
else currActivity()!!.getString(R.string.chapter_singular)
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@@ -245,43 +300,86 @@ class MediaAdaptor(
|
||||
return type
|
||||
}
|
||||
|
||||
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun randomOptionClick() { //used for user list
|
||||
val media = mediaList?.random()
|
||||
if (media != null) {
|
||||
mediaList?.let {
|
||||
clicked(
|
||||
it.indexOf(media),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
if (matchParent) itemView.updateLayoutParams { width = -1 }
|
||||
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
itemView.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
itemView.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
itemView.setOnTouchListener { _, _ -> true }
|
||||
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
binding.itemCompactTitleContainer.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
itemView.setOnTouchListener { _, _ -> true }
|
||||
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
fun clicked(position: Int) {
|
||||
fun clicked(position: Int, bitmap: Bitmap? = null) {
|
||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||
val media = mediaList?.get(position)
|
||||
if (bitmap != null) MediaSingleton.bitmap = bitmap
|
||||
ContextCompat.startActivity(
|
||||
activity,
|
||||
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
||||
@@ -296,10 +394,53 @@ class MediaAdaptor(
|
||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||
val media = mediaList?.get(position) ?: return false
|
||||
if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
|
||||
MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list")
|
||||
MediaListDialogSmallFragment.newInstance(media)
|
||||
.show(activity.supportFragmentManager, "list")
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getBitmapFromImageView(imageView: ImageView): Bitmap? {
|
||||
val drawable = imageView.drawable ?: return null
|
||||
|
||||
// If the drawable is a BitmapDrawable, then just get the bitmap
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
|
||||
// Create a bitmap with the same dimensions as the drawable
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
|
||||
// Draw the drawable onto the bitmap
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun resizeBitmap(source: Bitmap?, maxDimension: Int): Bitmap? {
|
||||
if (source == null) return null
|
||||
val width = source.width
|
||||
val height = source.height
|
||||
val newWidth: Int
|
||||
val newHeight: Int
|
||||
|
||||
if (width > height) {
|
||||
newWidth = maxDimension
|
||||
newHeight = (height * (maxDimension.toFloat() / width)).toInt()
|
||||
} else {
|
||||
newHeight = maxDimension
|
||||
newWidth = (width * (maxDimension.toFloat() / height)).toInt()
|
||||
}
|
||||
|
||||
return Bitmap.createScaledBitmap(source, newWidth, newHeight, true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,17 +31,18 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.media.manga.MangaReadFragment
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
@@ -71,8 +72,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
LangSet.setLocale(this)
|
||||
var media: Media = intent.getSerialized("media") ?: return
|
||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||
MediaSingleton.bitmap = null
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
@@ -117,8 +121,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
viewPager.isUserInputEnabled = false
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
|
||||
var media: Media = intent.getSerialized("media") ?: return
|
||||
media.selected = model.loadSelected(media)
|
||||
|
||||
val isDownload = intent.getBooleanExtra("download", false)
|
||||
media.selected = model.loadSelected(media, isDownload)
|
||||
|
||||
binding.mediaCoverImage.loadImage(media.cover)
|
||||
binding.mediaCoverImage.setOnLongClickListener {
|
||||
@@ -190,7 +195,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
R.drawable.ic_round_favorite_24,
|
||||
R.drawable.ic_round_favorite_border_24,
|
||||
R.color.bg_opp,
|
||||
R.color.violet_400,
|
||||
R.color.violet_400,//TODO: Change to colorSecondary
|
||||
media.isFav
|
||||
) {
|
||||
media.isFav = it
|
||||
@@ -324,7 +329,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
tabLayout.setOnItemSelectedListener { item ->
|
||||
selectFromID(item.itemId)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
val sel = model.loadSelected(media)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel, this)
|
||||
true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -9,18 +8,22 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.others.AniSkip
|
||||
import ani.dantotsu.others.Jikan
|
||||
import ani.dantotsu.others.Kitsu
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.Book
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
@@ -28,20 +31,10 @@ import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.AniyomiAdapter
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@@ -53,7 +46,7 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
|
||||
fun loadSelected(media: Media): Selected {
|
||||
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
||||
val sharedPreferences = Injekt.get<SharedPreferences>()
|
||||
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
||||
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
||||
@@ -64,13 +57,24 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
if (isDownload) {
|
||||
data.sourceIndex = if (media.anime != null) {
|
||||
AnimeSources.list.size - 1
|
||||
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||
MangaSources.list.size - 1
|
||||
} else {
|
||||
NovelSources.list.size - 1
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
fun loadSelectedStringLocation(sourceName: String): Int {
|
||||
//find the location of the source in the list
|
||||
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
||||
if (location == -1) {location = 0}
|
||||
if (location == -1) {
|
||||
location = 0
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
@@ -95,7 +99,9 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
|
||||
//Anime
|
||||
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
|
||||
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> =
|
||||
MutableLiveData<Map<String, Episode>>(null)
|
||||
|
||||
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
|
||||
suspend fun loadKitsuEpisodes(s: Media) {
|
||||
tryWithSuspend {
|
||||
@@ -103,7 +109,9 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
|
||||
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
|
||||
MutableLiveData<Map<String, Episode>>(null)
|
||||
|
||||
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
|
||||
suspend fun loadFillerEpisodes(s: Media) {
|
||||
tryWithSuspend {
|
||||
@@ -120,8 +128,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
|
||||
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
|
||||
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
|
||||
suspend fun loadEpisodes(media: Media, i: Int) {
|
||||
if (!epsLoaded.containsKey(i)) {
|
||||
suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
|
||||
if (!epsLoaded.containsKey(i) || invalidate) {
|
||||
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
|
||||
}
|
||||
episodes.postValue(epsLoaded)
|
||||
@@ -134,7 +142,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
|
||||
watchSources?.saveResponse(i, id, source)
|
||||
epsLoaded[i] = watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
|
||||
epsLoaded[i] =
|
||||
watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
|
||||
episodes.postValue(epsLoaded)
|
||||
}
|
||||
|
||||
@@ -173,7 +182,12 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
|
||||
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
|
||||
suspend fun loadTimeStamps(malId: Int?, episodeNum: Int?, duration: Long, useProxyForTimeStamps: Boolean) {
|
||||
suspend fun loadTimeStamps(
|
||||
malId: Int?,
|
||||
episodeNum: Int?,
|
||||
duration: Long,
|
||||
useProxyForTimeStamps: Boolean
|
||||
) {
|
||||
malId ?: return
|
||||
episodeNum ?: return
|
||||
if (timeStampsMap.containsKey(episodeNum))
|
||||
@@ -183,7 +197,11 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
timeStamps.postValue(result)
|
||||
}
|
||||
|
||||
suspend fun loadEpisodeSingleVideo(ep: Episode, selected: Selected, post: Boolean = true): Boolean {
|
||||
suspend fun loadEpisodeSingleVideo(
|
||||
ep: Episode,
|
||||
selected: Selected,
|
||||
post: Boolean = true
|
||||
): Boolean {
|
||||
if (ep.extractors.isNullOrEmpty()) {
|
||||
|
||||
val server = selected.server ?: return false
|
||||
@@ -193,8 +211,10 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
selected.sourceIndex = selected.sourceIndex
|
||||
if (!post && !it.allowsPreloading) null
|
||||
else ep.sEpisode?.let { it1 ->
|
||||
it.loadSingleVideoServer(server, link, ep.extra,
|
||||
it1, post)
|
||||
it.loadSingleVideoServer(
|
||||
server, link, ep.extra,
|
||||
it1, post
|
||||
)
|
||||
}
|
||||
} ?: return false)
|
||||
ep.allStreams = false
|
||||
@@ -217,7 +237,13 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
val epChanged = MutableLiveData(true)
|
||||
fun onEpisodeClick(media: Media, i: String, manager: FragmentManager, launch: Boolean = true, prevEp: String? = null) {
|
||||
fun onEpisodeClick(
|
||||
media: Media,
|
||||
i: String,
|
||||
manager: FragmentManager,
|
||||
launch: Boolean = true,
|
||||
prevEp: String? = null
|
||||
) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
||||
if (media.anime?.episodes?.get(i) != null) {
|
||||
@@ -227,7 +253,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
return@post
|
||||
}
|
||||
media.selected = this.loadSelected(media)
|
||||
val selector = SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
|
||||
val selector =
|
||||
SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
|
||||
selector.show(manager, "dialog")
|
||||
}
|
||||
}
|
||||
@@ -237,13 +264,17 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
//Manga
|
||||
var mangaReadSources: MangaReadSources? = null
|
||||
|
||||
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>>()
|
||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
|
||||
suspend fun loadMangaChapters(media: Media, i: Int) {
|
||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> =
|
||||
mangaChapters
|
||||
|
||||
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
||||
logger("Loading Manga Chapters : $mangaLoaded")
|
||||
if (!mangaLoaded.containsKey(i)) tryWithSuspend {
|
||||
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||
mangaLoaded[i] =
|
||||
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||
}
|
||||
mangaChapters.postValue(mangaLoaded)
|
||||
}
|
||||
@@ -258,10 +289,17 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
||||
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
|
||||
suspend fun loadMangaChapterImages(
|
||||
chapter: MangaChapter,
|
||||
selected: Selected,
|
||||
series: String,
|
||||
post: Boolean = true
|
||||
): Boolean {
|
||||
|
||||
return tryWithSuspend(true) {
|
||||
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
|
||||
)
|
||||
if (post) mangaChapter.postValue(chapter)
|
||||
true
|
||||
@@ -269,7 +307,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
|
||||
return if (mangaImage.useTransformation) mangaReadSources?.get(source)?.getTransformation() else null
|
||||
return if (mangaImage.useTransformation) mangaReadSources?.get(source)
|
||||
?.getTransformation() else null
|
||||
}
|
||||
|
||||
val novelSources = NovelSources
|
||||
@@ -284,7 +323,7 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
suspend fun autoSearchNovels(media: Media) {
|
||||
val source = novelSources[media.selected?.sourceIndex?:0]
|
||||
val source = novelSources[media.selected?.sourceIndex ?: 0]
|
||||
tryWithSuspend(post = true) {
|
||||
if (source != null) {
|
||||
novelResponses.postValue(source.sortedSearch(media))
|
||||
@@ -295,7 +334,9 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
val book: MutableLiveData<Book> = MutableLiveData(null)
|
||||
suspend fun loadBook(novel: ShowResponse, i: Int) {
|
||||
tryWithSuspend {
|
||||
book.postValue(novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend)
|
||||
book.postValue(
|
||||
novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,11 @@ class MediaInfoFragment : Fragment() {
|
||||
private var type = "ANIME"
|
||||
private val genreModel: GenresViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -59,8 +63,8 @@ class MediaInfoFragment : Fragment() {
|
||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||
|
||||
model.scrolledToTop.observe(viewLifecycleOwner){
|
||||
if(it) binding.mediaInfoScroll.scrollTo(0,0)
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
if (it) binding.mediaInfoScroll.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
@@ -68,30 +72,32 @@ class MediaInfoFragment : Fragment() {
|
||||
loaded = true
|
||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoName.text = "\t\t\t" + (media.name?:media.nameRomaji)
|
||||
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
|
||||
binding.mediaInfoName.setOnLongClickListener {
|
||||
copyToClipboard(media.name?:media.nameRomaji)
|
||||
copyToClipboard(media.name ?: media.nameRomaji)
|
||||
true
|
||||
}
|
||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = View.VISIBLE
|
||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
||||
View.VISIBLE
|
||||
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||
copyToClipboard(media.nameRomaji)
|
||||
true
|
||||
}
|
||||
binding.mediaInfoMeanScore.text = if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
|
||||
binding.mediaInfoMeanScore.text =
|
||||
if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
|
||||
binding.mediaInfoStatus.text = media.status
|
||||
binding.mediaInfoFormat.text = media.format
|
||||
binding.mediaInfoSource.text = media.source
|
||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||
binding.mediaInfoEnd.text =media.endDate?.toString() ?: "??"
|
||||
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||
if (media.anime != null) {
|
||||
binding.mediaInfoDuration.text =
|
||||
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
|
||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeason.text =
|
||||
(media.anime.season ?: "??")+ " " + (media.anime.seasonYear ?: "??")
|
||||
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
|
||||
if (media.anime.mainStudio != null) {
|
||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||
@@ -246,7 +252,12 @@ class MediaInfoFragment : Fragment() {
|
||||
val end = a.indexOf('"', first).let { if (it != -1) it else return a }
|
||||
val name = a.subSequence(first, end).toString()
|
||||
return "${a.subSequence(0, first)}" +
|
||||
"[$name](https://www.youtube.com/results?search_query=${URLEncoder.encode(name, "utf-8")})" +
|
||||
"[$name](https://www.youtube.com/results?search_query=${
|
||||
URLEncoder.encode(
|
||||
name,
|
||||
"utf-8"
|
||||
)
|
||||
})" +
|
||||
"${a.subSequence(end, a.length)}"
|
||||
}
|
||||
|
||||
@@ -270,7 +281,11 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
|
||||
if (media.anime.op.isNotEmpty()) {
|
||||
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
val bind = ItemTitleTextBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.opening)
|
||||
makeText(bind.itemText, media.anime.op)
|
||||
parent.addView(bind.root)
|
||||
@@ -278,7 +293,11 @@ class MediaInfoFragment : Fragment() {
|
||||
|
||||
|
||||
if (media.anime.ed.isNotEmpty()) {
|
||||
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
val bind = ItemTitleTextBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.ending)
|
||||
makeText(bind.itemText, media.anime.ed)
|
||||
parent.addView(bind.root)
|
||||
@@ -458,7 +477,8 @@ class MediaInfoFragment : Fragment() {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
|
||||
val cornerNotTop = ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
|
||||
val cornerNotTop =
|
||||
ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
|
||||
var cornered = true
|
||||
cornerTop.start()
|
||||
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->
|
||||
|
||||
@@ -14,8 +14,8 @@ import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -27,7 +27,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetMediaListBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -46,9 +50,13 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListLayout.visibility = View.VISIBLE
|
||||
|
||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||
val statusStrings = if (media?.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
||||
val userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
|
||||
|
||||
val statusStrings =
|
||||
if (media?.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||
R.array.status_manga
|
||||
)
|
||||
val userStatus =
|
||||
if (media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
|
||||
|
||||
binding.mediaListStatus.setText(userStatus)
|
||||
binding.mediaListStatus.setAdapter(
|
||||
ArrayAdapter(
|
||||
@@ -160,7 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
val init =
|
||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||
.toInt() else 0
|
||||
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
|
||||
if (init < (total
|
||||
?: 5000)
|
||||
) binding.mediaListProgress.setText((init + 1).toString())
|
||||
if (init + 1 == (total ?: 5000)) {
|
||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||
onComplete()
|
||||
@@ -201,11 +211,15 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (media != null) {
|
||||
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||
val progress =
|
||||
_binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||
val score =
|
||||
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
|
||||
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||
val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
||||
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||
?.times(10))?.toInt()
|
||||
val status =
|
||||
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||
val rewatch =
|
||||
_binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
||||
val notes = _binding?.mediaListNotes?.text?.toString()
|
||||
val startD = start.date
|
||||
val endD = end.date
|
||||
@@ -245,7 +259,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Anilist.mutation.deleteList(id)
|
||||
MAL.query.deleteList(media?.anime!=null,media?.idMAL)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
}
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
|
||||
@@ -12,10 +12,9 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -45,7 +44,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetMediaListSmallBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -59,8 +62,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListProgressBar.visibility = View.GONE
|
||||
binding.mediaListLayout.visibility = View.VISIBLE
|
||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
||||
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
||||
val statusStrings =
|
||||
if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||
R.array.status_manga
|
||||
)
|
||||
val userStatus =
|
||||
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
||||
|
||||
binding.mediaListStatus.setText(userStatus)
|
||||
binding.mediaListStatus.setAdapter(
|
||||
@@ -129,10 +136,26 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
withContext(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
|
||||
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||
Anilist.mutation.editList(media.id, progress, score, null, null, status, media.isListPrivate)
|
||||
MAL.query.editList(media.idMAL, media.anime != null, progress, score, status)
|
||||
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||
?.times(10))?.toInt()
|
||||
val status =
|
||||
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
progress,
|
||||
score,
|
||||
null,
|
||||
null,
|
||||
status,
|
||||
media.isListPrivate
|
||||
)
|
||||
MAL.query.editList(
|
||||
media.idMAL,
|
||||
media.anime != null,
|
||||
progress,
|
||||
score,
|
||||
status
|
||||
)
|
||||
}
|
||||
}
|
||||
Refresh.all()
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class OtherDetailsViewModel : ViewModel() {
|
||||
private val character: MutableLiveData<Character> = MutableLiveData(null)
|
||||
@@ -19,26 +19,28 @@ class OtherDetailsViewModel : ViewModel() {
|
||||
suspend fun loadStudio(m: Studio) {
|
||||
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
|
||||
}
|
||||
|
||||
private val author: MutableLiveData<Author> = MutableLiveData(null)
|
||||
fun getAuthor(): LiveData<Author> = author
|
||||
suspend fun loadAuthor(m: Author) {
|
||||
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
|
||||
}
|
||||
private val calendar: MutableLiveData<Map<String,MutableList<Media>>> = MutableLiveData(null)
|
||||
fun getCalendar(): LiveData<Map<String,MutableList<Media>>> = calendar
|
||||
|
||||
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
|
||||
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||
suspend fun loadCalendar() {
|
||||
val curr = System.currentTimeMillis()/1000
|
||||
val res = Anilist.query.recentlyUpdated(false,curr-86400,curr+(86400*6))
|
||||
val curr = System.currentTimeMillis() / 1000
|
||||
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||
val map = mutableMapOf<String,MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String,MutableList<Int>>()
|
||||
val map = mutableMapOf<String, MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||
res?.forEach {
|
||||
val v = it.relation?.split(",")?.map { i-> i.toLong() }!!
|
||||
val dateInfo = df.format(Date(v[1]*1000))
|
||||
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
|
||||
val dateInfo = df.format(Date(v[1] * 1000))
|
||||
val list = map.getOrPut(dateInfo) { mutableListOf() }
|
||||
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
|
||||
it.relation = "Episode ${v[0]}"
|
||||
if(!idList.contains(it.id)) {
|
||||
if (!idList.contains(it.id)) {
|
||||
idList.add(it.id)
|
||||
list.add(it)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
||||
var bar: ProgressBar? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
|
||||
val binding = ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ProgressViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -33,7 +34,12 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
||||
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
snackString(currContext()?.getString(R.string.cant_wait))
|
||||
ObjectAnimator.ofFloat(progressBar, "translationX", progressBar.translationX, progressBar.translationX + 100f)
|
||||
ObjectAnimator.ofFloat(
|
||||
progressBar,
|
||||
"translationX",
|
||||
progressBar.translationX,
|
||||
progressBar.translationX + 100f
|
||||
)
|
||||
.setDuration(300).start()
|
||||
}
|
||||
|
||||
@@ -51,7 +57,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -34,10 +35,11 @@ class SearchActivity : AppCompatActivity() {
|
||||
private lateinit var concatAdapter: ConcatAdapter
|
||||
|
||||
lateinit var result: SearchResults
|
||||
lateinit var updateChips: (()->Unit)
|
||||
lateinit var updateChips: (() -> Unit)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -81,10 +83,10 @@ class SearchActivity : AppCompatActivity() {
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (position) {
|
||||
0 -> gridSize
|
||||
0 -> gridSize
|
||||
concatAdapter.itemCount - 1 -> gridSize
|
||||
else -> when (style) {
|
||||
0 -> 1
|
||||
else -> when (style) {
|
||||
0 -> 1
|
||||
else -> gridSize
|
||||
}
|
||||
}
|
||||
@@ -147,7 +149,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
} else
|
||||
headerAdaptor.requestFocus?.run()
|
||||
|
||||
if(intent.getBooleanExtra("search",false)) search()
|
||||
if (intent.getBooleanExtra("search", false)) search()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,16 @@ import ani.dantotsu.saveData
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.*
|
||||
|
||||
|
||||
class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||
class SearchAdapter(private val activity: SearchActivity) :
|
||||
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||
private val itemViewType = 6969
|
||||
var search: Runnable? = null
|
||||
var requestFocus: Runnable? = null
|
||||
private var textWatcher: TextWatcher? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||
val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchHeaderViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -36,13 +38,15 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
val binding = holder.binding
|
||||
|
||||
|
||||
val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val imm: InputMethodManager =
|
||||
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
when (activity.style) {
|
||||
0 -> {
|
||||
binding.searchResultGrid.alpha = 1f
|
||||
binding.searchResultList.alpha = 0.33f
|
||||
}
|
||||
|
||||
1 -> {
|
||||
binding.searchResultList.alpha = 1f
|
||||
binding.searchResultGrid.alpha = 0.33f
|
||||
@@ -62,7 +66,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||
activity.updateChips = { it.update() }
|
||||
}
|
||||
binding.searchChipRecycler.layoutManager = LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||
binding.searchChipRecycler.layoutManager =
|
||||
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||
|
||||
binding.searchFilter.setOnClickListener {
|
||||
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
||||
@@ -70,7 +75,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
|
||||
fun searchTitle() {
|
||||
activity.result.apply {
|
||||
search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
|
||||
search =
|
||||
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
|
||||
onList = listOnly
|
||||
isAdult = adult
|
||||
}
|
||||
@@ -96,7 +102,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
binding.searchBar.setEndIconOnClickListener { searchTitle() }
|
||||
@@ -127,7 +134,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
binding.searchList.apply {
|
||||
if (Anilist.userid != null) {
|
||||
visibility = View.VISIBLE
|
||||
checkedState = when(listOnly){
|
||||
checkedState = when (listOnly) {
|
||||
null -> STATE_UNCHECKED
|
||||
true -> STATE_CHECKED
|
||||
false -> STATE_INDETERMINATE
|
||||
@@ -135,10 +142,10 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
|
||||
addOnCheckedStateChangedListener { _, state ->
|
||||
listOnly = when (state) {
|
||||
STATE_CHECKED -> true
|
||||
STATE_CHECKED -> true
|
||||
STATE_INDETERMINATE -> false
|
||||
STATE_UNCHECKED -> null
|
||||
else -> null
|
||||
STATE_UNCHECKED -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,20 +165,24 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return itemViewType
|
||||
}
|
||||
|
||||
|
||||
class SearchChipAdapter(val activity: SearchActivity) : RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||
class SearchChipAdapter(val activity: SearchActivity) :
|
||||
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||
private var chips = activity.result.toChipList()
|
||||
|
||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchChipViewHolder(binding)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,17 @@ import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import com.google.android.material.chip.Chip
|
||||
|
||||
class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
||||
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSearchFilterBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var activity: SearchActivity
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -99,7 +103,7 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(1970 until 2024).map { it.toString() }.reversed().toTypedArray()
|
||||
(1970 until 2025).map { it.toString() }.reversed().toTypedArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -129,24 +133,25 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
||||
}
|
||||
binding.searchGenresGrid.isChecked = false
|
||||
|
||||
binding.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||
val tag = chip.text.toString()
|
||||
chip.isChecked = selectedTags.contains(tag)
|
||||
chip.isCloseIconVisible = exTags.contains(tag)
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
chip.isCloseIconVisible = false
|
||||
exTags.remove(tag)
|
||||
selectedTags.add(tag)
|
||||
} else
|
||||
selectedTags.remove(tag)
|
||||
binding.searchFilterTags.adapter =
|
||||
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||
val tag = chip.text.toString()
|
||||
chip.isChecked = selectedTags.contains(tag)
|
||||
chip.isCloseIconVisible = exTags.contains(tag)
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
chip.isCloseIconVisible = false
|
||||
exTags.remove(tag)
|
||||
selectedTags.add(tag)
|
||||
} else
|
||||
selectedTags.remove(tag)
|
||||
}
|
||||
chip.setOnLongClickListener {
|
||||
chip.isChecked = false
|
||||
chip.isCloseIconVisible = true
|
||||
exTags.add(tag)
|
||||
}
|
||||
}
|
||||
chip.setOnLongClickListener {
|
||||
chip.isChecked = false
|
||||
chip.isCloseIconVisible = true
|
||||
exTags.add(tag)
|
||||
}
|
||||
}
|
||||
binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked ->
|
||||
binding.searchFilterTags.layoutManager =
|
||||
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||
@@ -158,10 +163,12 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
||||
|
||||
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
|
||||
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
|
||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchChipViewHolder(binding)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ data class Selected(
|
||||
var chip: Int = 0,
|
||||
//var source: String = "",
|
||||
var sourceIndex: Int = 0,
|
||||
var langIndex: Int = 0,
|
||||
var preferDub: Boolean = false,
|
||||
var server: String? = null,
|
||||
var video: Int = 0,
|
||||
var latest: Float = 0f,
|
||||
var scanlators: List<String>? = null,
|
||||
) : Serializable
|
||||
|
||||
@@ -17,7 +17,8 @@ abstract class SourceAdapter(
|
||||
private val scope: CoroutineScope
|
||||
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
|
||||
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SourceViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -34,7 +35,8 @@ abstract class SourceAdapter(
|
||||
|
||||
abstract suspend fun onItemClick(source: ShowResponse)
|
||||
|
||||
inner class SourceViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class SourceViewHolder(val binding: ItemCharacterBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
dialogFragment.dismiss()
|
||||
|
||||
@@ -13,8 +13,8 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
||||
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
|
||||
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
||||
import ani.dantotsu.media.manga.MangaSourceAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
@@ -38,7 +38,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
var id: Int? = null
|
||||
var media: Media? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -47,7 +51,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
|
||||
val scope = requireActivity().lifecycleScope
|
||||
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val imm =
|
||||
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
model.getMedia().observe(viewLifecycleOwner) {
|
||||
media = it
|
||||
if (media != null) {
|
||||
@@ -65,6 +70,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
anime = false
|
||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||
}
|
||||
|
||||
fun search() {
|
||||
binding.searchBarText.clearFocus()
|
||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||
@@ -86,7 +92,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
search()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
binding.searchBar.setEndIconOnClickListener { search() }
|
||||
@@ -101,7 +108,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
|
||||
binding.searchRecyclerView.layoutManager = GridLayoutManager(
|
||||
requireActivity(),
|
||||
clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4)
|
||||
clamp(
|
||||
requireActivity().resources.displayMetrics.widthPixels / 124f.px,
|
||||
1,
|
||||
4
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,16 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.EmptyAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityStudioBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -29,6 +36,7 @@ class StudioActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityStudioBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
|
||||
class SubtitleDownloader {
|
||||
|
||||
@@ -30,7 +25,7 @@ class SubtitleDownloader {
|
||||
|
||||
// Check if response is successful
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body?.string()
|
||||
val responseBody = response.body.string()
|
||||
|
||||
|
||||
val subtitleType = when {
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemTitleBinding
|
||||
|
||||
class TitleAdapter(private val text: String) : RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
||||
inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
class TitleAdapter(private val text: String) :
|
||||
RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
||||
inner class TitleViewHolder(val binding: ItemTitleBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
|
||||
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
||||
@@ -15,7 +15,7 @@ data class Anime(
|
||||
var ed: ArrayList<String> = arrayListOf(),
|
||||
|
||||
var mainStudio: Studio? = null,
|
||||
var author: Author?=null,
|
||||
var author: Author? = null,
|
||||
|
||||
var youtube: String? = null,
|
||||
var nextAiringEpisode: Int? = null,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AnimeNameAdapter {
|
||||
companion object {
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)"
|
||||
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package ani.dantotsu.media.anime
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
@@ -18,6 +18,8 @@ import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
@@ -54,22 +56,33 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
||||
binding.animeSourceDubbedText.text = if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
|
||||
binding.animeSourceDubbedText.text =
|
||||
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||
R.string.subbed
|
||||
)
|
||||
|
||||
//PreferDub
|
||||
var changing = false
|
||||
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
|
||||
binding.animeSourceDubbedText.text = if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
|
||||
binding.animeSourceDubbedText.text =
|
||||
if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||
R.string.subbed
|
||||
)
|
||||
if (!changing) fragment.onDubClicked(isChecked)
|
||||
}
|
||||
|
||||
//Wrong Title
|
||||
binding.animeSourceSearch.setOnClickListener {
|
||||
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
|
||||
SourceSearchDialogFragment().show(
|
||||
fragment.requireActivity().supportFragmentManager,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
//Source Selection
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
var source =
|
||||
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
setLanguageList(media.selected!!.langIndex, source)
|
||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||
binding.animeSource.setText(watchSources.names[source])
|
||||
watchSources[source].apply {
|
||||
@@ -81,7 +94,13 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, watchSources.names))
|
||||
binding.animeSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
watchSources.names
|
||||
)
|
||||
)
|
||||
binding.animeSourceTitle.isSelected = true
|
||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
fragment.onSourceChange(i).apply {
|
||||
@@ -90,14 +109,47 @@ class AnimeWatchAdapter(
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
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)
|
||||
fragment.loadEpisodes(i)
|
||||
fragment.loadEpisodes(i, false)
|
||||
}
|
||||
|
||||
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
} ?: run {
|
||||
}
|
||||
}
|
||||
|
||||
//settings
|
||||
binding.animeSourceSettings.setOnClickListener {
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
fragment.openSettings(ext.extension)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Subscription
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
R.drawable.ic_round_notifications_active_24,
|
||||
@@ -112,7 +164,7 @@ class AnimeWatchAdapter(
|
||||
subscribeButton(false)
|
||||
|
||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||
openSettings(fragment.requireContext(),getChannelId(true,media.id))
|
||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
}
|
||||
|
||||
//Icons
|
||||
@@ -151,12 +203,13 @@ class AnimeWatchAdapter(
|
||||
style = 2
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
|
||||
binding.animeScanlatorTop.visibility = View.GONE
|
||||
binding.animeDownloadTop.visibility = View.GONE
|
||||
//Episode Handling
|
||||
handleEpisodes()
|
||||
}
|
||||
|
||||
fun subscribeButton(enabled : Boolean) {
|
||||
fun subscribeButton(enabled: Boolean) {
|
||||
subscribe?.enabled(enabled)
|
||||
}
|
||||
|
||||
@@ -170,13 +223,26 @@ class AnimeWatchAdapter(
|
||||
for (position in arr.indices) {
|
||||
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
||||
val chip =
|
||||
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root
|
||||
ItemChipBinding.inflate(
|
||||
LayoutInflater.from(fragment.context),
|
||||
binding.animeSourceChipGroup,
|
||||
false
|
||||
).root
|
||||
chip.isCheckable = true
|
||||
fun selected() {
|
||||
chip.isChecked = true
|
||||
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.setTextColor(
|
||||
ContextCompat.getColorStateList(
|
||||
fragment.requireContext(),
|
||||
R.color.chip_text_color
|
||||
)
|
||||
)
|
||||
|
||||
chip.setOnClickListener {
|
||||
selected()
|
||||
@@ -189,7 +255,14 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
if (select != null)
|
||||
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } }
|
||||
binding.animeWatchChipScroll.apply {
|
||||
post {
|
||||
scrollTo(
|
||||
(select.left - screenWidth / 2) + (select.width / 2),
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +304,9 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
val ep = media.anime.episodes!![continueEp]!!
|
||||
binding.itemEpisodeImage.loadImage(ep.thumb ?: FileUrl[media.banner ?: media.cover], 0)
|
||||
binding.itemEpisodeImage.loadImage(
|
||||
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||
)
|
||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||
binding.animeSourceContinueText.text =
|
||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
|
||||
@@ -261,9 +336,36 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (watchSources is AnimeSources) {
|
||||
val parser = watchSources[source] as? DynamicAnimeParser
|
||||
if (parser != null) {
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
ext.sourceLanguage = lang
|
||||
}
|
||||
try {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
binding?.animeSourceLanguage?.setText(
|
||||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { it.lang })
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
//Timer
|
||||
countDown(media, binding.animeSourceContainer)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -13,20 +17,27 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.settings.PlayerSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -76,8 +87,10 @@ class AnimeWatchFragment : Fragment() {
|
||||
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
||||
|
||||
playerSettings =
|
||||
loadData("player_settings", toast = false) ?: PlayerSettings().apply { saveData("player_settings", this) }
|
||||
uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
loadData("player_settings", toast = false)
|
||||
?: PlayerSettings().apply { saveData("player_settings", this) }
|
||||
uiSettings = loadData("ui_settings", toast = false)
|
||||
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
|
||||
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
|
||||
|
||||
@@ -86,11 +99,11 @@ class AnimeWatchFragment : Fragment() {
|
||||
val style = episodeAdapter.getItemViewType(position)
|
||||
|
||||
return when (position) {
|
||||
0 -> maxGridSize
|
||||
0 -> maxGridSize
|
||||
else -> when (style) {
|
||||
0 -> maxGridSize
|
||||
1 -> 2
|
||||
2 -> 1
|
||||
0 -> maxGridSize
|
||||
1 -> 2
|
||||
2 -> 1
|
||||
else -> maxGridSize
|
||||
}
|
||||
}
|
||||
@@ -109,7 +122,8 @@ class AnimeWatchFragment : Fragment() {
|
||||
media = it
|
||||
media.selected = model.loadSelected(media)
|
||||
|
||||
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||
subscribed =
|
||||
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||
|
||||
style = media.selected!!.recyclerStyle
|
||||
reverse = media.selected!!.recyclerReversed
|
||||
@@ -121,9 +135,11 @@ class AnimeWatchFragment : Fragment() {
|
||||
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
||||
|
||||
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
|
||||
episodeAdapter = EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
|
||||
episodeAdapter =
|
||||
EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
|
||||
|
||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
binding.animeSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
awaitAll(
|
||||
@@ -145,15 +161,20 @@ class AnimeWatchFragment : Fragment() {
|
||||
episodes.forEach { (i, episode) ->
|
||||
if (media.anime?.fillerEpisodes != null) {
|
||||
if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
|
||||
episode.title = episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
|
||||
episode.title =
|
||||
episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
|
||||
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
|
||||
}
|
||||
}
|
||||
if (media.anime?.kitsuEpisodes != null) {
|
||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||
episode.desc = episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
|
||||
episode.title = episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
|
||||
episode.thumb = episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ?: FileUrl[media.cover]
|
||||
episode.desc =
|
||||
episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
|
||||
episode.title =
|
||||
episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
|
||||
episode.thumb =
|
||||
episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb
|
||||
?: FileUrl[media.cover]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +188,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
val limit = when {
|
||||
(divisions < 25) -> 25
|
||||
(divisions < 50) -> 50
|
||||
else -> 100
|
||||
else -> 100
|
||||
}
|
||||
headerAdapter.clearChips()
|
||||
if (total > limit) {
|
||||
@@ -214,17 +235,29 @@ class AnimeWatchFragment : Fragment() {
|
||||
return model.watchSources?.get(i)!!
|
||||
}
|
||||
|
||||
fun onLangChange(i: Int) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.langIndex = i
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
fun onDubClicked(checked: Boolean) {
|
||||
val selected = model.loadSelected(media)
|
||||
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
||||
selected.preferDub = checked
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.forceLoadEpisode(
|
||||
media,
|
||||
selected.sourceIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadEpisodes(i: Int) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) }
|
||||
fun loadEpisodes(i: Int, invalidate: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
|
||||
}
|
||||
|
||||
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||
@@ -263,6 +296,81 @@ class AnimeWatchFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
fun openSettings(pkg: AnimeExtension.Installed) {
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as MediaDetailsActivity
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||
try {
|
||||
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||
} catch (e: ClassCastException) {
|
||||
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||
}
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = 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
|
||||
requireActivity().runOnUiThread {
|
||||
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()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
requireActivity().runOnUiThread {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
fun onEpisodeClick(i: String) {
|
||||
model.continueMedia = false
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
@@ -274,8 +382,10 @@ class AnimeWatchFragment : Fragment() {
|
||||
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
|
||||
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()
|
||||
|
||||
@@ -14,12 +14,12 @@ data class Episode(
|
||||
var selectedExtractor: String? = null,
|
||||
var selectedVideo: Int = 0,
|
||||
var selectedSubtitle: Int? = -1,
|
||||
var extractors: MutableList<VideoExtractor>?=null,
|
||||
@Transient var extractorCallback: ((VideoExtractor) -> Unit)?=null,
|
||||
var extractors: MutableList<VideoExtractor>? = null,
|
||||
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
||||
var allStreams: Boolean = false,
|
||||
var watched: Long? = null,
|
||||
var maxLength: Long? = null,
|
||||
val extra: Map<String,String>?=null,
|
||||
val extra: Map<String, String>? = null,
|
||||
val sEpisode: eu.kanade.tachiyomi.animesource.model.SEpisode? = null
|
||||
) : Serializable
|
||||
|
||||
|
||||
@@ -41,15 +41,30 @@ class EpisodeAdapter(
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return (when (viewType) {
|
||||
0 -> EpisodeListViewHolder(ItemEpisodeListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
1 -> EpisodeGridViewHolder(ItemEpisodeGridBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
2 -> EpisodeCompactViewHolder(
|
||||
0 -> EpisodeListViewHolder(
|
||||
ItemEpisodeListBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
1 -> EpisodeGridViewHolder(
|
||||
ItemEpisodeGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
2 -> EpisodeCompactViewHolder(
|
||||
ItemEpisodeCompactBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
})
|
||||
}
|
||||
@@ -62,15 +77,21 @@ class EpisodeAdapter(
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val ep = arr[position]
|
||||
val title =
|
||||
"${if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(R.string.episode_singular)} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
|
||||
"${
|
||||
if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(
|
||||
R.string.episode_singular
|
||||
)
|
||||
} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
|
||||
|
||||
when (holder) {
|
||||
is EpisodeListViewHolder -> {
|
||||
is EpisodeListViewHolder -> {
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
|
||||
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage)
|
||||
val thumb =
|
||||
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
|
||||
.into(binding.itemEpisodeImage)
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeTitle.text = title
|
||||
|
||||
@@ -81,7 +102,8 @@ class EpisodeAdapter(
|
||||
binding.itemEpisodeFiller.visibility = View.GONE
|
||||
binding.itemEpisodeFillerView.visibility = View.GONE
|
||||
}
|
||||
binding.itemEpisodeDesc.visibility = if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
||||
binding.itemEpisodeDesc.visibility =
|
||||
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
||||
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
||||
|
||||
if (media.userProgress != null) {
|
||||
@@ -110,12 +132,14 @@ class EpisodeAdapter(
|
||||
)
|
||||
}
|
||||
|
||||
is EpisodeGridViewHolder -> {
|
||||
is EpisodeGridViewHolder -> {
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
|
||||
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage)
|
||||
val thumb =
|
||||
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
|
||||
.into(binding.itemEpisodeImage)
|
||||
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeTitle.text = title
|
||||
@@ -155,7 +179,8 @@ class EpisodeAdapter(
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeFillerView.visibility = if (ep.filler) View.VISIBLE else View.GONE
|
||||
binding.itemEpisodeFillerView.visibility =
|
||||
if (ep.filler) View.VISIBLE else View.GONE
|
||||
if (media.userProgress != null) {
|
||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
@@ -180,7 +205,8 @@ class EpisodeAdapter(
|
||||
|
||||
override fun getItemCount(): Int = arr.size
|
||||
|
||||
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||
@@ -189,7 +215,8 @@ class EpisodeAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||
@@ -198,7 +225,8 @@ class EpisodeAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.*
|
||||
@@ -61,6 +62,8 @@ import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.connections.discord.DiscordService
|
||||
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
||||
import ani.dantotsu.connections.discord.RPC
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||
@@ -70,6 +73,7 @@ import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.others.AniSkip
|
||||
import ani.dantotsu.others.AniSkip.getType
|
||||
import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.ResettableTimer
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.*
|
||||
@@ -79,6 +83,7 @@ import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.lagradost.nicehttp.ignoreAllSSLErrors
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -182,8 +187,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
|
||||
var rotation = 0
|
||||
|
||||
private var rpc: RPC? = null
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||
@@ -301,6 +304,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
1 -> ResourcesCompat.getFont(this, R.font.poppins_bold)
|
||||
2 -> ResourcesCompat.getFont(this, R.font.poppins)
|
||||
3 -> ResourcesCompat.getFont(this, R.font.poppins_thin)
|
||||
4 -> ResourcesCompat.getFont(this, R.font.century_gothic_regular)
|
||||
5 -> ResourcesCompat.getFont(this, R.font.century_gothic_bold)
|
||||
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
|
||||
}
|
||||
playerView.subtitleView?.setStyle(
|
||||
@@ -317,6 +322,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityExoplayerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -378,14 +384,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN)
|
||||
|
||||
if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) {
|
||||
requestedOrientation = rotation
|
||||
exoRotate.setOnClickListener {
|
||||
requestedOrientation = rotation
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
orientationListener =
|
||||
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
println(orientation)
|
||||
if (orientation in 45..135) {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility =
|
||||
View.VISIBLE
|
||||
@@ -398,6 +400,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
}
|
||||
orientationListener?.enable()
|
||||
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
exoRotate.setOnClickListener {
|
||||
requestedOrientation = rotation
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
setupSubFormatting(playerView, settings)
|
||||
@@ -806,23 +814,24 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
|
||||
fun fastForward() {
|
||||
val newSpeed = playbackParameters.speed * 2f
|
||||
exoPlayer.playbackParameters = playbackParameters.withSpeed(newSpeed)
|
||||
isFastForwarding = true
|
||||
snackString("Playing at ${newSpeed}x speed")
|
||||
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
|
||||
snackString("Playing at ${exoPlayer.playbackParameters.speed}x speed")
|
||||
}
|
||||
|
||||
fun stopFastForward() {
|
||||
if (isFastForwarding) {
|
||||
exoPlayer.playbackParameters = playbackParameters
|
||||
isFastForwarding = false
|
||||
snackString("Playing at normal speed")
|
||||
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
|
||||
snackString("Playing at default speed: ${exoPlayer.playbackParameters.speed}x")
|
||||
}
|
||||
}
|
||||
|
||||
//FastRewind (Left Panel)
|
||||
val fastRewindDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onLongClick(event: MotionEvent) = fastForward()
|
||||
override fun onLongClick(event: MotionEvent) {
|
||||
if (settings.fastforward) fastForward()
|
||||
}
|
||||
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
doubleTap(false, event)
|
||||
@@ -852,7 +861,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
|
||||
//FastForward (Right Panel)
|
||||
val fastForwardDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onLongClick(event: MotionEvent) = fastForward()
|
||||
override fun onLongClick(event: MotionEvent) {
|
||||
if (settings.fastforward) fastForward()
|
||||
}
|
||||
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
doubleTap(true, event)
|
||||
@@ -986,22 +997,49 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
playbackPosition = loadData("${media.id}_${ep.number}", this) ?: 0
|
||||
initPlayer()
|
||||
preloading = false
|
||||
rpc = Discord.defaultRPC()
|
||||
rpc?.send {
|
||||
type = RPC.Type.WATCHING
|
||||
activityName = media.userPreferredName
|
||||
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
|
||||
R.string.episode_num,
|
||||
ep.number
|
||||
)
|
||||
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}"
|
||||
media.cover?.let { cover ->
|
||||
largeImage = RPC.Link(media.userPreferredName, cover)
|
||||
}
|
||||
media.shareLink?.let { link ->
|
||||
buttons.add(0, RPC.Link(getString(R.string.view_anime), link))
|
||||
val context = this
|
||||
|
||||
val incognito = baseContext.getSharedPreferences("Dantotsu", MODE_PRIVATE)
|
||||
.getBoolean("incognito", false)
|
||||
if (isOnline(context) && Discord.token != null && !incognito) {
|
||||
lifecycleScope.launch {
|
||||
val presence = RPC.createPresence(RPC.Companion.RPCData(
|
||||
applicationId = Discord.application_Id,
|
||||
type = RPC.Type.WATCHING,
|
||||
activityName = media.userPreferredName,
|
||||
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
|
||||
R.string.episode_num,
|
||||
ep.number
|
||||
),
|
||||
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
|
||||
largeImage = media.cover?.let {
|
||||
RPC.Link(
|
||||
media.userPreferredName,
|
||||
it
|
||||
)
|
||||
},
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
Discord.small_Image
|
||||
),
|
||||
buttons = mutableListOf(
|
||||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||
RPC.Link(
|
||||
"Stream on Dantotsu",
|
||||
"https://github.com/rebelonion/Dantotsu/"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val intent = Intent(context, DiscordService::class.java).apply {
|
||||
putExtra("presence", presence)
|
||||
}
|
||||
DiscordServiceRunningSingleton.running = true
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
@@ -1073,7 +1111,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
val speedDialog =
|
||||
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.speed))
|
||||
exoSpeed.setOnClickListener {
|
||||
speedDialog.setSingleChoiceItems(speedsName, curSpeed) { dialog, i ->
|
||||
val dialog = speedDialog.setSingleChoiceItems(speedsName, curSpeed) { dialog, i ->
|
||||
if (isInitialized) {
|
||||
saveData("${media.id}_speed", i, this)
|
||||
speed = speeds[i]
|
||||
@@ -1084,6 +1122,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
hideSystemBars()
|
||||
}
|
||||
}.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
speedDialog.setOnCancelListener { hideSystemBars() }
|
||||
|
||||
@@ -1121,8 +1160,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
|
||||
?: true else false
|
||||
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
|
||||
AlertDialog.Builder(this, R.style.DialogTheme)
|
||||
AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle(getString(R.string.auto_update, media.userPreferredName))
|
||||
.setMessage(getString(R.string.incognito_will_not_update))
|
||||
.apply {
|
||||
setOnCancelListener { hideSystemBars() }
|
||||
setCancelable(false)
|
||||
@@ -1205,7 +1245,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
else -> MimeTypes.TEXT_SSA
|
||||
}
|
||||
)
|
||||
.setId("2")
|
||||
.setId("69")
|
||||
.build()
|
||||
}
|
||||
println("sub: $sub")
|
||||
@@ -1221,7 +1261,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
else -> MimeTypes.TEXT_UNKNOWN
|
||||
}
|
||||
)
|
||||
.setId("2")
|
||||
.setId("69")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1270,6 +1310,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
|
||||
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
||||
logger("url: ${video!!.file.url}")
|
||||
logger("mimeType: $mimeType")
|
||||
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
|
||||
@@ -1293,7 +1335,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
.setAllowMultipleAdaptiveSelections(true)
|
||||
.setPreferredTextLanguage(subtitle?.language ?: "en")
|
||||
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.setRendererDisabled(C.TRACK_TYPE_VIDEO, false)
|
||||
.setRendererDisabled(TRACK_TYPE_VIDEO, false)
|
||||
.setRendererDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||
.setRendererDisabled(C.TRACK_TYPE_TEXT, false)
|
||||
.setMinVideoSize(
|
||||
@@ -1302,6 +1344,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
)
|
||||
.setMaxVideoSize(1, 1)
|
||||
//.setOverrideForType(
|
||||
// TrackSelectionOverride(trackSelector, 2))
|
||||
)
|
||||
|
||||
if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) {
|
||||
@@ -1318,19 +1361,20 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
)
|
||||
)
|
||||
)
|
||||
AlertDialog.Builder(this, R.style.DialogTheme)
|
||||
val dialog = AlertDialog.Builder(this, R.style.DialogTheme)
|
||||
.setTitle(getString(R.string.continue_from, time)).apply {
|
||||
setCancelable(false)
|
||||
setPositiveButton(getString(R.string.yes)) { d, _ ->
|
||||
buildExoplayer()
|
||||
d.dismiss()
|
||||
}
|
||||
setNegativeButton(getString(R.string.no)) { d, _ ->
|
||||
playbackPosition = 0L
|
||||
buildExoplayer()
|
||||
d.dismiss()
|
||||
}
|
||||
}.show()
|
||||
setCancelable(false)
|
||||
setPositiveButton(getString(R.string.yes)) { d, _ ->
|
||||
buildExoplayer()
|
||||
d.dismiss()
|
||||
}
|
||||
setNegativeButton(getString(R.string.no)) { d, _ ->
|
||||
playbackPosition = 0L
|
||||
buildExoplayer()
|
||||
d.dismiss()
|
||||
}
|
||||
}.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
} else buildExoplayer()
|
||||
}
|
||||
|
||||
@@ -1352,6 +1396,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
playerView.player = exoPlayer
|
||||
|
||||
|
||||
try {
|
||||
mediaSession = MediaSession.Builder(this, exoPlayer).build()
|
||||
} catch (e: Exception) {
|
||||
@@ -1362,7 +1407,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
exoPlayer.addAnalyticsListener(EventLogger())
|
||||
isInitialized = true
|
||||
}
|
||||
/*private fun selectSubtitleTrack() {
|
||||
/*private fun selectSubtitleTrack() { saving this for later
|
||||
// Get the current track groups
|
||||
val trackGroups = exoPlayer.currentTrackGroups
|
||||
|
||||
@@ -1394,7 +1439,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
exoPlayer.release()
|
||||
VideoCache.release()
|
||||
mediaSession?.release()
|
||||
rpc?.close()
|
||||
if(DiscordServiceRunningSingleton.running) {
|
||||
val stopIntent = Intent(this, DiscordService::class.java)
|
||||
DiscordServiceRunningSingleton.running = false
|
||||
stopService(stopIntent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -1440,6 +1490,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
LangSet.setLocale(this)
|
||||
orientationListener?.enable()
|
||||
hideSystemBars()
|
||||
if (isInitialized) {
|
||||
@@ -1496,6 +1547,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
if (exoPlayer.duration < playbackPosition)
|
||||
exoPlayer.seekTo(0)
|
||||
|
||||
//if playbackPosition is within 92% of the episode length, reset it to 0
|
||||
if (playbackPosition > exoPlayer.duration.toFloat() * 0.92) {
|
||||
playbackPosition = 0
|
||||
exoPlayer.seekTo(0)
|
||||
}
|
||||
|
||||
if (!isTimeStampsLoaded && settings.timeStampsEnabled) {
|
||||
val dur = exoPlayer.duration
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -1572,6 +1629,29 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
tracks.groups.forEach {
|
||||
println("Track__: $it")
|
||||
println("Track__: ${it.length}")
|
||||
println("Track__: ${it.isSelected}")
|
||||
println("Track__: ${it.type}")
|
||||
println("Track__: ${it.mediaTrackGroup.id}")
|
||||
if (it.type == 3 && it.mediaTrackGroup.id == "1:") {
|
||||
playerView.player?.trackSelectionParameters =
|
||||
playerView.player?.trackSelectionParameters?.buildUpon()
|
||||
?.setOverrideForType(
|
||||
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)
|
||||
)
|
||||
?.build()!!
|
||||
} else if (it.type == 3) {
|
||||
playerView.player?.trackSelectionParameters =
|
||||
playerView.player?.trackSelectionParameters?.buildUpon()
|
||||
?.addOverride(
|
||||
TrackSelectionOverride(it.mediaTrackGroup, listOf())
|
||||
)
|
||||
?.build()!!
|
||||
}
|
||||
}
|
||||
println("Track: ${tracks.groups.size}")
|
||||
if (tracks.groups.size <= 2) exoQuality.visibility = View.GONE
|
||||
else {
|
||||
exoQuality.visibility = View.VISIBLE
|
||||
@@ -1591,7 +1671,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
|
||||
else
|
||||
-> toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
|
||||
-> {
|
||||
toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ package ani.dantotsu.media.anime
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -23,7 +25,6 @@ import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,8 +52,18 @@ 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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -62,7 +73,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
media = m
|
||||
if (media != null && !loaded) {
|
||||
loaded = true
|
||||
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
|
||||
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
|
||||
episode = ep
|
||||
if (ep != null) {
|
||||
|
||||
@@ -83,14 +94,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
fun load() {
|
||||
val size = ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||
if (size!=null && size >= media!!.selected!!.video) {
|
||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = selected
|
||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = media!!.selected!!.video
|
||||
val size =
|
||||
ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||
if (size != null && size >= media!!.selected!!.video) {
|
||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
|
||||
selected
|
||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo =
|
||||
media!!.selected!!.video
|
||||
startExoplayer(media!!)
|
||||
} else fail()
|
||||
}
|
||||
|
||||
|
||||
if (ep.extractors.isNullOrEmpty()) {
|
||||
model.getEpisode().observe(this) {
|
||||
if (it != null) {
|
||||
@@ -107,8 +121,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}) fail()
|
||||
}
|
||||
} else load()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
@@ -121,10 +134,14 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
saveData("make_default", makeDefault)
|
||||
}
|
||||
binding.selectorRecyclerView.layoutManager =
|
||||
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false)
|
||||
LinearLayoutManager(
|
||||
requireActivity(),
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
val adapter = ExtractorAdapter()
|
||||
binding.selectorRecyclerView.adapter = adapter
|
||||
if (!ep.allStreams ) {
|
||||
if (!ep.allStreams) {
|
||||
ep.extractorCallback = {
|
||||
scope.launch {
|
||||
adapter.add(it)
|
||||
@@ -132,12 +149,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
model.getEpisode().observe(this) {
|
||||
if (it != null) {
|
||||
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
|
||||
media!!.anime?.episodes?.set(
|
||||
media!!.anime?.selectedEpisode!!,
|
||||
ep
|
||||
)
|
||||
}
|
||||
}
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
|
||||
withContext(Dispatchers.Main){
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.selectorProgressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -166,7 +186,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
ExoplayerView.initialized = true
|
||||
startActivity(intent)
|
||||
} else {
|
||||
model.setEpisode(media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, "startExo no launch")
|
||||
model.setEpisode(
|
||||
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
|
||||
"startExo no launch"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,54 +200,72 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ExtractorAdapter : RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() {
|
||||
private inner class ExtractorAdapter :
|
||||
RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() {
|
||||
val links = mutableListOf<VideoExtractor>()
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
||||
StreamViewHolder(ItemStreamBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
StreamViewHolder(
|
||||
ItemStreamBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val extractor = links[position]
|
||||
holder.binding.streamName.text = extractor.server.name
|
||||
|
||||
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
|
||||
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = links.size
|
||||
|
||||
fun add(videoExtractor: VideoExtractor){
|
||||
if(videoExtractor.videos.isNotEmpty()) {
|
||||
fun add(videoExtractor: VideoExtractor) {
|
||||
if (videoExtractor.videos.isNotEmpty()) {
|
||||
links.add(videoExtractor)
|
||||
notifyItemInserted(links.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun addAll(extractors: List<VideoExtractor>?) {
|
||||
links.addAll(extractors?:return)
|
||||
notifyItemRangeInserted(0,extractors.size)
|
||||
links.addAll(extractors ?: return)
|
||||
notifyItemRangeInserted(0, extractors.size)
|
||||
}
|
||||
|
||||
private inner class StreamViewHolder(val binding: ItemStreamBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
private inner class StreamViewHolder(val binding: ItemStreamBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
private inner class VideoAdapter(private val extractor : VideoExtractor) : RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
|
||||
private inner class VideoAdapter(private val extractor: VideoExtractor) :
|
||||
RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
|
||||
return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
return UrlViewHolder(
|
||||
ItemUrlBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val video = extractor.videos[position]
|
||||
binding.urlQuality.text = if(video.quality!=null) "${video.quality}p" else "Default Quality"
|
||||
binding.urlQuality.text =
|
||||
if (video.quality != null) "${video.quality}p" else "Default Quality"
|
||||
binding.urlNote.text = video.extraNote ?: ""
|
||||
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
|
||||
binding.urlDownload.visibility = View.VISIBLE
|
||||
binding.urlDownload.setSafeOnClickListener {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = position
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||
extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
||||
position
|
||||
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
download(
|
||||
requireActivity(),
|
||||
@@ -236,9 +277,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
if (video.format == VideoType.CONTAINER) {
|
||||
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
|
||||
binding.urlSize.text =
|
||||
(if (video.extraNote != null) " : " else "") + DecimalFormat("#.##").format(video.size ?: 0).toString() + " MB"
|
||||
}
|
||||
else {
|
||||
// if video size is null or 0, show "Unknown Size" else show the size in MB
|
||||
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
|
||||
"#.##"
|
||||
).format(video.size ?: 0).toString() + " MB"))
|
||||
} else {
|
||||
binding.urlQuality.text = "Multi Quality"
|
||||
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
|
||||
binding.urlDownload.visibility = View.GONE
|
||||
@@ -248,12 +291,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
override fun getItemCount(): Int = extractor.videos.size
|
||||
|
||||
private inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private inner class UrlViewHolder(val binding: ItemUrlBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setSafeOnClickListener {
|
||||
tryWith(true) {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = bindingAdapterPosition
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
|
||||
extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo =
|
||||
bindingAdapterPosition
|
||||
if (makeDefault) {
|
||||
media!!.selected!!.server = extractor.server.name
|
||||
media!!.selected!!.video = bindingAdapterPosition
|
||||
@@ -264,12 +310,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
val video = extractor.videos[bindingAdapterPosition]
|
||||
val intent= Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(Uri.parse(video.file.url),"video/*")
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(Uri.parse(video.file.url), "video/*")
|
||||
}
|
||||
copyToClipboard(video.file.url,true)
|
||||
copyToClipboard(video.file.url, true)
|
||||
dismiss()
|
||||
startActivity(Intent.createChooser(intent,"Open Video in :"))
|
||||
startActivity(Intent.createChooser(intent, "Open Video in :"))
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -277,7 +323,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(server: String? = null, la: Boolean = true, prev: String? = null): SelectorDialogFragment =
|
||||
fun newInstance(
|
||||
server: String? = null,
|
||||
la: Boolean = true,
|
||||
prev: String? = null
|
||||
): SelectorDialogFragment =
|
||||
SelectorDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString("server", server)
|
||||
|
||||
@@ -24,7 +24,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
private lateinit var episode: Episode
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -34,17 +38,27 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe
|
||||
val currentExtractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return@observe
|
||||
val currentExtractor =
|
||||
episode.extractors?.find { it.server.name == episode.selectedExtractor }
|
||||
?: return@observe
|
||||
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles)
|
||||
}
|
||||
}
|
||||
|
||||
inner class SubtitleAdapter(val subtitles: List<Subtitle>) : RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() {
|
||||
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class SubtitleAdapter(val subtitles: List<Subtitle>) :
|
||||
RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() {
|
||||
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
||||
StreamViewHolder(ItemSubtitleTextBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
StreamViewHolder(
|
||||
ItemSubtitleTextBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
@@ -60,7 +74,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.root.setOnClickListener {
|
||||
episode.selectedSubtitle = null
|
||||
model.setEpisode(episode, "Subtitle")
|
||||
model.getMedia().observe(viewLifecycleOwner){media ->
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
saveData("subLang_${mediaID}", "None", activity)
|
||||
}
|
||||
@@ -87,7 +101,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
"pl-PL" -> "[pl-PL] Polish"
|
||||
"ro-RO" -> "[ro-RO] Romanian"
|
||||
"sv-SE" -> "[sv-SE] Swedish"
|
||||
else -> if(subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language
|
||||
else -> if (subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language
|
||||
}
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
@@ -100,7 +114,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.root.setOnClickListener {
|
||||
episode.selectedSubtitle = position - 1
|
||||
model.setEpisode(episode, "Subtitle")
|
||||
model.getMedia().observe(viewLifecycleOwner){media ->
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
saveData("subLang_${mediaID}", subtitles[position - 1].language, activity)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ object VideoCache {
|
||||
val databaseProvider = StandaloneDatabaseProvider(context)
|
||||
if (simpleCache == null)
|
||||
simpleCache = SimpleCache(
|
||||
File(context.cacheDir, "exoplayer").also { it.deleteOnExit() }, // Ensures always fresh file
|
||||
File(
|
||||
context.cacheDir,
|
||||
"exoplayer"
|
||||
).also { it.deleteOnExit() }, // Ensures always fresh file
|
||||
LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L),
|
||||
databaseProvider
|
||||
)
|
||||
|
||||
@@ -8,5 +8,5 @@ data class Manga(
|
||||
var selectedChapter: String? = null,
|
||||
var chapters: MutableMap<String, MangaChapter>? = null,
|
||||
var slug: String? = null,
|
||||
var author: Author?=null,
|
||||
var author: Author? = null,
|
||||
) : Serializable
|
||||
@@ -10,6 +10,8 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.LruCache
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.snackString
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -20,44 +22,59 @@ import java.io.FileOutputStream
|
||||
data class ImageData(
|
||||
val page: Page,
|
||||
val source: HttpSource
|
||||
){
|
||||
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
|
||||
) {
|
||||
suspend fun fetchAndProcessImage(
|
||||
page: Page,
|
||||
httpSource: HttpSource,
|
||||
context: Context
|
||||
): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
println("Response: ${response.code}")
|
||||
println("Response: ${response.message}")
|
||||
logger("Response: ${response.code}")
|
||||
logger("Response: ${response.message}")
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body?.byteStream()
|
||||
val inputStream = response.body.byteStream()
|
||||
|
||||
// Convert InputStream to Bitmap
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
|
||||
inputStream?.close()
|
||||
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
||||
inputStream.close()
|
||||
//saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
||||
|
||||
return@withContext bitmap
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
println("An error occurred: ${e.message}")
|
||||
logger("An error occurred: ${e.message}")
|
||||
snackString("An error occurred: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
|
||||
fun saveImage(
|
||||
bitmap: Bitmap,
|
||||
contentResolver: ContentResolver,
|
||||
filename: String,
|
||||
format: Bitmap.CompressFormat,
|
||||
quality: Int
|
||||
) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/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 {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
@@ -65,7 +82,8 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
val directory =
|
||||
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
@@ -81,7 +99,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCache() {
|
||||
class MangaCache {
|
||||
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt()
|
||||
private val cache = LruCache<String, ImageData>(maxMemory)
|
||||
|
||||
|
||||
@@ -11,9 +11,18 @@ data class MangaChapter(
|
||||
var link: String,
|
||||
var title: String? = null,
|
||||
var description: String? = null,
|
||||
var sChapter: SChapter
|
||||
var sChapter: SChapter,
|
||||
val scanlator: String? = null,
|
||||
var progress: String? = ""
|
||||
) : Serializable {
|
||||
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
|
||||
constructor(chapter: MangaChapter) : this(
|
||||
chapter.number,
|
||||
chapter.link,
|
||||
chapter.title,
|
||||
chapter.description,
|
||||
chapter.sChapter,
|
||||
chapter.scanlator
|
||||
)
|
||||
|
||||
private val images = mutableListOf<MangaImage>()
|
||||
fun images(): List<MangaImage> = images
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.NumberPicker
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemChapterListBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MangaChapterAdapter(
|
||||
private var type: Int,
|
||||
@@ -28,7 +35,15 @@ class MangaChapterAdapter(
|
||||
false
|
||||
)
|
||||
)
|
||||
0 -> ChapterListViewHolder(ItemChapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
|
||||
0 -> ChapterListViewHolder(
|
||||
ItemChapterListBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
@@ -39,7 +54,8 @@ class MangaChapterAdapter(
|
||||
|
||||
override fun getItemCount(): Int = arr.size
|
||||
|
||||
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
@@ -48,12 +64,184 @@ 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) {
|
||||
arr[position].progress = ""
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownload(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) {
|
||||
arr[position].progress = ""
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadNChaptersFrom(position: Int, n: Int) {
|
||||
//download next n chapters
|
||||
for (i in 0..<n) {
|
||||
if (position + i < arr.size) {
|
||||
val chapterNumber = arr[position + i].number
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
//do nothing
|
||||
continue
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
//do nothing
|
||||
continue
|
||||
} else {
|
||||
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||
startDownload(chapterNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private val activeCoroutines = mutableSetOf<String>()
|
||||
val typedValue1 = TypedValue()
|
||||
val typedValue2 = TypedValue()
|
||||
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_sync)
|
||||
startOrContinueRotation(chapterNumber)
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
// Show checkmark
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
|
||||
//binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
|
||||
binding.itemDownload.postDelayed({
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
|
||||
//binding.itemDownload.setColorFilter(typedValue2.data)
|
||||
}, 1000)
|
||||
} else {
|
||||
// Show download icon
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startOrContinueRotation(chapterNumber: String) {
|
||||
if (!isRotationCoroutineRunningFor(chapterNumber)) {
|
||||
val scope = fragment.lifecycle.coroutineScope
|
||||
scope.launch {
|
||||
// Add chapter number to active coroutines set
|
||||
activeCoroutines.add(chapterNumber)
|
||||
while (activeDownloads.contains(chapterNumber)) {
|
||||
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
|
||||
.setInterpolator(
|
||||
LinearInterpolator()
|
||||
).start()
|
||||
delay(1000)
|
||||
}
|
||||
// Remove chapter number from active coroutines set
|
||||
activeCoroutines.remove(chapterNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRotationCoroutineRunningFor(chapterNumber: String): Boolean {
|
||||
return chapterNumber in activeCoroutines
|
||||
}
|
||||
|
||||
init {
|
||||
val theme = currContext()?.theme
|
||||
theme?.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorError,
|
||||
typedValue1,
|
||||
true
|
||||
)
|
||||
theme?.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorPrimary,
|
||||
typedValue2,
|
||||
true
|
||||
)
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
||||
}
|
||||
binding.itemDownload.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
|
||||
val chapterNumber = arr[bindingAdapterPosition].number
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterStopDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||
startDownload(chapterNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.itemDownload.setOnLongClickListener {
|
||||
//Alert dialog asking for the number of chapters to download
|
||||
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
alertDialog.setTitle("Multi Chapter Downloader")
|
||||
alertDialog.setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = itemCount - bindingAdapterPosition
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { dialog, which ->
|
||||
downloadNChaptersFrom(bindingAdapterPosition, input.value)
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
val dialog = alertDialog.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,44 +251,50 @@ class MangaChapterAdapter(
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
val ep = arr[position]
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
|
||||
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
|
||||
if (media.userProgress != null) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat())
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number)
|
||||
?: 9999f) <= media.userProgress!!.toFloat()
|
||||
)
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
else {
|
||||
binding.itemEpisodeViewedCover.visibility = View.GONE
|
||||
binding.itemEpisodeCont.setOnLongClickListener {
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChapterListViewHolder -> {
|
||||
|
||||
is ChapterListViewHolder -> {
|
||||
val binding = holder.binding
|
||||
val ep = arr[position]
|
||||
holder.bind(ep.number, ep.progress)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
binding.itemChapterNumber.text = ep.number
|
||||
if (!ep.title.isNullOrEmpty()) {
|
||||
binding.itemChapterTitle.text = ep.title
|
||||
binding.itemChapterTitle.setOnLongClickListener {
|
||||
binding.itemChapterTitle.maxLines.apply {
|
||||
binding.itemChapterTitle.maxLines = if (this == 1) 3 else 1
|
||||
}
|
||||
true
|
||||
}
|
||||
binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
} else binding.itemChapterTitle.visibility = View.GONE
|
||||
if (ep.progress.isNullOrEmpty()) {
|
||||
binding.itemChapterTitle.visibility = View.GONE
|
||||
} else binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
|
||||
if (media.userProgress != null) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat()) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number)
|
||||
?: 9999f) <= media.userProgress!!.toFloat()
|
||||
) {
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
binding.itemEpisodeViewed.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.itemEpisodeViewedCover.visibility = View.GONE
|
||||
binding.itemEpisodeViewed.visibility = View.GONE
|
||||
binding.root.setOnLongClickListener {
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -117,4 +311,4 @@ class MangaChapterAdapter(
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,29 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.media.anime.handleProgress
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.media.anime.handleProgress
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import com.google.android.material.chip.Chip
|
||||
@@ -31,6 +38,9 @@ class MangaReadAdapter(
|
||||
|
||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||
private var _binding: ItemAnimeWatchBinding? = null
|
||||
val hiddenScanlators = mutableListOf<String>()
|
||||
var scanlatorSelectionListener: ScanlatorSelectionListener? = null
|
||||
var options = listOf<String>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
@@ -45,28 +55,66 @@ class MangaReadAdapter(
|
||||
|
||||
//Wrong Title
|
||||
binding.animeSourceSearch.setOnClickListener {
|
||||
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
|
||||
SourceSearchDialogFragment().show(
|
||||
fragment.requireActivity().supportFragmentManager,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
//Source Selection
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
var source =
|
||||
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
setLanguageList(media.selected!!.langIndex, source)
|
||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||
binding.animeSource.setText(mangaReadSources.names[source])
|
||||
|
||||
mangaReadSources[source].apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
}
|
||||
}
|
||||
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, mangaReadSources.names))
|
||||
binding.animeSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
mangaReadSources.names
|
||||
)
|
||||
)
|
||||
binding.animeSourceTitle.isSelected = true
|
||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
fragment.onSourceChange(i).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
source = i
|
||||
setLanguageList(0, i)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadChapters(i)
|
||||
//invalidate if it's the last source
|
||||
val invalidate = i == mangaReadSources.names.size - 1
|
||||
fragment.loadChapters(i, invalidate)
|
||||
}
|
||||
|
||||
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadChapters(media.selected!!.sourceIndex, true)
|
||||
} ?: run {
|
||||
}
|
||||
}
|
||||
|
||||
//settings
|
||||
binding.animeSourceSettings.setOnClickListener {
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
fragment.openSettings(ext.extension)
|
||||
}
|
||||
}
|
||||
|
||||
//Subscription
|
||||
@@ -98,9 +146,69 @@ class MangaReadAdapter(
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
|
||||
binding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView =
|
||||
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
|
||||
// Dynamically add checkboxes
|
||||
|
||||
options.forEach { option ->
|
||||
val checkBox = CheckBox(currContext()).apply {
|
||||
text = option
|
||||
}
|
||||
//set checked if it's already selected
|
||||
if (media.selected!!.scanlators != null) {
|
||||
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
} else {
|
||||
checkBox.isChecked = true
|
||||
}
|
||||
checkboxContainer.addView(checkBox)
|
||||
}
|
||||
|
||||
// Create AlertDialog
|
||||
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { dialog, which ->
|
||||
//add unchecked to hidden
|
||||
hiddenScanlators.clear()
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
if (!checkBox.isChecked) {
|
||||
hiddenScanlators.add(checkBox.text.toString())
|
||||
}
|
||||
}
|
||||
fragment.onScanlatorChange(hiddenScanlators)
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
binding.animeDownloadTop.setOnClickListener {
|
||||
//Alert dialog asking for the number of chapters to download
|
||||
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
alertDialog.setTitle("Multi Chapter Downloader")
|
||||
alertDialog.setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = 20
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { dialog, which ->
|
||||
fragment.multiDownload(input.value)
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
val dialog = alertDialog.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
var selected = when (style) {
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceCompact
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceCompact
|
||||
else -> binding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
@@ -138,13 +246,39 @@ class MangaReadAdapter(
|
||||
for (position in arr.indices) {
|
||||
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
||||
val chip =
|
||||
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root
|
||||
ItemChipBinding.inflate(
|
||||
LayoutInflater.from(fragment.context),
|
||||
binding.animeSourceChipGroup,
|
||||
false
|
||||
).root
|
||||
chip.isCheckable = true
|
||||
fun selected() {
|
||||
chip.isChecked = true
|
||||
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]}"
|
||||
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
|
||||
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
|
||||
val startChapterString = if (startChapter != null) {
|
||||
"Ch.$startChapter"
|
||||
} else {
|
||||
names[limit * (position)]
|
||||
}
|
||||
val endChapterString = if (endChapter != null) {
|
||||
"Ch.$endChapter"
|
||||
} else {
|
||||
names[last - 1]
|
||||
}
|
||||
//chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
chip.text = "$startChapterString - $endChapterString"
|
||||
chip.setTextColor(
|
||||
ContextCompat.getColorStateList(
|
||||
fragment.requireContext(),
|
||||
R.color.chip_text_color
|
||||
)
|
||||
)
|
||||
|
||||
chip.setOnClickListener {
|
||||
selected()
|
||||
@@ -157,7 +291,14 @@ class MangaReadAdapter(
|
||||
}
|
||||
}
|
||||
if (select != null)
|
||||
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } }
|
||||
binding.animeWatchChipScroll.apply {
|
||||
post {
|
||||
scrollTo(
|
||||
(select.left - screenWidth / 2) + (select.width / 2),
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +308,7 @@ class MangaReadAdapter(
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun handleChapters() {
|
||||
|
||||
val binding = _binding
|
||||
if (binding != null) {
|
||||
if (media.manga?.chapters != null) {
|
||||
@@ -174,7 +316,13 @@ class MangaReadAdapter(
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
val formattedChapters = chapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() }
|
||||
val filteredChapters = chapters.filter { chapterKey ->
|
||||
val chapter = media.manga.chapters!![chapterKey]!!
|
||||
chapter.scanlator !in hiddenScanlators
|
||||
}
|
||||
val formattedChapters = filteredChapters.map {
|
||||
MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
|
||||
}
|
||||
if (formattedChapters.contains(continueEp)) {
|
||||
continueEp = chapters[formattedChapters.indexOf(continueEp)]
|
||||
binding.animeSourceContinue.visibility = View.VISIBLE
|
||||
@@ -222,7 +370,38 @@ class MangaReadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (mangaReadSources is MangaSources) {
|
||||
val parser = mangaReadSources[source] as? DynamicMangaParser
|
||||
if (parser != null) {
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
ext.sourceLanguage = lang
|
||||
}
|
||||
try {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
binding?.animeSourceLanguage?.setText(
|
||||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { it.lang })
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
interface ScanlatorSelectionListener {
|
||||
fun onScanlatorsSelected()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -13,27 +26,43 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class MangaReadFragment : Fragment() {
|
||||
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
private var _binding: FragmentAnimeWatchBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val model: MediaDetailsViewModel by activityViewModels()
|
||||
@@ -48,13 +77,16 @@ open class MangaReadFragment : Fragment() {
|
||||
private lateinit var headerAdapter: MangaReadAdapter
|
||||
private lateinit var chapterAdapter: MangaChapterAdapter
|
||||
|
||||
val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
var screenWidth = 0f
|
||||
private var progress = View.VISIBLE
|
||||
|
||||
var continueEp: Boolean = false
|
||||
var loaded = false
|
||||
|
||||
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
val uiSettings = loadData("ui_settings", toast = false)
|
||||
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -67,10 +99,23 @@ open class MangaReadFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(ACTION_DOWNLOAD_STARTED)
|
||||
addAction(ACTION_DOWNLOAD_FINISHED)
|
||||
addAction(ACTION_DOWNLOAD_FAILED)
|
||||
addAction(ACTION_DOWNLOAD_PROGRESS)
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
requireContext(),
|
||||
downloadStatusReceiver,
|
||||
intentFilter,
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
|
||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||
|
||||
|
||||
var maxGridSize = (screenWidth / 100f).roundToInt()
|
||||
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
||||
|
||||
@@ -81,10 +126,10 @@ open class MangaReadFragment : Fragment() {
|
||||
val style = chapterAdapter.getItemViewType(position)
|
||||
|
||||
return when (position) {
|
||||
0 -> maxGridSize
|
||||
0 -> maxGridSize
|
||||
else -> when (style) {
|
||||
0 -> maxGridSize
|
||||
1 -> 1
|
||||
0 -> maxGridSize
|
||||
1 -> 1
|
||||
else -> maxGridSize
|
||||
}
|
||||
}
|
||||
@@ -107,7 +152,8 @@ open class MangaReadFragment : Fragment() {
|
||||
if (media.format == "MANGA" || media.format == "ONE SHOT") {
|
||||
media.selected = model.loadSelected(media)
|
||||
|
||||
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||
subscribed =
|
||||
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||
|
||||
style = media.selected!!.recyclerStyle
|
||||
reverse = media.selected!!.recyclerReversed
|
||||
@@ -116,9 +162,16 @@ open class MangaReadFragment : Fragment() {
|
||||
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
|
||||
|
||||
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
||||
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||
headerAdapter.scanlatorSelectionListener = this
|
||||
chapterAdapter =
|
||||
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||
|
||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
for (download in downloadManager.mangaDownloads) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.loadMangaChapters(media, media.selected!!.sourceIndex)
|
||||
@@ -129,48 +182,92 @@ open class MangaReadFragment : Fragment() {
|
||||
}
|
||||
} else {
|
||||
binding.animeNotSupported.visibility = View.VISIBLE
|
||||
binding.animeNotSupported.text = getString(R.string.not_supported, media.format ?: "")
|
||||
binding.animeNotSupported.text =
|
||||
getString(R.string.not_supported, media.format ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
|
||||
if (loadedChapters != null) {
|
||||
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
||||
if (chapters != null) {
|
||||
media.manga?.chapters = chapters
|
||||
model.getMangaChapters().observe(viewLifecycleOwner) { _ ->
|
||||
updateChapters()
|
||||
}
|
||||
}
|
||||
|
||||
//CHIP GROUP
|
||||
val total = chapters.size
|
||||
val divisions = total.toDouble() / 10
|
||||
start = 0
|
||||
end = null
|
||||
val limit = when {
|
||||
(divisions < 25) -> 25
|
||||
(divisions < 50) -> 50
|
||||
else -> 100
|
||||
}
|
||||
headerAdapter.clearChips()
|
||||
if (total > limit) {
|
||||
val arr = chapters.keys.toTypedArray()
|
||||
val stored = ceil((total).toDouble() / limit).toInt()
|
||||
val position = clamp(media.selected!!.chip, 0, stored - 1)
|
||||
val last = if (position + 1 == stored) total else (limit * (position + 1))
|
||||
start = limit * (position)
|
||||
end = last - 1
|
||||
headerAdapter.updateChips(
|
||||
limit,
|
||||
arr,
|
||||
(1..stored).toList().toTypedArray(),
|
||||
position
|
||||
)
|
||||
}
|
||||
override fun onScanlatorsSelected() {
|
||||
updateChapters()
|
||||
}
|
||||
|
||||
headerAdapter.subscribeButton(true)
|
||||
reload()
|
||||
}
|
||||
fun multiDownload(n: Int) {
|
||||
//get last viewed chapter
|
||||
val selected = media.userProgress
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
//filter by selected language
|
||||
val progressChapterIndex = chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected }?:0
|
||||
val chaptersToDownload = chapters?.subList(
|
||||
progressChapterIndex + 1,
|
||||
progressChapterIndex + n + 1
|
||||
)
|
||||
if (chaptersToDownload != null) {
|
||||
for (chapter in chaptersToDownload) {
|
||||
onMangaChapterDownloadClick(chapter.title!!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updateChapters() {
|
||||
val loadedChapters = model.getMangaChapters().value
|
||||
if (loadedChapters != null) {
|
||||
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
||||
if (chapters != null) {
|
||||
headerAdapter.options = getScanlators(chapters)
|
||||
val filteredChapters = chapters.filterNot { (_, chapter) ->
|
||||
chapter.scanlator in headerAdapter.hiddenScanlators
|
||||
}
|
||||
|
||||
media.manga?.chapters = filteredChapters.toMutableMap()
|
||||
|
||||
//CHIP GROUP
|
||||
val total = filteredChapters.size
|
||||
val divisions = total.toDouble() / 10
|
||||
start = 0
|
||||
end = null
|
||||
val limit = when {
|
||||
(divisions < 25) -> 25
|
||||
(divisions < 50) -> 50
|
||||
else -> 100
|
||||
}
|
||||
headerAdapter.clearChips()
|
||||
if (total > limit) {
|
||||
val arr = filteredChapters.keys.toTypedArray()
|
||||
val stored = ceil((total).toDouble() / limit).toInt()
|
||||
val position = clamp(media.selected!!.chip, 0, stored - 1)
|
||||
val last = if (position + 1 == stored) total else (limit * (position + 1))
|
||||
start = limit * (position)
|
||||
end = last - 1
|
||||
headerAdapter.updateChips(
|
||||
limit,
|
||||
arr,
|
||||
(1..stored).toList().toTypedArray(),
|
||||
position
|
||||
)
|
||||
}
|
||||
|
||||
headerAdapter.subscribeButton(true)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
|
||||
val scanlators = mutableListOf<String>()
|
||||
if (chap != null) {
|
||||
val chapters = chap.values
|
||||
for (chapter in chapters) {
|
||||
scanlators.add(chapter.scanlator ?: "Unknown")
|
||||
}
|
||||
}
|
||||
return scanlators.distinct()
|
||||
}
|
||||
|
||||
fun onSourceChange(i: Int): MangaParser {
|
||||
@@ -185,8 +282,22 @@ open class MangaReadFragment : Fragment() {
|
||||
return model.mangaReadSources?.get(i)!!
|
||||
}
|
||||
|
||||
fun loadChapters(i: Int) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) }
|
||||
fun onLangChange(i: Int) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.langIndex = i
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
fun onScanlatorChange(list: List<String>) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.scanlators = list
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
fun loadChapters(i: Int, invalidate: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i, invalidate) }
|
||||
}
|
||||
|
||||
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||
@@ -225,22 +336,217 @@ 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
|
||||
val dialog = 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()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
loadChapters(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
changeUIVisibility(false)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterClick(i: String) {
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let {
|
||||
media.manga?.selectedChapter = i
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
ChapterLoaderDialog.newInstance(it, true).show(requireActivity().supportFragmentManager, "dialog")
|
||||
ChapterLoaderDialog.newInstance(it, true)
|
||||
.show(requireActivity().supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
|
||||
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 ?: media.nameRomaji,
|
||||
chapter = chapter.title!!,
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
retries = 2,
|
||||
simultaneousDownloads = 2
|
||||
)
|
||||
|
||||
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||
|
||||
// If the service is not already running, start it
|
||||
if (!MangaServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, MangaDownloaderService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(requireContext(), intent)
|
||||
}
|
||||
MangaServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
|
||||
// Inform the adapter that the download has started
|
||||
withContext(Dispatchers.Main) {
|
||||
chapterAdapter.startDownload(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ?: media.nameRomaji,
|
||||
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 ?: media.nameRomaji,
|
||||
i,
|
||||
Download.Type.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.purgeDownload(i)
|
||||
}
|
||||
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (!this@MangaReadFragment::chapterAdapter.isInitialized) return
|
||||
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.purgeDownload(it)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_PROGRESS -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||
val progress = intent.getIntExtra("progress", 0)
|
||||
chapterNumber?.let {
|
||||
chapterAdapter.updateDownloadProgress(it, progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
|
||||
//Find latest chapter for subscription
|
||||
selected.latest = media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||
selected.latest =
|
||||
media.manga?.chapters?.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.handleChapters()
|
||||
@@ -249,7 +555,8 @@ open class MangaReadFragment : Fragment() {
|
||||
if (media.manga!!.chapters != null) {
|
||||
val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null
|
||||
arr.addAll(
|
||||
media.manga!!.chapters!!.values.toList().slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
|
||||
media.manga!!.chapters!!.values.toList()
|
||||
.slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
|
||||
)
|
||||
if (reverse)
|
||||
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr
|
||||
@@ -262,6 +569,7 @@ open class MangaReadFragment : Fragment() {
|
||||
override fun onDestroy() {
|
||||
model.mangaReadSources?.flushText()
|
||||
super.onDestroy()
|
||||
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||
}
|
||||
|
||||
private var state: Parcelable? = null
|
||||
@@ -275,4 +583,12 @@ open class MangaReadFragment : Fragment() {
|
||||
super.onPause()
|
||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.net.Uri
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -13,8 +14,8 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.Glide
|
||||
@@ -22,13 +23,11 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseImageAdapter(
|
||||
val activity: MangaReaderActivity,
|
||||
@@ -116,7 +115,10 @@ abstract class BaseImageAdapter(
|
||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||
|
||||
companion object {
|
||||
suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||
suspend fun Context.loadBitmap_old(
|
||||
link: FileUrl,
|
||||
transforms: List<BitmapTransformation>
|
||||
): Bitmap? { //still used in some places
|
||||
return tryWithSuspend {
|
||||
withContext(Dispatchers.IO) {
|
||||
Glide.with(this@loadBitmap_old)
|
||||
@@ -133,8 +135,7 @@ abstract class BaseImageAdapter(
|
||||
.let {
|
||||
if (transforms.isNotEmpty()) {
|
||||
it.transform(*transforms.toTypedArray())
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
@@ -144,24 +145,29 @@ abstract class BaseImageAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||
suspend fun Context.loadBitmap(
|
||||
link: FileUrl,
|
||||
transforms: List<BitmapTransformation>
|
||||
): Bitmap? {
|
||||
return tryWithSuspend {
|
||||
val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>()
|
||||
withContext(Dispatchers.IO) {
|
||||
Glide.with(this@loadBitmap)
|
||||
.asBitmap()
|
||||
.let {
|
||||
if (link.url.startsWith("file://")) {
|
||||
it.load(link.url)
|
||||
val fileUri = Uri.fromFile(File(link.url)).toString()
|
||||
val localFile = File(link.url)
|
||||
if (localFile.exists()) {
|
||||
it.load(localFile.absoluteFile)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
println("bitmap from cache")
|
||||
println(link.url)
|
||||
println(mangaCache.get(link.url))
|
||||
println("cache size: ${mangaCache.size()}")
|
||||
mangaCache.get(link.url)?.let { imageData ->
|
||||
val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap)
|
||||
val bitmap = imageData.fetchAndProcessImage(
|
||||
imageData.page,
|
||||
imageData.source,
|
||||
context = this@loadBitmap
|
||||
)
|
||||
it.load(bitmap)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package ani.dantotsu.media.manga.mangareader
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -12,9 +14,9 @@ import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaSingleton
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.tryWith
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,8 +29,8 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
||||
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
|
||||
private val launch : Boolean by lazy { arguments?.getBoolean("launch", false) ?: false }
|
||||
private val chp : MangaChapter by lazy { arguments?.getSerialized("next")!! }
|
||||
private val launch: Boolean by lazy { arguments?.getBoolean("launch", false) ?: false }
|
||||
private val chp: MangaChapter by lazy { arguments?.getSerialized("next")!! }
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
var loaded = false
|
||||
@@ -45,13 +47,21 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
||||
loaded = true
|
||||
binding.selectorAutoText.text = chp.title
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if(model.loadMangaChapterImages(chp, m.selected!!)) {
|
||||
if (model.loadMangaChapterImages(
|
||||
chp,
|
||||
m.selected!!,
|
||||
m.nameMAL ?: m.nameRomaji
|
||||
)
|
||||
) {
|
||||
val activity = currActivity()
|
||||
activity?.runOnUiThread {
|
||||
tryWith { dismiss() }
|
||||
if(launch) {
|
||||
if (launch) {
|
||||
MediaSingleton.media = m
|
||||
val intent = Intent(activity, MangaReaderActivity::class.java)//.apply { putExtra("media", m) }
|
||||
val intent = Intent(
|
||||
activity,
|
||||
MangaReaderActivity::class.java
|
||||
)//.apply { putExtra("media", m) }
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -61,8 +71,18 @@ 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)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -30,15 +30,15 @@ open class ImageAdapter(
|
||||
|
||||
inner class ImageViewHolder(binding: ItemImageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
open suspend fun loadBitmap(position: Int, parent: View) : Bitmap? {
|
||||
open suspend fun loadBitmap(position: Int, parent: View): Bitmap? {
|
||||
val link = images.getOrNull(position)?.url ?: return null
|
||||
if (link.url.isEmpty()) return null
|
||||
|
||||
val transforms = mutableListOf<BitmapTransformation>()
|
||||
val parserTransformation = activity.getTransformation(images[position])
|
||||
|
||||
if(parserTransformation!=null) transforms.add(parserTransformation)
|
||||
if(settings.cropBorders) {
|
||||
if (parserTransformation != null) transforms.add(parserTransformation)
|
||||
if (settings.cropBorders) {
|
||||
transforms.add(RemoveBordersTransformation(true, settings.cropBorderThreshold))
|
||||
transforms.add(RemoveBordersTransformation(false, settings.cropBorderThreshold))
|
||||
}
|
||||
@@ -47,7 +47,8 @@ open class ImageAdapter(
|
||||
}
|
||||
|
||||
override suspend fun loadImage(position: Int, parent: View): Boolean {
|
||||
val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures) ?: return false
|
||||
val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||
?: return false
|
||||
val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false
|
||||
imageView.recycle()
|
||||
imageView.visibility = View.GONE
|
||||
@@ -60,10 +61,12 @@ open class ImageAdapter(
|
||||
if (settings.layout != PAGED)
|
||||
parent.updateLayoutParams {
|
||||
if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) {
|
||||
sHeight = if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt()
|
||||
sHeight =
|
||||
if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt()
|
||||
height = sHeight
|
||||
} else {
|
||||
sWidth = if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt()
|
||||
sWidth =
|
||||
if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt()
|
||||
width = sWidth
|
||||
}
|
||||
}
|
||||
@@ -73,7 +76,8 @@ open class ImageAdapter(
|
||||
|
||||
val parentArea = sWidth * sHeight * 1f
|
||||
val bitmapArea = bitmap.width * bitmap.height * 1f
|
||||
val scale = if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea)
|
||||
val scale =
|
||||
if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea)
|
||||
|
||||
imageView.maxScale = scale * 1.1f
|
||||
imageView.minScale = scale
|
||||
|
||||
@@ -3,6 +3,7 @@ package ani.dantotsu.media.manga.mangareader
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
@@ -11,6 +12,7 @@ import android.view.*
|
||||
import android.view.KeyEvent.*
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AdapterView
|
||||
import android.widget.CheckBox
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -25,6 +27,8 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.connections.discord.DiscordService
|
||||
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
||||
import ani.dantotsu.connections.discord.RPC
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityMangaReaderBinding
|
||||
@@ -35,7 +39,7 @@ import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
@@ -53,8 +57,6 @@ import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -93,14 +95,17 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
var sliding = false
|
||||
var isAnimating = false
|
||||
|
||||
private var rpc : RPC? = null
|
||||
private var rpc: RPC? = null
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) {
|
||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())
|
||||
notchHeight = min(
|
||||
displayCutout.boundingRects[0].width(),
|
||||
displayCutout.boundingRects[0].height()
|
||||
)
|
||||
checkNotch()
|
||||
}
|
||||
}
|
||||
@@ -120,12 +125,17 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
mangaCache.clear()
|
||||
rpc?.close()
|
||||
if (DiscordServiceRunningSingleton.running) {
|
||||
DiscordServiceRunningSingleton.running = false
|
||||
val stopIntent = Intent(this, DiscordService::class.java)
|
||||
stopService(stopIntent)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -138,8 +148,14 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
progress { finish() }
|
||||
}
|
||||
|
||||
settings = loadData("reader_settings", this) ?: ReaderSettings().apply { saveData("reader_settings", this) }
|
||||
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
settings = loadData("reader_settings", this)
|
||||
?: ReaderSettings().apply { saveData("reader_settings", this) }
|
||||
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply {
|
||||
saveData(
|
||||
"ui_settings",
|
||||
this
|
||||
)
|
||||
}
|
||||
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
|
||||
|
||||
hideBars()
|
||||
@@ -164,9 +180,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
if (fromUser) {
|
||||
sliding = true
|
||||
if (settings.default.layout != PAGED)
|
||||
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } ?: 1))
|
||||
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
|
||||
?: 1))
|
||||
else
|
||||
binding.mangaReaderPager.currentItem = (value.toInt() - 1) / (dualPage { 2 } ?: 1)
|
||||
binding.mangaReaderPager.currentItem =
|
||||
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
|
||||
pageSliderHide()
|
||||
}
|
||||
}
|
||||
@@ -192,30 +210,35 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
|
||||
binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE
|
||||
if(model.mangaReadSources!!.names.isEmpty()){
|
||||
if (model.mangaReadSources!!.names.isEmpty()) {
|
||||
//try to reload sources
|
||||
try {
|
||||
if (media.isAdult) {
|
||||
val mangaSources = MangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
if (media.isAdult) {
|
||||
val mangaSources = MangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
} else {
|
||||
val mangaSources = HMangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
}else{
|
||||
val mangaSources = HMangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
}
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
Firebase.crashlytics.recordException(e)
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex]
|
||||
//check that index is not out of bounds (crash fix)
|
||||
if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) {
|
||||
media.selected!!.sourceIndex = 0
|
||||
}
|
||||
binding.mangaReaderSource.text =
|
||||
model.mangaReadSources!!.names[media.selected!!.sourceIndex]
|
||||
|
||||
binding.mangaReaderTitle.text = media.userPreferredName
|
||||
|
||||
@@ -228,39 +251,51 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}")
|
||||
}
|
||||
|
||||
showProgressDialog = if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
|
||||
showProgressDialog =
|
||||
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
|
||||
progressDialog =
|
||||
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
|
||||
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.title_update_progress)).apply {
|
||||
setMultiChoiceItems(
|
||||
arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)),
|
||||
booleanArrayOf(false)
|
||||
) { _, _, isChecked ->
|
||||
if (isChecked) progressDialog = null
|
||||
saveData("${media.id}_progressDialog", isChecked)
|
||||
showProgressDialog = isChecked
|
||||
}
|
||||
setOnCancelListener { hideBars() }
|
||||
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) {
|
||||
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
|
||||
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
|
||||
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) progressDialog = null
|
||||
saveData("${media.id}_progressDialog", isChecked)
|
||||
showProgressDialog = isChecked
|
||||
}
|
||||
else null
|
||||
AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle(getString(R.string.title_update_progress))
|
||||
.setView(dialogView)
|
||||
.apply {
|
||||
setOnCancelListener { hideBars() }
|
||||
}
|
||||
} else null
|
||||
|
||||
//Chapter Change
|
||||
fun change(index: Int) {
|
||||
mangaCache.clear()
|
||||
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
|
||||
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog")
|
||||
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!)
|
||||
.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
//ChapterSelector
|
||||
binding.mangaReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr)
|
||||
binding.mangaReaderChapterSelect.adapter =
|
||||
NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr)
|
||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||
binding.mangaReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
|
||||
if (position != currentChapterIndex) change(position)
|
||||
}
|
||||
binding.mangaReaderChapterSelect.onItemSelectedListener =
|
||||
object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
p0: AdapterView<*>?,
|
||||
p1: View?,
|
||||
position: Int,
|
||||
p3: Long
|
||||
) {
|
||||
if (position != currentChapterIndex) change(position)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) {}
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>) {}
|
||||
}
|
||||
|
||||
binding.mangaReaderSettings.setSafeOnClickListener {
|
||||
ReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, "settings")
|
||||
@@ -291,40 +326,71 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
saveData("${media.id}_current_chp", chap.number, this)
|
||||
currentChapterIndex = chaptersArr.indexOf(chap.number)
|
||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
applySettings()
|
||||
rpc?.close()
|
||||
rpc = Discord.defaultRPC()
|
||||
rpc?.send {
|
||||
type = RPC.Type.WATCHING
|
||||
activityName = media.userPreferredName
|
||||
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number)
|
||||
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}"
|
||||
media.cover?.let { cover ->
|
||||
largeImage = RPC.Link(media.userPreferredName, cover)
|
||||
}
|
||||
media.shareLink?.let { link ->
|
||||
buttons.add(0, RPC.Link(getString(R.string.view_manga), link))
|
||||
val context = this
|
||||
val incognito = context.getSharedPreferences("Dantotsu", 0)
|
||||
?.getBoolean("incognito", false) ?: false
|
||||
if (isOnline(context) && Discord.token != null && !incognito) {
|
||||
lifecycleScope.launch {
|
||||
val presence = RPC.createPresence(
|
||||
RPC.Companion.RPCData(
|
||||
applicationId = Discord.application_Id,
|
||||
type = RPC.Type.WATCHING,
|
||||
activityName = media.userPreferredName,
|
||||
details = chap.title?.takeIf { it.isNotEmpty() }
|
||||
?: getString(R.string.chapter_num, chap.number),
|
||||
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
|
||||
largeImage = media.cover?.let { cover ->
|
||||
RPC.Link(media.userPreferredName, cover)
|
||||
},
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
Discord.small_Image
|
||||
),
|
||||
buttons = mutableListOf(
|
||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
RPC.Link(
|
||||
"Stream on Dantotsu",
|
||||
"https://github.com/rebelonion/Dantotsu/"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val intent = Intent(context, DiscordService::class.java).apply {
|
||||
putExtra("presence", presence)
|
||||
}
|
||||
DiscordServiceRunningSingleton.running = true
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) }
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMangaChapterImages(
|
||||
chapter,
|
||||
media.selected!!,
|
||||
media.nameMAL ?: media.nameRomaji
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
|
||||
fun <T> dualPage(callback: () -> T): T? {
|
||||
return when (settings.default.dualPageMode) {
|
||||
No -> null
|
||||
No -> null
|
||||
Automatic -> {
|
||||
val orientation = resources.configuration.orientation
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke()
|
||||
else null
|
||||
}
|
||||
Force -> callback.invoke()
|
||||
|
||||
Force -> callback.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +421,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
maxChapterPage = chapImages.size.toLong()
|
||||
saveData("${media.id}_${chapter.number}_max", maxChapterPage)
|
||||
|
||||
imageAdapter = dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
|
||||
imageAdapter =
|
||||
dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
|
||||
|
||||
if (chapImages.size > 1) {
|
||||
binding.mangaReaderSlider.apply {
|
||||
@@ -376,8 +443,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) {
|
||||
binding.mangaReaderSwipy.vertical = true
|
||||
if (settings.default.direction == TOP_TO_BOTTOM) {
|
||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.mangaReaderSwipy.onTopSwiped = {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
@@ -385,8 +454,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
} else {
|
||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.mangaReaderSwipy.onTopSwiped = {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
@@ -409,8 +480,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
} else {
|
||||
binding.mangaReaderSwipy.vertical = false
|
||||
if (settings.default.direction == RIGHT_TO_LEFT) {
|
||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.mangaReaderSwipy.onLeftSwiped = {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
@@ -418,8 +491,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
} else {
|
||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.mangaReaderSwipy.onLeftSwiped = {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
@@ -444,7 +519,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
if (settings.default.layout != PAGED) {
|
||||
|
||||
binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE
|
||||
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled = settings.default.rotation
|
||||
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled =
|
||||
settings.default.rotation
|
||||
|
||||
val detector = GestureDetectorCompat(this, object : GesturesListener() {
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
@@ -452,18 +528,31 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
child ?: return@let false
|
||||
val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child)
|
||||
val callback: (ImageViewDialog) -> Unit = { dialog ->
|
||||
lifecycleScope.launch { imageAdapter?.loadImage(pos, child as GestureFrameLayout) }
|
||||
binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
lifecycleScope.launch {
|
||||
imageAdapter?.loadImage(
|
||||
pos,
|
||||
child as GestureFrameLayout
|
||||
)
|
||||
}
|
||||
binding.mangaReaderRecycler.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS
|
||||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
dualPage {
|
||||
val page = chapter.dualPages().getOrNull(pos) ?: return@dualPage false
|
||||
val page =
|
||||
chapter.dualPages().getOrNull(pos) ?: return@dualPage false
|
||||
val nextPage = page.second
|
||||
if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null)
|
||||
onImageLongClicked(pos * 2, nextPage, page.first, callback)
|
||||
else
|
||||
onImageLongClicked(pos * 2, page.first, nextPage, callback)
|
||||
} ?: onImageLongClicked(pos, chapImages.getOrNull(pos) ?: return@let false, null, callback)
|
||||
} ?: onImageLongClicked(
|
||||
pos,
|
||||
chapImages.getOrNull(pos) ?: return@let false,
|
||||
null,
|
||||
callback
|
||||
)
|
||||
}
|
||||
) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
super.onLongPress(e)
|
||||
@@ -505,12 +594,16 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
&& (!v.canScrollVertically(-1) || !v.canScrollVertically(1)))
|
||||
||
|
||||
((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT)
|
||||
&& (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(1)))
|
||||
&& (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(
|
||||
1
|
||||
)))
|
||||
) {
|
||||
handleController(true)
|
||||
} else handleController(false)
|
||||
}
|
||||
updatePageNumber(manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 } ?: 1) + 1)
|
||||
updatePageNumber(
|
||||
manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 }
|
||||
?: 1) + 1)
|
||||
super.onScrolled(v, dx, dy)
|
||||
}
|
||||
})
|
||||
@@ -572,7 +665,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
private var onVolumeDown: (() -> Unit)? = null
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return when (event.keyCode) {
|
||||
KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> {
|
||||
KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> {
|
||||
if (event.keyCode == KEYCODE_VOLUME_UP)
|
||||
if (!settings.default.volumeButtons)
|
||||
return false
|
||||
@@ -581,6 +674,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> {
|
||||
if (event.keyCode == KEYCODE_VOLUME_DOWN)
|
||||
if (!settings.default.volumeButtons)
|
||||
@@ -590,7 +684,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
true
|
||||
} else false
|
||||
}
|
||||
else -> {
|
||||
|
||||
else -> {
|
||||
super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
@@ -664,8 +759,14 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
isContVisible = false
|
||||
if (!isAnimating) {
|
||||
isAnimating = true
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f)
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
|
||||
.setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(
|
||||
binding.mangaReaderBottomLayout,
|
||||
"translationY",
|
||||
0f,
|
||||
128f
|
||||
)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
@@ -674,7 +775,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
} else {
|
||||
isContVisible = true
|
||||
binding.mangaReaderCont.visibility = View.VISIBLE
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f)
|
||||
.setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f)
|
||||
@@ -700,6 +802,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
model.loadMangaChapterImages(
|
||||
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
||||
media.selected!!,
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
false
|
||||
)
|
||||
loading = false
|
||||
@@ -712,7 +815,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
progressDialog?.setCancelable(false)
|
||||
?.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||
saveData("${media.id}_save_progress", true)
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString())
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
.toString()
|
||||
)
|
||||
dialog.dismiss()
|
||||
runnable.run()
|
||||
}
|
||||
@@ -724,7 +831,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
progressDialog?.show()
|
||||
} else {
|
||||
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString())
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
.toString()
|
||||
)
|
||||
runnable.run()
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -10,14 +10,15 @@ import kotlin.math.max
|
||||
|
||||
class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) :
|
||||
LinearLayoutManager(context, orientation, reverseLayout) {
|
||||
private val mOrientationHelper: OrientationHelper = OrientationHelper.createOrientationHelper(this, orientation)
|
||||
private val mOrientationHelper: OrientationHelper =
|
||||
OrientationHelper.createOrientationHelper(this, orientation)
|
||||
|
||||
/**
|
||||
* As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us,
|
||||
* we only need to prefetch additional ones.
|
||||
*/
|
||||
var preloadItemCount = 1
|
||||
set(count){
|
||||
set(count) {
|
||||
require(count >= 1) { "preloadItemCount must not be smaller than 1!" }
|
||||
field = count - 1
|
||||
}
|
||||
@@ -37,7 +38,8 @@ class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayo
|
||||
val currentPosition: Int = getPosition(child ?: return) + layoutDirection
|
||||
|
||||
if (layoutDirection == 1) {
|
||||
val scrollingOffset = (mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding)
|
||||
val scrollingOffset =
|
||||
(mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding)
|
||||
((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach {
|
||||
if (it >= 0 && it < state.itemCount) {
|
||||
layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user