commit
4508cada0f
230 changed files with 9161 additions and 3762 deletions
14
.github/workflows/beta.yml
vendored
14
.github/workflows/beta.yml
vendored
|
@ -4,10 +4,14 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/README.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: true
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
|
@ -28,11 +32,17 @@ jobs:
|
|||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Decode Keystore File
|
||||
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
|
||||
|
||||
- name: List files in the directory
|
||||
run: ls -l
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assembleDebug
|
||||
run: ./gradlew assembleDebug -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v3.0.0
|
||||
|
@ -44,7 +54,7 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" )
|
||||
curl -F "payload_json={\"content\":\" everyone **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "payload_json={\"content\":\" Debug-Build **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
- name: Delete Old Pre-Releases
|
||||
id: delete-pre-releases
|
||||
|
|
|
@ -21,7 +21,7 @@ android {
|
|||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "2.0.0-beta00-i"
|
||||
versionName "2.0.0-beta01-iv1"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
|
@ -29,12 +29,12 @@ android {
|
|||
debug {
|
||||
applicationIdSuffix ".beta"
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||
debuggable true
|
||||
debuggable System.getenv("CI") == null
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||
debuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
|
@ -54,19 +54,19 @@ android {
|
|||
dependencies {
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.browser:browser:1.6.0'
|
||||
implementation 'androidx.browser:browser:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.9.0'
|
||||
|
||||
// Glide
|
||||
|
@ -90,9 +90,12 @@ dependencies {
|
|||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||
implementation "androidx.media3:media3-session:$exo_version"
|
||||
//media3 casting
|
||||
implementation "androidx.media3:media3-cast:$exo_version"
|
||||
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||
|
@ -100,7 +103,7 @@ dependencies {
|
|||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation "com.github.skydoves:colorpickerview:2.3.0"
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
|
||||
// string matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
@ -115,13 +118,14 @@ dependencies {
|
|||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||
implementation 'com.squareup.okio:okio:3.3.0'
|
||||
implementation 'ch.acra:acra-http:5.9.7'
|
||||
implementation 'com.squareup.okio:okio:3.7.0'
|
||||
implementation 'ch.acra:acra-http:5.11.3'
|
||||
implementation 'org.jsoup:jsoup:1.15.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'app.cash.quickjs:quickjs-android:0.9.2'
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -10,6 +10,8 @@
|
|||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
@ -17,25 +19,22 @@
|
|||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<queries>
|
||||
|
@ -48,6 +47,7 @@
|
|||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner_foreground"
|
||||
android:icon="${icon_placeholder}"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
|
@ -56,14 +56,28 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup"
|
||||
android:banner="@drawable/ic_banner_foreground">
|
||||
tools:ignore="AllowBackup">
|
||||
<receiver
|
||||
android:name=".widgets.CurrentlyAiringWidget"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/currently_airing_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".subcriptions.NotificationClickReceiver" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity"
|
||||
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" >
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/epub+zip" />
|
||||
|
@ -71,13 +85,11 @@
|
|||
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||
<data android:mimeType="application/fb2+zip" />
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
|
||||
<data android:pathPattern=".*\\.epub" />
|
||||
<data android:pathPattern=".*\\.mobi" />
|
||||
<data android:pathPattern=".*\\.kf8" />
|
||||
<data android:pathPattern=".*\\.fb2" />
|
||||
<data android:pathPattern=".*\\.cbz" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
|
@ -103,9 +115,9 @@
|
|||
<activity
|
||||
android:name=".media.CalendarActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name="ani.dantotsu.media.user.ListActivity" />
|
||||
<activity android:name=".media.user.ListActivity" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity"
|
||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/manga"
|
||||
|
@ -118,7 +130,7 @@
|
|||
<activity android:name=".media.CharacterDetailsActivity" />
|
||||
<activity android:name=".home.NoInternet" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.anime.ExoplayerView"
|
||||
android:name=".media.anime.ExoplayerView"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
|
@ -127,7 +139,7 @@
|
|||
android:supportsPictureInPicture="true"
|
||||
tools:targetApi="n" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.anilist.Login"
|
||||
android:name=".connections.anilist.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
|
@ -144,7 +156,7 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.mal.Login"
|
||||
android:name=".connections.mal.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
|
@ -160,8 +172,8 @@
|
|||
android:scheme="dantotsu" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="ani.dantotsu.connections.discord.Login"
|
||||
<activity
|
||||
android:name=".connections.discord.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
|
@ -172,15 +184,32 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="dantotsu"/>
|
||||
<data android:scheme="dantotsu" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="discord.dantotsu.com"/>
|
||||
<data android:host="discord.dantotsu.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.anilist.UrlMedia"
|
||||
android:name=".others.webview.CookieCatcher"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter android:label="Discord Login for Dantotsu">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="dantotsu" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="discord.dantotsu.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".connections.anilist.UrlMedia"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
|
@ -216,30 +245,30 @@
|
|||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".download.DownloadContainerActivity" />
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver
|
||||
android:name=".subcriptions.AlarmReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM"/>
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
@ -258,32 +287,49 @@
|
|||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<service android:name=".download.video.MyDownloadService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<service
|
||||
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||
android:exported="true" />
|
||||
<service
|
||||
android:name=".download.video.ExoplayerDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".download.manga.MangaDownloaderService"
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".download.novel.NovelDownloaderService"
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".connections.discord.DiscordService"
|
||||
<service
|
||||
android:name=".download.manga.MangaDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".download.novel.NovelDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name=".download.anime.AnimeDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".connections.discord.DiscordService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="true"/>
|
||||
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -13,7 +13,9 @@ import ani.dantotsu.parsers.AnimeSources
|
|||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
|
@ -58,6 +60,24 @@ class App : MultiDexApplication() {
|
|||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||
|
||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).getBoolean("shared_user_id", true).let {
|
||||
if (!it) return@let
|
||||
val dUsername = getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).getString("discord_username", null)
|
||||
val aUsername = getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).getString("anilist_username", null)
|
||||
if (dUsername != null || aUsername != null) {
|
||||
Firebase.crashlytics.setUserId("$dUsername - $aUsername")
|
||||
}
|
||||
}
|
||||
FirebaseCrashlytics.getInstance().setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
|
@ -77,13 +97,13 @@ class App : MultiDexApplication() {
|
|||
animeScope.launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
|
||||
}
|
||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||
mangaScope.launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
|
||||
}
|
||||
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||
novelScope.launch {
|
||||
|
@ -91,7 +111,6 @@ class App : MultiDexApplication() {
|
|||
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.animation.ObjectAnimator
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
|
@ -28,6 +30,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|||
import android.view.animation.*
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
|
@ -44,6 +47,7 @@ import ani.dantotsu.databinding.ItemCountDownBinding
|
|||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.subcriptions.NotificationClickReceiver
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||
|
@ -53,6 +57,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.internal.ViewUtils
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import kotlinx.coroutines.*
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import java.io.*
|
||||
|
@ -124,6 +130,13 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
|
||||
//try to delete the file
|
||||
try {
|
||||
a?.deleteFile(fileName)
|
||||
} catch (e: Exception) {
|
||||
FirebaseCrashlytics.getInstance().log("Failed to delete file $fileName")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
|
@ -601,7 +614,7 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
|
|||
"$APPLICATION_ID.provider",
|
||||
saveImage(
|
||||
bitmap,
|
||||
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||
title
|
||||
) ?: return
|
||||
)
|
||||
|
@ -624,13 +637,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
|||
|
||||
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
||||
val imageFile = File(path, "$imageFileName.png")
|
||||
return tryWith {
|
||||
return try {
|
||||
val fOut: OutputStream = FileOutputStream(imageFile)
|
||||
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
|
||||
fOut.close()
|
||||
scanFile(imageFile.absolutePath, currContext()!!)
|
||||
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
|
||||
imageFile
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to save image: ${e.localizedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -673,7 +689,7 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
|||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun countDown(media: Media, view: ViewGroup) {
|
||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) {
|
||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
|
@ -783,35 +799,40 @@ fun toast(string: String?) {
|
|||
}
|
||||
|
||||
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(
|
||||
window.decorView.findViewById(android.R.id.content),
|
||||
s,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
width = WRAP_CONTENT
|
||||
}
|
||||
translationY = -(navBarHeight.dp + 32f)
|
||||
translationZ = 32f
|
||||
updatePadding(16f.px, right = 16f.px)
|
||||
setOnClickListener {
|
||||
snackBar.dismiss()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
copyToClipboard(clipboard ?: s, false)
|
||||
toast(getString(R.string.copied_to_clipboard))
|
||||
true
|
||||
try { //I have no idea why this sometimes crashes for some people...
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(
|
||||
window.decorView.findViewById(android.R.id.content),
|
||||
s,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
width = WRAP_CONTENT
|
||||
}
|
||||
translationY = -(navBarHeight.dp + 32f)
|
||||
translationZ = 32f
|
||||
updatePadding(16f.px, right = 16f.px)
|
||||
setOnClickListener {
|
||||
snackBar.dismiss()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
copyToClipboard(clipboard ?: s, false)
|
||||
toast(getString(R.string.copied_to_clipboard))
|
||||
true
|
||||
}
|
||||
}
|
||||
snackBar.show()
|
||||
}
|
||||
snackBar.show()
|
||||
}
|
||||
logger(s)
|
||||
}
|
||||
logger(s)
|
||||
} catch (e: Exception) {
|
||||
logger(e.stackTraceToString())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -930,6 +951,33 @@ fun checkCountry(context: Context): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
const val INCOGNITO_CHANNEL_ID = 26
|
||||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
fun incognitoNotification(context: Context) {
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val incognito = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("incognito", false)
|
||||
if (incognito) {
|
||||
val intent = Intent(context, NotificationClickReceiver::class.java)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context, 0, intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
|
||||
.setSmallIcon(R.drawable.ic_incognito_24)
|
||||
.setContentTitle("Incognito Mode")
|
||||
.setContentText("Disable Incognito Mode")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
notificationManager.notify(INCOGNITO_CHANNEL_ID, builder.build())
|
||||
} else {
|
||||
notificationManager.cancel(INCOGNITO_CHANNEL_ID)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun View.pop() {
|
||||
currActivity()?.runOnUiThread {
|
||||
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ani.dantotsu
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
|
@ -11,12 +12,14 @@ import android.os.Bundle
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -26,11 +29,14 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
|
@ -39,12 +45,14 @@ import ani.dantotsu.home.NoInternet
|
|||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.SharedPreferenceBooleanLiveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -54,17 +62,23 @@ import java.io.Serializable
|
|||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
|
||||
private val scope = lifecycleScope
|
||||
private var load = false
|
||||
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
|
||||
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
//get FRAGMENT_CLASS_NAME from intent
|
||||
val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
@ -73,17 +87,59 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
}
|
||||
|
||||
val offset = try {
|
||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
resources.getDimensionPixelSize(statusBarHeightId)
|
||||
} catch (e: Exception) {
|
||||
statusBarHeight
|
||||
}
|
||||
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
|
||||
layoutParams.topMargin = 11 * offset / 12
|
||||
binding.incognito.layoutParams = layoutParams
|
||||
incognitoLiveData = SharedPreferenceBooleanLiveData(
|
||||
sharedPreferences,
|
||||
"incognito",
|
||||
false
|
||||
)
|
||||
incognitoLiveData.observe(this) {
|
||||
if (it) {
|
||||
val slideDownAnim = ObjectAnimator.ofFloat(
|
||||
binding.incognito,
|
||||
View.TRANSLATION_Y,
|
||||
-(binding.incognito.height.toFloat() + statusBarHeight),
|
||||
0f
|
||||
)
|
||||
slideDownAnim.duration = 200
|
||||
slideDownAnim.start()
|
||||
binding.incognito.visibility = View.VISIBLE
|
||||
} else {
|
||||
val slideUpAnim = ObjectAnimator.ofFloat(
|
||||
binding.incognito,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-(binding.incognito.height.toFloat() + statusBarHeight)
|
||||
)
|
||||
slideUpAnim.duration = 200
|
||||
slideUpAnim.start()
|
||||
//wait for animation to finish
|
||||
Handler(Looper.getMainLooper()).postDelayed(
|
||||
{ binding.incognito.visibility = View.GONE },
|
||||
200
|
||||
)
|
||||
}
|
||||
}
|
||||
incognitoNotification(this)
|
||||
|
||||
var doubleBackToExitPressedOnce = false
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
|
@ -142,106 +198,146 @@ class MainActivity : AppCompatActivity() {
|
|||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
selectedOption = if (FRAGMENT_CLASS_NAME != null) {
|
||||
when (FRAGMENT_CLASS_NAME) {
|
||||
AnimeFragment::class.java.name -> 0
|
||||
HomeFragment::class.java.name -> 1
|
||||
MangaFragment::class.java.name -> 2
|
||||
else -> 1
|
||||
}
|
||||
} else {
|
||||
uiSettings.defaultStartUpTab
|
||||
}
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val offline = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("offlineMode", false)
|
||||
if (!isOnline(this)) {
|
||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
||||
val cont = intent.extras?.getBoolean("continue") ?: false
|
||||
if (id != null && id != 0) {
|
||||
val media = withContext(Dispatchers.IO) {
|
||||
Anilist.query.getMedia(id, isMAL)
|
||||
}
|
||||
if (media != null) {
|
||||
media.cameFromContinue = cont
|
||||
startActivity(
|
||||
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
||||
.putExtra("media", media as Serializable)
|
||||
)
|
||||
} else {
|
||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
startSubscription()
|
||||
}
|
||||
load = true
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (loadData<Boolean>("allow_opening_links", this) != true) {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
||||
addView(TextView(this@MainActivity).apply {
|
||||
val markWon =
|
||||
Markwon.builder(this@MainActivity)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
markWon.setMarkdown(this, md)
|
||||
})
|
||||
|
||||
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
tryWith(true) {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
if (offline) {
|
||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) { it ->
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
selectedOption,
|
||||
false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
}.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
||||
val cont = intent.extras?.getBoolean("continue") ?: false
|
||||
if (id != null && id != 0) {
|
||||
val media = withContext(Dispatchers.IO) {
|
||||
Anilist.query.getMedia(id, isMAL)
|
||||
}
|
||||
if (media != null) {
|
||||
media.cameFromContinue = cont
|
||||
startActivity(
|
||||
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
||||
.putExtra("media", media as Serializable)
|
||||
)
|
||||
} else {
|
||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
startSubscription()
|
||||
}
|
||||
load = true
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (loadData<Boolean>("allow_opening_links", this) != true) {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
||||
addView(TextView(this@MainActivity).apply {
|
||||
val markWon =
|
||||
Markwon.builder(this@MainActivity)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
markWon.setMarkdown(this, md)
|
||||
})
|
||||
|
||||
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
tryWith(true) {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: Remove this
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
val download = downloadCursor.download
|
||||
Log.e("Downloader", download.request.uri.toString())
|
||||
Log.e("Downloader", download.request.id.toString())
|
||||
Log.e("Downloader", download.request.mimeType.toString())
|
||||
Log.e("Downloader", download.request.data.size.toString())
|
||||
Log.e("Downloader", download.bytesDownloaded.toString())
|
||||
Log.e("Downloader", download.state.toString())
|
||||
Log.e("Downloader", download.failureReason.toString())
|
||||
|
||||
if (download.state == Download.STATE_FAILED) { //simple cleanup
|
||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -261,4 +357,4 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@ package ani.dantotsu.aniyomi.anime.custom
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
|
@ -27,6 +30,7 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
|||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
|
@ -51,6 +55,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||
}
|
||||
}
|
||||
|
||||
addSingletonFactory { StandaloneDatabaseProvider(app) }
|
||||
|
||||
addSingletonFactory { MangaCache() }
|
||||
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
|
|
|
@ -10,7 +10,6 @@ import ani.dantotsu.toast
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun updateProgress(media: Media, number: String) {
|
||||
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.checkGenreTime
|
||||
import ani.dantotsu.checkId
|
||||
|
@ -10,6 +11,7 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate
|
|||
import ani.dantotsu.connections.anilist.api.Page
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Author
|
||||
|
@ -33,6 +35,13 @@ class AnilistQueries {
|
|||
}.also { println("time : $it") }
|
||||
val user = response?.data?.user ?: return false
|
||||
|
||||
currContext()?.let {
|
||||
it.getSharedPreferences(it.getString(R.string.preference_file_key), Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString("anilist_username", user.name)
|
||||
.apply()
|
||||
}
|
||||
|
||||
Anilist.userid = user.id
|
||||
Anilist.username = user.name
|
||||
Anilist.bg = user.bannerImage
|
||||
|
@ -239,7 +248,9 @@ class AnilistQueries {
|
|||
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
||||
}
|
||||
} else {
|
||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||
if (currContext()?.let { isOnline(it) } == true) {
|
||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||
}
|
||||
}
|
||||
}
|
||||
val mal = async {
|
||||
|
@ -410,8 +421,9 @@ class AnilistQueries {
|
|||
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
||||
|
||||
sorted["All"] = all
|
||||
|
||||
val sort = sortOrder ?: options?.rowOrder
|
||||
val listsort = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getString("sort_order", "score")
|
||||
val sort = listsort ?: sortOrder ?: options?.rowOrder
|
||||
for (i in sorted.keys) {
|
||||
when (sort) {
|
||||
"score" -> sorted[i]?.sortWith { b, a ->
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package ani.dantotsu.download
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class DownloadContainerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
setContentView(R.layout.activity_container)
|
||||
|
||||
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
val fragment = Class.forName(fragmentClassName).newInstance() as Fragment
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
|
@ -15,43 +15,43 @@ class DownloadsManager(private val context: Context) {
|
|||
private val gson = Gson()
|
||||
private val downloadsList = loadDownloads().toMutableList()
|
||||
|
||||
val mangaDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.MANGA }
|
||||
val animeDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.ANIME }
|
||||
val novelDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.NOVEL }
|
||||
val mangaDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||
val animeDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||
val novelDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||
|
||||
private fun saveDownloads() {
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
prefs.edit().putString("downloads_key", jsonString).apply()
|
||||
}
|
||||
|
||||
private fun loadDownloads(): List<Download> {
|
||||
private fun loadDownloads(): List<DownloadedType> {
|
||||
val jsonString = prefs.getString("downloads_key", null)
|
||||
return if (jsonString != null) {
|
||||
val type = object : TypeToken<List<Download>>() {}.type
|
||||
val type = object : TypeToken<List<DownloadedType>>() {}.type
|
||||
gson.fromJson(jsonString, type)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addDownload(download: Download) {
|
||||
downloadsList.add(download)
|
||||
fun addDownload(downloadedType: DownloadedType) {
|
||||
downloadsList.add(downloadedType)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(download: Download) {
|
||||
downloadsList.remove(download)
|
||||
removeDirectory(download)
|
||||
fun removeDownload(downloadedType: DownloadedType) {
|
||||
downloadsList.remove(downloadedType)
|
||||
removeDirectory(downloadedType)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: Download.Type) {
|
||||
val subDirectory = if (type == Download.Type.MANGA) {
|
||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
|
@ -71,21 +71,31 @@ class DownloadsManager(private val context: Context) {
|
|||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
cleanDownloads()
|
||||
}
|
||||
downloadsList.removeAll { it.title == title }
|
||||
when (type) {
|
||||
DownloadedType.Type.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||
}
|
||||
DownloadedType.Type.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||
}
|
||||
DownloadedType.Type.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun cleanDownloads() {
|
||||
cleanDownload(Download.Type.MANGA)
|
||||
cleanDownload(Download.Type.ANIME)
|
||||
cleanDownload(Download.Type.NOVEL)
|
||||
cleanDownload(DownloadedType.Type.MANGA)
|
||||
cleanDownload(DownloadedType.Type.ANIME)
|
||||
cleanDownload(DownloadedType.Type.NOVEL)
|
||||
}
|
||||
|
||||
private fun cleanDownload(type: Download.Type) {
|
||||
private fun cleanDownload(type: DownloadedType.Type) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = if (type == Download.Type.MANGA) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
|
@ -94,18 +104,18 @@ class DownloadsManager(private val context: Context) {
|
|||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory"
|
||||
)
|
||||
val downloadsSubList = if (type == Download.Type.MANGA) {
|
||||
mangaDownloads
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
animeDownloads
|
||||
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||
mangaDownloadedTypes
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
animeDownloadedTypes
|
||||
} else {
|
||||
novelDownloads
|
||||
novelDownloadedTypes
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubList.any { it.title == file.name }) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
val deleted = file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
@ -122,11 +132,11 @@ class DownloadsManager(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<Download>) //for debugging
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
|
@ -138,25 +148,33 @@ class DownloadsManager(private val context: Context) {
|
|||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(download: Download): Boolean {
|
||||
return downloadsList.contains(download)
|
||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||
return downloadsList.contains(downloadedType)
|
||||
}
|
||||
|
||||
private fun removeDirectory(download: Download) {
|
||||
val directory = if (download.type == Download.Type.MANGA) {
|
||||
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||
return if (type == null) {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||
} else {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -173,26 +191,26 @@ class DownloadsManager(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
|
||||
val directory = if (download.type == Download.Type.MANGA) {
|
||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
val destination = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${download.title}/${download.chapter}"
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
|
@ -206,10 +224,10 @@ class DownloadsManager(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: Download.Type) {
|
||||
val directory = if (type == Download.Type.MANGA) {
|
||||
fun purgeDownloads(type: DownloadedType.Type) {
|
||||
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
|
@ -233,11 +251,51 @@ class DownloadsManager(private val context: Context) {
|
|||
const val novelLocation = "Dantotsu/Novel"
|
||||
const val mangaLocation = "Dantotsu/Manga"
|
||||
const val animeLocation = "Dantotsu/Anime"
|
||||
|
||||
fun getDirectory(context: Context, type: DownloadedType.Type, title: String, chapter: String? = null): File {
|
||||
return if (type == DownloadedType.Type.MANGA) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title"
|
||||
)
|
||||
}
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME,
|
||||
|
|
|
@ -0,0 +1,524 @@
|
|||
package ani.dantotsu.download.anime
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class AnimeDownloaderService : Service() {
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private lateinit var builder: NotificationCompat.Builder
|
||||
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
private val downloadJobs = mutableMapOf<String, Job>()
|
||||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder =
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Anime Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
cancelReceiver,
|
||||
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
AnimeServiceDataSingleton.downloadQueue.clear()
|
||||
downloadJobs.clear()
|
||||
AnimeServiceDataSingleton.isServiceRunning = false
|
||||
unregisterReceiver(cancelReceiver)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
snackString("Download started")
|
||||
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
serviceScope.launch {
|
||||
mutex.withLock {
|
||||
if (!isCurrentlyProcessing) {
|
||||
isCurrentlyProcessing = true
|
||||
processQueue()
|
||||
isCurrentlyProcessing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun processQueue() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
while (AnimeServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||
val task = AnimeServiceDataSingleton.downloadQueue.poll()
|
||||
if (task != null) {
|
||||
val job = launch { download(task) }
|
||||
currentTasks.add(task)
|
||||
mutex.withLock {
|
||||
downloadJobs[task.getTaskName()] = job
|
||||
}
|
||||
job.join() // Wait for the job to complete before continuing to the next task
|
||||
mutex.withLock {
|
||||
downloadJobs.remove(task.getTaskName())
|
||||
}
|
||||
updateNotification() // Update the notification after each task is completed
|
||||
}
|
||||
if (AnimeServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
stopSelf() // Stop the service when the queue is empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun cancelDownload(taskName: String) {
|
||||
val url =
|
||||
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
||||
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
||||
if (url.isEmpty()) {
|
||||
snackString("Failed to cancel download")
|
||||
return
|
||||
}
|
||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||
DownloadService.sendSetStopReason(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
|
||||
false
|
||||
)
|
||||
DownloadService.sendRemoveDownload(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
false
|
||||
)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
mutex.withLock {
|
||||
downloadJobs[taskName]?.cancel()
|
||||
downloadJobs.remove(taskName)
|
||||
AnimeServiceDataSingleton.downloadQueue.removeAll { it.getTaskName() == taskName }
|
||||
updateNotification() // Update the notification after cancellation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
// Update the notification to reflect the current state of the queue
|
||||
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
|
||||
val text = if (pendingDownloads > 0) {
|
||||
"Pending downloads: $pendingDownloads"
|
||||
} else {
|
||||
"All downloads completed"
|
||||
}
|
||||
builder.setContentText(text)
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun download(task: AnimeDownloadTask) {
|
||||
try {
|
||||
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||
withContext(Dispatchers.Main) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this@AnimeDownloaderService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
broadcastDownloadStarted(task.episode)
|
||||
|
||||
currActivity()?.let {
|
||||
Helper.downloadVideo(
|
||||
it,
|
||||
task.video,
|
||||
task.subtitle
|
||||
)
|
||||
}
|
||||
|
||||
saveMediaInfo(task)
|
||||
task.subtitle?.let {
|
||||
SubtitleDownloader.downloadSubtitle(
|
||||
this@AnimeDownloaderService,
|
||||
it.file.url,
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
)
|
||||
)
|
||||
}
|
||||
val downloadStarted =
|
||||
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
||||
|
||||
if (!downloadStarted) {
|
||||
logger("Download failed to start")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed to start")
|
||||
broadcastDownloadFailed(task.episode)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||
logger("Download failed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed")
|
||||
logger("Download failed: ${download.failureReason}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
)
|
||||
)
|
||||
FirebaseCrashlytics.getInstance().recordException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${download.failureReason}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
||||
logger("Download completed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download completed")
|
||||
getSharedPreferences(
|
||||
getString(R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFinished(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
|
||||
logger("Download stopped")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download stopped")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download stopped")
|
||||
break
|
||||
}
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
download.percentDownloaded.toInt()
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||
logger("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
e.printStackTrace()
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
broadcastDownloadFailed(task.episode)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun hasDownloadStarted(
|
||||
downloadManager: DownloadManager,
|
||||
task: AnimeDownloadTask,
|
||||
timeout: Long
|
||||
): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < timeout) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
return true
|
||||
}
|
||||
// Delay between each poll
|
||||
kotlinx.coroutines.delay(500)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.animeLocation}/${task.title}"
|
||||
)
|
||||
val episodeDirectory = File(directory, task.episode)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||
})
|
||||
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
})
|
||||
.create()
|
||||
val mediaJson = gson.toJson(task.sourceMedia)
|
||||
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||
if (media != null) {
|
||||
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||
if (task.episodeImage != null) {
|
||||
media.anime?.episodes?.get(task.episode)?.let { episode ->
|
||||
episode.thumb = downloadImage(
|
||||
task.episodeImage,
|
||||
episodeDirectory,
|
||||
"episodeImage.jpg"
|
||||
)?.let {
|
||||
FileUrl(
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg")
|
||||
}
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
try {
|
||||
connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@AnimeDownloaderService,
|
||||
"Exception while saving ${name}: ${e.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
null
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastDownloadStarted(episodeNumber: String) {
|
||||
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
|
||||
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFinished(episodeNumber: String) {
|
||||
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FINISHED).apply {
|
||||
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFailed(episodeNumber: String) {
|
||||
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FAILED).apply {
|
||||
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadProgress(episodeNumber: String, progress: Int) {
|
||||
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_PROGRESS).apply {
|
||||
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||
putExtra("progress", progress)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||
val taskName = intent.getStringExtra(EXTRA_TASK_NAME)
|
||||
taskName?.let {
|
||||
cancelDownload(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class AnimeDownloadTask(
|
||||
val title: String,
|
||||
val episode: String,
|
||||
val video: Video,
|
||||
val subtitle: Subtitle? = null,
|
||||
val sourceMedia: Media? = null,
|
||||
val episodeImage: String? = null,
|
||||
val retries: Int = 2,
|
||||
val simultaneousDownloads: Int = 2,
|
||||
) {
|
||||
fun getTaskName(): String {
|
||||
return "$title - $episode"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getTaskName(title: String, episode: String): String {
|
||||
return "$title - $episode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1103
|
||||
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||
const val EXTRA_TASK_NAME = "extra_task_name"
|
||||
}
|
||||
}
|
||||
|
||||
object AnimeServiceDataSingleton {
|
||||
var video: Video? = null
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
var isServiceRunning: Boolean = false
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package ani.dantotsu.download.anime
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import ani.dantotsu.R
|
||||
|
||||
|
||||
class OfflineAnimeAdapter(
|
||||
private val context: Context,
|
||||
private var items: List<OfflineAnimeModel>,
|
||||
private val searchListener: OfflineAnimeSearchListener
|
||||
) : BaseAdapter() {
|
||||
private val inflater: LayoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var originalItems: List<OfflineAnimeModel> = items
|
||||
private var style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any {
|
||||
return items[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
|
||||
val view: View = convertView ?: when (style) {
|
||||
0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
|
||||
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||
}
|
||||
|
||||
val item = getItem(position) as OfflineAnimeModel
|
||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||
|
||||
if (style == 0) {
|
||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||
val episodes = view.findViewById<TextView>(R.id.itemTotal)
|
||||
episodes.text = " Episodes"
|
||||
bannerView.setImageURI(item.banner)
|
||||
totalepisodes.text = item.totalEpisodeList
|
||||
} else if (style == 1) {
|
||||
val watchedEpisodes =
|
||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||
watchedEpisodes.text = item.watchedEpisode
|
||||
totalepisodes.text = " | " + item.totalEpisode
|
||||
}
|
||||
|
||||
// Bind item data to the views
|
||||
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
|
||||
type.text = item.type
|
||||
typeView.visibility = View.VISIBLE
|
||||
imageView.setImageURI(item.image)
|
||||
titleTextView.text = item.title
|
||||
itemScore.text = item.score
|
||||
|
||||
if (item.isOngoing) {
|
||||
ongoing.visibility = View.VISIBLE
|
||||
} else {
|
||||
ongoing.visibility = View.GONE
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
fun onSearchQuery(query: String) {
|
||||
// Implement the filtering logic here, for example:
|
||||
items = if (query.isEmpty()) {
|
||||
// Return the original list if the query is empty
|
||||
originalItems
|
||||
} else {
|
||||
// Filter the list based on the query
|
||||
originalItems.filter { it.title.contains(query, ignoreCase = true) }
|
||||
}
|
||||
notifyDataSetChanged() // Notify the adapter that the data set has changed
|
||||
}
|
||||
|
||||
fun setItems(items: List<OfflineAnimeModel>) {
|
||||
this.items = items
|
||||
this.originalItems = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun notifyNewGrid() {
|
||||
style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,450 @@
|
|||
package ani.dantotsu.download.anime
|
||||
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.GridView
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private var downloads: List<OfflineAnimeModel> = listOf()
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineAnimeAdapter
|
||||
private lateinit var total : TextView
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
|
||||
|
||||
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
|
||||
textInputLayout.hint = "Anime"
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineANIME)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
if (!uiSettings.immersiveMode) {
|
||||
view.rootView.fitsSystemWindows = true
|
||||
}
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||
searchView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
onSearchQuery(s.toString())
|
||||
}
|
||||
})
|
||||
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getInt("offline_view", 0)
|
||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
0 -> layoutList
|
||||
1 -> layoutcompact
|
||||
else -> layoutList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
|
||||
layoutList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putInt("offline_view", style!!)?.apply()
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
|
||||
layoutcompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putInt("offline_view", style!!)?.apply()
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView1)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
|
||||
gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||
total = view.findViewById(R.id.total)
|
||||
grid()
|
||||
return view
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
gridView.setOnItemClickListener { _, _, position, _ ->
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val media =
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
media?.let {
|
||||
val mediaModel = getMedia(it)
|
||||
if (mediaModel == null) {
|
||||
snackString("Error loading media.json")
|
||||
return@let
|
||||
}
|
||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||
startActivity(
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("download", true)
|
||||
)
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
}
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val type: DownloadedType.Type =
|
||||
DownloadedType.Type.ANIME
|
||||
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete ${item.title}?")
|
||||
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
val mediaIds = requireContext().getSharedPreferences(
|
||||
getString(R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
?.all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
|
||||
if (mediaIds.isEmpty()) {
|
||||
snackString("No media found") // if this happens, terrible things have happened
|
||||
}
|
||||
for (mediaId in mediaIds) {
|
||||
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
|
||||
.removeDownload(mediaId.toString())
|
||||
}
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearchQuery(query: String) {
|
||||
adapter.onSearchQuery(query)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
var height = statusBarHeight
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
height = max(
|
||||
statusBarHeight,
|
||||
min(
|
||||
displayCutout.boundingRects[0].width(),
|
||||
displayCutout.boundingRects[0].height()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
val visible = false
|
||||
|
||||
fun animate() {
|
||||
val start = if (visible) 0f else 1f
|
||||
val end = if (!visible) 0f else 1f
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop.setOnClickListener {
|
||||
gridView.smoothScrollToPositionFromTop(0, 0)
|
||||
}
|
||||
|
||||
// Assuming 'scrollTop' is a view that you want to hide/show
|
||||
scrollTop.visibility = View.GONE
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
view: AbsListView,
|
||||
firstVisibleItem: Int,
|
||||
visibleItemCount: Int,
|
||||
totalItemCount: Int
|
||||
) {
|
||||
val first = view.getChildAt(0)
|
||||
val visibility = first != null && first.top < -height
|
||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
}
|
||||
})
|
||||
initActivity(requireActivity())
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val _downloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
return try {
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||
})
|
||||
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalEpisode =
|
||||
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
|
||||
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
|
||||
val chapters = " Chapters"
|
||||
val totalEpisodesList =
|
||||
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
|
||||
?: "~").toString()
|
||||
return OfflineAnimeModel(
|
||||
title,
|
||||
score,
|
||||
totalEpisode,
|
||||
totalEpisodesList,
|
||||
watchedEpisodes,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
isUserScored,
|
||||
coverUri,
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OfflineAnimeSearchListener {
|
||||
fun onSearchQuery(query: String)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package ani.dantotsu.download.anime
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class OfflineAnimeModel(
|
||||
val title: String,
|
||||
val score: String,
|
||||
val totalEpisode: String,
|
||||
val totalEpisodeList: String,
|
||||
val watchedEpisode: String,
|
||||
val type: String,
|
||||
val episodes: String,
|
||||
val isOngoing: Boolean,
|
||||
val isUserScored: Boolean,
|
||||
val image: Uri?,
|
||||
val banner: Uri?,
|
||||
)
|
|
@ -18,7 +18,7 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
|
@ -187,7 +187,8 @@ class MangaDownloaderService : Service() {
|
|||
true
|
||||
}
|
||||
|
||||
val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
|
@ -196,15 +197,12 @@ class MangaDownloaderService : Service() {
|
|||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
// Limit the number of simultaneous downloads from the task
|
||||
if (deferredList.size >= task.simultaneousDownloads) {
|
||||
// Wait for all deferred to complete and clear the list
|
||||
deferredList.awaitAll()
|
||||
deferredList.clear()
|
||||
if (deferredMap.size >= task.simultaneousDownloads) {
|
||||
deferredMap.values.awaitAll()
|
||||
deferredMap.clear()
|
||||
}
|
||||
|
||||
// Download the image and add to deferred list
|
||||
val deferred = async(Dispatchers.IO) {
|
||||
deferredMap[index] = async(Dispatchers.IO) {
|
||||
var bitmap: Bitmap? = null
|
||||
var retryCount = 0
|
||||
|
||||
|
@ -217,7 +215,6 @@ class MangaDownloaderService : Service() {
|
|||
retryCount++
|
||||
}
|
||||
|
||||
// Cache the image if successful
|
||||
if (bitmap != null) {
|
||||
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||
}
|
||||
|
@ -233,12 +230,10 @@ class MangaDownloaderService : Service() {
|
|||
|
||||
bitmap
|
||||
}
|
||||
|
||||
deferredList.add(deferred)
|
||||
}
|
||||
|
||||
// Wait for any remaining deferred to complete
|
||||
deferredList.awaitAll()
|
||||
deferredMap.values.awaitAll()
|
||||
|
||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
|
@ -246,10 +241,10 @@ class MangaDownloaderService : Service() {
|
|||
|
||||
saveMediaInfo(task)
|
||||
downloadsManager.addDownload(
|
||||
Download(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
Download.Type.MANGA
|
||||
DownloadedType.Type.MANGA
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
|
@ -314,7 +309,16 @@ class MangaDownloaderService : Service() {
|
|||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.writeText(jsonString)
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@MangaDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import ani.dantotsu.R
|
||||
|
@ -19,6 +21,8 @@ class OfflineMangaAdapter(
|
|||
private val inflater: LayoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var originalItems: List<OfflineMangaModel> = items
|
||||
private var style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
|
@ -32,23 +36,47 @@ class OfflineMangaAdapter(
|
|||
return position.toLong()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
var view = convertView
|
||||
if (view == null) {
|
||||
view = inflater.inflate(R.layout.item_media_compact, parent, false)
|
||||
|
||||
val view: View = convertView ?: when (style) {
|
||||
0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
|
||||
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||
}
|
||||
|
||||
val item = getItem(position) as OfflineMangaModel
|
||||
val imageView = view!!.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
val totalchapter = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||
|
||||
if (style == 0) {
|
||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||
val chapters = view.findViewById<TextView>(R.id.itemTotal)
|
||||
chapters.text = " Chapters"
|
||||
bannerView.setImageURI(item.banner)
|
||||
totalchapter.text = item.totalChapter
|
||||
} else if (style == 1) {
|
||||
val readchapter =
|
||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||
readchapter.text = item.readChapter
|
||||
totalchapter.text = " | " + item.totalChapter
|
||||
}
|
||||
|
||||
// Bind item data to the views
|
||||
// For example:
|
||||
typeimage.setImageResource(if (item.type == "Novel") R.drawable.ic_round_book_24 else R.drawable.ic_round_import_contacts_24)
|
||||
type.text = item.type
|
||||
typeView.visibility = View.VISIBLE
|
||||
imageView.setImageURI(item.image)
|
||||
titleTextView.text = item.title
|
||||
itemScore.text = item.score
|
||||
|
||||
if (item.isOngoing) {
|
||||
ongoing.visibility = View.VISIBLE
|
||||
} else {
|
||||
|
@ -74,4 +102,10 @@ class OfflineMangaAdapter(
|
|||
this.originalItems = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun notifyNewGrid() {
|
||||
style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -13,21 +13,33 @@ import android.util.TypedValue
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.GridView
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
|
@ -45,19 +57,24 @@ import kotlin.math.max
|
|||
import kotlin.math.min
|
||||
|
||||
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private var downloads: List<OfflineMangaModel> = listOf()
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineMangaAdapter
|
||||
private lateinit var total : TextView
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
|
||||
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
|
||||
|
||||
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
|
||||
textInputLayout.hint = "Manga"
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
|
@ -69,16 +86,13 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
|
||||
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
val dialogFragment = SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
|
||||
dialogFragment.show(
|
||||
(it.context as AppCompatActivity).supportFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
if (!uiSettings.immersiveMode) {
|
||||
view.rootView.fitsSystemWindows = true
|
||||
}
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
|
@ -98,17 +112,65 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
onSearchQuery(s.toString())
|
||||
}
|
||||
})
|
||||
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getInt("offline_view", 0)
|
||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
0 -> layoutList
|
||||
1 -> layoutcompact
|
||||
else -> layoutList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
|
||||
layoutList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("offline_view", style!!).apply()
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
|
||||
}
|
||||
|
||||
layoutcompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("offline_view", style!!).apply()
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView1)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||
total = view.findViewById(R.id.total)
|
||||
grid()
|
||||
return view
|
||||
}
|
||||
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||
gridView.adapter = adapter
|
||||
gridView.setOnItemClickListener { parent, view, position, id ->
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text = if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
gridView.setOnItemClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val media =
|
||||
downloadManager.mangaDownloads.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title }
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
media?.let {
|
||||
startActivity(
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
|
@ -120,32 +182,33 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
}
|
||||
}
|
||||
|
||||
gridView.setOnItemLongClickListener { parent, view, position, id ->
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) {
|
||||
Download.Type.MANGA
|
||||
} else {
|
||||
Download.Type.NOVEL
|
||||
}
|
||||
val type: DownloadedType.Type =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||
DownloadedType.Type.MANGA
|
||||
} else {
|
||||
DownloadedType.Type.NOVEL
|
||||
}
|
||||
// Alert dialog to confirm deletion
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
val builder =
|
||||
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete ${item.title}?")
|
||||
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
val dialog = builder.show()
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onSearchQuery(query: String) {
|
||||
|
@ -154,6 +217,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initActivity(requireActivity())
|
||||
var height = statusBarHeight
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||
|
@ -170,7 +234,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
}
|
||||
}
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
var visible = false
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
val visible = false
|
||||
|
||||
fun animate() {
|
||||
val start = if (visible) 0f else 1f
|
||||
val end = if (!visible) 0f else 1f
|
||||
|
@ -187,9 +254,30 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
}
|
||||
|
||||
scrollTop.setOnClickListener {
|
||||
//TODO: scroll to top
|
||||
gridView.smoothScrollToPositionFromTop(0, 0)
|
||||
}
|
||||
|
||||
// Assuming 'scrollTop' is a view that you want to hide/show
|
||||
scrollTop.visibility = View.GONE
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
view: AbsListView,
|
||||
firstVisibleItem: Int,
|
||||
visibleItemCount: Int,
|
||||
totalItemCount: Int
|
||||
) {
|
||||
val first = view.getChildAt(0)
|
||||
val visibility = first != null && first.top < -height
|
||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -215,19 +303,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
|
||||
val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val _downloads = downloadManager.novelDownloads.filter { it.title == title }
|
||||
val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
|
@ -236,17 +324,17 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
|
||||
}
|
||||
|
||||
private fun getMedia(download: Download): Media? {
|
||||
val type = if (download.type == Download.Type.MANGA) {
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${download.title}"
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
return try {
|
||||
|
@ -266,44 +354,72 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel {
|
||||
val type = if (download.type == Download.Type.MANGA) {
|
||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${download.title}"
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val mediaModel = getMedia(download)!!
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val title = mediaModel.nameMAL ?: mediaModel.nameRomaji
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
val isOngoing = false
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
|
||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val chapters = " Chapters"
|
||||
return OfflineMangaModel(
|
||||
title,
|
||||
score,
|
||||
totalchapter,
|
||||
readchapter,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
isUserScored,
|
||||
coverUri,
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
return OfflineMangaModel("unknown", "0", false, false, null)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OfflineMangaSearchListener {
|
||||
fun onSearchQuery(query: String)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,12 @@ import android.net.Uri
|
|||
data class OfflineMangaModel(
|
||||
val title: String,
|
||||
val score: String,
|
||||
val totalChapter: String,
|
||||
val readChapter: String,
|
||||
val type: String,
|
||||
val chapters: String,
|
||||
val isOngoing: Boolean,
|
||||
val isUserScored: Boolean,
|
||||
val image: Uri?
|
||||
val image: Uri?,
|
||||
val banner: Uri?
|
||||
)
|
|
@ -17,7 +17,7 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
|
@ -330,10 +330,10 @@ class NovelDownloaderService : Service() {
|
|||
|
||||
saveMediaInfo(task)
|
||||
downloadsManager.addDownload(
|
||||
Download(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
|
|
|
@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
|
|||
import ani.dantotsu.R
|
||||
|
||||
@UnstableApi
|
||||
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) {
|
||||
class ExoplayerDownloadService :
|
||||
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||
companion object {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
|
@ -1,8 +1,19 @@
|
|||
package ani.dantotsu.download.video
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getString
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
|
@ -15,6 +26,7 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor
|
|||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
|
@ -22,7 +34,12 @@ import androidx.media3.exoplayer.scheduler.Requirements
|
|||
import androidx.media3.ui.TrackSelectionDialogBuilder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
|
@ -37,6 +54,8 @@ import java.util.concurrent.*
|
|||
|
||||
object Helper {
|
||||
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
|
@ -82,27 +101,13 @@ object Helper {
|
|||
)
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||
override fun onPrepared(helper: DownloadHelper) {
|
||||
TrackSelectionDialogBuilder(
|
||||
context, "Select thingy", helper.getTracks(0).groups
|
||||
) { _, overrides ->
|
||||
val params = TrackSelectionParameters.Builder(context)
|
||||
overrides.forEach {
|
||||
params.addOverride(it.value)
|
||||
}
|
||||
helper.addTrackSelection(0, params.build())
|
||||
MyDownloadService
|
||||
helper.getDownloadRequest(null).let {
|
||||
DownloadService.sendAddDownload(
|
||||
context,
|
||||
MyDownloadService::class.java,
|
||||
helper.getDownloadRequest(null),
|
||||
ExoplayerDownloadService::class.java,
|
||||
it,
|
||||
false
|
||||
)
|
||||
}.apply {
|
||||
setTheme(R.style.DialogTheme)
|
||||
setTrackNameProvider {
|
||||
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
|
||||
}
|
||||
build().show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,13 +119,13 @@ object Helper {
|
|||
|
||||
|
||||
private var download: DownloadManager? = null
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
|
||||
@Synchronized
|
||||
@UnstableApi
|
||||
fun downloadManager(context: Context): DownloadManager {
|
||||
return download ?: let {
|
||||
val database = StandaloneDatabaseProvider(context)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
|
@ -133,17 +138,42 @@ object Helper {
|
|||
}
|
||||
dataSource
|
||||
}
|
||||
DownloadManager(
|
||||
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
database,
|
||||
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
|
||||
getSimpleCache(context),
|
||||
dataSourceFactory,
|
||||
Executor(Runnable::run)
|
||||
executorService
|
||||
).apply {
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
maxParallelDownloads = 3
|
||||
}
|
||||
downloadManager.addListener( //for testing
|
||||
object : DownloadManager.Listener {
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
Log.e("Downloader", "Download Completed")
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
Log.e("Downloader", "Download Failed")
|
||||
} else if (download.state == Download.STATE_STOPPED) {
|
||||
Log.e("Downloader", "Download Stopped")
|
||||
} else if (download.state == Download.STATE_QUEUED) {
|
||||
Log.e("Downloader", "Download Queued")
|
||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||
Log.e("Downloader", "Download Downloading")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
downloadManager
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,4 +189,108 @@ object Helper {
|
|||
}
|
||||
return downloadDirectory!!
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun startAnimeDownloadService(
|
||||
context: Context,
|
||||
title: String,
|
||||
episode: String,
|
||||
video: Video,
|
||||
subtitle: Subtitle? = null,
|
||||
sourceMedia: Media? = null,
|
||||
episodeImage: String? = null
|
||||
) {
|
||||
if (!isNotificationPermissionGranted(context)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context as Activity,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
|
||||
title,
|
||||
episode,
|
||||
video,
|
||||
subtitle,
|
||||
sourceMedia,
|
||||
episodeImage
|
||||
)
|
||||
|
||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManger
|
||||
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||
|
||||
if (downloadCheck) {
|
||||
AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Exists")
|
||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
.setPositiveButton("Yes") { _, _ ->
|
||||
DownloadService.sendRemoveDownload(
|
||||
context,
|
||||
ExoplayerDownloadService::class.java,
|
||||
context.getSharedPreferences(
|
||||
getString(context, R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).getString(
|
||||
animeDownloadTask.getTaskName(),
|
||||
""
|
||||
) ?: "",
|
||||
false
|
||||
)
|
||||
context.getSharedPreferences(
|
||||
getString(context, R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
.remove(animeDownloadTask.getTaskName())
|
||||
.apply()
|
||||
downloadsManger.removeDownload(
|
||||
DownloadedType(
|
||||
title,
|
||||
episode,
|
||||
DownloadedType.Type.ANIME
|
||||
)
|
||||
)
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
.setNegativeButton("No") { _, _ -> }
|
||||
.show()
|
||||
} else {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getSimpleCache(context: Context): SimpleCache {
|
||||
return if (simpleCache == null) {
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||
simpleCache!!
|
||||
} else {
|
||||
simpleCache!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ import kotlin.math.min
|
|||
class AnimeFragment : Fragment() {
|
||||
private var _binding: FragmentAnimeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var animePageAdapter: AnimePageAdapter
|
||||
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
@ -95,7 +96,7 @@ class AnimeFragment : Fragment() {
|
|||
|
||||
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||
|
||||
val animePageAdapter = AnimePageAdapter()
|
||||
animePageAdapter = AnimePageAdapter()
|
||||
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
|
@ -277,6 +278,11 @@ class AnimeFragment : Fragment() {
|
|||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||
if (animePageAdapter.trendingViewPager != null) {
|
||||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
}
|
|
@ -111,7 +111,6 @@ class HomeFragment : Fragment() {
|
|||
snackString(currContext()?.getString(R.string.please_reload))
|
||||
}
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
|
||||
|
|
|
@ -28,5 +28,6 @@ class LoginFragment : Fragment() {
|
|||
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
|
||||
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
||||
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
||||
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ import kotlin.math.min
|
|||
class MangaFragment : Fragment() {
|
||||
private var _binding: FragmentMangaBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var mangaPageAdapter: MangaPageAdapter
|
||||
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
@ -90,7 +91,7 @@ class MangaFragment : Fragment() {
|
|||
|
||||
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||
|
||||
val mangaPageAdapter = MangaPageAdapter()
|
||||
mangaPageAdapter = MangaPageAdapter()
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
|
@ -251,6 +252,11 @@ class MangaFragment : Fragment() {
|
|||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||
//make sure mangaPageAdapter is initialized
|
||||
if (mangaPageAdapter.trendingViewPager != null) {
|
||||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,11 @@ import android.content.Context
|
|||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
|
@ -17,6 +20,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
|||
import ani.dantotsu.R
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||
import ani.dantotsu.download.anime.OfflineAnimeFragment
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
|
@ -25,6 +29,7 @@ import ani.dantotsu.offline.OfflineFragment
|
|||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.selectedOption
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
|
@ -56,10 +61,24 @@ class NoInternet : AppCompatActivity() {
|
|||
|
||||
}
|
||||
|
||||
var doubleBackToExitPressedOnce = false
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (doubleBackToExitPressedOnce) {
|
||||
finishAffinity()
|
||||
}
|
||||
doubleBackToExitPressedOnce = true
|
||||
snackString(this@NoInternet.getString(R.string.back_to_exit))
|
||||
Handler(Looper.getMainLooper()).postDelayed(
|
||||
{ doubleBackToExitPressedOnce = false },
|
||||
2000
|
||||
)
|
||||
}
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
|
@ -96,12 +115,11 @@ class NoInternet : AppCompatActivity() {
|
|||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
when (position) {
|
||||
0 -> return OfflineFragment()
|
||||
1 -> return OfflineFragment()
|
||||
2 -> return OfflineMangaFragment()
|
||||
return when (position) {
|
||||
0 -> OfflineAnimeFragment()
|
||||
2 -> OfflineMangaFragment()
|
||||
else -> OfflineFragment()
|
||||
}
|
||||
return LoginFragment()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,13 @@ import android.annotation.SuppressLint
|
|||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
|
@ -16,8 +18,10 @@ import ani.dantotsu.Refresh
|
|||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
@ -76,6 +80,10 @@ class CalendarActivity : AppCompatActivity() {
|
|||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
||||
|
|
|
@ -118,6 +118,19 @@ data class Media(
|
|||
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
||||
}
|
||||
|
||||
fun emptyMedia() = Media(
|
||||
id = 0,
|
||||
name = "No media found",
|
||||
nameRomaji = "No media found",
|
||||
userPreferredName = "",
|
||||
isAdult = false,
|
||||
isFav = false,
|
||||
isListPrivate = false,
|
||||
userScore = 0,
|
||||
userStatus = "",
|
||||
format = "",
|
||||
)
|
||||
|
||||
object MediaSingleton {
|
||||
var media: Media? = null
|
||||
var bitmap: Bitmap? = null
|
||||
|
|
|
@ -13,7 +13,10 @@ import android.view.ViewGroup
|
|||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -136,7 +139,7 @@ class MediaAdaptor(
|
|||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
|
||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
|
@ -319,6 +322,7 @@ class MediaAdaptor(
|
|||
itemView.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
|
@ -332,6 +336,7 @@ class MediaAdaptor(
|
|||
itemView.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
|
@ -346,6 +351,7 @@ class MediaAdaptor(
|
|||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
|
@ -361,12 +367,14 @@ class MediaAdaptor(
|
|||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
binding.itemCompactTitleContainer.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
|
@ -375,7 +383,7 @@ class MediaAdaptor(
|
|||
}
|
||||
}
|
||||
|
||||
fun clicked(position: Int, bitmap: Bitmap? = null) {
|
||||
fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
|
||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||
val media = mediaList?.get(position)
|
||||
if (bitmap != null) MediaSingleton.bitmap = bitmap
|
||||
|
@ -384,7 +392,13 @@ class MediaAdaptor(
|
|||
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
||||
"media",
|
||||
media as Serializable
|
||||
), null
|
||||
), ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
Pair.create(
|
||||
itemCompactImage,
|
||||
ViewCompat.getTransitionName(activity.findViewById(R.id.itemCompactImage))!!
|
||||
),
|
||||
).toBundle()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package ani.dantotsu.media
|
|||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
|
@ -42,7 +43,6 @@ import ani.dantotsu.media.novel.NovelReadFragment
|
|||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
|
@ -72,11 +72,17 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
LangSet.setLocale(this)
|
||||
var media: Media = intent.getSerialized("media") ?: return
|
||||
super.onCreate(savedInstanceState)
|
||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||
if (media.name == "No media found") {
|
||||
snackString(media.name)
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return
|
||||
}
|
||||
mediaSingleton = null
|
||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||
MediaSingleton.bitmap = null
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
|
@ -85,12 +91,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
|
||||
initActivity(this)
|
||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
|
||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||
|
||||
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
|
@ -154,7 +159,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
}
|
||||
})
|
||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
if (this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("incognito", false)) {
|
||||
binding.mediaTitle.text = " ${media.userPreferredName}"
|
||||
binding.incognito.visibility = View.VISIBLE
|
||||
}else {
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
}
|
||||
binding.mediaTitle.setOnLongClickListener {
|
||||
copyToClipboard(media.userPreferredName)
|
||||
true
|
||||
|
@ -305,7 +316,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
}
|
||||
|
||||
adult = media.isAdult
|
||||
|
||||
tabLayout.menu.clear()
|
||||
if (media.anime != null) {
|
||||
viewPager.adapter =
|
||||
|
@ -317,7 +327,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
lifecycle,
|
||||
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
||||
)
|
||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||
if (media.format == "NOVEL") {
|
||||
tabLayout.inflateMenu(R.menu.novel_menu_detail)
|
||||
} else {
|
||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||
}
|
||||
anime = false
|
||||
}
|
||||
|
||||
|
@ -442,7 +456,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
binding.mediaBanner.pause()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
|
@ -455,7 +468,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||
}
|
||||
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
||||
false
|
||||
|
@ -532,5 +544,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
image.alpha = if (disabled) 0.33f else 1f
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var mediaSingleton: Media? = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -242,7 +242,8 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
i: String,
|
||||
manager: FragmentManager,
|
||||
launch: Boolean = true,
|
||||
prevEp: String? = null
|
||||
prevEp: String? = null,
|
||||
isDownload: Boolean = false
|
||||
) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
||||
|
@ -254,13 +255,17 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
}
|
||||
media.selected = this.loadSelected(media)
|
||||
val selector =
|
||||
SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
|
||||
SelectorDialogFragment.newInstance(
|
||||
media.selected!!.server,
|
||||
launch,
|
||||
prevEp,
|
||||
isDownload
|
||||
)
|
||||
selector.show(manager, "dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Manga
|
||||
var mangaReadSources: MangaReadSources? = null
|
||||
|
||||
|
@ -314,7 +319,8 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
val novelSources = NovelSources
|
||||
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
|
||||
suspend fun searchNovels(query: String, i: Int) {
|
||||
val source = novelSources[i]
|
||||
val position = if (i >= novelSources.list.size) 0 else i
|
||||
val source = novelSources[position]
|
||||
tryWithSuspend(post = true) {
|
||||
if (source != null) {
|
||||
novelResponses.postValue(source.search(query))
|
||||
|
|
|
@ -2,6 +2,7 @@ package ani.dantotsu.media
|
|||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -59,6 +60,7 @@ class MediaInfoFragment : Fragment() {
|
|||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
val offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("offlineMode", false) || !isOnline(requireContext())
|
||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||
|
@ -101,29 +103,33 @@ class MediaInfoFragment : Fragment() {
|
|||
if (media.anime.mainStudio != null) {
|
||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, StudioActivity::class.java).putExtra(
|
||||
"studio",
|
||||
media.anime.mainStudio!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, StudioActivity::class.java).putExtra(
|
||||
"studio",
|
||||
media.anime.mainStudio!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (media.anime.author != null) {
|
||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoAuthor.text = media.anime.author!!.name
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.anime.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.anime.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||
|
@ -137,15 +143,17 @@ class MediaInfoFragment : Fragment() {
|
|||
if (media.manga.author != null) {
|
||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoAuthor.text = media.manga.author!!.name
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.manga.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.manga.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +197,7 @@ class MediaInfoFragment : Fragment() {
|
|||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.trailer != null) {
|
||||
if (media.trailer != null && !offline) {
|
||||
@Suppress("DEPRECATION")
|
||||
class MyChrome : WebChromeClient() {
|
||||
private var mCustomView: View? = null
|
||||
|
@ -243,7 +251,7 @@ class MediaInfoFragment : Fragment() {
|
|||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) {
|
||||
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
|
||||
val markWon = Markwon.builder(requireContext())
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
|
||||
|
@ -304,7 +312,7 @@ class MediaInfoFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
if (media.genres.isNotEmpty()) {
|
||||
if (media.genres.isNotEmpty() && !offline) {
|
||||
val bind = ActivityGenreBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
|
@ -335,7 +343,7 @@ class MediaInfoFragment : Fragment() {
|
|||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.tags.isNotEmpty()) {
|
||||
if (media.tags.isNotEmpty() && !offline) {
|
||||
val bind = ItemTitleChipgroupBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
|
@ -376,7 +384,7 @@ class MediaInfoFragment : Fragment() {
|
|||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.characters.isNullOrEmpty()) {
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
|
@ -393,7 +401,7 @@ class MediaInfoFragment : Fragment() {
|
|||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.relations.isNullOrEmpty()) {
|
||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||
if (media.sequel != null || media.prequel != null) {
|
||||
val bind = ItemQuelsBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
|
@ -456,7 +464,7 @@ class MediaInfoFragment : Fragment() {
|
|||
parent.addView(bindi.root)
|
||||
}
|
||||
|
||||
if (!media.recommendations.isNullOrEmpty()) {
|
||||
if (!media.recommendations.isNullOrEmpty() && !offline ) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
|
|
|
@ -16,7 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -196,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
media?.inCustomListsOf?.forEach {
|
||||
SwitchMaterial(requireContext()).apply {
|
||||
MaterialSwitch(requireContext()).apply {
|
||||
isChecked = it.value
|
||||
text = it.key
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
|
|
|
@ -78,7 +78,7 @@ class SearchActivity : AppCompatActivity() {
|
|||
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
|
||||
val headerAdaptor = SearchAdapter(this)
|
||||
|
||||
val gridSize = (screenWidth / 124f).toInt()
|
||||
val gridSize = (screenWidth / 120f).toInt()
|
||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
|
@ -159,7 +159,7 @@ class SearchActivity : AppCompatActivity() {
|
|||
fun search() {
|
||||
val size = model.searchResults.results.size
|
||||
model.searchResults.results.clear()
|
||||
runOnUiThread {
|
||||
binding.searchRecyclerView.post {
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
|
@ -10,10 +12,14 @@ import android.view.ViewGroup
|
|||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||
import ani.dantotsu.App.Companion.context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||
import ani.dantotsu.saveData
|
||||
|
@ -54,6 +60,14 @@ class SearchAdapter(private val activity: SearchActivity) :
|
|||
}
|
||||
|
||||
binding.searchBar.hint = activity.result.type
|
||||
if (currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("incognito", false ) == true){
|
||||
val startIconDrawableRes = R.drawable.ic_incognito_24
|
||||
val startIconDrawable: Drawable? =
|
||||
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
|
||||
binding.searchBar.startIconDrawable = startIconDrawable
|
||||
}
|
||||
|
||||
var adult = activity.result.isAdult
|
||||
var listOnly = activity.result.onList
|
||||
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
package ani.dantotsu.media
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.snackString
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class SubtitleDownloader {
|
||||
|
||||
companion object {
|
||||
//doesn't really download the subtitles -\_(o_o)_/-
|
||||
suspend fun downloadSubtitles(context: Context, url: String): SubtitleType =
|
||||
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
||||
withContext(Dispatchers.IO) {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
@ -29,8 +33,8 @@ class SubtitleDownloader {
|
|||
|
||||
|
||||
val subtitleType = when {
|
||||
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS
|
||||
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT
|
||||
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
}
|
||||
|
||||
|
@ -39,5 +43,41 @@ class SubtitleDownloader {
|
|||
return@withContext SubtitleType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
//actually downloads lol
|
||||
suspend fun downloadSubtitle(context: Context, url: String, downloadedType: DownloadedType) {
|
||||
try {
|
||||
val directory = DownloadsManager.getDirectory(context, downloadedType.type, downloadedType.title, downloadedType.chapter)
|
||||
if (!directory.exists()) { //just in case
|
||||
directory.mkdirs()
|
||||
}
|
||||
val type = loadSubtitleType(context, url)
|
||||
val subtiteFile = File(directory, "subtitle.${type}")
|
||||
if (subtiteFile.exists()) {
|
||||
subtiteFile.delete()
|
||||
}
|
||||
subtiteFile.createNewFile()
|
||||
|
||||
val client = Injekt.get<NetworkHelper>().client
|
||||
val request = Request.Builder().url(url).build()
|
||||
val reponse = client.newCall(request).execute()
|
||||
|
||||
if (!reponse.isSuccessful) {
|
||||
snackString("Failed to download subtitle")
|
||||
return
|
||||
}
|
||||
|
||||
reponse.body.byteStream().use { input ->
|
||||
subtiteFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to download subtitle")
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,13 @@ import java.util.regex.Pattern
|
|||
|
||||
class AnimeNameAdapter {
|
||||
companion object {
|
||||
const val episodeRegex =
|
||||
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
const val failedEpisodeNumberRegex =
|
||||
"(?<!part\\s)\\b(\\d+)\\b"
|
||||
const val seasonRegex = "\\s+(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)"
|
||||
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
|
@ -16,5 +21,56 @@ class AnimeNameAdapter {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun findEpisodeNumber(text: String): Float? {
|
||||
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
|
||||
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||
|
||||
return if (episodeMatcher.find()) {
|
||||
if (episodeMatcher.group(2) != null) {
|
||||
episodeMatcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedEpisodeNumberPattern: Pattern =
|
||||
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||
val failedEpisodeNumberMatcher: Matcher =
|
||||
failedEpisodeNumberPattern.matcher(text)
|
||||
if (failedEpisodeNumberMatcher.find()) {
|
||||
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEpisodeNumber(text: String): String {
|
||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||
text
|
||||
}
|
||||
val letterPattern = Regex("[a-zA-Z]")
|
||||
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||
removedNumber
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeEpisodeNumberCompletely(text: String): String {
|
||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "")
|
||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||
val failedEpisodeNumberPattern: Regex =
|
||||
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||
}
|
||||
} else {
|
||||
removedNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,41 @@
|
|||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.webview.CookieCatcher
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class AnimeWatchAdapter(
|
||||
private val media: Media,
|
||||
private val fragment: AnimeWatchFragment,
|
||||
|
@ -41,6 +50,9 @@ class AnimeWatchAdapter(
|
|||
return ViewHolder(bind)
|
||||
}
|
||||
|
||||
private var nestedDialog: AlertDialog? = null
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
|
@ -78,6 +90,17 @@ class AnimeWatchAdapter(
|
|||
null
|
||||
)
|
||||
}
|
||||
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
|
||||
"Dantotsu",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
?.getBoolean("offlineMode", false) == true
|
||||
) View.GONE else View.VISIBLE
|
||||
|
||||
binding.animeSourceNameContainer.visibility = offline
|
||||
binding.animeSourceSettings.visibility = offline
|
||||
binding.animeSourceSearch.visibility = offline
|
||||
binding.animeSourceTitle.visibility = offline
|
||||
|
||||
//Source Selection
|
||||
var source =
|
||||
|
@ -147,8 +170,9 @@ class AnimeWatchAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
//Icons
|
||||
|
||||
//Subscription
|
||||
//subscribe
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
|
@ -167,44 +191,99 @@ class AnimeWatchAdapter(
|
|||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
}
|
||||
|
||||
//Icons
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
binding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
var selected = when (style) {
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceGrid
|
||||
2 -> binding.animeSourceCompact
|
||||
else -> binding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
//Nested Button
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
val dialogView =
|
||||
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
dialogBinding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
//Grids
|
||||
var selected = when (style) {
|
||||
0 -> dialogBinding.animeSourceList
|
||||
1 -> dialogBinding.animeSourceGrid
|
||||
2 -> dialogBinding.animeSourceCompact
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> dialogBinding.layoutText.text = "List"
|
||||
1 -> dialogBinding.layoutText.text = "Grid"
|
||||
2 -> dialogBinding.layoutText.text = "Compact"
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageButton) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
dialogBinding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
dialogBinding.layoutText.text = "List"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 1
|
||||
dialogBinding.layoutText.text = "Grid"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 2
|
||||
dialogBinding.layoutText.text = "Compact"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||
toast("WebView not installed")
|
||||
}
|
||||
//start CookieCatcher activity
|
||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||
val sourceAHH = watchSources[source] as? DynamicAnimeParser
|
||||
val sourceHttp =
|
||||
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
|
||||
val url = sourceHttp?.baseUrl
|
||||
url?.let {
|
||||
refresh = true
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//hidden
|
||||
dialogBinding.animeScanlatorContainer.visibility = View.GONE
|
||||
dialogBinding.animeDownloadContainer.visibility = View.GONE
|
||||
|
||||
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
|
||||
.setTitle("Options")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
if (run) fragment.onIconPressed(style, reversed)
|
||||
if (refresh) fragment.loadEpisodes(source, true)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ ->
|
||||
if (refresh) fragment.loadEpisodes(source, true)
|
||||
}
|
||||
.setOnCancelListener {
|
||||
if (refresh) fragment.loadEpisodes(source, true)
|
||||
}
|
||||
.create()
|
||||
nestedDialog?.show()
|
||||
}
|
||||
binding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceGrid.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 2
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeScanlatorTop.visibility = View.GONE
|
||||
binding.animeDownloadTop.visibility = View.GONE
|
||||
//Episode Handling
|
||||
handleEpisodes()
|
||||
}
|
||||
|
@ -304,12 +383,15 @@ class AnimeWatchAdapter(
|
|||
}
|
||||
}
|
||||
val ep = media.anime.episodes!![continueEp]!!
|
||||
|
||||
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||
|
||||
binding.itemEpisodeImage.loadImage(
|
||||
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||
)
|
||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||
binding.animeSourceContinueText.text =
|
||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
|
||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
|
||||
binding.animeSourceContinue.setOnClickListener {
|
||||
fragment.onEpisodeClick(continueEp)
|
||||
}
|
||||
|
@ -322,6 +404,7 @@ class AnimeWatchAdapter(
|
|||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.animeSourceProgressBar.visibility = View.GONE
|
||||
if (media.anime.episodes!!.isNotEmpty())
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
|
@ -336,7 +419,7 @@ class AnimeWatchAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
private fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (watchSources is AnimeSources) {
|
||||
val parser = watchSources[source] as? DynamicAnimeParser
|
||||
|
@ -351,12 +434,16 @@ class AnimeWatchAdapter(
|
|||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { it.lang })
|
||||
val adapter = ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||
)
|
||||
val items = adapter.count
|
||||
|
||||
binding?.animeSourceLanguageContainer?.visibility =
|
||||
if (items > 1) View.VISIBLE else View.GONE
|
||||
binding?.animeSourceLanguage?.setAdapter(adapter)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -371,4 +458,4 @@ class AnimeWatchAdapter(
|
|||
countDown(media, binding.animeSourceContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,10 @@ package ani.dantotsu.media.anime
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
|
@ -9,20 +13,29 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
|
@ -42,6 +55,8 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
@ -61,6 +76,8 @@ class AnimeWatchFragment : Fragment() {
|
|||
private lateinit var headerAdapter: AnimeWatchAdapter
|
||||
private lateinit var episodeAdapter: EpisodeAdapter
|
||||
|
||||
val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
var screenWidth = 0f
|
||||
private var progress = View.VISIBLE
|
||||
|
||||
|
@ -80,6 +97,21 @@ class AnimeWatchFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(ACTION_DOWNLOAD_STARTED)
|
||||
addAction(ACTION_DOWNLOAD_FINISHED)
|
||||
addAction(ACTION_DOWNLOAD_FAILED)
|
||||
addAction(ACTION_DOWNLOAD_PROGRESS)
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
requireContext(),
|
||||
downloadStatusReceiver,
|
||||
intentFilter,
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
|
||||
|
||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||
|
||||
|
@ -134,9 +166,17 @@ class AnimeWatchFragment : Fragment() {
|
|||
if (!loaded) {
|
||||
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
||||
|
||||
val offlineMode =
|
||||
model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
|
||||
|
||||
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
|
||||
episodeAdapter =
|
||||
EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
|
||||
EpisodeAdapter(
|
||||
style ?: uiSettings.animeDefaultView,
|
||||
media,
|
||||
this,
|
||||
offlineMode = offlineMode
|
||||
)
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
|
@ -169,12 +209,15 @@ class AnimeWatchFragment : Fragment() {
|
|||
if (media.anime?.kitsuEpisodes != null) {
|
||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||
episode.desc =
|
||||
episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
|
||||
episode.title =
|
||||
episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
|
||||
episode.thumb =
|
||||
episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb
|
||||
?: FileUrl[media.cover]
|
||||
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
|
||||
episode.title ?: ""
|
||||
).isBlank()
|
||||
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||
?: episode.title else episode.title
|
||||
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
|
||||
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
|
||||
?: FileUrl[media.cover]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -314,19 +357,20 @@ class AnimeWatchFragment : Fragment() {
|
|||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
var itemSelected = false
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
itemSelected = true
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
|
@ -343,10 +387,10 @@ class AnimeWatchFragment : Fragment() {
|
|||
.commit()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
changeUIVisibility(true)
|
||||
return@setNegativeButton
|
||||
.setOnDismissListener {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
|
@ -379,6 +423,93 @@ class AnimeWatchFragment : Fragment() {
|
|||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
||||
}
|
||||
|
||||
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
}
|
||||
|
||||
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
||||
val cancelIntent = Intent().apply {
|
||||
action = AnimeDownloaderService.ACTION_CANCEL_DOWNLOAD
|
||||
putExtra(
|
||||
AnimeDownloaderService.EXTRA_TASK_NAME,
|
||||
AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
)
|
||||
}
|
||||
requireContext().sendBroadcast(cancelIntent)
|
||||
|
||||
// Remove the download from the manager and update the UI
|
||||
downloadManager.removeDownload(
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.ANIME
|
||||
)
|
||||
)
|
||||
episodeAdapter.purgeDownload(i)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
|
||||
downloadManager.removeDownload(
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.ANIME
|
||||
)
|
||||
)
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
val id = requireContext().getSharedPreferences(
|
||||
ContextCompat.getString(requireContext(), R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
requireContext().getSharedPreferences(
|
||||
ContextCompat.getString(requireContext(), R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).edit().remove(taskName).apply()
|
||||
DownloadService.sendRemoveDownload(
|
||||
requireContext(),
|
||||
ExoplayerDownloadService::class.java,
|
||||
id,
|
||||
true
|
||||
)
|
||||
episodeAdapter.deleteDownload(i)
|
||||
}
|
||||
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (!this@AnimeWatchFragment::episodeAdapter.isInitialized) return
|
||||
when (intent.action) {
|
||||
ACTION_DOWNLOAD_STARTED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||
chapterNumber?.let { episodeAdapter.startDownload(it) }
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_FINISHED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||
chapterNumber?.let { episodeAdapter.stopDownload(it) }
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_FAILED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||
chapterNumber?.let {
|
||||
episodeAdapter.purgeDownload(it)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_PROGRESS -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||
val progress = intent.getIntExtra("progress", 0)
|
||||
chapterNumber?.let {
|
||||
episodeAdapter.updateDownloadProgress(it, progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
|
@ -391,6 +522,8 @@ class AnimeWatchFragment : Fragment() {
|
|||
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
headerAdapter.handleEpisodes()
|
||||
val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
|
||||
episodeAdapter.offlineMode = isDownloaded
|
||||
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
||||
var arr: ArrayList<Episode> = arrayListOf()
|
||||
if (media.anime!!.episodes != null) {
|
||||
|
@ -405,11 +538,20 @@ class AnimeWatchFragment : Fragment() {
|
|||
episodeAdapter.arr = arr
|
||||
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
for (download in downloadManager.animeDownloadedTypes) {
|
||||
if (download.title == media.mainName()) {
|
||||
episodeAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
model.watchSources?.flushText()
|
||||
super.onDestroy()
|
||||
try {
|
||||
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
var state: Parcelable? = null
|
||||
|
@ -424,4 +566,12 @@ class AnimeWatchFragment : Fragment() {
|
|||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
}
|
||||
companion object {
|
||||
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.mediarouter.app.MediaRouteActionProvider
|
||||
import androidx.mediarouter.app.MediaRouteChooserDialog
|
||||
import androidx.mediarouter.app.MediaRouteChooserDialogFragment
|
||||
import androidx.mediarouter.app.MediaRouteControllerDialog
|
||||
import androidx.mediarouter.app.MediaRouteControllerDialogFragment
|
||||
import androidx.mediarouter.app.MediaRouteDialogFactory
|
||||
import ani.dantotsu.R
|
||||
|
||||
class CustomCastProvider(context: Context) : MediaRouteActionProvider(context) {
|
||||
init {
|
||||
dialogFactory = CustomCastThemeFactory()
|
||||
}
|
||||
}
|
||||
|
||||
class CustomCastThemeFactory : MediaRouteDialogFactory() {
|
||||
override fun onCreateChooserDialogFragment(): MediaRouteChooserDialogFragment {
|
||||
return CustomMediaRouterChooserDialogFragment()
|
||||
}
|
||||
|
||||
override fun onCreateControllerDialogFragment(): MediaRouteControllerDialogFragment {
|
||||
return CustomMediaRouteControllerDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
class CustomMediaRouterChooserDialogFragment : MediaRouteChooserDialogFragment() {
|
||||
override fun onCreateChooserDialog(
|
||||
context: Context,
|
||||
savedInstanceState: Bundle?
|
||||
): MediaRouteChooserDialog =
|
||||
MediaRouteChooserDialog(context, R.style.MyPopup)
|
||||
}
|
||||
|
||||
class CustomMediaRouteControllerDialogFragment : MediaRouteControllerDialogFragment() {
|
||||
override fun onCreateControllerDialog(
|
||||
context: Context,
|
||||
savedInstanceState: Bundle?
|
||||
): MediaRouteControllerDialog =
|
||||
MediaRouteControllerDialog(context, R.style.MyPopup)
|
||||
}
|
|
@ -14,7 +14,8 @@ data class Episode(
|
|||
var selectedExtractor: String? = null,
|
||||
var selectedVideo: Int = 0,
|
||||
var selectedSubtitle: Int? = -1,
|
||||
var extractors: MutableList<VideoExtractor>? = null,
|
||||
var downloadProgress: String? = null,
|
||||
@Transient var extractors: MutableList<VideoExtractor>? = null,
|
||||
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
||||
var allStreams: Boolean = false,
|
||||
var watched: Long? = null,
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.media.Media
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
|
||||
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
|
||||
val curr = loadData<Long>("${mediaId}_${ep}")
|
||||
|
@ -32,13 +45,24 @@ fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep:
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class EpisodeAdapter(
|
||||
private var type: Int,
|
||||
private val media: Media,
|
||||
private val fragment: AnimeWatchFragment,
|
||||
var arr: List<Episode> = arrayListOf()
|
||||
var arr: List<Episode> = arrayListOf(),
|
||||
var offlineMode: Boolean
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private lateinit var index: DownloadIndex
|
||||
|
||||
|
||||
init {
|
||||
if (offlineMode) {
|
||||
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return (when (viewType) {
|
||||
0 -> EpisodeListViewHolder(
|
||||
|
@ -76,12 +100,11 @@ class EpisodeAdapter(
|
|||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val ep = arr[position]
|
||||
val title =
|
||||
"${
|
||||
if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(
|
||||
R.string.episode_singular
|
||||
)
|
||||
} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
|
||||
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
|
||||
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||
} else {
|
||||
ep.number
|
||||
} ?: ""
|
||||
|
||||
when (holder) {
|
||||
is EpisodeListViewHolder -> {
|
||||
|
@ -93,7 +116,7 @@ class EpisodeAdapter(
|
|||
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
|
||||
.into(binding.itemEpisodeImage)
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeTitle.text = title
|
||||
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
|
||||
|
||||
if (ep.filler) {
|
||||
binding.itemEpisodeFiller.visibility = View.VISIBLE
|
||||
|
@ -102,6 +125,7 @@ class EpisodeAdapter(
|
|||
binding.itemEpisodeFiller.visibility = View.GONE
|
||||
binding.itemEpisodeFillerView.visibility = View.GONE
|
||||
}
|
||||
holder.bind(ep.number, ep.downloadProgress)
|
||||
binding.itemEpisodeDesc.visibility =
|
||||
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
||||
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
||||
|
@ -205,6 +229,80 @@ class EpisodeAdapter(
|
|||
|
||||
override fun getItemCount(): Int = arr.size
|
||||
|
||||
private val activeDownloads = mutableSetOf<String>()
|
||||
private val downloadedEpisodes = mutableSetOf<String>()
|
||||
|
||||
fun startDownload(episodeNumber: String) {
|
||||
activeDownloads.add(episodeNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun stopDownload(episodeNumber: String) {
|
||||
activeDownloads.remove(episodeNumber)
|
||||
downloadedEpisodes.add(episodeNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||
if (position != -1) {
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
|
||||
media.mainName(),
|
||||
episodeNumber
|
||||
)
|
||||
val id = fragment.requireContext().getSharedPreferences(
|
||||
ContextCompat.getString(fragment.requireContext(), R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
val size = try {
|
||||
val download = index.getDownload(id)
|
||||
bytesToHuman(download?.bytesDownloaded ?: 0)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
arr[position].downloadProgress = "Downloaded" + if (size != null) ": ($size)" else ""
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDownload(episodeNumber: String) {
|
||||
downloadedEpisodes.remove(episodeNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||
if (position != -1) {
|
||||
arr[position].downloadProgress = null
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownload(episodeNumber: String) {
|
||||
activeDownloads.remove(episodeNumber)
|
||||
downloadedEpisodes.remove(episodeNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||
if (position != -1) {
|
||||
arr[position].downloadProgress = "Failed"
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDownloadProgress(episodeNumber: String, progress: Int) {
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||
if (position != -1) {
|
||||
arr[position].downloadProgress = "Downloading: $progress%"
|
||||
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
|
@ -227,11 +325,27 @@ class EpisodeAdapter(
|
|||
|
||||
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private val activeCoroutines = mutableSetOf<String>()
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
|
||||
}
|
||||
binding.itemDownload.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
|
||||
val episodeNumber = arr[bindingAdapterPosition].number
|
||||
if (activeDownloads.contains(episodeNumber)) {
|
||||
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
|
||||
return@setOnClickListener
|
||||
} else if (downloadedEpisodes.contains(episodeNumber)) {
|
||||
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
fragment.onAnimeEpisodeDownloadClick(episodeNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.itemEpisodeDesc.setOnClickListener {
|
||||
if (binding.itemEpisodeDesc.maxLines == 3)
|
||||
binding.itemEpisodeDesc.maxLines = 100
|
||||
|
@ -239,11 +353,73 @@ class EpisodeAdapter(
|
|||
binding.itemEpisodeDesc.maxLines = 3
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(episodeNumber: String, progress: String?) {
|
||||
if (progress != null) {
|
||||
binding.itemDownloadStatus.visibility = View.VISIBLE
|
||||
binding.itemDownloadStatus.text = progress
|
||||
} else {
|
||||
binding.itemDownloadStatus.visibility = View.GONE
|
||||
binding.itemDownloadStatus.text = ""
|
||||
}
|
||||
if (activeDownloads.contains(episodeNumber)) {
|
||||
// Show spinner
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_sync)
|
||||
startOrContinueRotation(episodeNumber)
|
||||
} else if (downloadedEpisodes.contains(episodeNumber)) {
|
||||
binding.itemDownloadStatus.visibility = View.VISIBLE
|
||||
// Show checkmark
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
|
||||
//binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
|
||||
binding.itemDownload.postDelayed({
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
|
||||
binding.itemDownload.rotation = 0f
|
||||
//binding.itemDownload.setColorFilter(typedValue2.data)
|
||||
}, 1000)
|
||||
} else {
|
||||
binding.itemDownloadStatus.visibility = View.GONE
|
||||
// Show download icon
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
|
||||
binding.itemDownload.rotation = 0f
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startOrContinueRotation(episodeNumber: String) {
|
||||
if (!isRotationCoroutineRunningFor(episodeNumber)) {
|
||||
val scope = fragment.lifecycle.coroutineScope
|
||||
scope.launch {
|
||||
// Add chapter number to active coroutines set
|
||||
activeCoroutines.add(episodeNumber)
|
||||
while (activeDownloads.contains(episodeNumber)) {
|
||||
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
|
||||
.setInterpolator(
|
||||
LinearInterpolator()
|
||||
).start()
|
||||
delay(1000)
|
||||
}
|
||||
// Remove chapter number from active coroutines set
|
||||
activeCoroutines.remove(episodeNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRotationCoroutineRunningFor(episodeNumber: String): Boolean {
|
||||
return episodeNumber in activeCoroutines
|
||||
}
|
||||
}
|
||||
|
||||
fun updateType(t: Int) {
|
||||
type = t
|
||||
}
|
||||
|
||||
private fun bytesToHuman(bytes: Long): String? {
|
||||
if (bytes < 0) return null
|
||||
val unit = 1000
|
||||
if (bytes < unit) return "$bytes B"
|
||||
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||
val pre = ("KMGTPE")[exp - 1]
|
||||
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import android.content.pm.PackageManager
|
|||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.*
|
||||
|
@ -43,11 +42,15 @@ import androidx.core.math.MathUtils.clamp
|
|||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
|
||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSourceFactory
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
|
@ -58,6 +61,7 @@ import androidx.media3.exoplayer.util.EventLogger
|
|||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.ui.*
|
||||
import androidx.media3.ui.CaptionStyleCompat.*
|
||||
import androidx.mediarouter.app.MediaRouteButton
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
|
@ -67,12 +71,12 @@ import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
|||
import ani.dantotsu.connections.discord.RPC
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.others.AniSkip
|
||||
import ani.dantotsu.others.AniSkip.getType
|
||||
import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.ResettableTimer
|
||||
import ani.dantotsu.others.getSerialized
|
||||
|
@ -82,6 +86,10 @@ import ani.dantotsu.settings.PlayerSettingsActivity
|
|||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.lagradost.nicehttp.ignoreAllSSLErrors
|
||||
|
@ -97,10 +105,9 @@ import kotlin.math.max
|
|||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@UnstableApi
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityListener {
|
||||
|
||||
private val resumeWindow = "resumeWindow"
|
||||
private val resumePosition = "resumePosition"
|
||||
|
@ -108,6 +115,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
private val playerOnPlay = "playerOnPlay"
|
||||
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
private var castPlayer: CastPlayer? = null
|
||||
private var castContext: CastContext? = null
|
||||
private var isCastApiAvailable = false
|
||||
private lateinit var trackSelector: DefaultTrackSelector
|
||||
private lateinit var cacheFactory: CacheDataSource.Factory
|
||||
private lateinit var playbackParameters: PlaybackParameters
|
||||
|
@ -145,6 +155,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
|
||||
private var orientationListener: OrientationEventListener? = null
|
||||
|
||||
private var downloadId: String? = null
|
||||
|
||||
companion object {
|
||||
var initialized = false
|
||||
lateinit var media: Media
|
||||
|
@ -328,6 +340,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
setContentView(binding.root)
|
||||
|
||||
//Initialize
|
||||
isCastApiAvailable = GoogleApiAvailability.getInstance()
|
||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this)
|
||||
castPlayer = CastPlayer(castContext!!)
|
||||
castPlayer!!.setSessionAvailabilityListener(this)
|
||||
} catch (e: Exception) {
|
||||
isCastApiAvailable = false
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
hideSystemBars()
|
||||
|
||||
|
@ -387,7 +409,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
orientationListener =
|
||||
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
println(orientation)
|
||||
if (orientation in 45..135) {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility =
|
||||
View.VISIBLE
|
||||
|
@ -466,12 +487,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
if (isInitialized) {
|
||||
isPlayerPlaying = exoPlayer.isPlaying
|
||||
(exoPlay.drawable as Animatable?)?.start()
|
||||
if (isPlayerPlaying) {
|
||||
if (isPlayerPlaying || castPlayer?.isPlaying == true) {
|
||||
Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay)
|
||||
exoPlayer.pause()
|
||||
castPlayer?.pause()
|
||||
} else {
|
||||
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
|
||||
exoPlayer.play()
|
||||
if (castPlayer?.isPlaying == false && castPlayer?.currentMediaItem != null) {
|
||||
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
|
||||
castPlayer?.play()
|
||||
} else if (!isPlayerPlaying) {
|
||||
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
|
||||
exoPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -812,18 +839,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
audioManager.setStreamVolume(STREAM_MUSIC, volume, 0)
|
||||
volumeHide()
|
||||
}
|
||||
|
||||
val fastForward = playerView.findViewById<TextView>(R.id.exo_fast_forward_text)
|
||||
fun fastForward() {
|
||||
isFastForwarding = true
|
||||
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
|
||||
snackString("Playing at ${exoPlayer.playbackParameters.speed}x speed")
|
||||
fastForward.visibility = View.VISIBLE
|
||||
fastForward.text = ("${exoPlayer.playbackParameters.speed}x")
|
||||
}
|
||||
|
||||
fun stopFastForward() {
|
||||
if (isFastForwarding) {
|
||||
isFastForwarding = false
|
||||
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
|
||||
snackString("Playing at default speed: ${exoPlayer.playbackParameters.speed}x")
|
||||
fastForward.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -925,10 +953,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
episodeArr = episodes.keys.toList()
|
||||
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
|
||||
|
||||
episodeTitleArr = arrayListOf()
|
||||
episodeTitleArr = arrayListOf<String>()
|
||||
episodes.forEach {
|
||||
val episode = it.value
|
||||
episodeTitleArr.add("${if (!episode.title.isNullOrEmpty() && episode.title != "null") "" else "Episode "}${episode.number}${if (episode.filler) " [Filler]" else ""}${if (!episode.title.isNullOrEmpty() && episode.title != "null") " : " + episode.title else ""}")
|
||||
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
|
||||
episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotBlank() && cleanedTitle != "null") ": $cleanedTitle" else ""}")
|
||||
}
|
||||
|
||||
//Episode Change
|
||||
|
@ -1074,10 +1103,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
|
||||
//Cast
|
||||
if (settings.cast) {
|
||||
playerView.findViewById<ImageButton>(R.id.exo_cast).apply {
|
||||
playerView.findViewById<MediaRouteButton>(R.id.exo_cast).apply {
|
||||
visibility = View.VISIBLE
|
||||
setSafeOnClickListener {
|
||||
try {
|
||||
CastButtonFactory.setUpMediaRouteButton(context, this)
|
||||
dialogFactory = CustomCastThemeFactory()
|
||||
} catch (e: Exception) {
|
||||
isCastApiAvailable = false
|
||||
}
|
||||
setOnLongClickListener {
|
||||
cast()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1101,7 +1137,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
if (settings.cursedSpeeds)
|
||||
arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
|
||||
else
|
||||
arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f)
|
||||
arrayOf(
|
||||
0.25f,
|
||||
0.33f,
|
||||
0.5f,
|
||||
0.66f,
|
||||
0.75f,
|
||||
1f,
|
||||
1.15f,
|
||||
1.25f,
|
||||
1.33f,
|
||||
1.5f,
|
||||
1.66f,
|
||||
1.75f,
|
||||
2f
|
||||
)
|
||||
|
||||
val speedsName = speeds.map { "${it}x" }.toTypedArray()
|
||||
var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed
|
||||
|
@ -1156,14 +1206,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
}
|
||||
|
||||
preloading = false
|
||||
val incognito = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("incognito", false) ?: false
|
||||
val showProgressDialog =
|
||||
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
|
||||
?: true else false
|
||||
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
|
||||
AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle(getString(R.string.auto_update, media.userPreferredName))
|
||||
.setMessage(getString(R.string.incognito_will_not_update))
|
||||
.apply {
|
||||
if (incognito) {
|
||||
setMessage(getString(R.string.incognito_will_not_update))
|
||||
}
|
||||
setOnCancelListener { hideSystemBars() }
|
||||
setCancelable(false)
|
||||
setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||
|
@ -1232,7 +1286,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
if (subtitle?.type == SubtitleType.UNKNOWN) {
|
||||
val context = this
|
||||
runBlocking {
|
||||
val type = SubtitleDownloader.downloadSubtitles(context, subtitle!!.file.url)
|
||||
val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
|
||||
val fileUri = Uri.parse(subtitle!!.file.url)
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
.Builder(fileUri)
|
||||
|
@ -1250,8 +1304,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
}
|
||||
println("sub: $sub")
|
||||
} else {
|
||||
val subUri = Uri.parse((subtitle!!.file.url))
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
.Builder(Uri.parse(subtitle!!.file.url))
|
||||
.Builder(subUri)
|
||||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||
.setMimeType(
|
||||
when (subtitle?.type) {
|
||||
|
@ -1270,18 +1325,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
ext.onVideoPlayed(video)
|
||||
}
|
||||
|
||||
val but = playerView.findViewById<ImageButton>(R.id.exo_download)
|
||||
if (video?.format == VideoType.CONTAINER || (loadData<Int>("settings_download_manager")
|
||||
?: 0) != 0
|
||||
) {
|
||||
but.visibility = View.VISIBLE
|
||||
but.setOnClickListener {
|
||||
download(this, episode, animeTitle.text.toString())
|
||||
}
|
||||
} else {
|
||||
but.visibility = View.GONE
|
||||
}
|
||||
|
||||
val simpleCache = VideoCache.getInstance(this)
|
||||
val httpClient = okHttpClient.newBuilder().apply {
|
||||
ignoreAllSSLErrors()
|
||||
|
@ -1298,9 +1341,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
}
|
||||
dataSource
|
||||
}
|
||||
val dafuckDataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
|
||||
cacheFactory = CacheDataSource.Factory().apply {
|
||||
setCache(simpleCache)
|
||||
setUpstreamDataSourceFactory(dataSourceFactory)
|
||||
setCache(Helper.getSimpleCache(this@ExoplayerView))
|
||||
if (ext.server.offline) {
|
||||
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
|
||||
} else {
|
||||
setUpstreamDataSourceFactory(dataSourceFactory)
|
||||
}
|
||||
setCacheWriteDataSinkFactory(null)
|
||||
}
|
||||
|
||||
val mimeType = when (video?.format) {
|
||||
|
@ -1309,15 +1358,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
else -> MimeTypes.APPLICATION_MP4
|
||||
}
|
||||
|
||||
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
||||
logger("url: ${video!!.file.url}")
|
||||
logger("mimeType: $mimeType")
|
||||
val downloadedMediaItem = if (ext.server.offline) {
|
||||
val key = ext.server.name
|
||||
downloadId = getSharedPreferences(getString(R.string.anime_downloads), MODE_PRIVATE)
|
||||
.getString(key, null)
|
||||
if (downloadId != null) {
|
||||
Helper.downloadManager(this)
|
||||
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
|
||||
} else {
|
||||
snackString("Download not found")
|
||||
null
|
||||
}
|
||||
} else null
|
||||
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
|
||||
builder.setSubtitleConfigurations(listofnotnullsubs)
|
||||
mediaItem = if (downloadedMediaItem == null) {
|
||||
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
||||
logger("url: ${video!!.file.url}")
|
||||
logger("mimeType: $mimeType")
|
||||
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
|
||||
builder.setSubtitleConfigurations(listofnotnullsubs)
|
||||
}
|
||||
builder.build()
|
||||
} else {
|
||||
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
|
||||
val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build()
|
||||
addedSubsDownloadedMediaItem.setSubtitleConfigurations(immutableListOf(addLanguage))
|
||||
episode.selectedSubtitle = 0
|
||||
}
|
||||
addedSubsDownloadedMediaItem.build()
|
||||
}
|
||||
mediaItem = builder.build()
|
||||
|
||||
|
||||
//Source
|
||||
exoSource.setOnClickListener {
|
||||
|
@ -1439,7 +1513,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
exoPlayer.release()
|
||||
VideoCache.release()
|
||||
mediaSession?.release()
|
||||
if(DiscordServiceRunningSingleton.running) {
|
||||
if (DiscordServiceRunningSingleton.running) {
|
||||
val stopIntent = Intent(this, DiscordService::class.java)
|
||||
DiscordServiceRunningSingleton.running = false
|
||||
stopService(stopIntent)
|
||||
|
@ -1479,7 +1553,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
super.onPause()
|
||||
orientationListener?.disable()
|
||||
if (isInitialized) {
|
||||
playerView.player?.pause()
|
||||
if (castPlayer?.isPlaying == false) {
|
||||
playerView.player?.pause()
|
||||
}
|
||||
saveData(
|
||||
"${media.id}_${media.anime!!.selectedEpisode}",
|
||||
exoPlayer.currentPosition,
|
||||
|
@ -1500,7 +1576,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
}
|
||||
|
||||
override fun onStop() {
|
||||
playerView.player?.pause()
|
||||
if (castPlayer?.isPlaying == false) {
|
||||
playerView.player?.pause()
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
|
@ -1793,7 +1871,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
|
||||
// Enter PiP Mode
|
||||
@Suppress("DEPRECATION")
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun enterPipMode() {
|
||||
wasPlaying = isPlayerPlaying
|
||||
if (!pipEnabled) return
|
||||
|
@ -1866,6 +1943,55 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun startCastPlayer() {
|
||||
if (!isCastApiAvailable) {
|
||||
snackString("Cast API not available")
|
||||
return
|
||||
}
|
||||
//make sure mediaItem is initialized and castPlayer is not null
|
||||
if (!this::mediaItem.isInitialized || castPlayer == null) return
|
||||
castPlayer?.setMediaItem(mediaItem)
|
||||
castPlayer?.prepare()
|
||||
playerView.player = castPlayer
|
||||
exoPlayer.stop()
|
||||
castPlayer?.addListener(object : Player.Listener {
|
||||
//if the player is paused changed, we want to update the UI
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
||||
if (playWhenReady) {
|
||||
(exoPlay.drawable as Animatable?)?.start()
|
||||
Glide.with(this@ExoplayerView)
|
||||
.load(R.drawable.anim_play_to_pause)
|
||||
.into(exoPlay)
|
||||
} else {
|
||||
(exoPlay.drawable as Animatable?)?.start()
|
||||
Glide.with(this@ExoplayerView)
|
||||
.load(R.drawable.anim_pause_to_play)
|
||||
.into(exoPlay)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun startExoPlayer() {
|
||||
exoPlayer.setMediaItem(mediaItem)
|
||||
exoPlayer.prepare()
|
||||
playerView.player = exoPlayer
|
||||
castPlayer?.stop()
|
||||
}
|
||||
|
||||
override fun onCastSessionAvailable() {
|
||||
if (isCastApiAvailable) {
|
||||
startCastPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCastSessionUnavailable() {
|
||||
startExoPlayer()
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class ExtendedTimeBar(
|
||||
context: Context,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
|
@ -20,17 +21,21 @@ import ani.dantotsu.*
|
|||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||
import ani.dantotsu.databinding.ItemStreamBinding
|
||||
import ani.dantotsu.databinding.ItemUrlBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.DecimalFormat
|
||||
|
||||
|
||||
class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSelectorBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
@ -42,6 +47,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
private var makeDefault = false
|
||||
private var selected: String? = null
|
||||
private var launch: Boolean? = null
|
||||
private var isDownloadMenu: Boolean? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -49,6 +55,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
selected = it.getString("server")
|
||||
launch = it.getBoolean("launch", true)
|
||||
prevEpisode = it.getString("prev")
|
||||
isDownloadMenu = it.getBoolean("isDownload")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,8 +83,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
|
||||
episode = ep
|
||||
if (ep != null) {
|
||||
if (isDownloadMenu == true) {
|
||||
binding.selectorMakeDefault.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (selected != null) {
|
||||
if (selected != null && isDownloadMenu == false) {
|
||||
binding.selectorListContainer.visibility = View.GONE
|
||||
binding.selectorAutoListContainer.visibility = View.VISIBLE
|
||||
binding.selectorAutoText.text = selected
|
||||
|
@ -95,7 +105,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
|
||||
fun load() {
|
||||
val size =
|
||||
ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||
if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
|
||||
ep.extractors?.firstOrNull()?.videos?.size
|
||||
} else {
|
||||
ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||
}
|
||||
|
||||
if (size != null && size >= media!!.selected!!.video) {
|
||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
|
||||
selected
|
||||
|
@ -145,6 +160,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
ep.extractorCallback = {
|
||||
scope.launch {
|
||||
adapter.add(it)
|
||||
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
|
||||
adapter.performClick(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
model.getEpisode().observe(this) {
|
||||
|
@ -164,6 +182,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
} else {
|
||||
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
|
||||
adapter.addAll(ep.extractors)
|
||||
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
|
||||
adapter.performClick(0)
|
||||
}
|
||||
binding.selectorProgressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +200,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
prevEpisode = null
|
||||
|
||||
dismiss()
|
||||
if (launch!!) {
|
||||
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||
stopAddingToList()
|
||||
val intent = Intent(activity, ExoplayerView::class.java)
|
||||
ExoplayerView.media = media
|
||||
|
@ -214,7 +235,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val extractor = links[position]
|
||||
holder.binding.streamName.text = extractor.server.name
|
||||
holder.binding.streamName.text = ""//extractor.server.name
|
||||
holder.binding.streamName.visibility = View.GONE
|
||||
|
||||
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
|
||||
|
@ -235,6 +257,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
notifyItemRangeInserted(0, extractors.size)
|
||||
}
|
||||
|
||||
fun performClick(position: Int) {
|
||||
try { //bandaid fix for crash
|
||||
val extractor = links[position]
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
|
||||
extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = 0
|
||||
startExoplayer(media!!)
|
||||
} catch (e: Exception) {
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class StreamViewHolder(val binding: ItemStreamBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
@ -256,24 +290,107 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val video = extractor.videos[position]
|
||||
binding.urlQuality.text =
|
||||
if (video.quality != null) "${video.quality}p" else "Default Quality"
|
||||
binding.urlNote.text = video.extraNote ?: ""
|
||||
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
|
||||
binding.urlDownload.visibility = View.VISIBLE
|
||||
if (isDownloadMenu == true) {
|
||||
binding.urlDownload.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.urlDownload.visibility = View.GONE
|
||||
}
|
||||
binding.urlDownload.setSafeOnClickListener {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||
extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
||||
position
|
||||
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
download(
|
||||
requireActivity(),
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
||||
media!!.userPreferredName
|
||||
)
|
||||
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||
val selectedVideo =
|
||||
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||
|
||||
val subtitles = extractor.subtitles
|
||||
val subtitleNames = subtitles.map { it.language }
|
||||
var subtitleToDownload: Subtitle? = null
|
||||
if (subtitles.isNotEmpty()) {
|
||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Subtitle")
|
||||
.setSingleChoiceItems(
|
||||
subtitleNames.toTypedArray(),
|
||||
-1
|
||||
) { dialog, which ->
|
||||
subtitleToDownload = subtitles[which]
|
||||
}
|
||||
.setPositiveButton("Download") { _, _ ->
|
||||
dialog?.dismiss()
|
||||
if (selectedVideo != null) {
|
||||
Helper.startAnimeDownloadService(
|
||||
currActivity()!!,
|
||||
media!!.mainName(),
|
||||
episode.number,
|
||||
selectedVideo,
|
||||
subtitleToDownload,
|
||||
media,
|
||||
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||
)
|
||||
} else {
|
||||
snackString("No Video Selected")
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Skip") { dialog, _ ->
|
||||
subtitleToDownload = null
|
||||
if (selectedVideo != null) {
|
||||
Helper.startAnimeDownloadService(
|
||||
currActivity()!!,
|
||||
media!!.mainName(),
|
||||
episode.number,
|
||||
selectedVideo,
|
||||
subtitleToDownload,
|
||||
media,
|
||||
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||
)
|
||||
} else {
|
||||
snackString("No Video Selected")
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton("Cancel") { dialog, _ ->
|
||||
subtitleToDownload = null
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
|
||||
} else {
|
||||
if (selectedVideo != null) {
|
||||
Helper.startAnimeDownloadService(
|
||||
requireActivity(),
|
||||
media!!.mainName(),
|
||||
episode.number,
|
||||
selectedVideo,
|
||||
subtitleToDownload,
|
||||
media,
|
||||
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||
)
|
||||
} else {
|
||||
snackString("No Video Selected")
|
||||
}
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
binding.urlDownload.setOnLongClickListener {
|
||||
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
if ((loadData<Int>("settings_download_manager") ?: 0) != 0) {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||
extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
||||
position
|
||||
download(
|
||||
requireActivity(),
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
||||
media!!.userPreferredName
|
||||
)
|
||||
} else {
|
||||
snackString("No Download Manager Selected")
|
||||
}
|
||||
true
|
||||
}
|
||||
if (video.format == VideoType.CONTAINER) {
|
||||
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
|
||||
binding.urlSize.text =
|
||||
|
@ -281,12 +398,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
|
||||
"#.##"
|
||||
).format(video.size ?: 0).toString() + " MB"))
|
||||
} else {
|
||||
binding.urlQuality.text = "Multi Quality"
|
||||
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
|
||||
binding.urlDownload.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
binding.urlNote.visibility = View.VISIBLE
|
||||
binding.urlNote.text = video.format.name
|
||||
binding.urlQuality.text = extractor.server.name
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = extractor.videos.size
|
||||
|
@ -295,6 +410,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setSafeOnClickListener {
|
||||
if (isDownloadMenu == true) {
|
||||
binding.urlDownload.performClick()
|
||||
return@setSafeOnClickListener
|
||||
}
|
||||
tryWith(true) {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
|
||||
extractor.server.name
|
||||
|
@ -326,13 +445,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
fun newInstance(
|
||||
server: String? = null,
|
||||
la: Boolean = true,
|
||||
prev: String? = null
|
||||
prev: String? = null,
|
||||
isDownload: Boolean
|
||||
): SelectorDialogFragment =
|
||||
SelectorDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString("server", server)
|
||||
putBoolean("launch", la)
|
||||
putString("prev", prev)
|
||||
putBoolean("isDownload", isDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
|
@ -60,6 +62,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||
)
|
||||
)
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
if (position == 0) {
|
||||
|
|
|
@ -141,8 +141,8 @@ class MangaChapterAdapter(
|
|||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private val activeCoroutines = mutableSetOf<String>()
|
||||
val typedValue1 = TypedValue()
|
||||
val typedValue2 = TypedValue()
|
||||
private val typedValue1 = TypedValue()
|
||||
private val typedValue2 = TypedValue()
|
||||
fun bind(chapterNumber: String, progress: String?) {
|
||||
if (progress != null) {
|
||||
binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
|
@ -167,6 +167,7 @@ class MangaChapterAdapter(
|
|||
} else {
|
||||
// Show download icon
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
|
||||
binding.itemDownload.rotation = 0f
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -235,7 +236,7 @@ class MangaChapterAdapter(
|
|||
input.maxValue = itemCount - bindingAdapterPosition
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { dialog, which ->
|
||||
alertDialog.setPositiveButton("OK") { _, _ ->
|
||||
downloadNChaptersFrom(bindingAdapterPosition, input.value)
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
|
|
|
@ -5,16 +5,25 @@ import java.util.regex.Pattern
|
|||
|
||||
class MangaNameAdapter {
|
||||
companion object {
|
||||
const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
|
||||
const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
|
||||
fun findChapterNumber(text: String): Float? {
|
||||
val regex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)"
|
||||
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
|
||||
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
||||
return if (matcher.find()) {
|
||||
matcher.group(2)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
val failedChapterNumberPattern: Pattern =
|
||||
Pattern.compile(filedChapterNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||
val failedChapterNumberMatcher: Matcher =
|
||||
failedChapterNumberPattern.matcher(text)
|
||||
if (failedChapterNumberMatcher.find()) {
|
||||
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,41 @@ package ani.dantotsu.media.manga
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.media.anime.handleProgress
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.webview.CookieCatcher
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class MangaReadAdapter(
|
||||
private val media: Media,
|
||||
private val fragment: MangaReadFragment,
|
||||
|
@ -47,6 +54,8 @@ class MangaReadAdapter(
|
|||
return ViewHolder(bind)
|
||||
}
|
||||
|
||||
private var nestedDialog: AlertDialog? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
|
@ -60,7 +69,17 @@ class MangaReadAdapter(
|
|||
null
|
||||
)
|
||||
}
|
||||
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
|
||||
"Dantotsu",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
?.getBoolean("offlineMode", false) == true
|
||||
) View.GONE else View.VISIBLE
|
||||
|
||||
binding.animeSourceNameContainer.visibility = offline
|
||||
binding.animeSourceSettings.visibility = offline
|
||||
binding.animeSourceSearch.visibility = offline
|
||||
binding.animeSourceTitle.visibility = offline
|
||||
//Source Selection
|
||||
var source =
|
||||
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
|
@ -117,7 +136,7 @@ class MangaReadAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
//Subscription
|
||||
//Grids
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
|
@ -136,98 +155,156 @@ class MangaReadAdapter(
|
|||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
}
|
||||
|
||||
//Icons
|
||||
binding.animeSourceGrid.visibility = View.GONE
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
binding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
|
||||
binding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView =
|
||||
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer = dialogView.findViewById<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)
|
||||
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
dialogBinding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
|
||||
// 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)
|
||||
//Grids
|
||||
dialogBinding.animeSourceGrid.visibility = View.GONE
|
||||
var selected = when (style) {
|
||||
0 -> dialogBinding.animeSourceList
|
||||
1 -> dialogBinding.animeSourceCompact
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> dialogBinding.layoutText.text = "List"
|
||||
1 -> dialogBinding.layoutText.text = "Compact"
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
val dialog = alertDialog.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
var selected = when (style) {
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceCompact
|
||||
else -> binding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
binding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
fun selected(it: ImageButton) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
dialogBinding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
dialogBinding.layoutText.text = "List"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 1
|
||||
dialogBinding.layoutText.text = "Compact"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||
toast("WebView not installed")
|
||||
}
|
||||
//start CookieCatcher activity
|
||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
|
||||
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
|
||||
val url = sourceHttp?.baseUrl
|
||||
url?.let {
|
||||
refresh = true
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
ContextCompat.startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Multi download
|
||||
dialogBinding.downloadNo.text = "0"
|
||||
dialogBinding.animeDownloadTop.setOnClickListener {
|
||||
//Alert dialog asking for the number of chapters to download
|
||||
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
alertDialog.setTitle("Multi Chapter Downloader")
|
||||
alertDialog.setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = 20
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { _, _ ->
|
||||
dialogBinding.downloadNo.text = "${input.value}"
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
val dialog = alertDialog.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
//Scanlator
|
||||
dialogBinding.animeScanlatorContainer.visibility =
|
||||
if (options.count() > 1) View.VISIBLE else View.GONE
|
||||
dialogBinding.scanlatorNo.text = "${options.count()}"
|
||||
dialogBinding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView2 =
|
||||
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer =
|
||||
dialogView2.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(dialogView2)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
//add unchecked to hidden
|
||||
hiddenScanlators.clear()
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
if (!checkBox.isChecked) {
|
||||
hiddenScanlators.add(checkBox.text.toString())
|
||||
}
|
||||
}
|
||||
fragment.onScanlatorChange(hiddenScanlators)
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
|
||||
.setTitle("Options")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
if (run) fragment.onIconPressed(style, reversed)
|
||||
if (dialogBinding.downloadNo.text != "0") {
|
||||
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
|
||||
}
|
||||
if (refresh) fragment.loadChapters(source, true)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ ->
|
||||
if (refresh) fragment.loadChapters(source, true)
|
||||
}
|
||||
.setOnCancelListener {
|
||||
if (refresh) fragment.loadChapters(source, true)
|
||||
}
|
||||
.create()
|
||||
nestedDialog?.show()
|
||||
}
|
||||
//Chapter Handling
|
||||
handleChapters()
|
||||
}
|
||||
|
@ -259,6 +336,7 @@ class MangaReadAdapter(
|
|||
0
|
||||
)
|
||||
}
|
||||
|
||||
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
|
||||
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
|
||||
val startChapterString = if (startChapter != null) {
|
||||
|
@ -370,7 +448,7 @@ class MangaReadAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
private fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (mangaReadSources is MangaSources) {
|
||||
val parser = mangaReadSources[source] as? DynamicMangaParser
|
||||
|
@ -385,12 +463,16 @@ class MangaReadAdapter(
|
|||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { it.lang })
|
||||
val adapter = ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||
)
|
||||
val items = adapter.count
|
||||
binding?.animeSourceLanguageContainer?.visibility =
|
||||
if (items > 1) View.VISIBLE else View.GONE
|
||||
|
||||
binding?.animeSourceLanguage?.setAdapter(adapter)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||
|
@ -37,6 +37,7 @@ import ani.dantotsu.media.Media
|
|||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
|
@ -166,7 +167,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
chapterAdapter =
|
||||
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||
|
||||
for (download in downloadManager.mangaDownloads) {
|
||||
for (download in downloadManager.mangaDownloadedTypes) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
|
||||
|
@ -202,20 +203,25 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
val selected = media.userProgress
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
//filter by selected language
|
||||
val progressChapterIndex = chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected }?:0
|
||||
if (progressChapterIndex < 0 || n < 1) return
|
||||
val chaptersToDownload = chapters?.subList(
|
||||
progressChapterIndex + 1,
|
||||
progressChapterIndex + n + 1
|
||||
)
|
||||
if (chaptersToDownload != null) {
|
||||
for (chapter in chaptersToDownload) {
|
||||
onMangaChapterDownloadClick(chapter.title!!)
|
||||
}
|
||||
}
|
||||
val progressChapterIndex = (chapters?.indexOfFirst {
|
||||
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||
} ?: 0) + 1
|
||||
|
||||
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
|
||||
|
||||
// Calculate the end index
|
||||
val endIndex = minOf(progressChapterIndex + n, chapters.size)
|
||||
|
||||
//make sure there are enough chapters
|
||||
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
|
||||
|
||||
|
||||
for (chapter in chaptersToDownload) {
|
||||
onMangaChapterDownloadClick(chapter.title!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateChapters() {
|
||||
val loadedChapters = model.getMangaChapters().value
|
||||
if (loadedChapters != null) {
|
||||
|
@ -260,7 +266,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
|
||||
private fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
|
||||
val scanlators = mutableListOf<String>()
|
||||
if (chap != null) {
|
||||
val chapters = chap.values
|
||||
|
@ -355,19 +361,20 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
var itemSelected = false
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
itemSelected = true
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
|
@ -382,10 +389,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
changeUIVisibility(true)
|
||||
return@setNegativeButton
|
||||
.setOnDismissListener {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
|
@ -440,7 +447,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
|
||||
// Create a download task
|
||||
val downloadTask = MangaDownloaderService.DownloadTask(
|
||||
title = media.nameMAL ?: media.nameRomaji,
|
||||
title = media.mainName(),
|
||||
chapter = chapter.title!!,
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
|
@ -481,10 +488,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
|
||||
fun onMangaChapterRemoveDownloadClick(i: String) {
|
||||
downloadManager.removeDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
Download.Type.MANGA
|
||||
DownloadedType.Type.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.deleteDownload(i)
|
||||
|
@ -499,10 +506,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
|
||||
// Remove the download from the manager and update the UI
|
||||
downloadManager.removeDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
Download.Type.MANGA
|
||||
DownloadedType.Type.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.purgeDownload(i)
|
||||
|
|
|
@ -89,7 +89,8 @@ abstract class BaseImageAdapter(
|
|||
}
|
||||
} else {
|
||||
val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
|
||||
override fun onSingleClick(event: MotionEvent) = activity.handleController()
|
||||
override fun onSingleClick(event: MotionEvent) =
|
||||
activity.handleController(event = event)
|
||||
})
|
||||
view.findViewById<View>(R.id.imgProgCover).apply {
|
||||
setOnTouchListener { _, event ->
|
||||
|
@ -112,6 +113,9 @@ abstract class BaseImageAdapter(
|
|||
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
|
||||
}
|
||||
|
||||
abstract fun isZoomed(): Boolean
|
||||
abstract fun setZoom(zoom: Float)
|
||||
|
||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -50,7 +50,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
|||
if (model.loadMangaChapterImages(
|
||||
chp,
|
||||
m.selected!!,
|
||||
m.nameMAL ?: m.nameRomaji
|
||||
m.mainName()
|
||||
)
|
||||
) {
|
||||
val activity = currActivity()
|
||||
|
|
|
@ -91,4 +91,16 @@ open class ImageAdapter(
|
|||
}
|
||||
|
||||
override fun getItemCount(): Int = images.size
|
||||
|
||||
override fun isZoomed(): Boolean {
|
||||
val imageView =
|
||||
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||
return imageView.scale > imageView.minScale
|
||||
}
|
||||
|
||||
override fun setZoom(zoom: Float) {
|
||||
val imageView =
|
||||
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||
imageView.setScaleAndCenter(zoom, imageView.center)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@ package ani.dantotsu.media.manga.mangareader
|
|||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -218,7 +220,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
val mangaSources = MangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow, this@MangaReaderActivity)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
} else {
|
||||
|
@ -253,7 +255,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
showProgressDialog =
|
||||
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
|
||||
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
|
||||
?: true else false
|
||||
|
||||
//Chapter Change
|
||||
fun change(index: Int) {
|
||||
|
@ -358,7 +361,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
model.loadMangaChapterImages(
|
||||
chapter,
|
||||
media.selected!!,
|
||||
media.nameMAL ?: media.nameRomaji
|
||||
media.mainName()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -701,8 +704,60 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
goneTimer.schedule(timerTask, controllerDuration)
|
||||
}
|
||||
|
||||
fun handleController(shouldShow: Boolean? = null) {
|
||||
enum class pressPos {
|
||||
LEFT, RIGHT, CENTER
|
||||
}
|
||||
|
||||
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
|
||||
var pressLocation = pressPos.CENTER
|
||||
if (!sliding) {
|
||||
if (event != null && settings.default.layout == PAGED) {
|
||||
if (event.action != MotionEvent.ACTION_UP) return
|
||||
val x = event.rawX.toInt()
|
||||
val y = event.rawY.toInt()
|
||||
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
||||
if (screenWidth / 5 in (x + 1)..<y) {
|
||||
pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) {
|
||||
pressPos.RIGHT
|
||||
} else {
|
||||
pressPos.LEFT
|
||||
}
|
||||
}
|
||||
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
|
||||
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
|
||||
pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) {
|
||||
pressPos.LEFT
|
||||
} else {
|
||||
pressPos.RIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if pressLocation is left or right go to previous or next page (paged mode only)
|
||||
if (pressLocation == pressPos.LEFT) {
|
||||
|
||||
if (binding.mangaReaderPager.currentItem > 0) {
|
||||
//if the current images zoomed in, go back to normal before going to previous page
|
||||
if (imageAdapter?.isZoomed() == true) {
|
||||
imageAdapter?.setZoom(1f)
|
||||
}
|
||||
binding.mangaReaderPager.currentItem -= 1
|
||||
return
|
||||
}
|
||||
|
||||
} else if (pressLocation == pressPos.RIGHT) {
|
||||
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
|
||||
//if the current images zoomed in, go back to normal before going to next page
|
||||
if (imageAdapter?.isZoomed() == true) {
|
||||
imageAdapter?.setZoom(1f)
|
||||
}
|
||||
//if right to left, go to previous page
|
||||
binding.mangaReaderPager.currentItem += 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings.showSystemBars) {
|
||||
hideBars()
|
||||
checkNotch()
|
||||
|
@ -786,7 +841,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
model.loadMangaChapterImages(
|
||||
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
||||
media.selected!!,
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
media.mainName(),
|
||||
false
|
||||
)
|
||||
loading = false
|
||||
|
@ -795,17 +850,28 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
|
||||
private fun progress(runnable: Runnable) {
|
||||
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
|
||||
showProgressDialog =
|
||||
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
|
||||
?: true else false
|
||||
if (showProgressDialog) {
|
||||
|
||||
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
|
||||
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
|
||||
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("${media.id}_progressDialog", isChecked)
|
||||
saveData("${media.id}_progressDialog", !isChecked)
|
||||
showProgressDialog = !isChecked
|
||||
}
|
||||
|
||||
val incognito =
|
||||
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("incognito", false) ?: false
|
||||
AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle(getString(R.string.title_update_progress))
|
||||
.apply {
|
||||
if (incognito) {
|
||||
setMessage(getString(R.string.incognito_will_not_update))
|
||||
}
|
||||
}
|
||||
.setView(dialogView)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||
|
|
|
@ -22,7 +22,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.novel.NovelDownloaderService
|
||||
import ani.dantotsu.download.novel.NovelServiceDataSingleton
|
||||
|
@ -67,7 +67,7 @@ class NovelReadFragment : Fragment(),
|
|||
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
|
||||
Log.e("downloadTrigger", novelDownloadPackage.link)
|
||||
val downloadTask = NovelDownloaderService.DownloadTask(
|
||||
title = media.nameMAL ?: media.nameRomaji,
|
||||
title = media.mainName(),
|
||||
chapter = novelDownloadPackage.novelName,
|
||||
downloadLink = novelDownloadPackage.link,
|
||||
originalLink = novelDownloadPackage.originalLink,
|
||||
|
@ -92,16 +92,16 @@ class NovelReadFragment : Fragment(),
|
|||
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
if (downloadsManager.queryDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
) {
|
||||
val file = File(
|
||||
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.novelLocation}/${media.nameMAL ?: media.nameRomaji}/${novel.name}/0.epub"
|
||||
"${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
|
||||
)
|
||||
if (!file.exists()) return false
|
||||
val fileUri = FileProvider.getUriForFile(
|
||||
|
@ -124,10 +124,10 @@ class NovelReadFragment : Fragment(),
|
|||
override fun downloadedCheck(novel: ShowResponse): Boolean {
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
return downloadsManager.queryDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -135,10 +135,10 @@ class NovelReadFragment : Fragment(),
|
|||
override fun deleteDownload(novel: ShowResponse) {
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.removeDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -247,8 +247,7 @@ class NovelReadFragment : Fragment(),
|
|||
headerAdapter.progress?.visibility = View.VISIBLE
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (auto || query == "") model.autoSearchNovels(media)
|
||||
//else model.searchNovels(query, source)
|
||||
else model.autoSearchNovels(media) //testing
|
||||
else model.searchNovels(query, source)
|
||||
}
|
||||
searching = true
|
||||
if (save) {
|
||||
|
|
|
@ -42,7 +42,11 @@ class NovelResponseAdapter(
|
|||
.into(binding.itemEpisodeImage)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true)
|
||||
fragment.requireContext().theme?.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
val color = typedValue.data
|
||||
|
||||
binding.itemEpisodeTitle.text = novel.name
|
||||
|
@ -98,14 +102,19 @@ class NovelResponseAdapter(
|
|||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme)
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(
|
||||
fragment.requireContext(),
|
||||
R.style.DialogTheme
|
||||
)
|
||||
builder.setTitle("Delete ${novel.name}?")
|
||||
builder.setMessage("Are you sure you want to delete ${novel.name}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadedCheckCallback.deleteDownload(novel)
|
||||
deleteDownload(novel.link)
|
||||
snackString("Deleted ${novel.name}")
|
||||
if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) {
|
||||
if (binding.itemEpisodeFiller.text.toString()
|
||||
.contains("Download", ignoreCase = true)
|
||||
) {
|
||||
binding.itemEpisodeFiller.text = ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.webkit.WebView
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
|
@ -36,7 +37,7 @@ import ani.dantotsu.saveData
|
|||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.CurrentNovelReaderSettings
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import ani.dantotsu.settings.NovelReaderSettings
|
||||
import ani.dantotsu.settings.ReaderSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
@ -62,7 +63,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
private lateinit var binding: ActivityNovelReaderBinding
|
||||
private val scope = lifecycleScope
|
||||
|
||||
lateinit var settings: NovelReaderSettings
|
||||
lateinit var settings: ReaderSettings
|
||||
private lateinit var uiSettings: UserInterfaceSettings
|
||||
|
||||
private var notchHeight: Int? = null
|
||||
|
@ -139,16 +140,31 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
}
|
||||
|
||||
|
||||
@SuppressLint("WebViewApiAvailability")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//check for supported webview
|
||||
val webViewVersion = WebViewCompat.getCurrentWebViewPackage(this)?.versionName
|
||||
val webViewVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
WebView.getCurrentWebViewPackage()?.versionName
|
||||
} else {
|
||||
WebViewCompat.getCurrentWebViewPackage(this)?.versionName
|
||||
}
|
||||
val firstVersion = webViewVersion?.split(".")?.firstOrNull()?.toIntOrNull()
|
||||
if (webViewVersion == null || firstVersion == null || firstVersion < 87) {
|
||||
Toast.makeText(this, "Please update WebView from PlayStore", Toast.LENGTH_LONG).show()
|
||||
val text = if (webViewVersion == null) {
|
||||
"Could not find webView installed"
|
||||
} else if (firstVersion == null) {
|
||||
"Could not find WebView Version Number: $webViewVersion"
|
||||
} else if (firstVersion < 87) { //false positive?
|
||||
"Webview Versiom: $firstVersion. PLease update"
|
||||
} else {
|
||||
"Please update WebView from PlayStore"
|
||||
}
|
||||
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
|
||||
//open playstore
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
|
||||
intent.data =
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
|
||||
startActivity(intent)
|
||||
//stop reader
|
||||
finish()
|
||||
|
@ -159,9 +175,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
settings = loadData("novel_reader_settings", this)
|
||||
?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) }
|
||||
settings = loadData("reader_settings", this)
|
||||
?: ReaderSettings().apply { saveData("reader_settings", this) }
|
||||
uiSettings = loadData("ui_settings", this)
|
||||
?: UserInterfaceSettings().also { saveData("ui_settings", it) }
|
||||
|
||||
|
@ -271,7 +286,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
binding.bookReader.getAppearance {
|
||||
currentTheme = it
|
||||
themes.add(0, it)
|
||||
settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default
|
||||
settings.defaultLN =
|
||||
loadData("${sanitizedBookId}_current_settings") ?: settings.defaultLN
|
||||
applySettings()
|
||||
}
|
||||
|
||||
|
@ -323,7 +339,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP)
|
||||
if (!settings.default.volumeButtons)
|
||||
if (!settings.defaultLN.volumeButtons)
|
||||
return false
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
onVolumeUp?.invoke()
|
||||
|
@ -333,7 +349,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
|
||||
if (!settings.default.volumeButtons)
|
||||
if (!settings.defaultLN.volumeButtons)
|
||||
return false
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
onVolumeDown?.invoke()
|
||||
|
@ -349,13 +365,18 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
|
||||
|
||||
fun applySettings() {
|
||||
saveData("${sanitizedBookId}_current_settings", settings.default)
|
||||
saveData("${sanitizedBookId}_current_settings", settings.defaultLN)
|
||||
hideBars()
|
||||
|
||||
if (settings.defaultLN.useOledTheme) {
|
||||
themes.forEach { theme ->
|
||||
theme.darkBg = Color.parseColor("#000000")
|
||||
}
|
||||
}
|
||||
currentTheme =
|
||||
themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) }
|
||||
themes.first { it.name.equals(settings.defaultLN.currentThemeName, ignoreCase = true) }
|
||||
|
||||
when (settings.default.layout) {
|
||||
when (settings.defaultLN.layout) {
|
||||
CurrentNovelReaderSettings.Layouts.PAGED -> {
|
||||
currentTheme?.flow = ReaderFlow.PAGINATED
|
||||
}
|
||||
|
@ -366,22 +387,22 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
}
|
||||
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
when (settings.default.dualPageMode) {
|
||||
when (settings.defaultLN.dualPageMode) {
|
||||
CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1
|
||||
CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2
|
||||
CurrentReaderSettings.DualPageModes.Force -> requestedOrientation =
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
currentTheme?.lineHeight = settings.default.lineHeight
|
||||
currentTheme?.gap = settings.default.margin
|
||||
currentTheme?.maxInlineSize = settings.default.maxInlineSize
|
||||
currentTheme?.maxBlockSize = settings.default.maxBlockSize
|
||||
currentTheme?.useDark = settings.default.useDarkTheme
|
||||
currentTheme?.lineHeight = settings.defaultLN.lineHeight
|
||||
currentTheme?.gap = settings.defaultLN.margin
|
||||
currentTheme?.maxInlineSize = settings.defaultLN.maxInlineSize
|
||||
currentTheme?.maxBlockSize = settings.defaultLN.maxBlockSize
|
||||
currentTheme?.useDark = settings.defaultLN.useDarkTheme
|
||||
|
||||
currentTheme?.let { binding.bookReader.setAppearance(it) }
|
||||
|
||||
if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
if (settings.defaultLN.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
|
|
|
@ -30,8 +30,7 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = requireActivity() as NovelReaderActivity
|
||||
val settings = activity.settings.default
|
||||
|
||||
val settings = activity.settings.defaultLN
|
||||
val themeLabels = activity.themes.map { it.name }
|
||||
binding.themeSelect.adapter =
|
||||
NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels)
|
||||
|
@ -49,7 +48,11 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
binding.useOledTheme.isChecked = settings.useOledTheme
|
||||
binding.useOledTheme.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.useOledTheme = isChecked
|
||||
activity.applySettings()
|
||||
}
|
||||
val layoutList = listOf(
|
||||
binding.paged,
|
||||
binding.continuous
|
||||
|
@ -173,6 +176,20 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||
binding.maxBlockSize.setText(value.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
}
|
||||
binding.incrementMaxBlockSize.setOnClickListener {
|
||||
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxBlockSize = value + 10
|
||||
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.decrementMaxBlockSize.setOnClickListener {
|
||||
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxBlockSize = value - 10
|
||||
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.useDarkTheme.isChecked = settings.useDarkTheme
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
package ani.dantotsu.media.user
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
@ -74,13 +80,16 @@ class ListActivity : AppCompatActivity() {
|
|||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
||||
val anime = intent.getBooleanExtra("anime", true)
|
||||
binding.listTitle.text =
|
||||
intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
|
||||
|
||||
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
this@ListActivity.selectedTabIdx = tab?.position ?: 0
|
||||
|
@ -145,7 +154,8 @@ class ListActivity : AppCompatActivity() {
|
|||
R.id.release -> "release"
|
||||
else -> null
|
||||
}
|
||||
|
||||
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putString("sort_order", sort)?.apply()
|
||||
binding.listProgressBar.visibility = View.VISIBLE
|
||||
binding.listViewPager.adapter = null
|
||||
scope.launch {
|
||||
|
|
|
@ -46,7 +46,7 @@ class ListFragment : Fragment() {
|
|||
binding.listRecyclerView.layoutManager =
|
||||
GridLayoutManager(
|
||||
requireContext(),
|
||||
if (grid!!) (screenWidth / 124f).toInt() else 1
|
||||
if (grid!!) (screenWidth / 120f).toInt() else 1
|
||||
)
|
||||
binding.listRecyclerView.adapter = adapter
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package ani.dantotsu.offline
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentOfflineBinding
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.navBarHeight
|
||||
|
@ -13,6 +15,7 @@ import ani.dantotsu.startMainActivity
|
|||
import ani.dantotsu.statusBarHeight
|
||||
|
||||
class OfflineFragment : Fragment() {
|
||||
private var offline = false
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -23,6 +26,11 @@ class OfflineFragment : Fragment() {
|
|||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("offlineMode", false) ?: false
|
||||
binding.noInternet.text =
|
||||
if (offline) "Offline Mode" else getString(R.string.no_internet)
|
||||
binding.refreshButton.visibility = if (offline) View.GONE else View.VISIBLE
|
||||
binding.refreshButton.setOnClickListener {
|
||||
if (isOnline(requireContext())) {
|
||||
startMainActivity(requireActivity())
|
||||
|
@ -30,4 +38,10 @@ class OfflineFragment : Fragment() {
|
|||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("offlineMode", false) ?: false
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
const val DisabledReports = false
|
||||
//Setting this to false, will allow sending crash reports to Dantotsu's Firebase Crashlytics
|
||||
//Setting this to false, will allow sending crash reports to Dantotsu's Firebase Crashlytics
|
||||
//if you want a custom build without crash reporting, set this to true
|
|
@ -6,26 +6,38 @@ import ani.dantotsu.logger
|
|||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import com.google.gson.Gson
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
object Kitsu {
|
||||
private suspend fun getKitsuData(query: String): KitsuResponse? {
|
||||
val headers = mapOf(
|
||||
"Content-Type" to "application/json",
|
||||
"Accept" to "application/json",
|
||||
"Accept-Encoding" to "gzip, deflate",
|
||||
"Accept-Language" to "en-US,en;q=0.5",
|
||||
"Host" to "kitsu.io",
|
||||
"Connection" to "keep-alive",
|
||||
"DNT" to "1",
|
||||
"Origin" to "https://kitsu.io"
|
||||
"Origin" to "https://kitsu.io",
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"Sec-Fetch-Mode" to "cors",
|
||||
"Sec-Fetch-Site" to "cross-site",
|
||||
)
|
||||
val json = tryWithSuspend {
|
||||
client.post(
|
||||
val response = tryWithSuspend {
|
||||
val res = client.post(
|
||||
"https://kitsu.io/api/graphql",
|
||||
headers,
|
||||
data = mapOf("query" to query)
|
||||
)
|
||||
res
|
||||
}
|
||||
return json?.parsed()
|
||||
val json = decodeToString(response)
|
||||
val gson = Gson()
|
||||
return gson.fromJson(json, KitsuResponse::class.java)
|
||||
}
|
||||
|
||||
suspend fun getKitsuEpisodesDetails(media: Media): Map<String, Episode>? {
|
||||
|
@ -54,14 +66,14 @@ query {
|
|||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
}""".trimIndent()
|
||||
|
||||
|
||||
val result = getKitsuData(query) ?: return null
|
||||
logger("Kitsu : result=$result", print)
|
||||
media.idKitsu = result.data?.lookupMapping?.id
|
||||
return (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
|
||||
val num = ep?.num?.toString() ?: return@mapNotNull null
|
||||
val a = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
|
||||
val num = ep?.number?.toString() ?: return@mapNotNull null
|
||||
num to Episode(
|
||||
number = num,
|
||||
title = ep.titles?.canonical,
|
||||
|
@ -69,6 +81,25 @@ query {
|
|||
thumb = FileUrl[ep.thumbnail?.original?.url],
|
||||
)
|
||||
}.toMap()
|
||||
logger("Kitsu : a=$a", print)
|
||||
return a
|
||||
}
|
||||
|
||||
private fun decodeToString(res: NiceResponse?): String? {
|
||||
return when (res?.headers?.get("Content-Encoding")) {
|
||||
"gzip" -> {
|
||||
res.body.byteStream().use { inputStream ->
|
||||
GZIPInputStream(inputStream).use { gzipInputStream ->
|
||||
InputStreamReader(gzipInputStream).use { reader ->
|
||||
reader.readText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
res?.body?.string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
@ -93,7 +124,7 @@ query {
|
|||
|
||||
@Serializable
|
||||
data class Node(
|
||||
@SerialName("number") val num: Long? = null,
|
||||
@SerialName("number") val number: Int? = null,
|
||||
@SerialName("titles") val titles: Titles? = null,
|
||||
@SerialName("description") val description: Description? = null,
|
||||
@SerialName("thumbnail") val thumbnail: Thumbnail? = null
|
||||
|
|
|
@ -6,25 +6,116 @@ class LanguageMapper {
|
|||
fun mapLanguageCodeToName(code: String): String {
|
||||
return when (code) {
|
||||
"all" -> "Multi"
|
||||
"af" -> "Afrikaans"
|
||||
"am" -> "Amharic"
|
||||
"ar" -> "Arabic"
|
||||
"as" -> "Assamese"
|
||||
"az" -> "Azerbaijani"
|
||||
"be" -> "Belarusian"
|
||||
"bg" -> "Bulgarian"
|
||||
"bn" -> "Bengali"
|
||||
"bs" -> "Bosnian"
|
||||
"ca" -> "Catalan"
|
||||
"ceb" -> "Cebuano"
|
||||
"cs" -> "Czech"
|
||||
"da" -> "Danish"
|
||||
"de" -> "German"
|
||||
"el" -> "Greek"
|
||||
"en" -> "English"
|
||||
"en-Us" -> "English (United States)"
|
||||
"eo" -> "Esperanto"
|
||||
"es" -> "Spanish"
|
||||
"es-419" -> "Spanish (Latin America)"
|
||||
"et" -> "Estonian"
|
||||
"eu" -> "Basque"
|
||||
"fa" -> "Persian"
|
||||
"fi" -> "Finnish"
|
||||
"fil" -> "Filipino"
|
||||
"fo" -> "Faroese"
|
||||
"fr" -> "French"
|
||||
"ga" -> "Irish"
|
||||
"gn" -> "Guarani"
|
||||
"gu" -> "Gujarati"
|
||||
"ha" -> "Hausa"
|
||||
"he" -> "Hebrew"
|
||||
"hi" -> "Hindi"
|
||||
"hr" -> "Croatian"
|
||||
"ht" -> "Haitian Creole"
|
||||
"hu" -> "Hungarian"
|
||||
"hy" -> "Armenian"
|
||||
"id" -> "Indonesian"
|
||||
"ig" -> "Igbo"
|
||||
"is" -> "Icelandic"
|
||||
"it" -> "Italian"
|
||||
"ja" -> "Japanese"
|
||||
"jv" -> "Javanese"
|
||||
"ka" -> "Georgian"
|
||||
"kk" -> "Kazakh"
|
||||
"km" -> "Khmer"
|
||||
"kn" -> "Kannada"
|
||||
"ko" -> "Korean"
|
||||
"ku" -> "Kurdish"
|
||||
"ky" -> "Kyrgyz"
|
||||
"la" -> "Latin"
|
||||
"lb" -> "Luxembourgish"
|
||||
"lo" -> "Lao"
|
||||
"lt" -> "Lithuanian"
|
||||
"lv" -> "Latvian"
|
||||
"mg" -> "Malagasy"
|
||||
"mi" -> "Maori"
|
||||
"mk" -> "Macedonian"
|
||||
"ml" -> "Malayalam"
|
||||
"mn" -> "Mongolian"
|
||||
"mo" -> "Moldovan"
|
||||
"mr" -> "Marathi"
|
||||
"ms" -> "Malay"
|
||||
"mt" -> "Maltese"
|
||||
"my" -> "Burmese"
|
||||
"ne" -> "Nepali"
|
||||
"nl" -> "Dutch"
|
||||
"no" -> "Norwegian"
|
||||
"ny" -> "Chichewa"
|
||||
"pl" -> "Polish"
|
||||
"pt" -> "Portuguese"
|
||||
"pt-BR" -> "Portuguese (Brazil)"
|
||||
"pt-PT" -> "Portuguese (Portugal)"
|
||||
"ps" -> "Pashto"
|
||||
"ro" -> "Romanian"
|
||||
"rm" -> "Romansh"
|
||||
"ru" -> "Russian"
|
||||
"sd" -> "Sindhi"
|
||||
"sh" -> "Serbo-Croatian"
|
||||
"si" -> "Sinhala"
|
||||
"sk" -> "Slovak"
|
||||
"sl" -> "Slovenian"
|
||||
"sm" -> "Samoan"
|
||||
"sn" -> "Shona"
|
||||
"so" -> "Somali"
|
||||
"sq" -> "Albanian"
|
||||
"sr" -> "Serbian"
|
||||
"st" -> "Southern Sotho"
|
||||
"sv" -> "Swedish"
|
||||
"sw" -> "Swahili"
|
||||
"ta" -> "Tamil"
|
||||
"te" -> "Telugu"
|
||||
"tg" -> "Tajik"
|
||||
"th" -> "Thai"
|
||||
"ti" -> "Tigrinya"
|
||||
"tk" -> "Turkmen"
|
||||
"tl" -> "Tagalog"
|
||||
"to" -> "Tongan"
|
||||
"tr" -> "Turkish"
|
||||
"uk" -> "Ukrainian"
|
||||
"ur" -> "Urdu"
|
||||
"uz" -> "Uzbek"
|
||||
"vi" -> "Vietnamese"
|
||||
"yo" -> "Yoruba"
|
||||
"zh" -> "Chinese"
|
||||
"zh-Hans" -> "Chinese (Simplified)"
|
||||
else -> ""
|
||||
"zh-Hant" -> "Chinese (Traditional)"
|
||||
"zh-Habt" -> "Chinese (Hakka)"
|
||||
"zu" -> "Zulu"
|
||||
else -> code
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ object MalScraper {
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is TimeoutCancellationException) snackString(currContext()?.getString(R.string.error_loading_mal_data))
|
||||
// if (e is TimeoutCancellationException) snackString(currContext()?.getString(R.string.error_loading_mal_data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
abstract class SharedPreferenceLiveData<T>(
|
||||
val sharedPrefs: SharedPreferences,
|
||||
val key: String,
|
||||
val defValue: T
|
||||
) : LiveData<T>() {
|
||||
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
if (key == this.key) {
|
||||
value = getValueFromPreferences(key, defValue)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getValueFromPreferences(key: String, defValue: T): T
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
value = getValueFromPreferences(key, defValue)
|
||||
sharedPrefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
sharedPrefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
super.onInactive()
|
||||
}
|
||||
}
|
||||
|
||||
class SharedPreferenceIntLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Int) :
|
||||
SharedPreferenceLiveData<Int>(sharedPrefs, key, defValue) {
|
||||
override fun getValueFromPreferences(key: String, defValue: Int): Int =
|
||||
sharedPrefs.getInt(key, defValue)
|
||||
}
|
||||
|
||||
class SharedPreferenceStringLiveData(
|
||||
sharedPrefs: SharedPreferences,
|
||||
key: String,
|
||||
defValue: String
|
||||
) :
|
||||
SharedPreferenceLiveData<String>(sharedPrefs, key, defValue) {
|
||||
override fun getValueFromPreferences(key: String, defValue: String): String =
|
||||
sharedPrefs.getString(key, defValue).toString()
|
||||
}
|
||||
|
||||
class SharedPreferenceBooleanLiveData(
|
||||
sharedPrefs: SharedPreferences,
|
||||
key: String,
|
||||
defValue: Boolean
|
||||
) :
|
||||
SharedPreferenceLiveData<Boolean>(sharedPrefs, key, defValue) {
|
||||
override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean =
|
||||
sharedPrefs.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
class SharedPreferenceFloatLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Float) :
|
||||
SharedPreferenceLiveData<Float>(sharedPrefs, key, defValue) {
|
||||
override fun getValueFromPreferences(key: String, defValue: Float): Float =
|
||||
sharedPrefs.getFloat(key, defValue)
|
||||
}
|
||||
|
||||
class SharedPreferenceLongLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Long) :
|
||||
SharedPreferenceLiveData<Long>(sharedPrefs, key, defValue) {
|
||||
override fun getValueFromPreferences(key: String, defValue: Long): Long =
|
||||
sharedPrefs.getLong(key, defValue)
|
||||
}
|
||||
|
||||
class SharedPreferenceStringSetLiveData(
|
||||
sharedPrefs: SharedPreferences,
|
||||
key: String,
|
||||
defValue: Set<String>
|
||||
) :
|
||||
SharedPreferenceLiveData<Set<String>>(sharedPrefs, key, defValue) {
|
||||
override fun getValueFromPreferences(key: String, defValue: Set<String>): Set<String> =
|
||||
sharedPrefs.getStringSet(key, defValue)?.toSet() ?: defValue
|
||||
}
|
||||
|
||||
fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData<Int> {
|
||||
return SharedPreferenceIntLiveData(this, key, defValue)
|
||||
}
|
||||
|
||||
fun SharedPreferences.stringLiveData(
|
||||
key: String,
|
||||
defValue: String
|
||||
): SharedPreferenceLiveData<String> {
|
||||
return SharedPreferenceStringLiveData(this, key, defValue)
|
||||
}
|
||||
|
||||
fun SharedPreferences.booleanLiveData(
|
||||
key: String,
|
||||
defValue: Boolean
|
||||
): SharedPreferenceLiveData<Boolean> {
|
||||
return SharedPreferenceBooleanLiveData(this, key, defValue)
|
||||
}
|
||||
|
||||
fun SharedPreferences.floatLiveData(key: String, defValue: Float): SharedPreferenceLiveData<Float> {
|
||||
return SharedPreferenceFloatLiveData(this, key, defValue)
|
||||
}
|
||||
|
||||
fun SharedPreferences.longLiveData(key: String, defValue: Long): SharedPreferenceLiveData<Long> {
|
||||
return SharedPreferenceLongLiveData(this, key, defValue)
|
||||
}
|
||||
|
||||
fun SharedPreferences.stringSetLiveData(
|
||||
key: String,
|
||||
defValue: Set<String>
|
||||
): SharedPreferenceLiveData<Set<String>> {
|
||||
return SharedPreferenceStringSetLiveData(this, key, defValue)
|
||||
}
|
|
@ -4,9 +4,11 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -14,7 +16,9 @@ import ani.dantotsu.App.Companion.context
|
|||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.ActivityImageSearchBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toast
|
||||
|
@ -49,10 +53,13 @@ class ImageSearchActivity : AppCompatActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
initActivity(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityImageSearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.uploadImage.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
binding.uploadImage.setOnClickListener {
|
||||
viewModel.clearResults()
|
||||
imageSelectionLauncher.launch("image/*")
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package ani.dantotsu.others.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class CookieCatcher : AppCompatActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
//get url from intent
|
||||
val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = Application.getProcessName()
|
||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||
}
|
||||
setContentView(R.layout.activity_discord)
|
||||
|
||||
val webView = findViewById<WebView>(R.id.discordWebview)
|
||||
|
||||
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
|
||||
cookies.setAcceptThirdPartyCookies(webView, true)
|
||||
|
||||
webView.apply {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.databaseEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
}
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
}
|
||||
|
||||
webView.loadUrl(url)
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,9 @@ import ani.dantotsu.BottomSheetDialogFragment
|
|||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.databinding.BottomSheetWebviewBinding
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
|
||||
|
||||
|
@ -30,7 +33,8 @@ abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
|
|||
dismiss()
|
||||
}
|
||||
|
||||
val cookies: CookieManager = CookieManager.getInstance()
|
||||
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
|
||||
//CookieManager.getInstance()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.Lazier
|
||||
import ani.dantotsu.lazyList
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
|
@ -8,24 +9,52 @@ import kotlinx.coroutines.flow.first
|
|||
|
||||
object AnimeSources : WatchSources() {
|
||||
override var list: List<Lazier<BaseParser>> = emptyList()
|
||||
var pinnedAnimeSources: Set<String> = emptySet()
|
||||
|
||||
suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.Installed>>, context: Context) {
|
||||
val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
pinnedAnimeSources = sharedPrefs.getStringSet("pinned_anime_sources", emptySet()) ?: emptySet()
|
||||
|
||||
suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.Installed>>) {
|
||||
// Initialize with the first value from StateFlow
|
||||
val initialExtensions = fromExtensions.first()
|
||||
list = createParsersFromExtensions(initialExtensions)
|
||||
list = createParsersFromExtensions(initialExtensions) + Lazier(
|
||||
{ OfflineAnimeParser() },
|
||||
"Downloaded"
|
||||
)
|
||||
|
||||
// Update as StateFlow emits new values
|
||||
fromExtensions.collect { extensions ->
|
||||
list = createParsersFromExtensions(extensions)
|
||||
list = sortPinnedAnimeSources(createParsersFromExtensions(extensions), pinnedAnimeSources) + Lazier(
|
||||
{ OfflineAnimeParser() },
|
||||
"Downloaded"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun performReorderAnimeSources() {
|
||||
//remove the downloaded source from the list to avoid duplicates
|
||||
list = list.filter { it.name != "Downloaded" }
|
||||
list = sortPinnedAnimeSources(list, pinnedAnimeSources) + Lazier(
|
||||
{ OfflineAnimeParser() },
|
||||
"Downloaded"
|
||||
)
|
||||
}
|
||||
|
||||
private fun createParsersFromExtensions(extensions: List<AnimeExtension.Installed>): List<Lazier<BaseParser>> {
|
||||
return extensions.map { extension ->
|
||||
val name = extension.name
|
||||
Lazier({ DynamicAnimeParser(extension) }, name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortPinnedAnimeSources(Sources: List<Lazier<BaseParser>>, pinnedAnimeSources: Set<String>): List<Lazier<BaseParser>> {
|
||||
//find the pinned sources
|
||||
val pinnedSources = Sources.filter { pinnedAnimeSources.contains(it.name) }
|
||||
//find the unpinned sources
|
||||
val unpinnedSources = Sources.filter { !pinnedAnimeSources.contains(it.name) }
|
||||
//put the pinned sources at the top of the list
|
||||
return pinnedSources + unpinnedSources
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,23 +9,22 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -40,7 +39,10 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
@ -81,50 +83,56 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
}
|
||||
if (source is AnimeCatalogueSource) {
|
||||
try {
|
||||
val res = source.getEpisodeList(sAnime)
|
||||
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
|
||||
?: return emptyList())
|
||||
try {
|
||||
val res = source.getEpisodeList(sAnime)
|
||||
|
||||
val sortedEpisodes = if (res[0].episode_number == -1f) {
|
||||
// Find the number in the string and sort by that number
|
||||
val sortedByStringNumber = res.sortedBy {
|
||||
val matchResult = "\\d+".toRegex().find(it.name)
|
||||
val number = matchResult?.value?.toFloat() ?: Float.MAX_VALUE
|
||||
it.episode_number = number // Store the found number in episode_number
|
||||
number
|
||||
}
|
||||
|
||||
// If there is no number, reverse the order and give them an incrementing number
|
||||
var incrementingNumber = 1f
|
||||
sortedByStringNumber.map {
|
||||
if (it.episode_number == Float.MAX_VALUE) {
|
||||
it.episode_number =
|
||||
incrementingNumber++ // Update episode_number with the incrementing number
|
||||
}
|
||||
it
|
||||
}
|
||||
} else {
|
||||
var episodeCounter = 1f
|
||||
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
|
||||
val seasonGroups =
|
||||
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
||||
seasonGroups.keys.sorted().flatMap { season ->
|
||||
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
||||
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
||||
episode.episode_number = episodeCounter++
|
||||
}
|
||||
episode
|
||||
} ?: emptyList()
|
||||
}
|
||||
val sortedEpisodes = if (res[0].episode_number == -1f) {
|
||||
// Find the number in the string and sort by that number
|
||||
val sortedByStringNumber = res.sortedBy {
|
||||
val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
|
||||
val number = matchResult ?: Float.MAX_VALUE
|
||||
it.episode_number = number // Store the found number in episode_number
|
||||
number
|
||||
}
|
||||
|
||||
// If there is no number, reverse the order and give them an incrementing number
|
||||
var incrementingNumber = 1f
|
||||
sortedByStringNumber.map {
|
||||
if (it.episode_number == Float.MAX_VALUE) {
|
||||
it.episode_number =
|
||||
incrementingNumber++ // Update episode_number with the incrementing number
|
||||
}
|
||||
it
|
||||
}
|
||||
} else {
|
||||
var episodeCounter = 1f
|
||||
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
|
||||
val seasonGroups =
|
||||
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
||||
seasonGroups.keys.sortedBy { it.toInt() }
|
||||
.flatMap { season ->
|
||||
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
||||
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
||||
val potentialNumber =
|
||||
AnimeNameAdapter.findEpisodeNumber(episode.name)
|
||||
if (potentialNumber != null) {
|
||||
episode.episode_number = potentialNumber
|
||||
} else {
|
||||
episode.episode_number = episodeCounter
|
||||
}
|
||||
episodeCounter++
|
||||
}
|
||||
episode
|
||||
} ?: emptyList()
|
||||
}
|
||||
return sortedEpisodes.map { SEpisodeToEpisode(it) }
|
||||
} catch (e: Exception) {
|
||||
println("Exception: $e")
|
||||
}
|
||||
return emptyList()
|
||||
return sortedEpisodes.map { SEpisodeToEpisode(it) }
|
||||
} catch (e: Exception) {
|
||||
logger("Exception: $e")
|
||||
}
|
||||
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun loadVideoServers(
|
||||
|
@ -137,7 +145,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? AnimeCatalogueSource ?: return emptyList()
|
||||
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
|
||||
?: return emptyList())
|
||||
|
||||
return try {
|
||||
val videos = source.getVideoList(sEpisode)
|
||||
|
@ -159,15 +168,16 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? AnimeCatalogueSource ?: return emptyList()
|
||||
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
|
||||
?: return emptyList())
|
||||
return try {
|
||||
val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle()
|
||||
logger("query: $query")
|
||||
convertAnimesPageToShowResponse(res)
|
||||
} catch (e: CloudflareBypassException) {
|
||||
logger("Exception in search: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
snackString( "Failed to bypass Cloudflare")
|
||||
}
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
|
@ -202,7 +212,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
}
|
||||
return Episode(
|
||||
if (episodeNumberInt.toInt() != -1) {
|
||||
episodeNumberInt.toString()
|
||||
if (sEpisode.episode_number % 1 == 0f) {
|
||||
episodeNumberInt.toInt().toString()
|
||||
} else {
|
||||
sEpisode.episode_number.toString()
|
||||
}
|
||||
} else {
|
||||
sEpisode.name
|
||||
},
|
||||
|
@ -277,7 +291,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
var imageDataList: List<ImageData> = listOf()
|
||||
val ret = coroutineScope {
|
||||
try {
|
||||
println("source.name " + source.name)
|
||||
logger("source.name " + source.name)
|
||||
val res = source.getPageList(sChapter)
|
||||
val reIndexedPages =
|
||||
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
|
@ -295,8 +309,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
|
||||
} catch (e: Exception) {
|
||||
logger("loadImages Exception: $e")
|
||||
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
snackString("Failed to load images: $e")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
@ -310,29 +323,30 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
var imageDataList: List<ImageData> = listOf()
|
||||
coroutineScope {
|
||||
|
||||
return coroutineScope {
|
||||
try {
|
||||
println("source.name " + source.name)
|
||||
logger("source.name " + source.name)
|
||||
val res = source.getPageList(sChapter)
|
||||
val reIndexedPages =
|
||||
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
|
||||
val semaphore = Semaphore(5)
|
||||
val deferreds = reIndexedPages.map { page ->
|
||||
async(Dispatchers.IO) {
|
||||
imageDataList += ImageData(page, source)
|
||||
semaphore.withPermit {
|
||||
ImageData(page, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deferreds.awaitAll()
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger("loadImages Exception: $e")
|
||||
snackString("Failed to load images: $e")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
return imageDataList
|
||||
}
|
||||
|
||||
suspend fun fetchAndProcessImage(
|
||||
|
@ -344,8 +358,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
println("Response: ${response.code}")
|
||||
println("Response: ${response.message}")
|
||||
logger("Response: ${response.code}")
|
||||
logger("Response: ${response.message}")
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body.byteStream()
|
||||
|
@ -353,7 +367,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
// Convert InputStream to Bitmap
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
|
||||
inputStream?.close()
|
||||
inputStream.close()
|
||||
ani.dantotsu.media.manga.saveImage(
|
||||
bitmap,
|
||||
context.contentResolver,
|
||||
|
@ -365,7 +379,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
return@withContext bitmap
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
println("An error occurred: ${e.message}")
|
||||
logger("An error occurred: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
@ -395,10 +409,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
)
|
||||
}
|
||||
|
||||
inputStream?.close()
|
||||
inputStream.close()
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
println("An error occurred: ${e.message}")
|
||||
logger("An error occurred: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -445,7 +459,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
// Handle exception here
|
||||
println("Exception while saving image: ${e.message}")
|
||||
logger("Exception while saving image: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -465,8 +479,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
} catch (e: CloudflareBypassException) {
|
||||
logger("Exception in search: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
snackString("Failed to bypass Cloudflare")
|
||||
}
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
|
@ -603,13 +616,18 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
|
||||
|
||||
format = getVideoType(fileName)
|
||||
// this solves a problem no one has, so I'm commenting it out for now
|
||||
//if (format == null) {
|
||||
// val networkHelper = Injekt.get<NetworkHelper>()
|
||||
// format = headRequest(videoUrl, networkHelper)
|
||||
//}
|
||||
}
|
||||
|
||||
// If the format is still undetermined, log an error or handle it appropriately
|
||||
// If the format is still undetermined, log an error
|
||||
if (format == null) {
|
||||
logger("Unknown video format: $videoUrl")
|
||||
FirebaseCrashlytics.getInstance()
|
||||
.recordException(Exception("Unknown video format: $videoUrl"))
|
||||
//FirebaseCrashlytics.getInstance()
|
||||
// .recordException(Exception("Unknown video format: $videoUrl"))
|
||||
format = VideoType.CONTAINER
|
||||
}
|
||||
val headersMap: Map<String, String> =
|
||||
|
@ -620,12 +638,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
number,
|
||||
format,
|
||||
FileUrl(videoUrl, headersMap),
|
||||
aniVideo.totalContentLength.toDouble()
|
||||
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVideoType(fileName: String): VideoType? {
|
||||
return when {
|
||||
val type = when {
|
||||
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
|
||||
".mkv",
|
||||
ignoreCase = true
|
||||
|
@ -635,6 +653,47 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||
else -> null
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
private fun headRequest(fileName: String, networkHelper: NetworkHelper): VideoType? {
|
||||
return try {
|
||||
logger("attempting head request for $fileName")
|
||||
val request = Request.Builder()
|
||||
.url(fileName)
|
||||
.head()
|
||||
.build()
|
||||
|
||||
networkHelper.client.newCall(request).execute().use { response ->
|
||||
val contentType = response.header("Content-Type")
|
||||
val contentDisposition = response.header("Content-Disposition")
|
||||
|
||||
if (contentType != null) {
|
||||
when {
|
||||
contentType.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
|
||||
contentType.contains("dash", ignoreCase = true) -> VideoType.DASH
|
||||
contentType.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
|
||||
else -> null
|
||||
}
|
||||
} else if (contentDisposition != null) {
|
||||
when {
|
||||
contentDisposition.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
|
||||
contentDisposition.contains("dash", ignoreCase = true) -> VideoType.DASH
|
||||
contentDisposition.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
logger("failed head request for $fileName")
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception in headRequest: $e")
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun TrackToSubtitle(track: Track): Subtitle {
|
||||
|
@ -646,7 +705,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
|
||||
}
|
||||
|
||||
private fun findSubtitleType(url: String): SubtitleType? {
|
||||
private fun findSubtitleType(url: String): SubtitleType {
|
||||
// First, try to determine the type based on the URL file extension
|
||||
val type: SubtitleType = when {
|
||||
url.endsWith(".vtt", true) -> SubtitleType.VTT
|
||||
|
|
|
@ -56,7 +56,7 @@ abstract class BaseParser {
|
|||
* **/
|
||||
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
|
||||
var response: ShowResponse? = loadSavedShowResponse(mediaObj.id)
|
||||
if (response != null && this !is OfflineMangaParser) {
|
||||
if (response != null && this !is OfflineMangaParser && this !is OfflineAnimeParser) {
|
||||
saveShowResponse(mediaObj.id, response, true)
|
||||
} else {
|
||||
setUserText("Searching : ${mediaObj.mainName()}")
|
||||
|
@ -156,9 +156,9 @@ abstract class BaseParser {
|
|||
}
|
||||
|
||||
fun checkIfVariablesAreEmpty() {
|
||||
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `hostUrl` for the Parser")
|
||||
if (name.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `name` for the Parser")
|
||||
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `saveName` for the Parser")
|
||||
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
|
||||
if (name.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
|
||||
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
|
||||
}
|
||||
|
||||
open var showUserText = ""
|
||||
|
|
|
@ -16,6 +16,9 @@ abstract class WatchSources : BaseSources() {
|
|||
?: EmptyAnimeParser()
|
||||
}
|
||||
|
||||
fun isDownloadedSource(i: Int): Boolean {
|
||||
return get(i) is OfflineAnimeParser
|
||||
}
|
||||
|
||||
suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap<String, Episode> {
|
||||
return tryWithSuspend(true) {
|
||||
|
@ -46,6 +49,19 @@ abstract class WatchSources : BaseSources() {
|
|||
sEpisode = it.sEpisode
|
||||
)
|
||||
}
|
||||
} else if (parser is OfflineAnimeParser) {
|
||||
parser.loadEpisodes(showLink, extra, SAnime.create()).forEach {
|
||||
map[it.number] = Episode(
|
||||
it.number,
|
||||
it.link,
|
||||
it.title,
|
||||
it.description,
|
||||
it.thumbnail,
|
||||
it.isFiller,
|
||||
extra = it.extra,
|
||||
sEpisode = it.sEpisode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.Lazier
|
||||
import ani.dantotsu.lazyList
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
|
@ -7,12 +8,13 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.first
|
||||
|
||||
object MangaSources : MangaReadSources() {
|
||||
// Instantiate the static parser
|
||||
private val offlineMangaParser by lazy { OfflineMangaParser() }
|
||||
|
||||
override var list: List<Lazier<BaseParser>> = emptyList()
|
||||
var pinnedMangaSources: Set<String> = emptySet()
|
||||
|
||||
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>, context: Context) {
|
||||
val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
pinnedMangaSources = sharedPrefs.getStringSet("pinned_manga_sources", emptySet()) ?: emptySet()
|
||||
|
||||
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
|
||||
// Initialize with the first value from StateFlow
|
||||
val initialExtensions = fromExtensions.first()
|
||||
list = createParsersFromExtensions(initialExtensions) + Lazier(
|
||||
|
@ -22,19 +24,37 @@ object MangaSources : MangaReadSources() {
|
|||
|
||||
// Update as StateFlow emits new values
|
||||
fromExtensions.collect { extensions ->
|
||||
list = createParsersFromExtensions(extensions) + Lazier(
|
||||
list = sortPinnedMangaSources(createParsersFromExtensions(extensions), pinnedMangaSources) + Lazier(
|
||||
{ OfflineMangaParser() },
|
||||
"Downloaded"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun performReorderMangaSources() {
|
||||
//remove the downloaded source from the list to avoid duplicates
|
||||
list = list.filter { it.name != "Downloaded" }
|
||||
list = sortPinnedMangaSources(list, pinnedMangaSources) + Lazier(
|
||||
{ OfflineMangaParser() },
|
||||
"Downloaded"
|
||||
)
|
||||
}
|
||||
|
||||
private fun createParsersFromExtensions(extensions: List<MangaExtension.Installed>): List<Lazier<BaseParser>> {
|
||||
return extensions.map { extension ->
|
||||
val name = extension.name
|
||||
Lazier({ DynamicMangaParser(extension) }, name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortPinnedMangaSources(Sources: List<Lazier<BaseParser>>, pinnedMangaSources: Set<String>): List<Lazier<BaseParser>> {
|
||||
//find the pinned sources
|
||||
val pinnedSources = Sources.filter { pinnedMangaSources.contains(it.name) }
|
||||
//find the unpinned sources
|
||||
val unpinnedSources = Sources.filter { !pinnedMangaSources.contains(it.name) }
|
||||
//put the pinned sources at the top of the list
|
||||
return pinnedSources + unpinnedSources
|
||||
}
|
||||
}
|
||||
|
||||
object HMangaSources : MangaReadSources() {
|
||||
|
|
|
@ -34,7 +34,7 @@ abstract class NovelParser : BaseParser() {
|
|||
//val query = mediaObj.name ?: mediaObj.nameRomaji
|
||||
//return search(query).sortByVolume(query)
|
||||
val results: List<ShowResponse>
|
||||
return if(mediaObj.name != null) {
|
||||
return if (mediaObj.name != null) {
|
||||
val query = mediaObj.name
|
||||
results = search(query).sortByVolume(query)
|
||||
results.ifEmpty {
|
||||
|
|
161
app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt
Normal file
161
app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt
Normal file
|
@ -0,0 +1,161 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
class OfflineAnimeParser : AnimeParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
override val name = "Offline"
|
||||
override val saveName = "Offline"
|
||||
override val hostUrl = "Offline"
|
||||
override val isDubAvailableSeparately = false
|
||||
override val isNSFW = false
|
||||
|
||||
override suspend fun loadEpisodes(
|
||||
animeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sAnime: SAnime
|
||||
): List<Episode> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.animeLocation}/$animeLink"
|
||||
)
|
||||
//get all of the folder names and add them to the list
|
||||
val episodes = mutableListOf<Episode>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
//put the title and episdode number in the extra data
|
||||
val extraData = mutableMapOf<String, String>()
|
||||
extraData["title"] = animeLink
|
||||
extraData["episode"] = it.name
|
||||
if (it.isDirectory) {
|
||||
val episode = Episode(
|
||||
it.name,
|
||||
"$animeLink - ${it.name}",
|
||||
it.name,
|
||||
null,
|
||||
null,
|
||||
extra = extraData,
|
||||
sEpisode = SEpisodeImpl()
|
||||
)
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
episodes.sortBy { AnimeNameAdapter.findEpisodeNumber(it.number) }
|
||||
return episodes
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun loadVideoServers(
|
||||
episodeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sEpisode: SEpisode
|
||||
): List<VideoServer> {
|
||||
return listOf(
|
||||
VideoServer(
|
||||
episodeLink,
|
||||
offline = true,
|
||||
extraData = extra
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val returnTitles: MutableList<String> = mutableListOf()
|
||||
for (title in titles) {
|
||||
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
|
||||
returnTitles.add(title)
|
||||
}
|
||||
}
|
||||
val returnList: MutableList<ShowResponse> = mutableListOf()
|
||||
for (title in returnTitles) {
|
||||
returnList.add(ShowResponse(title, title, title))
|
||||
}
|
||||
return returnList
|
||||
}
|
||||
|
||||
override suspend fun loadByVideoServers(
|
||||
episodeUrl: String,
|
||||
extra: Map<String, String>?,
|
||||
sEpisode: SEpisode,
|
||||
callback: (VideoExtractor) -> Unit
|
||||
) {
|
||||
val server = loadVideoServers(episodeUrl, extra, sEpisode).first()
|
||||
OfflineVideoExtractor(server).apply {
|
||||
tryWithSuspend {
|
||||
load()
|
||||
}
|
||||
callback.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor {
|
||||
return OfflineVideoExtractor(server)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
|
||||
override val server: VideoServer
|
||||
get() = videoServer
|
||||
|
||||
override suspend fun extract(): VideoContainer {
|
||||
val sublist = getSubtitle(
|
||||
videoServer.extraData?.get("title") ?: "",
|
||||
videoServer.extraData?.get("episode") ?: ""
|
||||
)?: emptyList()
|
||||
//we need to return a "fake" video so that the app doesn't crash
|
||||
val video = Video(
|
||||
null,
|
||||
VideoType.CONTAINER,
|
||||
"",
|
||||
)
|
||||
return VideoContainer(listOf(video), sublist)
|
||||
}
|
||||
|
||||
private fun getSubtitle(title: String, episode: String): List<Subtitle>? {
|
||||
currContext()?.let {
|
||||
DownloadsManager.getDirectory(
|
||||
it,
|
||||
ani.dantotsu.download.DownloadedType.Type.ANIME,
|
||||
title,
|
||||
episode
|
||||
).listFiles()?.forEach {
|
||||
if (it.name.contains("subtitle")) {
|
||||
return listOf(
|
||||
Subtitle(
|
||||
"Downloaded Subtitle",
|
||||
Uri.fromFile(it).toString(),
|
||||
determineSubtitletype(it.absolutePath)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun determineSubtitletype(url: String): SubtitleType {
|
||||
return when {
|
||||
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
|
||||
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,7 +76,7 @@ class OfflineMangaParser : MangaParser() {
|
|||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
|
||||
val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val returnTitles: MutableList<String> = mutableListOf()
|
||||
for (title in titles) {
|
||||
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
|
||||
|
|
|
@ -3,16 +3,13 @@ package ani.dantotsu.parsers
|
|||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineNovelParser: NovelParser() {
|
||||
class OfflineNovelParser : NovelParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
override val hostUrl: String = "Offline"
|
||||
|
@ -34,7 +31,7 @@ class OfflineNovelParser: NovelParser() {
|
|||
if (it.isDirectory) {
|
||||
val chapter = Book(
|
||||
it.name,
|
||||
it.absolutePath + "/cover.jpg",
|
||||
it.absolutePath + "/cover.jpg",
|
||||
null,
|
||||
listOf(it.absolutePath + "/0.epub")
|
||||
)
|
||||
|
@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() {
|
|||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val titles = downloadManager.novelDownloads.map { it.title }.distinct()
|
||||
val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val returnTitles: MutableList<String> = mutableListOf()
|
||||
for (title in titles) {
|
||||
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
|
||||
|
@ -75,7 +72,8 @@ class OfflineNovelParser: NovelParser() {
|
|||
}
|
||||
}
|
||||
}
|
||||
val cover = currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
|
||||
val cover =
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
|
||||
names.forEach {
|
||||
returnList.add(ShowResponse(it, it, cover))
|
||||
}
|
||||
|
|
|
@ -57,10 +57,13 @@ data class VideoServer(
|
|||
val name: String,
|
||||
val embed: FileUrl,
|
||||
val extraData: Map<String, String>? = null,
|
||||
val video: eu.kanade.tachiyomi.animesource.model.Video? = null
|
||||
val video: eu.kanade.tachiyomi.animesource.model.Video? = null,
|
||||
val offline: Boolean = false
|
||||
) : Serializable {
|
||||
constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null)
|
||||
: this(name, FileUrl(embedUrl), extraData)
|
||||
constructor(name: String, offline: Boolean, extraData: Map<String, String>?)
|
||||
: this(name, FileUrl(""), extraData, null, offline)
|
||||
|
||||
constructor(
|
||||
name: String,
|
||||
|
|
|
@ -22,10 +22,10 @@ class DynamicNovelParser(extension: NovelExtension.Installed) : NovelParser() {
|
|||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = extension.sources.firstOrNull()
|
||||
if (source is NovelInterface) {
|
||||
return source.search(query, client)
|
||||
return if (source is NovelInterface) {
|
||||
source.search(query, client)
|
||||
} else {
|
||||
return emptyList()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -111,11 +111,12 @@ internal object NovelExtensionLoader {
|
|||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getSignatureHash(pkgInfo: PackageInfo): List<String>? {
|
||||
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
|
||||
pkgInfo.signingInfo.apkContentsSigners
|
||||
} else {
|
||||
pkgInfo.signatures
|
||||
}
|
||||
val signatures =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
|
||||
pkgInfo.signingInfo.apkContentsSigners
|
||||
} else {
|
||||
pkgInfo.signatures
|
||||
}
|
||||
return if (!signatures.isNullOrEmpty()) {
|
||||
signatures.map { Hash.sha256(it.toByteArray()) }
|
||||
} else {
|
||||
|
|
|
@ -11,6 +11,7 @@ data class CurrentNovelReaderSettings(
|
|||
var justify: Boolean = true,
|
||||
var hyphenation: Boolean = true,
|
||||
var useDarkTheme: Boolean = false,
|
||||
var useOledTheme: Boolean = false,
|
||||
var invert: Boolean = false,
|
||||
var maxInlineSize: Int = 720,
|
||||
var maxBlockSize: Int = 1440,
|
||||
|
|
|
@ -6,11 +6,10 @@ import android.os.Build.VERSION.*
|
|||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AutoCompleteTextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
@ -23,12 +22,8 @@ import com.google.android.material.tabs.TabLayout
|
|||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
||||
class ExtensionsActivity : AppCompatActivity() {
|
||||
private val restartMainActivity = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity)
|
||||
}
|
||||
lateinit var binding: ActivityExtensionsBinding
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -119,17 +114,18 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||
|
||||
|
||||
initActivity(this)
|
||||
/* TODO
|
||||
binding.languageselect.setOnClickListener {
|
||||
val popup = PopupMenu(this, it)
|
||||
popup.inflate(R.menu.launguage_selector_menu)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
true
|
||||
}
|
||||
popup.setOnDismissListener {
|
||||
}
|
||||
popup.show()
|
||||
}*/
|
||||
binding.languageselect.visibility = View.GONE
|
||||
/* TODO
|
||||
binding.languageselect.setOnClickListener {
|
||||
val popup = PopupMenu(this, it)
|
||||
popup.inflate(R.menu.launguage_selector_menu)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
true
|
||||
}
|
||||
popup.setOnDismissListener {
|
||||
}
|
||||
popup.show()
|
||||
}*/
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
|
|
|
@ -18,88 +18,83 @@ class FAQActivity : AppCompatActivity() {
|
|||
|
||||
Triple(
|
||||
R.drawable.ic_round_help_24,
|
||||
currContext()!!.getString(R.string.question_1),
|
||||
currContext()!!.getString(R.string.answer_1)
|
||||
currContext()?.getString(R.string.question_1) ?: "",
|
||||
currContext()?.getString(R.string.answer_1) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_auto_awesome_24,
|
||||
currContext()!!.getString(R.string.question_2),
|
||||
currContext()!!.getString(R.string.answer_2)
|
||||
currContext()?.getString(R.string.question_2) ?: "",
|
||||
currContext()?.getString(R.string.answer_2) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_auto_awesome_24,
|
||||
currContext()!!.getString(R.string.question_17),
|
||||
currContext()!!.getString(R.string.answer_17)
|
||||
currContext()?.getString(R.string.question_17) ?: "",
|
||||
currContext()?.getString(R.string.answer_17) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_download_24,
|
||||
currContext()!!.getString(R.string.question_3),
|
||||
currContext()!!.getString(R.string.answer_3)
|
||||
currContext()?.getString(R.string.question_3) ?: "",
|
||||
currContext()?.getString(R.string.answer_3) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_help_24,
|
||||
currContext()!!.getString(R.string.question_16),
|
||||
currContext()!!.getString(R.string.answer_16)
|
||||
currContext()?.getString(R.string.question_16) ?: "",
|
||||
currContext()?.getString(R.string.answer_16) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_dns_24,
|
||||
currContext()!!.getString(R.string.question_4),
|
||||
currContext()!!.getString(R.string.answer_4)
|
||||
currContext()?.getString(R.string.question_4) ?: "",
|
||||
currContext()?.getString(R.string.answer_4) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_baseline_screen_lock_portrait_24,
|
||||
currContext()!!.getString(R.string.question_5),
|
||||
currContext()!!.getString(R.string.answer_5)
|
||||
currContext()?.getString(R.string.question_5) ?: "",
|
||||
currContext()?.getString(R.string.answer_5) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_anilist,
|
||||
currContext()!!.getString(R.string.question_6),
|
||||
currContext()!!.getString(R.string.answer_6)
|
||||
currContext()?.getString(R.string.question_6) ?: "",
|
||||
currContext()?.getString(R.string.answer_6) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_movie_filter_24,
|
||||
currContext()!!.getString(R.string.question_7),
|
||||
currContext()!!.getString(R.string.answer_7)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_menu_book_24,
|
||||
currContext()!!.getString(R.string.question_8),
|
||||
currContext()!!.getString(R.string.answer_8)
|
||||
currContext()?.getString(R.string.question_7) ?: "",
|
||||
currContext()?.getString(R.string.answer_7) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_lock_open_24,
|
||||
currContext()!!.getString(R.string.question_9),
|
||||
currContext()!!.getString(R.string.answer_9)
|
||||
currContext()?.getString(R.string.question_9) ?: "",
|
||||
currContext()?.getString(R.string.answer_9) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_smart_button_24,
|
||||
currContext()!!.getString(R.string.question_10),
|
||||
currContext()!!.getString(R.string.answer_10)
|
||||
currContext()?.getString(R.string.question_10) ?: "",
|
||||
currContext()?.getString(R.string.answer_10) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_smart_button_24,
|
||||
currContext()!!.getString(R.string.question_11),
|
||||
currContext()!!.getString(R.string.answer_11)
|
||||
currContext()?.getString(R.string.question_11) ?: "",
|
||||
currContext()?.getString(R.string.answer_11) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_info_24,
|
||||
currContext()!!.getString(R.string.question_12),
|
||||
currContext()!!.getString(R.string.answer_12)
|
||||
currContext()?.getString(R.string.question_12) ?: "",
|
||||
currContext()?.getString(R.string.answer_12) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_help_24,
|
||||
currContext()!!.getString(R.string.question_13),
|
||||
currContext()!!.getString(R.string.answer_13)
|
||||
currContext()?.getString(R.string.question_13) ?: "",
|
||||
currContext()?.getString(R.string.answer_13) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_art_track_24,
|
||||
currContext()!!.getString(R.string.question_14),
|
||||
currContext()!!.getString(R.string.answer_14)
|
||||
currContext()?.getString(R.string.question_14) ?: "",
|
||||
currContext()?.getString(R.string.answer_14) ?: ""
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_video_settings_24,
|
||||
currContext()!!.getString(R.string.question_15),
|
||||
currContext()!!.getString(R.string.answer_15)
|
||||
currContext()?.getString(R.string.question_15) ?: "",
|
||||
currContext()?.getString(R.string.answer_15) ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
|
@ -45,90 +46,84 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
private var _binding: FragmentAnimeExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
private val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
||||
private val extensionsAdapter = AnimeExtensionsAdapter(
|
||||
{ pkg ->
|
||||
val name = pkg.name
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
|
||||
activity.findViewById<ImageView>(R.id.languageselect).visibility = visibility
|
||||
activity.findViewById<TextView>(R.id.extensions).text =
|
||||
if (show) getString(R.string.extensions) else name
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
var itemSelected = false
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||
.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||
itemSelected = true
|
||||
selectedIndex = which
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
val eActivity = requireActivity() as ExtensionsActivity
|
||||
eActivity.runOnUiThread {
|
||||
val fragment =
|
||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
|
||||
eActivity.findViewById<ViewPager2>(R.id.viewPager).visibility =
|
||||
View.VISIBLE
|
||||
eActivity.findViewById<TabLayout>(R.id.tabLayout).visibility =
|
||||
View.VISIBLE
|
||||
eActivity.findViewById<TextInputLayout>(R.id.searchView).visibility =
|
||||
View.VISIBLE
|
||||
eActivity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
View.GONE
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
val fragment =
|
||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val eActivity = requireActivity() as ExtensionsActivity
|
||||
eActivity.runOnUiThread {
|
||||
val fragment =
|
||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
val fragment =
|
||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
|
||||
eActivity.findViewById<ViewPager2>(R.id.viewPager).visibility =
|
||||
View.VISIBLE
|
||||
eActivity.findViewById<TabLayout>(R.id.tabLayout).visibility =
|
||||
View.VISIBLE
|
||||
eActivity.findViewById<TextInputLayout>(R.id.searchView).visibility =
|
||||
View.VISIBLE
|
||||
eActivity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
View.GONE
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// Hide ViewPager2 and TabLayout
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.GONE
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.GONE
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.GONE
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
View.VISIBLE
|
||||
changeUIVisibility(false)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
{ pkg ->
|
||||
{ pkg, forceDelete ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
||||
|
||||
if (pkg.hasUpdate) {
|
||||
if (pkg.hasUpdate && !forceDelete) {
|
||||
animeExtensionManager.updateExtension(pkg)
|
||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||
.subscribe(
|
||||
|
@ -209,7 +204,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
|
||||
private class AnimeExtensionsAdapter(
|
||||
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (AnimeExtension.Installed, Boolean) -> Unit,
|
||||
val skipIcons: Boolean
|
||||
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
|
@ -225,6 +220,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||
val nsfw = if (extension.isNsfw) "(18+)" else ""
|
||||
|
@ -240,11 +236,15 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
onUninstallClicked(extension, false)
|
||||
}
|
||||
holder.settingsImageView.setOnClickListener {
|
||||
onSettingsClicked(extension)
|
||||
}
|
||||
holder.card.setOnLongClickListener {
|
||||
onUninstallClicked(extension, true)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun filter(query: String, currentList: List<AnimeExtension.Installed>) {
|
||||
|
@ -264,6 +264,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
|
||||
val card = view.findViewById<View>(R.id.extensionCardView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
|
@ -44,70 +45,84 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
private var _binding: FragmentMangaExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
private val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
||||
private val extensionsAdapter = MangaExtensionsAdapter({ pkg ->
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||
selectedIndex = which
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
private val extensionsAdapter = MangaExtensionsAdapter(
|
||||
{ pkg ->
|
||||
val name = pkg.name
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
|
||||
activity.findViewById<ImageView>(R.id.languageselect).visibility = visibility
|
||||
activity.findViewById<TextView>(R.id.extensions).text =
|
||||
if (show) getString(R.string.extensions) else name
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
var itemSelected = false
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||
.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||
itemSelected = true
|
||||
selectedIndex = which
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
val fragment =
|
||||
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
// Move the fragment transaction here
|
||||
val fragment =
|
||||
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment =
|
||||
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
// Hide ViewPager2 and TabLayout
|
||||
changeUIVisibility(false)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
{ pkg ->
|
||||
// Hide ViewPager2 and TabLayout
|
||||
changeUIVisibility(false)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
{ pkg: MangaExtension.Installed, forceDelete: Boolean ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
||||
|
||||
if (pkg.hasUpdate) {
|
||||
if (pkg.hasUpdate && !forceDelete) {
|
||||
mangaExtensionManager.updateExtension(pkg)
|
||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||
.subscribe(
|
||||
|
@ -188,7 +203,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
|
||||
private class MangaExtensionsAdapter(
|
||||
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (MangaExtension.Installed, Boolean) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
|
@ -206,6 +221,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||
val nsfw = if (extension.isNsfw) "(18+)" else ""
|
||||
|
@ -221,11 +237,16 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
onUninstallClicked(extension, false)
|
||||
}
|
||||
holder.settingsImageView.setOnClickListener {
|
||||
onSettingsClicked(extension)
|
||||
}
|
||||
|
||||
holder.card.setOnLongClickListener {
|
||||
onUninstallClicked(extension, true)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun filter(query: String, currentList: List<MangaExtension.Installed>) {
|
||||
|
@ -245,6 +266,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
|
||||
val card: View = view.findViewById(R.id.extensionCardView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -39,17 +39,18 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
private val novelExtensionManager: NovelExtensionManager = Injekt.get()
|
||||
private val extensionsAdapter = NovelExtensionsAdapter({ pkg ->
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
},
|
||||
private val extensionsAdapter = NovelExtensionsAdapter(
|
||||
{ pkg ->
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
},
|
||||
{ pkg, forceDelete ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
||||
|
||||
if (pkg.hasUpdate) {
|
||||
if (pkg.hasUpdate && !forceDelete) {
|
||||
novelExtensionManager.updateExtension(pkg)
|
||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||
.subscribe(
|
||||
|
@ -130,7 +131,7 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
|
||||
private class NovelExtensionsAdapter(
|
||||
private val onSettingsClicked: (NovelExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (NovelExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (NovelExtension.Installed, Boolean) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<NovelExtension.Installed, NovelExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
|
@ -165,11 +166,15 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
onUninstallClicked(extension, false)
|
||||
}
|
||||
holder.settingsImageView.setOnClickListener {
|
||||
onSettingsClicked(extension)
|
||||
}
|
||||
holder.card.setOnLongClickListener {
|
||||
onUninstallClicked(extension, true)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun filter(query: String, currentList: List<NovelExtension.Installed>) {
|
||||
|
@ -189,6 +194,7 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
|
|||
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
|
||||
val card = view.findViewById<View>(R.id.extensionCardView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class NovelReaderSettings(
|
||||
var showSource: Boolean = true,
|
||||
var showSystemBars: Boolean = false,
|
||||
var default: CurrentNovelReaderSettings = CurrentNovelReaderSettings(),
|
||||
var askIndividual: Boolean = true,
|
||||
) : Serializable
|
|
@ -22,7 +22,7 @@ data class PlayerSettings(
|
|||
|
||||
//TimeStamps
|
||||
var timeStampsEnabled: Boolean = true,
|
||||
var useProxyForTimeStamps: Boolean = true,
|
||||
var useProxyForTimeStamps: Boolean = false,
|
||||
var showTimeStampButton: Boolean = true,
|
||||
|
||||
//Auto
|
||||
|
@ -45,6 +45,6 @@ data class PlayerSettings(
|
|||
var skipTime: Int = 85,
|
||||
|
||||
//Other
|
||||
var cast: Boolean = false,
|
||||
var cast: Boolean = true,
|
||||
var pip: Boolean = true
|
||||
) : Serializable
|
||||
|
|
|
@ -97,7 +97,21 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
|||
|
||||
|
||||
val speeds =
|
||||
arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f)
|
||||
arrayOf(
|
||||
0.25f,
|
||||
0.33f,
|
||||
0.5f,
|
||||
0.66f,
|
||||
0.75f,
|
||||
1f,
|
||||
1.15f,
|
||||
1.25f,
|
||||
1.33f,
|
||||
1.5f,
|
||||
1.66f,
|
||||
1.75f,
|
||||
2f
|
||||
)
|
||||
val cursedSpeeds = arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
|
||||
var curSpeedArr = if (settings.cursedSpeeds) cursedSpeeds else speeds
|
||||
var speedsName = curSpeedArr.map { "${it}x" }.toTypedArray()
|
||||
|
@ -106,13 +120,14 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
|||
val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme)
|
||||
.setTitle(getString(R.string.default_speed))
|
||||
binding.playerSettingsSpeed.setOnClickListener {
|
||||
val dialog = speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i ->
|
||||
settings.defaultSpeed = i
|
||||
binding.playerSettingsSpeed.text =
|
||||
getString(R.string.default_playback_speed, speedsName[i])
|
||||
saveData(player, settings)
|
||||
dialog.dismiss()
|
||||
}.show()
|
||||
val dialog =
|
||||
speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i ->
|
||||
settings.defaultSpeed = i
|
||||
binding.playerSettingsSpeed.text =
|
||||
getString(R.string.default_playback_speed, speedsName[i])
|
||||
saveData(player, settings)
|
||||
dialog.dismiss()
|
||||
}.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
|
@ -256,11 +271,12 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
|||
val resizeDialog = AlertDialog.Builder(this, R.style.DialogTheme)
|
||||
.setTitle(getString(R.string.default_resize_mode))
|
||||
binding.playerResizeMode.setOnClickListener {
|
||||
val dialog = resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count ->
|
||||
settings.resize = count
|
||||
saveData(player, settings)
|
||||
dialog.dismiss()
|
||||
}.show()
|
||||
val dialog =
|
||||
resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count ->
|
||||
settings.resize = count
|
||||
saveData(player, settings)
|
||||
dialog.dismiss()
|
||||
}.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
fun restartApp() {
|
||||
|
@ -382,7 +398,10 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
|||
val outlineDialog = AlertDialog.Builder(this, R.style.DialogTheme)
|
||||
.setTitle(getString(R.string.outline_type))
|
||||
binding.videoSubOutline.setOnClickListener {
|
||||
val dialog = outlineDialog.setSingleChoiceItems(typesOutline, settings.outline) { dialog, count ->
|
||||
val dialog = outlineDialog.setSingleChoiceItems(
|
||||
typesOutline,
|
||||
settings.outline
|
||||
) { dialog, count ->
|
||||
settings.outline = count
|
||||
saveData(player, settings)
|
||||
dialog.dismiss()
|
||||
|
|
|
@ -8,6 +8,7 @@ data class ReaderSettings(
|
|||
|
||||
var autoDetectWebtoon: Boolean = true,
|
||||
var default: CurrentReaderSettings = CurrentReaderSettings(),
|
||||
var defaultLN: CurrentNovelReaderSettings = CurrentNovelReaderSettings(),
|
||||
|
||||
var askIndividual: Boolean = true,
|
||||
var updateForH: Boolean = false
|
||||
|
|
|
@ -42,7 +42,7 @@ class ReaderSettingsActivity : AppCompatActivity() {
|
|||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
//General
|
||||
//Manga Settings
|
||||
binding.readerSettingsSourceName.isChecked = settings.showSource
|
||||
binding.readerSettingsSourceName.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.showSource = isChecked
|
||||
|
@ -54,14 +54,14 @@ class ReaderSettingsActivity : AppCompatActivity() {
|
|||
settings.showSystemBars = isChecked
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
//Default Manga
|
||||
binding.readerSettingsAutoWebToon.isChecked = settings.autoDetectWebtoon
|
||||
binding.readerSettingsAutoWebToon.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.autoDetectWebtoon = isChecked
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
//Default
|
||||
|
||||
val layoutList = listOf(
|
||||
binding.readerSettingsPaged,
|
||||
binding.readerSettingsContinuousPaged,
|
||||
|
@ -185,6 +185,169 @@ class ReaderSettingsActivity : AppCompatActivity() {
|
|||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
//LN settings
|
||||
val layoutListLN = listOf(
|
||||
binding.LNpaged,
|
||||
binding.LNcontinuous
|
||||
)
|
||||
|
||||
binding.LNlayoutText.text = settings.defaultLN.layout.string
|
||||
var selectedLN = layoutListLN[settings.defaultLN.layout.ordinal]
|
||||
selectedLN.alpha = 1f
|
||||
|
||||
layoutListLN.forEachIndexed { index, imageButton ->
|
||||
imageButton.setOnClickListener {
|
||||
selectedLN.alpha = 0.33f
|
||||
selectedLN = imageButton
|
||||
selectedLN.alpha = 1f
|
||||
settings.defaultLN.layout = CurrentNovelReaderSettings.Layouts[index]
|
||||
?: CurrentNovelReaderSettings.Layouts.PAGED
|
||||
binding.LNlayoutText.text = settings.defaultLN.layout.string
|
||||
saveData(reader, settings)
|
||||
}
|
||||
}
|
||||
|
||||
val dualListLN = listOf(
|
||||
binding.LNdualNo,
|
||||
binding.LNdualAuto,
|
||||
binding.LNdualForce
|
||||
)
|
||||
|
||||
binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString()
|
||||
var selectedDualLN = dualListLN[settings.defaultLN.dualPageMode.ordinal]
|
||||
selectedDualLN.alpha = 1f
|
||||
|
||||
dualListLN.forEachIndexed { index, imageButton ->
|
||||
imageButton.setOnClickListener {
|
||||
selectedDualLN.alpha = 0.33f
|
||||
selectedDualLN = imageButton
|
||||
selectedDualLN.alpha = 1f
|
||||
settings.defaultLN.dualPageMode = CurrentReaderSettings.DualPageModes[index]
|
||||
?: CurrentReaderSettings.DualPageModes.Automatic
|
||||
binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString()
|
||||
saveData(reader, settings)
|
||||
}
|
||||
}
|
||||
|
||||
binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
|
||||
binding.LNlineHeight.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
|
||||
settings.defaultLN.lineHeight = value
|
||||
binding.LNlineHeight.setText(value.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
}
|
||||
|
||||
binding.LNincrementLineHeight.setOnClickListener {
|
||||
val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
|
||||
settings.defaultLN.lineHeight = value + 0.1f
|
||||
binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNdecrementLineHeight.setOnClickListener {
|
||||
val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
|
||||
settings.defaultLN.lineHeight = value - 0.1f
|
||||
binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNmargin.setText(settings.defaultLN.margin.toString())
|
||||
binding.LNmargin.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
|
||||
settings.defaultLN.margin = value
|
||||
binding.LNmargin.setText(value.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
}
|
||||
|
||||
binding.LNincrementMargin.setOnClickListener {
|
||||
val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
|
||||
settings.defaultLN.margin = value + 0.01f
|
||||
binding.LNmargin.setText(settings.defaultLN.margin.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNdecrementMargin.setOnClickListener {
|
||||
val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
|
||||
settings.defaultLN.margin = value - 0.01f
|
||||
binding.LNmargin.setText(settings.defaultLN.margin.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
|
||||
binding.LNmaxInlineSize.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.defaultLN.maxInlineSize = value
|
||||
binding.LNmaxInlineSize.setText(value.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
}
|
||||
|
||||
binding.LNincrementMaxInlineSize.setOnClickListener {
|
||||
val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.defaultLN.maxInlineSize = value + 10
|
||||
binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNdecrementMaxInlineSize.setOnClickListener {
|
||||
val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.defaultLN.maxInlineSize = value - 10
|
||||
binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString())
|
||||
binding.LNmaxBlockSize.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.defaultLN.maxBlockSize = value
|
||||
binding.LNmaxBlockSize.setText(value.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
}
|
||||
binding.LNincrementMaxBlockSize.setOnClickListener {
|
||||
val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.defaultLN.maxInlineSize = value + 10
|
||||
binding.LNmaxBlockSize.setText(settings.defaultLN.maxInlineSize.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNdecrementMaxBlockSize.setOnClickListener {
|
||||
val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.defaultLN.maxBlockSize = value - 10
|
||||
binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString())
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNuseDarkTheme.isChecked = settings.defaultLN.useDarkTheme
|
||||
binding.LNuseDarkTheme.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.defaultLN.useDarkTheme = isChecked
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNuseOledTheme.isChecked = settings.defaultLN.useOledTheme
|
||||
binding.LNuseOledTheme.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.defaultLN.useOledTheme = isChecked
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNkeepScreenOn.isChecked = settings.defaultLN.keepScreenOn
|
||||
binding.LNkeepScreenOn.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.defaultLN.keepScreenOn = isChecked
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
binding.LNvolumeButton.isChecked = settings.defaultLN.volumeButtons
|
||||
binding.LNvolumeButton.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.defaultLN.volumeButtons = isChecked
|
||||
saveData(reader, settings)
|
||||
}
|
||||
|
||||
//Update Progress
|
||||
binding.readerSettingsAskUpdateProgress.isChecked = settings.askIndividual
|
||||
binding.readerSettingsAskUpdateProgress.setOnCheckedChangeListener { _, isChecked ->
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Build.*
|
||||
import android.os.Build.VERSION.*
|
||||
|
@ -11,19 +12,24 @@ import android.os.Bundle
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.ActivitySettingsBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
@ -37,7 +43,9 @@ import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
|
|||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.skydoves.colorpickerview.listeners.ColorListener
|
||||
import eltos.simpledialogfragment.SimpleDialog
|
||||
import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE
|
||||
import eltos.simpledialogfragment.color.SimpleColorDialog
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import io.noties.markwon.Markwon
|
||||
|
@ -50,7 +58,7 @@ import uy.kohesive.injekt.api.get
|
|||
import kotlin.random.Random
|
||||
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListener {
|
||||
private val restartMainActivity = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
|
||||
}
|
||||
|
@ -59,6 +67,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
private val networkPreferences = Injekt.get<NetworkPreferences>()
|
||||
private var cursedCounter = 0
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -71,26 +80,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
|
||||
binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
|
||||
binding.settingsVersion.setOnLongClickListener {
|
||||
fun getArch(): String {
|
||||
SUPPORTED_ABIS.forEach {
|
||||
when (it) {
|
||||
"arm64-v8a" -> return "aarch64"
|
||||
"armeabi-v7a" -> return "arm"
|
||||
"x86_64" -> return "x86_64"
|
||||
"x86" -> return "i686"
|
||||
}
|
||||
}
|
||||
return System.getProperty("os.arch") ?: System.getProperty("os.product.cpu.abi")
|
||||
?: "Unknown Architecture"
|
||||
}
|
||||
|
||||
val info = """
|
||||
dantotsu Version: ${BuildConfig.VERSION_NAME}
|
||||
Device: $BRAND $DEVICE
|
||||
Architecture: ${getArch()}
|
||||
OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
""".trimIndent()
|
||||
copyToClipboard(info, false)
|
||||
copyToClipboard(getDeviceInfo(), false)
|
||||
toast(getString(R.string.copied_device_info))
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
@ -176,34 +166,30 @@ class SettingsActivity : AppCompatActivity() {
|
|||
|
||||
|
||||
binding.customTheme.setOnClickListener {
|
||||
var passedColor: Int = 0
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_color_picker, null)
|
||||
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Custom Theme")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putInt("custom_theme_int", passedColor).apply()
|
||||
logger("Custom Theme: $passedColor")
|
||||
dialog.dismiss()
|
||||
val originalColor = getSharedPreferences("Dantotsu", MODE_PRIVATE).getInt(
|
||||
"custom_theme_int",
|
||||
Color.parseColor("#6200EE")
|
||||
)
|
||||
|
||||
class CustomColorDialog : SimpleColorDialog() { //idk where to put it
|
||||
override fun onPositiveButtonClick() {
|
||||
restartApp()
|
||||
super.onPositiveButtonClick()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
val colorPickerView =
|
||||
dialogView.findViewById<com.skydoves.colorpickerview.ColorPickerView>(R.id.colorPickerView)
|
||||
colorPickerView.setColorListener(ColorListener { color, fromUser ->
|
||||
val linearLayout = dialogView.findViewById<LinearLayout>(R.id.linear)
|
||||
passedColor = color
|
||||
linearLayout.setBackgroundColor(color)
|
||||
})
|
||||
alertDialog.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
val tag = "colorPicker"
|
||||
CustomColorDialog().title("Custom Theme")
|
||||
.colorPreset(originalColor)
|
||||
.colors(this, SimpleColorDialog.BEIGE_COLOR_PALLET)
|
||||
.allowCustom(true)
|
||||
.showOutline(0x46000000)
|
||||
.gridNumColumn(5)
|
||||
.choiceMode(SimpleColorDialog.SINGLE_CHOICE)
|
||||
.neg()
|
||||
.show(this, tag)
|
||||
}
|
||||
|
||||
//val animeSource = loadData<Int>("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
|
||||
val animeSource = getSharedPreferences(
|
||||
"Dantotsu",
|
||||
Context.MODE_PRIVATE
|
||||
|
@ -228,6 +214,47 @@ class SettingsActivity : AppCompatActivity() {
|
|||
binding.animeSource.clearFocus()
|
||||
}
|
||||
|
||||
binding.settingsPinnedAnimeSources.setOnClickListener {
|
||||
val animeSourcesWithoutDownloadsSource = AnimeSources.list.filter { it.name != "Downloaded" }
|
||||
val names = animeSourcesWithoutDownloadsSource.map { it.name }
|
||||
val pinnedSourcesBoolean = animeSourcesWithoutDownloadsSource.map { it.name in AnimeSources.pinnedAnimeSources }
|
||||
val pinnedSourcesOriginal = getSharedPreferences(
|
||||
"Dantotsu",
|
||||
Context.MODE_PRIVATE
|
||||
).getStringSet("pinned_anime_sources", null)
|
||||
val pinnedSources = pinnedSourcesOriginal?.toMutableSet() ?: mutableSetOf()
|
||||
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Pinned Anime Sources")
|
||||
.setMultiChoiceItems(
|
||||
names.toTypedArray(),
|
||||
pinnedSourcesBoolean.toBooleanArray()
|
||||
) { _, which, isChecked ->
|
||||
if (isChecked) {
|
||||
pinnedSources.add(AnimeSources.names[which])
|
||||
} else {
|
||||
pinnedSources.remove(AnimeSources.names[which])
|
||||
}
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
val oldDefaultSourceIndex = getSharedPreferences(
|
||||
"Dantotsu",
|
||||
Context.MODE_PRIVATE
|
||||
).getInt("settings_def_anime_source_s_r", 0)
|
||||
val oldName = AnimeSources.names[oldDefaultSourceIndex]
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putStringSet("pinned_anime_sources", pinnedSources).apply()
|
||||
AnimeSources.pinnedAnimeSources = pinnedSources
|
||||
AnimeSources.performReorderAnimeSources()
|
||||
val newDefaultSourceIndex = AnimeSources.names.indexOf(oldName)
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putInt("settings_def_anime_source_s_r", newDefaultSourceIndex).apply()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
alertDialog.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
binding.settingsPlayer.setOnClickListener {
|
||||
startActivity(Intent(this, PlayerSettingsActivity::class.java))
|
||||
}
|
||||
|
@ -237,7 +264,10 @@ class SettingsActivity : AppCompatActivity() {
|
|||
AlertDialog.Builder(this, R.style.DialogTheme).setTitle("Download Manager")
|
||||
var downloadManager = loadData<Int>("settings_download_manager") ?: 0
|
||||
binding.settingsDownloadManager.setOnClickListener {
|
||||
val dialog = downloadManagerDialog.setSingleChoiceItems(managers, downloadManager) { dialog, count ->
|
||||
val dialog = downloadManagerDialog.setSingleChoiceItems(
|
||||
managers,
|
||||
downloadManager
|
||||
) { dialog, count ->
|
||||
downloadManager = count
|
||||
saveData("settings_download_manager", downloadManager)
|
||||
dialog.dismiss()
|
||||
|
@ -245,6 +275,62 @@ class SettingsActivity : AppCompatActivity() {
|
|||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
binding.purgeAnimeDownloads.setOnClickListener {
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Purge Anime Downloads")
|
||||
.setMessage("Are you sure you want to purge all anime downloads?")
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(DownloadedType.Type.ANIME)
|
||||
DownloadService.sendRemoveAllDownloads(
|
||||
this,
|
||||
ExoplayerDownloadService::class.java,
|
||||
false
|
||||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
binding.purgeMangaDownloads.setOnClickListener {
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Purge Manga Downloads")
|
||||
.setMessage("Are you sure you want to purge all manga downloads?")
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(DownloadedType.Type.MANGA)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
binding.purgeNovelDownloads.setOnClickListener {
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Purge Novel Downloads")
|
||||
.setMessage("Are you sure you want to purge all novel downloads?")
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(DownloadedType.Type.NOVEL)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
binding.settingsForceLegacyInstall.isChecked =
|
||||
extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY
|
||||
binding.settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked ->
|
||||
|
@ -259,9 +345,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||
binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("skip_extension_icons", isChecked)
|
||||
}
|
||||
binding.NSFWExtension.isChecked = loadData("NFSWExtension") ?: true
|
||||
binding.NSFWExtension.isChecked = loadData("NSFWExtension") ?: true
|
||||
binding.NSFWExtension.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("NFSWExtension", isChecked)
|
||||
saveData("NSFWExtension", isChecked)
|
||||
|
||||
}
|
||||
|
||||
|
@ -278,7 +364,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
.setNeutralButton("Reset") { dialog, _ ->
|
||||
networkPreferences.defaultUserAgent()
|
||||
.set("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0") // Reset to default or empty
|
||||
.set("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")
|
||||
editText.setText("")
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
@ -339,6 +425,17 @@ class SettingsActivity : AppCompatActivity() {
|
|||
binding.settingsRecentlyListOnly.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("recently_list_only", isChecked)
|
||||
}
|
||||
binding.settingsShareUsername.isChecked = getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).getBoolean("shared_user_id", true)
|
||||
binding.settingsShareUsername.setOnCheckedChangeListener { _, isChecked ->
|
||||
getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).edit().putBoolean("shared_user_id", isChecked).apply()
|
||||
}
|
||||
|
||||
binding.settingsPreferDub.isChecked = loadData("settings_prefer_dub") ?: false
|
||||
binding.settingsPreferDub.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("settings_prefer_dub", isChecked)
|
||||
|
@ -370,6 +467,47 @@ class SettingsActivity : AppCompatActivity() {
|
|||
binding.mangaSource.clearFocus()
|
||||
}
|
||||
|
||||
binding.settingsPinnedMangaSources.setOnClickListener {
|
||||
val mangaSourcesWithoutDownloadsSource = MangaSources.list.filter { it.name != "Downloaded" }
|
||||
val names = mangaSourcesWithoutDownloadsSource.map { it.name }
|
||||
val pinnedSourcesBoolean = mangaSourcesWithoutDownloadsSource.map { it.name in MangaSources.pinnedMangaSources }
|
||||
val pinnedSourcesOriginal = getSharedPreferences(
|
||||
"Dantotsu",
|
||||
Context.MODE_PRIVATE
|
||||
).getStringSet("pinned_manga_sources", null)
|
||||
val pinnedSources = pinnedSourcesOriginal?.toMutableSet() ?: mutableSetOf()
|
||||
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Pinned Manga Sources")
|
||||
.setMultiChoiceItems(
|
||||
names.toTypedArray(),
|
||||
pinnedSourcesBoolean.toBooleanArray()
|
||||
) { _, which, isChecked ->
|
||||
if (isChecked) {
|
||||
pinnedSources.add(MangaSources.names[which])
|
||||
} else {
|
||||
pinnedSources.remove(MangaSources.names[which])
|
||||
}
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
val oldDefaultSourceIndex = getSharedPreferences(
|
||||
"Dantotsu",
|
||||
Context.MODE_PRIVATE
|
||||
).getInt("settings_def_manga_source_s_r", 0)
|
||||
val oldName = MangaSources.names[oldDefaultSourceIndex]
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putStringSet("pinned_manga_sources", pinnedSources).apply()
|
||||
MangaSources.pinnedMangaSources = pinnedSources
|
||||
MangaSources.performReorderMangaSources()
|
||||
val newDefaultSourceIndex = MangaSources.names.indexOf(oldName)
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putInt("settings_def_manga_source_s_r", newDefaultSourceIndex).apply()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
alertDialog.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
binding.settingsReader.setOnClickListener {
|
||||
startActivity(Intent(this, ReaderSettingsActivity::class.java))
|
||||
}
|
||||
|
@ -408,16 +546,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||
uiTheme(true, it)
|
||||
}
|
||||
|
||||
binding.settingsIncognito.isChecked =
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean(
|
||||
"incognito",
|
||||
false
|
||||
)
|
||||
binding.settingsIncognito.setOnCheckedChangeListener { _, isChecked ->
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putBoolean("incognito", isChecked).apply()
|
||||
}
|
||||
|
||||
var previousStart: View = when (uiSettings.defaultStartUpTab) {
|
||||
0 -> binding.uiSettingsAnime
|
||||
1 -> binding.uiSettingsHome
|
||||
|
@ -434,6 +562,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
initActivity(this)
|
||||
}
|
||||
|
||||
|
||||
binding.uiSettingsAnime.setOnClickListener {
|
||||
uiTheme(0, it)
|
||||
}
|
||||
|
@ -510,10 +639,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||
lifecycleScope.launch {
|
||||
binding.settingBuyMeCoffee.pop()
|
||||
}
|
||||
binding.settingUPI.visibility = if (checkCountry(this)) View.VISIBLE else View.GONE
|
||||
lifecycleScope.launch {
|
||||
binding.settingUPI.pop()
|
||||
}
|
||||
|
||||
binding.loginDiscord.setOnClickListener {
|
||||
openLinkInBrowser(getString(R.string.discord))
|
||||
|
@ -521,7 +646,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||
binding.loginGithub.setOnClickListener {
|
||||
openLinkInBrowser(getString(R.string.github))
|
||||
}
|
||||
|
||||
binding.loginTelegram.setOnClickListener {
|
||||
openLinkInBrowser(getString(R.string.telegram))
|
||||
}
|
||||
binding.settingsUi.setOnClickListener {
|
||||
startActivity(Intent(this, UserInterfaceSettingsActivity::class.java))
|
||||
}
|
||||
|
@ -759,8 +886,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
setPositiveButton("denote :)") {
|
||||
if (binding.settingUPI.visibility == View.VISIBLE) binding.settingUPI.performClick()
|
||||
else binding.settingBuyMeCoffee.performClick()
|
||||
binding.settingBuyMeCoffee.performClick()
|
||||
dismiss()
|
||||
}
|
||||
show(supportFragmentManager, "dialog")
|
||||
|
@ -770,6 +896,18 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean {
|
||||
if (which == BUTTON_POSITIVE) {
|
||||
if (dialogTag == "colorPicker") {
|
||||
val color = extras.getInt(SimpleColorDialog.COLOR)
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putInt("custom_theme_int", color).apply()
|
||||
logger("Custom Theme: $color")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun restartApp() {
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
|
@ -788,4 +926,28 @@ class SettingsActivity : AppCompatActivity() {
|
|||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getDeviceInfo(): String {
|
||||
return """
|
||||
dantotsu Version: ${BuildConfig.VERSION_NAME}
|
||||
Device: $BRAND $DEVICE
|
||||
Architecture: ${getArch()}
|
||||
OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun getArch(): String {
|
||||
SUPPORTED_ABIS.forEach {
|
||||
when (it) {
|
||||
"arm64-v8a" -> return "aarch64"
|
||||
"armeabi-v7a" -> return "arm"
|
||||
"x86_64" -> return "x86_64"
|
||||
"x86" -> return "i686"
|
||||
}
|
||||
}
|
||||
return System.getProperty("os.arch") ?: System.getProperty("os.product.cpu.abi")
|
||||
?: "Unknown Architecture"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,43 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.MainActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetSettingsBinding
|
||||
import ani.dantotsu.download.DownloadContainerActivity
|
||||
import ani.dantotsu.download.anime.OfflineAnimeFragment
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
import ani.dantotsu.home.MangaFragment
|
||||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.incognitoNotification
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.offline.OfflineFragment
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.imagesearch.ImageSearchActivity
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.toast
|
||||
|
||||
|
||||
class SettingsDialogFragment() : BottomSheetDialogFragment() {
|
||||
class SettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var pageType: PageType
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
pageType = arguments?.getSerializable("pageType") as? PageType ?: PageType.HOME
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -72,6 +74,17 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
binding.settingsIncognito.isChecked =
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean(
|
||||
"incognito",
|
||||
false
|
||||
) ?: false
|
||||
|
||||
binding.settingsIncognito.setOnCheckedChangeListener { _, isChecked ->
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putBoolean("incognito", isChecked)?.apply()
|
||||
incognitoNotification(requireContext())
|
||||
}
|
||||
binding.settingsExtensionSettings.setSafeOnClickListener {
|
||||
startActivity(Intent(activity, ExtensionsActivity::class.java))
|
||||
dismiss()
|
||||
|
@ -88,48 +101,61 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
|
|||
startActivity(Intent(activity, ImageSearchActivity::class.java))
|
||||
dismiss()
|
||||
}
|
||||
binding.settingsDownloads.setSafeOnClickListener {
|
||||
|
||||
binding.settingsDownloads.isChecked =
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("offlineMode", false) ?: false
|
||||
binding.settingsDownloads.setOnCheckedChangeListener { _, isChecked ->
|
||||
when (pageType) {
|
||||
PageType.MANGA -> {
|
||||
val intent = Intent(activity, DownloadContainerActivity::class.java)
|
||||
intent.putExtra("FRAGMENT_CLASS_NAME", OfflineMangaFragment::class.java.name)
|
||||
val intent = Intent(activity, NoInternet::class.java)
|
||||
intent.putExtra(
|
||||
"FRAGMENT_CLASS_NAME",
|
||||
OfflineMangaFragment::class.java.name
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
PageType.ANIME -> {
|
||||
try {
|
||||
val arrayOfFiles =
|
||||
ContextCompat.getExternalFilesDirs(requireContext(), null)
|
||||
startActivity(
|
||||
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
|
||||
val parentDirectory = arrayOfFiles[1].toString()
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
|
||||
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(getString(R.string.file_manager_not_found))
|
||||
}
|
||||
val intent = Intent(activity, NoInternet::class.java)
|
||||
intent.putExtra(
|
||||
"FRAGMENT_CLASS_NAME",
|
||||
OfflineAnimeFragment::class.java.name
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
PageType.HOME -> {
|
||||
try {
|
||||
val arrayOfFiles =
|
||||
ContextCompat.getExternalFilesDirs(requireContext(), null)
|
||||
startActivity(
|
||||
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
|
||||
val parentDirectory = arrayOfFiles[1].toString()
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
|
||||
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(getString(R.string.file_manager_not_found))
|
||||
}
|
||||
val intent = Intent(activity, NoInternet::class.java)
|
||||
intent.putExtra("FRAGMENT_CLASS_NAME", OfflineFragment::class.java.name)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
PageType.OfflineMANGA -> {
|
||||
val intent = Intent(activity, MainActivity::class.java)
|
||||
intent.putExtra("FRAGMENT_CLASS_NAME", MangaFragment::class.java.name)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
PageType.OfflineHOME -> {
|
||||
val intent = Intent(activity, MainActivity::class.java)
|
||||
intent.putExtra(
|
||||
"FRAGMENT_CLASS_NAME",
|
||||
if (Anilist.token != null) HomeFragment::class.java.name else LoginFragment::class.java.name
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
PageType.OfflineANIME -> {
|
||||
val intent = Intent(activity, MainActivity::class.java)
|
||||
intent.putExtra("FRAGMENT_CLASS_NAME", AnimeFragment::class.java.name)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putBoolean("offlineMode", isChecked)?.apply()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,7 +166,7 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
|
|||
|
||||
companion object {
|
||||
enum class PageType {
|
||||
MANGA, ANIME, HOME
|
||||
MANGA, ANIME, HOME, OfflineMANGA, OfflineANIME, OfflineHOME
|
||||
}
|
||||
|
||||
fun newInstance(pageType: PageType): SettingsDialogFragment {
|
||||
|
|
|
@ -44,14 +44,14 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
|
|||
binding.uiSettingsHomeLayout.setOnClickListener {
|
||||
val dialog = AlertDialog.Builder(this, R.style.DialogTheme)
|
||||
.setTitle(getString(R.string.home_layout_show)).apply {
|
||||
setMultiChoiceItems(
|
||||
views,
|
||||
settings.homeLayoutShow.toBooleanArray()
|
||||
) { _, i, value ->
|
||||
settings.homeLayoutShow[i] = value
|
||||
saveData(ui, settings)
|
||||
}
|
||||
}.show()
|
||||
setMultiChoiceItems(
|
||||
views,
|
||||
settings.homeLayoutShow.toBooleanArray()
|
||||
) { _, i, value ->
|
||||
settings.homeLayoutShow[i] = value
|
||||
saveData(ui, settings)
|
||||
}
|
||||
}.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,6 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
|
|||
saveData(ui, settings)
|
||||
restartApp()
|
||||
}
|
||||
|
||||
binding.uiSettingsBannerAnimation.isChecked = settings.bannerAnimations
|
||||
binding.uiSettingsBannerAnimation.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.bannerAnimations = isChecked
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.preference.EditTextPreference
|
|||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.forEach
|
||||
import androidx.preference.getOnBindEditTextListener
|
||||
import ani.dantotsu.snackString
|
||||
import eu.kanade.tachiyomi.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||
|
@ -21,7 +22,12 @@ import uy.kohesive.injekt.api.get
|
|||
|
||||
class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceScreen = populateAnimePreferenceScreen()
|
||||
preferenceScreen = try {
|
||||
populateAnimePreferenceScreen()
|
||||
} catch (e: Exception) {
|
||||
snackString(e.message ?: "Unknown error")
|
||||
preferenceManager.createPreferenceScreen(requireContext())
|
||||
}
|
||||
//set background color
|
||||
val color = TypedValue()
|
||||
requireContext().theme.resolveAttribute(
|
||||
|
@ -42,8 +48,8 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
|
|||
|
||||
private fun populateAnimePreferenceScreen(): PreferenceScreen {
|
||||
val sourceId = requireArguments().getLong(SOURCE_ID)
|
||||
val source = Injekt.get<AnimeSourceManager>().get(sourceId)!!
|
||||
check(source is ConfigurableAnimeSource)
|
||||
val source = Injekt.get<AnimeSourceManager>().get(sourceId) as? ConfigurableAnimeSource
|
||||
?: error("Source with id: $sourceId not found!")
|
||||
val sharedPreferences =
|
||||
requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||
val dataStore = SharedPreferencesDataStore(sharedPreferences)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ani.dantotsu.settings.paging
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
|
@ -91,18 +92,15 @@ class AnimeExtensionPagingSource(
|
|||
val availableExtensions =
|
||||
availableExtensionsFlow.filterNot { it.pkgName in installedExtensions }
|
||||
val query = searchQuery
|
||||
val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true
|
||||
val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true
|
||||
|
||||
val filteredExtensions = if (query.isEmpty()) {
|
||||
availableExtensions
|
||||
} else {
|
||||
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
val filternfsw = if (isNsfwEnabled) {
|
||||
filteredExtensions
|
||||
} else {
|
||||
filteredExtensions.filterNot { it.isNsfw }
|
||||
}
|
||||
val filternfsw =
|
||||
if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw }
|
||||
return try {
|
||||
val sublist = filternfsw.subList(
|
||||
fromIndex = position,
|
||||
|
@ -176,6 +174,7 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
|
|||
|
||||
init {
|
||||
binding.closeTextView.setOnClickListener {
|
||||
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||
val extension = getItem(bindingAdapterPosition)
|
||||
if (extension != null) {
|
||||
clickListener.onInstallClick(extension)
|
||||
|
@ -198,6 +197,7 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
|
|||
|
||||
val extensionIconImageView: ImageView = binding.extensionIconImageView
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(extension: AnimeExtension.Available) {
|
||||
val nsfw = if (extension.isNsfw) "(18+)" else ""
|
||||
val lang = LanguageMapper.mapLanguageCodeToName(extension.lang)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ani.dantotsu.settings.paging
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
|
@ -91,17 +92,14 @@ class MangaExtensionPagingSource(
|
|||
val availableExtensions =
|
||||
availableExtensionsFlow.filterNot { it.pkgName in installedExtensions }
|
||||
val query = searchQuery
|
||||
val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true
|
||||
val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true
|
||||
val filteredExtensions = if (query.isEmpty()) {
|
||||
availableExtensions
|
||||
} else {
|
||||
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
val filternfsw = if (isNsfwEnabled) {
|
||||
filteredExtensions
|
||||
} else {
|
||||
filteredExtensions.filterNot { it.isNsfw }
|
||||
}
|
||||
val filternfsw =
|
||||
if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw }
|
||||
return try {
|
||||
val sublist = filternfsw.subList(
|
||||
fromIndex = position,
|
||||
|
@ -173,6 +171,7 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
|
|||
|
||||
init {
|
||||
binding.closeTextView.setOnClickListener {
|
||||
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||
val extension = getItem(bindingAdapterPosition)
|
||||
if (extension != null) {
|
||||
clickListener.onInstallClick(extension)
|
||||
|
@ -194,6 +193,8 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
|
|||
}
|
||||
|
||||
val extensionIconImageView: ImageView = binding.extensionIconImageView
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(extension: MangaExtension.Available) {
|
||||
val nsfw = if (extension.isNsfw) "(18+)" else ""
|
||||
val lang = LanguageMapper.mapLanguageCodeToName(extension.lang)
|
||||
|
|
|
@ -176,6 +176,7 @@ class NovelExtensionAdapter(private val clickListener: OnNovelInstallClickListen
|
|||
|
||||
init {
|
||||
binding.closeTextView.setOnClickListener {
|
||||
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||
val extension = getItem(bindingAdapterPosition)
|
||||
if (extension != null) {
|
||||
clickListener.onInstallClick(extension)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue