Merge pull request #152 from rebelonion/dev

Dev
This commit is contained in:
rebel onion 2024-01-22 08:41:40 -06:00 committed by GitHub
commit 4508cada0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
230 changed files with 9161 additions and 3762 deletions

View file

@ -4,10 +4,14 @@ on:
push: push:
branches: branches:
- dev - dev
paths-ignore:
- '**/README.md'
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CI: true
steps: steps:
- name: Checkout repo - name: Checkout repo
@ -28,11 +32,17 @@ jobs:
java-version: 17 java-version: 17
cache: gradle 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 - name: Make gradlew executable
run: chmod +x ./gradlew run: chmod +x ./gradlew
- name: Build with Gradle - 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 - name: Upload a Build Artifact
uses: actions/upload-artifact@v3.0.0 uses: actions/upload-artifact@v3.0.0
@ -44,7 +54,7 @@ jobs:
shell: bash shell: bash
run: | run: |
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" ) 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 - name: Delete Old Pre-Releases
id: delete-pre-releases id: delete-pre-releases

View file

@ -21,7 +21,7 @@ android {
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger()) versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "2.0.0-beta00-i" versionName "2.0.0-beta01-iv1"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
@ -29,12 +29,12 @@ android {
debug { debug {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"] manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
debuggable true debuggable System.getenv("CI") == null
} }
release { release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"] manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
debuggable false debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
} }
} }
buildFeatures { buildFeatures {
@ -54,19 +54,19 @@ android {
dependencies { dependencies {
// Core // Core
implementation 'androidx.appcompat:appcompat:1.6.1' 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.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.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1' 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-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.code.gson:gson:2.10'
implementation 'com.github.Blatzar:NiceHttp:0.4.4' implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.9.0' implementation 'androidx.webkit:webkit:1.9.0'
// Glide // Glide
@ -90,9 +90,12 @@ dependencies {
implementation "androidx.media3:media3-exoplayer-dash:$exo_version" implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version" implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$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 // 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 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'
implementation 'com.flaviofaria:kenburnsview:1.0.7' implementation 'com.flaviofaria:kenburnsview:1.0.7'
@ -100,7 +103,7 @@ dependencies {
implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6' implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' 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 // string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0' 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:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.3.0' implementation 'com.squareup.okio:okio:3.7.0'
implementation 'ch.acra:acra-http:5.9.7' implementation 'ch.acra:acra-http:5.11.3'
implementation 'org.jsoup:jsoup:1.15.4' 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.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43' implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' 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'
} }

View file

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

View file

@ -10,6 +10,8 @@
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <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.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@ -17,25 +19,22 @@
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission
android:maxSdkVersion="32" /> android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs -->
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <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_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
<!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
<!-- To view extension packages in API 30+ --> <uses-permission
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<queries> <queries>
@ -48,6 +47,7 @@
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:banner="@mipmap/ic_banner_foreground"
android:icon="${icon_placeholder}" android:icon="${icon_placeholder}"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
@ -56,14 +56,28 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu" android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="AllowBackup" tools:ignore="AllowBackup">
android:banner="@drawable/ic_banner_foreground"> <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 <activity
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity" android:name=".media.novel.novelreader.NovelReaderActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:exported="true" > android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" /> <data android:mimeType="application/epub+zip" />
@ -71,13 +85,11 @@
<data android:mimeType="application/vnd.amazon.ebook" /> <data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" /> <data android:mimeType="application/fb2+zip" />
<data android:mimeType="application/vnd.comicbook+zip" /> <data android:mimeType="application/vnd.comicbook+zip" />
<data android:pathPattern=".*\\.epub" /> <data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.mobi" /> <data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.kf8" /> <data android:pathPattern=".*\\.kf8" />
<data android:pathPattern=".*\\.fb2" /> <data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.cbz" /> <data android:pathPattern=".*\\.cbz" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:scheme="file" /> <data android:scheme="file" />
</intent-filter> </intent-filter>
@ -103,9 +115,9 @@
<activity <activity
android:name=".media.CalendarActivity" android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity android:name="ani.dantotsu.media.user.ListActivity" /> <activity android:name=".media.user.ListActivity" />
<activity <activity
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity" android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:label="@string/manga" android:label="@string/manga"
@ -118,7 +130,7 @@
<activity android:name=".media.CharacterDetailsActivity" /> <activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" /> <activity android:name=".home.NoInternet" />
<activity <activity
android:name="ani.dantotsu.media.anime.ExoplayerView" android:name=".media.anime.ExoplayerView"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@ -127,7 +139,7 @@
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
tools:targetApi="n" /> tools:targetApi="n" />
<activity <activity
android:name="ani.dantotsu.connections.anilist.Login" android:name=".connections.anilist.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@ -144,7 +156,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="ani.dantotsu.connections.mal.Login" android:name=".connections.mal.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@ -160,8 +172,8 @@
android:scheme="dantotsu" /> android:scheme="dantotsu" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
<activity android:name="ani.dantotsu.connections.discord.Login" android:name=".connections.discord.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@ -172,15 +184,32 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="dantotsu"/> <data android:scheme="dantotsu" />
<data android:scheme="http" /> <data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="discord.dantotsu.com"/> <data android:host="discord.dantotsu.com" />
</intent-filter> </intent-filter>
</activity> </activity>
<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:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@ -216,30 +245,30 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.Main" /> <action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".download.DownloadContainerActivity" />
<activity <activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported="false"
android:exported="false" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity <activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity" 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 <receiver
android:name=".subcriptions.AlarmReceiver" android:name=".subcriptions.AlarmReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="Aani.dantotsu.ACTION_ALARM"/> <action android:name="Aani.dantotsu.ACTION_ALARM" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@ -258,32 +287,49 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<service android:name=".download.video.MyDownloadService" <service
android:exported="false"> android:name=".widgets.CurrentlyAiringRemoteViewsService"
<intent-filter> android:permission="android.permission.BIND_REMOTEVIEWS"
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/> android:exported="true" />
<category android:name="android.intent.category.DEFAULT"/> <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> </intent-filter>
</service> </service>
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService" <service
android:foregroundServiceType="dataSync" android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false" />
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<service android:name=".download.manga.MangaDownloaderService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
<service android:name=".download.novel.NovelDownloaderService" android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
<service android:name=".connections.discord.DiscordService" android:name=".download.manga.MangaDownloaderService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> 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> </application>
</manifest> </manifest>

View file

@ -13,7 +13,9 @@ import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.NovelSources import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.novel.NovelExtensionManager import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
@ -58,6 +60,24 @@ class App : MultiDexApplication() {
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks) registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports) 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(AppModule(this))
Injekt.importModule(PreferenceModule(this)) Injekt.importModule(PreferenceModule(this))
@ -77,13 +97,13 @@ class App : MultiDexApplication() {
animeScope.launch { animeScope.launch {
animeExtensionManager.findAvailableExtensions() animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}") logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow) AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
} }
val mangaScope = CoroutineScope(Dispatchers.Default) val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch { mangaScope.launch {
mangaExtensionManager.findAvailableExtensions() mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow) MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
} }
val novelScope = CoroutineScope(Dispatchers.Default) val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch { novelScope.launch {
@ -91,7 +111,6 @@ class App : MultiDexApplication() {
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow) NovelSources.init(novelExtensionManager.installedExtensionsFlow)
} }
} }

View file

@ -4,6 +4,8 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
@ -28,6 +30,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.* import android.view.animation.*
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
@ -44,6 +47,7 @@ import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.NotificationClickReceiver
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade 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.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.* import kotlinx.coroutines.*
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import java.io.* import java.io.*
@ -124,6 +130,13 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
} }
} catch (e: Exception) { } catch (e: Exception) {
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName)) 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() e.printStackTrace()
} }
return null return null
@ -601,7 +614,7 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
"$APPLICATION_ID.provider", "$APPLICATION_ID.provider",
saveImage( saveImage(
bitmap, bitmap,
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title title
) ?: return ) ?: return
) )
@ -624,13 +637,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? { fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png") val imageFile = File(path, "$imageFileName.png")
return tryWith { return try {
val fOut: OutputStream = FileOutputStream(imageFile) val fOut: OutputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 0, fOut) image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
fOut.close() fOut.close()
scanFile(imageFile.absolutePath, currContext()!!) scanFile(imageFile.absolutePath, currContext()!!)
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path))) toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
imageFile 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") @SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) { 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) val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0) view.addView(v.root, 0)
v.mediaCountdownText.text = v.mediaCountdownText.text =
@ -783,35 +799,40 @@ fun toast(string: String?) {
} }
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) { fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
if (s != null) { try { //I have no idea why this sometimes crashes for some people...
(activity ?: currActivity())?.apply { if (s != null) {
runOnUiThread { (activity ?: currActivity())?.apply {
val snackBar = Snackbar.make( runOnUiThread {
window.decorView.findViewById(android.R.id.content), val snackBar = Snackbar.make(
s, window.decorView.findViewById(android.R.id.content),
Snackbar.LENGTH_SHORT s,
) Snackbar.LENGTH_SHORT
snackBar.view.apply { )
updateLayoutParams<FrameLayout.LayoutParams> { snackBar.view.apply {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) updateLayoutParams<FrameLayout.LayoutParams> {
width = WRAP_CONTENT gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
} width = WRAP_CONTENT
translationY = -(navBarHeight.dp + 32f) }
translationZ = 32f translationY = -(navBarHeight.dp + 32f)
updatePadding(16f.px, right = 16f.px) translationZ = 32f
setOnClickListener { updatePadding(16f.px, right = 16f.px)
snackBar.dismiss() setOnClickListener {
} snackBar.dismiss()
setOnLongClickListener { }
copyToClipboard(clipboard ?: s, false) setOnLongClickListener {
toast(getString(R.string.copied_to_clipboard)) copyToClipboard(clipboard ?: s, false)
true 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() { suspend fun View.pop() {
currActivity()?.runOnUiThread { currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start() ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()

View file

@ -1,6 +1,7 @@
package ani.dantotsu package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
@ -11,12 +12,14 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator import android.view.animation.AnticipateInterpolator
import android.widget.TextView import android.widget.TextView
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -26,11 +29,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.SplashScreenBinding import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment import ani.dantotsu.home.LoginFragment
@ -39,12 +45,14 @@ import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.others.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -54,17 +62,23 @@ import java.io.Serializable
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
private val scope = lifecycleScope private val scope = lifecycleScope
private var load = false private var load = false
private var uiSettings = UserInterfaceSettings() private var uiSettings = UserInterfaceSettings()
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
LangSet.setLocale(this) LangSet.setLocale(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//get FRAGMENT_CLASS_NAME from intent
val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -73,17 +87,59 @@ class MainActivity : AppCompatActivity() {
val backgroundDrawable = _bottomBar.background as GradientDrawable val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0 val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor) backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable _bottomBar.background = backgroundDrawable
} }
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("colorOverflow", false) val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
if (!colorOverflow) { if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray) _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 var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
@ -142,106 +198,146 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach { binding.root.doOnAttach {
initActivity(this) initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings 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> { binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
} }
val offline = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("offlineMode", false)
if (!isOnline(this)) { if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection)) snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java)) startActivity(Intent(this, NoInternet::class.java))
} else { } else {
val model: AnilistHomeViewModel by viewModels() if (offline) {
model.genres.observe(this) { snackString(this@MainActivity.getString(R.string.no_internet_connection))
if (it != null) { startActivity(Intent(this, NoInternet::class.java))
if (it) { } else {
val navbar = binding.includedNavbar.navbar val model: AnilistHomeViewModel by viewModels()
bottomBar = navbar model.genres.observe(this) { it ->
navbar.visibility = View.VISIBLE if (it != null) {
binding.mainProgressBar.visibility = View.GONE if (it) {
val mainViewPager = binding.viewpager val navbar = binding.includedNavbar.navbar
mainViewPager.isUserInputEnabled = false bottomBar = navbar
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle) navbar.visibility = View.VISIBLE
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) binding.mainProgressBar.visibility = View.GONE
navbar.setOnTabSelectListener(object : val mainViewPager = binding.viewpager
AnimatedBottomBar.OnTabSelectListener { mainViewPager.isUserInputEnabled = false
override fun onTabSelected( mainViewPager.adapter =
lastIndex: Int, ViewPagerAdapter(supportFragmentManager, lifecycle)
lastTab: AnimatedBottomBar.Tab?, mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
newIndex: Int, navbar.setOnTabSelectListener(object :
newTab: AnimatedBottomBar.Tab AnimatedBottomBar.OnTabSelectListener {
) { override fun onTabSelected(
navbar.animate().translationZ(12f).setDuration(200).start() lastIndex: Int,
selectedOption = newIndex lastTab: AnimatedBottomBar.Tab?,
mainViewPager.setCurrentItem(newIndex, false) newIndex: Int,
} newTab: AnimatedBottomBar.Tab
}) ) {
navbar.selectTabAt(selectedOption) navbar.animate().translationZ(12f).setDuration(200).start()
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) } selectedOption = newIndex
} else { mainViewPager.setCurrentItem(newIndex, false)
binding.mainProgressBar.visibility = View.GONE }
} })
} navbar.selectTabAt(selectedOption)
} mainViewPager.post {
//Load Data mainViewPager.setCurrentItem(
if (!load) { selectedOption,
scope.launch(Dispatchers.IO) { false
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"))
) )
} }
} 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() {
} }
} }
} }

View file

@ -3,7 +3,10 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.parsers.novel.NovelExtensionManager import ani.dantotsu.parsers.novel.NovelExtensionManager
@ -27,6 +30,7 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
@OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
@ -51,6 +55,8 @@ class AppModule(val app: Application) : InjektModule {
} }
} }
addSingletonFactory { StandaloneDatabaseProvider(app) }
addSingletonFactory { MangaCache() } addSingletonFactory { MangaCache() }
ContextCompat.getMainExecutor(app).execute { ContextCompat.getMainExecutor(app).execute {

View file

@ -10,7 +10,6 @@ import ani.dantotsu.toast
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
fun updateProgress(media: Media, number: String) { fun updateProgress(media: Media, number: String) {
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0) val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)

View file

@ -1,6 +1,7 @@
package ani.dantotsu.connections.anilist package ani.dantotsu.connections.anilist
import android.app.Activity import android.app.Activity
import android.content.Context
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.checkGenreTime import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId 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.Page
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.isOnline
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.media.Author import ani.dantotsu.media.Author
@ -33,6 +35,13 @@ class AnilistQueries {
}.also { println("time : $it") } }.also { println("time : $it") }
val user = response?.data?.user ?: return false 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.userid = user.id
Anilist.username = user.name Anilist.username = user.name
Anilist.bg = user.bannerImage 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.what_did_you_open))
} }
} else { } 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 { val mal = async {
@ -410,8 +421,9 @@ class AnilistQueries {
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder }) sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
sorted["All"] = all sorted["All"] = all
val listsort = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
val sort = sortOrder ?: options?.rowOrder ?.getString("sort_order", "score")
val sort = listsort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) { for (i in sorted.keys) {
when (sort) { when (sort) {
"score" -> sorted[i]?.sortWith { b, a -> "score" -> sorted[i]?.sortWith { b, a ->

View file

@ -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()
}
}

View file

@ -15,43 +15,43 @@ class DownloadsManager(private val context: Context) {
private val gson = Gson() private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList() private val downloadsList = loadDownloads().toMutableList()
val mangaDownloads: List<Download> val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == Download.Type.MANGA } get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
val animeDownloads: List<Download> val animeDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == Download.Type.ANIME } get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
val novelDownloads: List<Download> val novelDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == Download.Type.NOVEL } get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
private fun saveDownloads() { private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList) val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply() prefs.edit().putString("downloads_key", jsonString).apply()
} }
private fun loadDownloads(): List<Download> { private fun loadDownloads(): List<DownloadedType> {
val jsonString = prefs.getString("downloads_key", null) val jsonString = prefs.getString("downloads_key", null)
return if (jsonString != null) { return if (jsonString != null) {
val type = object : TypeToken<List<Download>>() {}.type val type = object : TypeToken<List<DownloadedType>>() {}.type
gson.fromJson(jsonString, type) gson.fromJson(jsonString, type)
} else { } else {
emptyList() emptyList()
} }
} }
fun addDownload(download: Download) { fun addDownload(downloadedType: DownloadedType) {
downloadsList.add(download) downloadsList.add(downloadedType)
saveDownloads() saveDownloads()
} }
fun removeDownload(download: Download) { fun removeDownload(downloadedType: DownloadedType) {
downloadsList.remove(download) downloadsList.remove(downloadedType)
removeDirectory(download) removeDirectory(downloadedType)
saveDownloads() saveDownloads()
} }
fun removeMedia(title: String, type: Download.Type) { fun removeMedia(title: String, type: DownloadedType.Type) {
val subDirectory = if (type == Download.Type.MANGA) { val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga" "Manga"
} else if (type == Download.Type.ANIME) { } else if (type == DownloadedType.Type.ANIME) {
"Anime" "Anime"
} else { } else {
"Novel" "Novel"
@ -71,21 +71,31 @@ class DownloadsManager(private val context: Context) {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
cleanDownloads() 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() saveDownloads()
} }
private fun cleanDownloads() { private fun cleanDownloads() {
cleanDownload(Download.Type.MANGA) cleanDownload(DownloadedType.Type.MANGA)
cleanDownload(Download.Type.ANIME) cleanDownload(DownloadedType.Type.ANIME)
cleanDownload(Download.Type.NOVEL) 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 // 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" "Manga"
} else if (type == Download.Type.ANIME) { } else if (type == DownloadedType.Type.ANIME) {
"Anime" "Anime"
} else { } else {
"Novel" "Novel"
@ -94,18 +104,18 @@ class DownloadsManager(private val context: Context) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory" "Dantotsu/$subDirectory"
) )
val downloadsSubList = if (type == Download.Type.MANGA) { val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
mangaDownloads mangaDownloadedTypes
} else if (type == Download.Type.ANIME) { } else if (type == DownloadedType.Type.ANIME) {
animeDownloads animeDownloadedTypes
} else { } else {
novelDownloads novelDownloadedTypes
} }
if (directory.exists()) { if (directory.exists()) {
val files = directory.listFiles() val files = directory.listFiles()
if (files != null) { if (files != null) {
for (file in files) { for (file in files) {
if (!downloadsSubList.any { it.title == file.name }) { if (!downloadsSubLists.any { it.title == file.name }) {
val deleted = file.deleteRecursively() 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 jsonString = gson.toJson(downloadsList)
val file = File( val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/downloads.json" "Dantotsu/downloads.json"
) )
if (file.parentFile?.exists() == false) { if (file.parentFile?.exists() == false) {
@ -138,25 +148,33 @@ class DownloadsManager(private val context: Context) {
file.writeText(jsonString) file.writeText(jsonString)
} }
fun queryDownload(download: Download): Boolean { fun queryDownload(downloadedType: DownloadedType): Boolean {
return downloadsList.contains(download) return downloadsList.contains(downloadedType)
} }
private fun removeDirectory(download: Download) { fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
val directory = if (download.type == Download.Type.MANGA) { 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( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), 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( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}" "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
) )
} else { } else {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), 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 fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
val directory = if (download.type == Download.Type.MANGA) { val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), 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( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}" "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
) )
} else { } else {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${download.title}/${download.chapter}" "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
) )
} }
val destination = File( val destination = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${download.title}/${download.chapter}" "Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
) )
if (directory.exists()) { if (directory.exists()) {
val copied = directory.copyRecursively(destination, true) val copied = directory.copyRecursively(destination, true)
@ -206,10 +224,10 @@ class DownloadsManager(private val context: Context) {
} }
} }
fun purgeDownloads(type: Download.Type) { fun purgeDownloads(type: DownloadedType.Type) {
val directory = if (type == Download.Type.MANGA) { val directory = if (type == DownloadedType.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/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") File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else { } else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel") 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 novelLocation = "Dantotsu/Novel"
const val mangaLocation = "Dantotsu/Manga" const val mangaLocation = "Dantotsu/Manga"
const val animeLocation = "Dantotsu/Anime" 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 { enum class Type {
MANGA, MANGA,
ANIME, ANIME,

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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?,
)

View file

@ -18,7 +18,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@ -187,7 +187,8 @@ class MangaDownloaderService : Service() {
true true
} }
val deferredList = mutableListOf<Deferred<Bitmap?>>() //val deferredList = mutableListOf<Deferred<Bitmap?>>()
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}") builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
@ -196,15 +197,12 @@ class MangaDownloaderService : Service() {
// Loop through each ImageData object from the task // Loop through each ImageData object from the task
var farthest = 0 var farthest = 0
for ((index, image) in task.imageData.withIndex()) { for ((index, image) in task.imageData.withIndex()) {
// Limit the number of simultaneous downloads from the task if (deferredMap.size >= task.simultaneousDownloads) {
if (deferredList.size >= task.simultaneousDownloads) { deferredMap.values.awaitAll()
// Wait for all deferred to complete and clear the list deferredMap.clear()
deferredList.awaitAll()
deferredList.clear()
} }
// Download the image and add to deferred list deferredMap[index] = async(Dispatchers.IO) {
val deferred = async(Dispatchers.IO) {
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
var retryCount = 0 var retryCount = 0
@ -217,7 +215,6 @@ class MangaDownloaderService : Service() {
retryCount++ retryCount++
} }
// Cache the image if successful
if (bitmap != null) { if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter) saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
} }
@ -233,12 +230,10 @@ class MangaDownloaderService : Service() {
bitmap bitmap
} }
deferredList.add(deferred)
} }
// Wait for any remaining deferred to complete // Wait for any remaining deferred to complete
deferredList.awaitAll() deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete") builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false) .setProgress(0, 0, false)
@ -246,10 +241,10 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task) saveMediaInfo(task)
downloadsManager.addDownload( downloadsManager.addDownload(
Download( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
Download.Type.MANGA DownloadedType.Type.MANGA
) )
) )
broadcastDownloadFinished(task.chapter) broadcastDownloadFinished(task.chapter)
@ -314,7 +309,16 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media) val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) { 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()
}
} }
} }
} }

View file

@ -1,11 +1,13 @@
package ani.dantotsu.download.manga package ani.dantotsu.download.manga
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import ani.dantotsu.R import ani.dantotsu.R
@ -19,6 +21,8 @@ class OfflineMangaAdapter(
private val inflater: LayoutInflater = private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineMangaModel> = items private var originalItems: List<OfflineMangaModel> = items
private var style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
override fun getCount(): Int { override fun getCount(): Int {
return items.size return items.size
@ -32,23 +36,47 @@ class OfflineMangaAdapter(
return position.toLong() return position.toLong()
} }
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view = convertView
if (view == null) { val view: View = convertView ?: when (style) {
view = inflater.inflate(R.layout.item_media_compact, parent, false) 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 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 titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore) val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG) val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing) 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 // 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) imageView.setImageURI(item.image)
titleTextView.text = item.title titleTextView.text = item.title
itemScore.text = item.score itemScore.text = item.score
if (item.isOngoing) { if (item.isOngoing) {
ongoing.visibility = View.VISIBLE ongoing.visibility = View.VISIBLE
} else { } else {
@ -74,4 +102,10 @@ class OfflineMangaAdapter(
this.originalItems = items this.originalItems = items
notifyDataSetChanged() notifyDataSetChanged()
} }
fun notifyNewGrid() {
style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
notifyDataSetChanged()
}
} }

View file

@ -13,21 +13,33 @@ import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.view.animation.OvershootInterpolator import android.view.animation.OvershootInterpolator
import android.widget.AbsListView
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.GridView import android.widget.GridView
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.currActivity
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -45,19 +57,24 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>() private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf() private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter private lateinit var adapter: OfflineMangaAdapter
private lateinit var total : TextView
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): 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) val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
textInputLayout.hint = "Manga"
val currentColor = textInputLayout.boxBackgroundColor val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor textInputLayout.boxBackgroundColor = semiTransparentColor
@ -69,16 +86,13 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar) val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener { animeUserAvatar.setSafeOnClickListener {
animeUserAvatar.setSafeOnClickListener { val dialogFragment =
val dialogFragment = SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
dialogFragment.show( dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
(it.context as AppCompatActivity).supportFragmentManager, }
"dialog" if (!uiSettings.immersiveMode) {
) view.rootView.fitsSystemWindows = true
}
} }
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false ?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) { if (!colorOverflow) {
@ -98,17 +112,65 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
onSearchQuery(s.toString()) 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() getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this) adapter = OfflineMangaAdapter(requireContext(), downloads, this)
gridView.adapter = adapter 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 // Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel val item = adapter.getItem(position) as OfflineMangaModel
val media = val media =
downloadManager.mangaDownloads.firstOrNull { it.title == item.title } downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title } ?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let { media?.let {
startActivity( startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java) 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 // Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel val item = adapter.getItem(position) as OfflineMangaModel
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) { val type: DownloadedType.Type =
Download.Type.MANGA if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
} else { DownloadedType.Type.MANGA
Download.Type.NOVEL } else {
} DownloadedType.Type.NOVEL
}
// Alert dialog to confirm deletion // 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.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?") builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ -> builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type) downloadManager.removeMedia(item.title, type)
getDownloads() getDownloads()
adapter.setItems(downloads) adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
} }
builder.setNegativeButton("No") { _, _ -> builder.setNegativeButton("No") { _, _ ->
// Do nothing // Do nothing
} }
val dialog = builder.show() val dialog = builder.show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
true true
} }
return view
} }
override fun onSearchQuery(query: String) { override fun onSearchQuery(query: String) {
@ -154,6 +217,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initActivity(requireActivity())
var height = statusBarHeight var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
@ -170,7 +234,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
} }
} }
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop) val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
var visible = false scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
val visible = false
fun animate() { fun animate() {
val start = if (visible) 0f else 1f val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f val end = if (!visible) 0f else 1f
@ -187,9 +254,30 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
} }
scrollTop.setOnClickListener { 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() { override fun onResume() {
@ -215,19 +303,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() { private fun getDownloads() {
downloads = listOf() downloads = listOf()
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct() val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>() val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) { 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 download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel newMangaDownloads += offlineMangaModel
} }
downloads = newMangaDownloads downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct() val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>() val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) { 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 download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel newNovelDownloads += offlineMangaModel
@ -236,17 +324,17 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
} }
private fun getMedia(download: Download): Media? { private fun getMedia(downloadedType: DownloadedType): Media? {
val type = if (download.type == Download.Type.MANGA) { val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga" "Manga"
} else if (download.type == Download.Type.ANIME) { } else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime" "Anime"
} else { } else {
"Novel" "Novel"
} }
val directory = File( val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}" "Dantotsu/$type/${downloadedType.title}"
) )
//load media.json and convert to media class with gson //load media.json and convert to media class with gson
return try { return try {
@ -266,44 +354,72 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
} }
} }
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel { private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = if (download.type == Download.Type.MANGA) { val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga" "Manga"
} else if (download.type == Download.Type.ANIME) { } else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime" "Anime"
} else { } else {
"Novel" "Novel"
} }
val directory = File( val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}" "Dantotsu/$type/${downloadedType.title}"
) )
//load media.json and convert to media class with gson //load media.json and convert to media class with gson
try { try {
val media = File(directory, "media.json") val media = File(directory, "media.json")
val mediaJson = media.readText() val mediaJson = media.readText()
val mediaModel = getMedia(download)!! val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg") val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) { val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover) Uri.fromFile(cover)
} else { } else null
null val banner = File(directory, "banner.jpg")
} val bannerUri: Uri? = if (banner.exists()) {
val title = mediaModel.nameMAL ?: mediaModel.nameRomaji Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString() ?: 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 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) { } catch (e: Exception) {
logger("Error loading media.json: ${e.message}") logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e) FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel("unknown", "0", false, false, null) return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
false,
false,
null,
null
)
} }
} }
} }
interface OfflineMangaSearchListener { interface OfflineMangaSearchListener {
fun onSearchQuery(query: String) fun onSearchQuery(query: String)
} }

View file

@ -5,7 +5,12 @@ import android.net.Uri
data class OfflineMangaModel( data class OfflineMangaModel(
val title: String, val title: String,
val score: String, val score: String,
val totalChapter: String,
val readChapter: String,
val type: String,
val chapters: String,
val isOngoing: Boolean, val isOngoing: Boolean,
val isUserScored: Boolean, val isUserScored: Boolean,
val image: Uri? val image: Uri?,
val banner: Uri?
) )

View file

@ -17,7 +17,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@ -330,10 +330,10 @@ class NovelDownloaderService : Service() {
saveMediaInfo(task) saveMediaInfo(task)
downloadsManager.addDownload( downloadsManager.addDownload(
Download( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
Download.Type.NOVEL DownloadedType.Type.NOVEL
) )
) )
broadcastDownloadFinished(task.originalLink) broadcastDownloadFinished(task.originalLink)

View file

@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R import ani.dantotsu.R
@UnstableApi @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 { companion object {
private const val JOB_ID = 1 private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1 private const val FOREGROUND_NOTIFICATION_ID = 1

View file

@ -1,8 +1,19 @@
package ani.dantotsu.download.video package ani.dantotsu.download.video
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri 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.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes 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.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadHelper import androidx.media3.exoplayer.offline.DownloadHelper
import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.offline.DownloadService
@ -22,7 +34,12 @@ import androidx.media3.exoplayer.scheduler.Requirements
import androidx.media3.ui.TrackSelectionDialogBuilder import androidx.media3.ui.TrackSelectionDialogBuilder
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.defaultHeaders 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.logError
import ani.dantotsu.media.Media
import ani.dantotsu.okHttpClient import ani.dantotsu.okHttpClient
import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
@ -37,6 +54,8 @@ import java.util.concurrent.*
object Helper { object Helper {
private var simpleCache: SimpleCache? = null
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) { fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory { val dataSourceFactory = DataSource.Factory {
@ -82,27 +101,13 @@ object Helper {
) )
downloadHelper.prepare(object : DownloadHelper.Callback { downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) { override fun onPrepared(helper: DownloadHelper) {
TrackSelectionDialogBuilder( helper.getDownloadRequest(null).let {
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
DownloadService.sendAddDownload( DownloadService.sendAddDownload(
context, context,
MyDownloadService::class.java, ExoplayerDownloadService::class.java,
helper.getDownloadRequest(null), it,
false 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 var download: DownloadManager? = null
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads" private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Synchronized @Synchronized
@UnstableApi @UnstableApi
fun downloadManager(context: Context): DownloadManager { fun downloadManager(context: Context): DownloadManager {
return download ?: let { return download ?: let {
val database = StandaloneDatabaseProvider(context) val database = Injekt.get<StandaloneDatabaseProvider>()
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory { val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
@ -133,17 +138,42 @@ object Helper {
} }
dataSource dataSource
} }
DownloadManager( val threadPoolSize = Runtime.getRuntime().availableProcessors()
val executorService = Executors.newFixedThreadPool(threadPoolSize)
val downloadManager = DownloadManager(
context, context,
database, database,
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database), getSimpleCache(context),
dataSourceFactory, dataSourceFactory,
Executor(Runnable::run) executorService
).apply { ).apply {
requirements = requirements =
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3 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!! 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
}
} }

View file

@ -47,6 +47,7 @@ import kotlin.math.min
class AnimeFragment : Fragment() { class AnimeFragment : Fragment() {
private var _binding: FragmentAnimeBinding? = null private var _binding: FragmentAnimeBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var animePageAdapter: AnimePageAdapter
private var uiSettings: UserInterfaceSettings = private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings() loadData("ui_settings") ?: UserInterfaceSettings()
@ -95,7 +96,7 @@ class AnimeFragment : Fragment() {
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val animePageAdapter = AnimePageAdapter() animePageAdapter = AnimePageAdapter()
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
@ -277,6 +278,11 @@ class AnimeFragment : Fragment() {
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
if (animePageAdapter.trendingViewPager != null) {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
super.onResume() super.onResume()
} }
} }

View file

@ -111,7 +111,6 @@ class HomeFragment : Fragment() {
snackString(currContext()?.getString(R.string.please_reload)) snackString(currContext()?.getString(R.string.please_reload))
} }
} }
binding.homeUserAvatarContainer.setSafeOnClickListener { binding.homeUserAvatarContainer.setSafeOnClickListener {
val dialogFragment = val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)

View file

@ -28,5 +28,6 @@ class LoginFragment : Fragment() {
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) } binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) } binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) } binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
} }
} }

View file

@ -43,6 +43,7 @@ import kotlin.math.min
class MangaFragment : Fragment() { class MangaFragment : Fragment() {
private var _binding: FragmentMangaBinding? = null private var _binding: FragmentMangaBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var mangaPageAdapter: MangaPageAdapter
private var uiSettings: UserInterfaceSettings = private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings() loadData("ui_settings") ?: UserInterfaceSettings()
@ -90,7 +91,7 @@ class MangaFragment : Fragment() {
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val mangaPageAdapter = MangaPageAdapter() mangaPageAdapter = MangaPageAdapter()
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
model.notSet = false model.notSet = false
@ -251,6 +252,11 @@ class MangaFragment : Fragment() {
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) 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() super.onResume()
} }

View file

@ -4,8 +4,11 @@ import android.content.Context
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
@ -17,6 +20,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.databinding.ActivityNoInternetBinding import ani.dantotsu.databinding.ActivityNoInternetBinding
import ani.dantotsu.download.anime.OfflineAnimeFragment
import ani.dantotsu.download.manga.OfflineMangaFragment import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData import ani.dantotsu.loadData
@ -25,6 +29,7 @@ import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.selectedOption import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import nl.joery.animatedbottombar.AnimatedBottomBar 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 { binding.root.doOnAttach {
initActivity(this) initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab selectedOption = uiSettings.defaultStartUpTab
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
@ -96,12 +115,11 @@ class NoInternet : AppCompatActivity() {
override fun getItemCount(): Int = 3 override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
when (position) { return when (position) {
0 -> return OfflineFragment() 0 -> OfflineAnimeFragment()
1 -> return OfflineFragment() 2 -> OfflineMangaFragment()
2 -> return OfflineMangaFragment() else -> OfflineFragment()
} }
return LoginFragment()
} }
} }
} }

View file

@ -4,11 +4,13 @@ import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R import ani.dantotsu.R
@ -16,8 +18,10 @@ import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.media.user.ListViewPagerAdapter import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@ -76,6 +80,10 @@ class CalendarActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN WindowManager.LayoutParams.FLAG_FULLSCREEN
) )
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
} }
setContentView(binding.root) setContentView(binding.root)

View file

@ -118,6 +118,19 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji 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 { object MediaSingleton {
var media: Media? = null var media: Media? = null
var bitmap: Bitmap? = null var bitmap: Bitmap? = null

View file

@ -13,7 +13,10 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -136,7 +139,7 @@ class MediaAdaptor(
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400) b.itemCompactBanner.loadImage(media.banner ?: media.cover)
b.itemCompactOngoing.visibility = b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
@ -319,6 +322,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
@ -332,6 +336,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
@ -346,6 +351,7 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener { binding.itemCompactImage.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
@ -361,12 +367,14 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener { binding.itemCompactImage.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
binding.itemCompactTitleContainer.setSafeOnClickListener { binding.itemCompactTitleContainer.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) 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) { if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (bitmap != null) MediaSingleton.bitmap = bitmap if (bitmap != null) MediaSingleton.bitmap = bitmap
@ -384,7 +392,13 @@ class MediaAdaptor(
Intent(activity, MediaDetailsActivity::class.java).putExtra( Intent(activity, MediaDetailsActivity::class.java).putExtra(
"media", "media",
media as Serializable media as Serializable
), null ), ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
Pair.create(
itemCompactImage,
ViewCompat.getTransitionName(activity.findViewById(R.id.itemCompactImage))!!
),
).toBundle()
) )
} }
} }

View file

@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
@ -42,7 +43,6 @@ import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.saveData import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
@ -72,11 +72,17 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
LangSet.setLocale(this) super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: return 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) ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null MediaSingleton.bitmap = null
super.onCreate(savedInstanceState)
binding = ActivityMediaBinding.inflate(layoutInflater) binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat() screenWidth = resources.displayMetrics.widthPixels.toFloat()
@ -85,12 +91,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
initActivity(this) initActivity(this)
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() 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.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight } binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> { 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 } 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 { binding.mediaTitle.setOnLongClickListener {
copyToClipboard(media.userPreferredName) copyToClipboard(media.userPreferredName)
true true
@ -305,7 +316,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
adult = media.isAdult adult = media.isAdult
tabLayout.menu.clear() tabLayout.menu.clear()
if (media.anime != null) { if (media.anime != null) {
viewPager.adapter = viewPager.adapter =
@ -317,7 +327,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
lifecycle, lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA 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 anime = false
} }
@ -442,7 +456,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth) ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
.setDuration(duration).start() .setDuration(duration).start()
binding.mediaBanner.pause() binding.mediaBanner.pause()
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
} }
if (percentage <= percent && isCollapsed) { if (percentage <= percent && isCollapsed) {
isCollapsed = false isCollapsed = false
@ -455,7 +468,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f) ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
.setDuration(duration).start() .setDuration(duration).start()
if (uiSettings.bannerAnimations) binding.mediaBanner.resume() if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
} }
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue( if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
false false
@ -532,5 +544,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
image.alpha = if (disabled) 0.33f else 1f image.alpha = if (disabled) 0.33f else 1f
} }
} }
companion object {
var mediaSingleton: Media? = null
}
} }

View file

@ -242,7 +242,8 @@ class MediaDetailsViewModel : ViewModel() {
i: String, i: String,
manager: FragmentManager, manager: FragmentManager,
launch: Boolean = true, launch: Boolean = true,
prevEp: String? = null prevEp: String? = null,
isDownload: Boolean = false
) { ) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) { if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
@ -254,13 +255,17 @@ class MediaDetailsViewModel : ViewModel() {
} }
media.selected = this.loadSelected(media) media.selected = this.loadSelected(media)
val selector = val selector =
SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp) SelectorDialogFragment.newInstance(
media.selected!!.server,
launch,
prevEp,
isDownload
)
selector.show(manager, "dialog") selector.show(manager, "dialog")
} }
} }
} }
//Manga //Manga
var mangaReadSources: MangaReadSources? = null var mangaReadSources: MangaReadSources? = null
@ -314,7 +319,8 @@ class MediaDetailsViewModel : ViewModel() {
val novelSources = NovelSources val novelSources = NovelSources
val novelResponses = MutableLiveData<List<ShowResponse>>(null) val novelResponses = MutableLiveData<List<ShowResponse>>(null)
suspend fun searchNovels(query: String, i: Int) { 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) { tryWithSuspend(post = true) {
if (source != null) { if (source != null) {
novelResponses.postValue(source.search(query)) novelResponses.postValue(source.search(query))

View file

@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -59,6 +60,7 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels() 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.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.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 } binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
@ -101,29 +103,33 @@ class MediaInfoFragment : Fragment() {
if (media.anime.mainStudio != null) { if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
binding.mediaInfoStudioContainer.setOnClickListener { if (!offline) {
ContextCompat.startActivity( binding.mediaInfoStudioContainer.setOnClickListener {
requireActivity(), ContextCompat.startActivity(
Intent(activity, StudioActivity::class.java).putExtra( requireActivity(),
"studio", Intent(activity, StudioActivity::class.java).putExtra(
media.anime.mainStudio!! as Serializable "studio",
), media.anime.mainStudio!! as Serializable
null ),
) null
)
}
} }
} }
if (media.anime.author != null) { if (media.anime.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.anime.author!!.name binding.mediaInfoAuthor.text = media.anime.author!!.name
binding.mediaInfoAuthorContainer.setOnClickListener { if (!offline) {
ContextCompat.startActivity( binding.mediaInfoAuthorContainer.setOnClickListener {
requireActivity(), ContextCompat.startActivity(
Intent(activity, AuthorActivity::class.java).putExtra( requireActivity(),
"author", Intent(activity, AuthorActivity::class.java).putExtra(
media.anime.author!! as Serializable "author",
), media.anime.author!! as Serializable
null ),
) null
)
}
} }
} }
binding.mediaInfoTotalTitle.setText(R.string.total_eps) binding.mediaInfoTotalTitle.setText(R.string.total_eps)
@ -137,15 +143,17 @@ class MediaInfoFragment : Fragment() {
if (media.manga.author != null) { if (media.manga.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.manga.author!!.name binding.mediaInfoAuthor.text = media.manga.author!!.name
binding.mediaInfoAuthorContainer.setOnClickListener { if (!offline) {
ContextCompat.startActivity( binding.mediaInfoAuthorContainer.setOnClickListener {
requireActivity(), ContextCompat.startActivity(
Intent(activity, AuthorActivity::class.java).putExtra( requireActivity(),
"author", Intent(activity, AuthorActivity::class.java).putExtra(
media.manga.author!! as Serializable "author",
), media.manga.author!! as Serializable
null ),
) null
)
}
} }
} }
} }
@ -189,7 +197,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.trailer != null) { if (media.trailer != null && !offline) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class MyChrome : WebChromeClient() { class MyChrome : WebChromeClient() {
private var mCustomView: View? = null private var mCustomView: View? = null
@ -243,7 +251,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) 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()) val markWon = Markwon.builder(requireContext())
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build() .usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
@ -304,7 +312,7 @@ class MediaInfoFragment : Fragment() {
} }
} }
if (media.genres.isNotEmpty()) { if (media.genres.isNotEmpty() && !offline) {
val bind = ActivityGenreBinding.inflate( val bind = ActivityGenreBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@ -335,7 +343,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.tags.isNotEmpty()) { if (media.tags.isNotEmpty() && !offline) {
val bind = ItemTitleChipgroupBinding.inflate( val bind = ItemTitleChipgroupBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@ -376,7 +384,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (!media.characters.isNullOrEmpty()) { if (!media.characters.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate( val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@ -393,7 +401,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (!media.relations.isNullOrEmpty()) { if (!media.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) { if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate( val bind = ItemQuelsBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
@ -456,7 +464,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bindi.root) parent.addView(bindi.root)
} }
if (!media.recommendations.isNullOrEmpty()) { if (!media.recommendations.isNullOrEmpty() && !offline ) {
val bind = ItemTitleRecyclerBinding.inflate( val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,

View file

@ -16,7 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListBinding 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -196,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
} }
media?.inCustomListsOf?.forEach { media?.inCustomListsOf?.forEach {
SwitchMaterial(requireContext()).apply { MaterialSwitch(requireContext()).apply {
isChecked = it.value isChecked = it.value
text = it.key text = it.key
setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->

View file

@ -78,7 +78,7 @@ class SearchActivity : AppCompatActivity() {
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
val headerAdaptor = SearchAdapter(this) val headerAdaptor = SearchAdapter(this)
val gridSize = (screenWidth / 124f).toInt() val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize) val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
@ -159,7 +159,7 @@ class SearchActivity : AppCompatActivity() {
fun search() { fun search() {
val size = model.searchResults.results.size val size = model.searchResults.results.size
model.searchResults.results.clear() model.searchResults.results.clear()
runOnUiThread { binding.searchRecyclerView.post {
mediaAdaptor.notifyItemRangeRemoved(0, size) mediaAdaptor.notifyItemRangeRemoved(0, size)
} }

View file

@ -1,6 +1,8 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
@ -10,10 +12,14 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL 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.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.saveData import ani.dantotsu.saveData
@ -54,6 +60,14 @@ class SearchAdapter(private val activity: SearchActivity) :
} }
binding.searchBar.hint = activity.result.type 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 adult = activity.result.isAdult
var listOnly = activity.result.onList var listOnly = activity.result.onList

View file

@ -1,19 +1,23 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Context import android.content.Context
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
class SubtitleDownloader { class SubtitleDownloader {
companion object { companion object {
//doesn't really download the subtitles -\_(o_o)_/- //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) { withContext(Dispatchers.IO) {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val networkHelper = Injekt.get<NetworkHelper>() val networkHelper = Injekt.get<NetworkHelper>()
@ -29,8 +33,8 @@ class SubtitleDownloader {
val subtitleType = when { val subtitleType = when {
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS responseBody.contains("[Script Info]") -> SubtitleType.ASS
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT responseBody.contains("WEBVTT") -> SubtitleType.VTT
else -> SubtitleType.SRT else -> SubtitleType.SRT
} }
@ -39,5 +43,41 @@ class SubtitleDownloader {
return@withContext SubtitleType.UNKNOWN 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
}
}
} }
} }

View file

@ -5,8 +5,13 @@ import java.util.regex.Pattern
class AnimeNameAdapter { class AnimeNameAdapter {
companion object { 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? { fun findSeasonNumber(text: String): Int? {
val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)"
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE) val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text) val seasonMatcher: Matcher = seasonPattern.matcher(text)
@ -16,5 +21,56 @@ class AnimeNameAdapter {
null 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
}
}
} }
} }

View file

@ -1,32 +1,41 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageView import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AnimeWatchAdapter( class AnimeWatchAdapter(
private val media: Media, private val media: Media,
private val fragment: AnimeWatchFragment, private val fragment: AnimeWatchFragment,
@ -41,6 +50,9 @@ class AnimeWatchAdapter(
return ViewHolder(bind) return ViewHolder(bind)
} }
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
@ -78,6 +90,17 @@ class AnimeWatchAdapter(
null 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 //Source Selection
var source = var source =
@ -147,8 +170,9 @@ class AnimeWatchAdapter(
} }
} }
//Icons
//Subscription //subscribe
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.animeSourceSubscribe,
@ -167,44 +191,99 @@ class AnimeWatchAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id)) openSettings(fragment.requireContext(), getChannelId(true, media.id))
} }
//Icons //Nested Button
var reversed = media.selected!!.recyclerReversed binding.animeNestedButton.setOnClickListener {
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView val dialogView =
binding.animeSourceTop.rotation = if (reversed) -90f else 90f LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
binding.animeSourceTop.setOnClickListener { val dialogBinding = DialogLayoutBinding.bind(dialogView)
reversed = !reversed var refresh = false
binding.animeSourceTop.rotation = if (reversed) -90f else 90f var run = false
fragment.onIconPressed(style, reversed) var reversed = media.selected!!.recyclerReversed
} var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
var selected = when (style) { dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
0 -> binding.animeSourceList dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
1 -> binding.animeSourceGrid dialogBinding.animeSourceTop.setOnClickListener {
2 -> binding.animeSourceCompact reversed = !reversed
else -> binding.animeSourceList dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
} dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
selected.alpha = 1f run = true
fun selected(it: ImageView) { }
selected.alpha = 0.33f //Grids
selected = it 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 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 //Episode Handling
handleEpisodes() handleEpisodes()
} }
@ -304,12 +383,15 @@ class AnimeWatchAdapter(
} }
} }
val ep = media.anime.episodes!![continueEp]!! val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage( binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0 ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
) )
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text = 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 { binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp) fragment.onEpisodeClick(continueEp)
} }
@ -322,6 +404,7 @@ class AnimeWatchAdapter(
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty()) if (media.anime.episodes!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE 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 val binding = _binding
if (watchSources is AnimeSources) { if (watchSources is AnimeSources) {
val parser = watchSources[source] as? DynamicAnimeParser val parser = watchSources[source] as? DynamicAnimeParser
@ -351,12 +434,16 @@ class AnimeWatchAdapter(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown" parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
) )
} }
binding?.animeSourceLanguage?.setAdapter( val adapter = ArrayAdapter(
ArrayAdapter( fragment.requireContext(),
fragment.requireContext(), R.layout.item_dropdown,
R.layout.item_dropdown, parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
parser.extension.sources.map { 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) countDown(media, binding.animeSourceContainer)
} }
} }
} }

View file

@ -2,6 +2,10 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog 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.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -9,20 +13,29 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope 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.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.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.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.HAnimeSources
@ -42,6 +55,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -61,6 +76,8 @@ class AnimeWatchFragment : Fragment() {
private lateinit var headerAdapter: AnimeWatchAdapter private lateinit var headerAdapter: AnimeWatchAdapter
private lateinit var episodeAdapter: EpisodeAdapter private lateinit var episodeAdapter: EpisodeAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f var screenWidth = 0f
private var progress = View.VISIBLE private var progress = View.VISIBLE
@ -80,6 +97,21 @@ class AnimeWatchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
addAction(ACTION_DOWNLOAD_FAILED)
addAction(ACTION_DOWNLOAD_PROGRESS)
}
ContextCompat.registerReceiver(
requireContext(),
downloadStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
@ -134,9 +166,17 @@ class AnimeWatchFragment : Fragment() {
if (!loaded) { if (!loaded) {
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
val offlineMode =
model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!) headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
episodeAdapter = episodeAdapter =
EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this) EpisodeAdapter(
style ?: uiSettings.animeDefaultView,
media,
this,
offlineMode = offlineMode
)
binding.animeSourceRecycler.adapter = binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter) ConcatAdapter(headerAdapter, episodeAdapter)
@ -169,12 +209,15 @@ class AnimeWatchFragment : Fragment() {
if (media.anime?.kitsuEpisodes != null) { if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) { if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc = episode.desc =
episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title episode.title ?: ""
episode.thumb = ).isBlank()
episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ) media.anime!!.kitsuEpisodes!![i]?.title
?: FileUrl[media.cover] ?: 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 if (show) View.GONE else View.VISIBLE
} }
} }
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>() val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) { if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0] var selectedSetting = allSettings[0]
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray() val names =
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0 var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source") .setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which -> .setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex] selectedSetting = allSettings[selectedIndex]
itemSelected = true
dialog.dismiss() dialog.dismiss()
// Move the fragment transaction here // Move the fragment transaction here
@ -343,10 +387,10 @@ class AnimeWatchFragment : Fragment() {
.commit() .commit()
} }
} }
.setNegativeButton("Cancel") { dialog, _ -> .setOnDismissListener {
dialog.cancel() if (!itemSelected) {
changeUIVisibility(true) changeUIVisibility(true)
return@setNegativeButton }
} }
.show() .show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
@ -379,6 +423,93 @@ class AnimeWatchFragment : Fragment() {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) 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") @SuppressLint("NotifyDataSetChanged")
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
@ -391,6 +522,8 @@ class AnimeWatchFragment : Fragment() {
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes() headerAdapter.handleEpisodes()
val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
episodeAdapter.offlineMode = isDownloaded
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf() var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) { if (media.anime!!.episodes != null) {
@ -405,11 +538,20 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.arr = arr episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView) episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size) episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName()) {
episodeAdapter.stopDownload(download.chapter)
}
}
} }
override fun onDestroy() { override fun onDestroy() {
model.watchSources?.flushText() model.watchSources?.flushText()
super.onDestroy() super.onDestroy()
try {
requireContext().unregisterReceiver(downloadStatusReceiver)
} catch (_: IllegalArgumentException) {
}
} }
var state: Parcelable? = null var state: Parcelable? = null
@ -424,4 +566,12 @@ class AnimeWatchFragment : Fragment() {
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
} }
} companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
}
}

View file

@ -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)
}

View file

@ -14,7 +14,8 @@ data class Episode(
var selectedExtractor: String? = null, var selectedExtractor: String? = null,
var selectedVideo: Int = 0, var selectedVideo: Int = 0,
var selectedSubtitle: Int? = -1, 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, @Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
var allStreams: Boolean = false, var allStreams: Boolean = false,
var watched: Long? = null, var watched: Long? = null,

View file

@ -1,19 +1,32 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.LinearLayout 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 androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding import ani.dantotsu.databinding.ItemEpisodeListBinding
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl 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) { fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
val curr = loadData<Long>("${mediaId}_${ep}") 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( class EpisodeAdapter(
private var type: Int, private var type: Int,
private val media: Media, private val media: Media,
private val fragment: AnimeWatchFragment, private val fragment: AnimeWatchFragment,
var arr: List<Episode> = arrayListOf() var arr: List<Episode> = arrayListOf(),
var offlineMode: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) { return (when (viewType) {
0 -> EpisodeListViewHolder( 0 -> EpisodeListViewHolder(
@ -76,12 +100,11 @@ class EpisodeAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position] val ep = arr[position]
val title = val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
"${ ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString( } else {
R.string.episode_singular ep.number
) } ?: ""
} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
when (holder) { when (holder) {
is EpisodeListViewHolder -> { is EpisodeListViewHolder -> {
@ -93,7 +116,7 @@ class EpisodeAdapter(
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage) .into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
if (ep.filler) { if (ep.filler) {
binding.itemEpisodeFiller.visibility = View.VISIBLE binding.itemEpisodeFiller.visibility = View.VISIBLE
@ -102,6 +125,7 @@ class EpisodeAdapter(
binding.itemEpisodeFiller.visibility = View.GONE binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE binding.itemEpisodeFillerView.visibility = View.GONE
} }
holder.bind(ep.number, ep.downloadProgress)
binding.itemEpisodeDesc.visibility = binding.itemEpisodeDesc.visibility =
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: "" binding.itemEpisodeDesc.text = ep.desc ?: ""
@ -205,6 +229,80 @@ class EpisodeAdapter(
override fun getItemCount(): Int = arr.size 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) : inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
@ -227,11 +325,27 @@ class EpisodeAdapter(
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>()
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number) 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 { binding.itemEpisodeDesc.setOnClickListener {
if (binding.itemEpisodeDesc.maxLines == 3) if (binding.itemEpisodeDesc.maxLines == 3)
binding.itemEpisodeDesc.maxLines = 100 binding.itemEpisodeDesc.maxLines = 100
@ -239,11 +353,73 @@ class EpisodeAdapter(
binding.itemEpisodeDesc.maxLines = 3 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) { fun updateType(t: Int) {
type = t 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)
}
} }

View file

@ -14,7 +14,6 @@ import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.hardware.Sensor
import android.hardware.SensorManager import android.hardware.SensorManager
import android.media.AudioManager import android.media.AudioManager
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.WindowCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.* import androidx.media3.common.*
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
import androidx.media3.common.C.TRACK_TYPE_VIDEO import androidx.media3.common.C.TRACK_TYPE_VIDEO
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
@ -58,6 +61,7 @@ import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.ui.* import androidx.media3.ui.*
import androidx.media3.ui.CaptionStyleCompat.* import androidx.media3.ui.CaptionStyleCompat.*
import androidx.mediarouter.app.MediaRouteButton
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist 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.discord.RPC
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding import ani.dantotsu.databinding.ActivityExoplayerBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.SubtitleDownloader import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.others.AniSkip import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.AniSkip.getType import ani.dantotsu.others.AniSkip.getType
import ani.dantotsu.others.Download.download
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.others.ResettableTimer import ani.dantotsu.others.ResettableTimer
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
@ -82,6 +86,10 @@ import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.bumptech.glide.Glide 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.android.material.slider.Slider
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.lagradost.nicehttp.ignoreAllSSLErrors import com.lagradost.nicehttp.ignoreAllSSLErrors
@ -97,10 +105,9 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@UnstableApi @UnstableApi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
class ExoplayerView : AppCompatActivity(), Player.Listener { class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityListener {
private val resumeWindow = "resumeWindow" private val resumeWindow = "resumeWindow"
private val resumePosition = "resumePosition" private val resumePosition = "resumePosition"
@ -108,6 +115,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
private val playerOnPlay = "playerOnPlay" private val playerOnPlay = "playerOnPlay"
private lateinit var exoPlayer: ExoPlayer 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 trackSelector: DefaultTrackSelector
private lateinit var cacheFactory: CacheDataSource.Factory private lateinit var cacheFactory: CacheDataSource.Factory
private lateinit var playbackParameters: PlaybackParameters private lateinit var playbackParameters: PlaybackParameters
@ -145,6 +155,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
private var orientationListener: OrientationEventListener? = null private var orientationListener: OrientationEventListener? = null
private var downloadId: String? = null
companion object { companion object {
var initialized = false var initialized = false
lateinit var media: Media lateinit var media: Media
@ -328,6 +340,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
setContentView(binding.root) setContentView(binding.root)
//Initialize //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) WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars() hideSystemBars()
@ -387,7 +409,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
orientationListener = orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) { object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) { override fun onOrientationChanged(orientation: Int) {
println(orientation)
if (orientation in 45..135) { if (orientation in 45..135) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility = if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility =
View.VISIBLE View.VISIBLE
@ -466,12 +487,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (isInitialized) { if (isInitialized) {
isPlayerPlaying = exoPlayer.isPlaying isPlayerPlaying = exoPlayer.isPlaying
(exoPlay.drawable as Animatable?)?.start() (exoPlay.drawable as Animatable?)?.start()
if (isPlayerPlaying) { if (isPlayerPlaying || castPlayer?.isPlaying == true) {
Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay) Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay)
exoPlayer.pause() exoPlayer.pause()
castPlayer?.pause()
} else { } else {
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay) if (castPlayer?.isPlaying == false && castPlayer?.currentMediaItem != null) {
exoPlayer.play() 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) audioManager.setStreamVolume(STREAM_MUSIC, volume, 0)
volumeHide() volumeHide()
} }
val fastForward = playerView.findViewById<TextView>(R.id.exo_fast_forward_text)
fun fastForward() { fun fastForward() {
isFastForwarding = true isFastForwarding = true
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2) 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() { fun stopFastForward() {
if (isFastForwarding) { if (isFastForwarding) {
isFastForwarding = false isFastForwarding = false
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2) 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() episodeArr = episodes.keys.toList()
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!) currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
episodeTitleArr = arrayListOf() episodeTitleArr = arrayListOf<String>()
episodes.forEach { episodes.forEach {
val episode = it.value 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 //Episode Change
@ -1074,10 +1103,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
//Cast //Cast
if (settings.cast) { if (settings.cast) {
playerView.findViewById<ImageButton>(R.id.exo_cast).apply { playerView.findViewById<MediaRouteButton>(R.id.exo_cast).apply {
visibility = View.VISIBLE visibility = View.VISIBLE
setSafeOnClickListener { try {
CastButtonFactory.setUpMediaRouteButton(context, this)
dialogFactory = CustomCastThemeFactory()
} catch (e: Exception) {
isCastApiAvailable = false
}
setOnLongClickListener {
cast() cast()
true
} }
} }
} }
@ -1101,7 +1137,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (settings.cursedSpeeds) if (settings.cursedSpeeds)
arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f) arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
else 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() val speedsName = speeds.map { "${it}x" }.toTypedArray()
var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed
@ -1156,14 +1206,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
preloading = false preloading = false
val incognito = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("incognito", false) ?: false
val showProgressDialog = val showProgressDialog =
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
?: true else false ?: true else false
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
AlertDialog.Builder(this, R.style.MyPopup) AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.auto_update, media.userPreferredName)) .setTitle(getString(R.string.auto_update, media.userPreferredName))
.setMessage(getString(R.string.incognito_will_not_update))
.apply { .apply {
if (incognito) {
setMessage(getString(R.string.incognito_will_not_update))
}
setOnCancelListener { hideSystemBars() } setOnCancelListener { hideSystemBars() }
setCancelable(false) setCancelable(false)
setPositiveButton(getString(R.string.yes)) { dialog, _ -> setPositiveButton(getString(R.string.yes)) { dialog, _ ->
@ -1232,7 +1286,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (subtitle?.type == SubtitleType.UNKNOWN) { if (subtitle?.type == SubtitleType.UNKNOWN) {
val context = this val context = this
runBlocking { runBlocking {
val type = SubtitleDownloader.downloadSubtitles(context, subtitle!!.file.url) val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
val fileUri = Uri.parse(subtitle!!.file.url) val fileUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration sub = MediaItem.SubtitleConfiguration
.Builder(fileUri) .Builder(fileUri)
@ -1250,8 +1304,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
println("sub: $sub") println("sub: $sub")
} else { } else {
val subUri = Uri.parse((subtitle!!.file.url))
sub = MediaItem.SubtitleConfiguration sub = MediaItem.SubtitleConfiguration
.Builder(Uri.parse(subtitle!!.file.url)) .Builder(subUri)
.setSelectionFlags(C.SELECTION_FLAG_FORCED) .setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType( .setMimeType(
when (subtitle?.type) { when (subtitle?.type) {
@ -1270,18 +1325,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
ext.onVideoPlayed(video) 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 simpleCache = VideoCache.getInstance(this)
val httpClient = okHttpClient.newBuilder().apply { val httpClient = okHttpClient.newBuilder().apply {
ignoreAllSSLErrors() ignoreAllSSLErrors()
@ -1298,9 +1341,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
dataSource dataSource
} }
val dafuckDataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
cacheFactory = CacheDataSource.Factory().apply { cacheFactory = CacheDataSource.Factory().apply {
setCache(simpleCache) setCache(Helper.getSimpleCache(this@ExoplayerView))
setUpstreamDataSourceFactory(dataSourceFactory) if (ext.server.offline) {
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
} else {
setUpstreamDataSourceFactory(dataSourceFactory)
}
setCacheWriteDataSinkFactory(null)
} }
val mimeType = when (video?.format) { val mimeType = when (video?.format) {
@ -1309,15 +1358,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
else -> MimeTypes.APPLICATION_MP4 else -> MimeTypes.APPLICATION_MP4
} }
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) val downloadedMediaItem = if (ext.server.offline) {
logger("url: ${video!!.file.url}") val key = ext.server.name
logger("mimeType: $mimeType") 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) { mediaItem = if (downloadedMediaItem == null) {
val listofnotnullsubs = immutableListOf(sub).filterNotNull() val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
builder.setSubtitleConfigurations(listofnotnullsubs) 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 //Source
exoSource.setOnClickListener { exoSource.setOnClickListener {
@ -1439,7 +1513,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
exoPlayer.release() exoPlayer.release()
VideoCache.release() VideoCache.release()
mediaSession?.release() mediaSession?.release()
if(DiscordServiceRunningSingleton.running) { if (DiscordServiceRunningSingleton.running) {
val stopIntent = Intent(this, DiscordService::class.java) val stopIntent = Intent(this, DiscordService::class.java)
DiscordServiceRunningSingleton.running = false DiscordServiceRunningSingleton.running = false
stopService(stopIntent) stopService(stopIntent)
@ -1479,7 +1553,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
super.onPause() super.onPause()
orientationListener?.disable() orientationListener?.disable()
if (isInitialized) { if (isInitialized) {
playerView.player?.pause() if (castPlayer?.isPlaying == false) {
playerView.player?.pause()
}
saveData( saveData(
"${media.id}_${media.anime!!.selectedEpisode}", "${media.id}_${media.anime!!.selectedEpisode}",
exoPlayer.currentPosition, exoPlayer.currentPosition,
@ -1500,7 +1576,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
override fun onStop() { override fun onStop() {
playerView.player?.pause() if (castPlayer?.isPlaying == false) {
playerView.player?.pause()
}
super.onStop() super.onStop()
} }
@ -1793,7 +1871,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
// Enter PiP Mode // Enter PiP Mode
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.N)
private fun enterPipMode() { private fun enterPipMode() {
wasPlaying = isPlayerPlaying wasPlaying = isPlayerPlaying
if (!pipEnabled) return 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") @SuppressLint("ViewConstructor")
class ExtendedTimeBar( class ExtendedTimeBar(
context: Context, context: Context,

View file

@ -1,6 +1,7 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
@ -20,17 +21,21 @@ import ani.dantotsu.*
import ani.dantotsu.databinding.BottomSheetSelectorBinding import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.Download.download import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.DecimalFormat import java.text.DecimalFormat
class SelectorDialogFragment : BottomSheetDialogFragment() { class SelectorDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSelectorBinding? = null private var _binding: BottomSheetSelectorBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -42,6 +47,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
private var makeDefault = false private var makeDefault = false
private var selected: String? = null private var selected: String? = null
private var launch: Boolean? = null private var launch: Boolean? = null
private var isDownloadMenu: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -49,6 +55,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
selected = it.getString("server") selected = it.getString("server")
launch = it.getBoolean("launch", true) launch = it.getBoolean("launch", true)
prevEpisode = it.getString("prev") prevEpisode = it.getString("prev")
isDownloadMenu = it.getBoolean("isDownload")
} }
} }
@ -76,8 +83,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode) val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
episode = ep episode = ep
if (ep != null) { 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.selectorListContainer.visibility = View.GONE
binding.selectorAutoListContainer.visibility = View.VISIBLE binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorAutoText.text = selected binding.selectorAutoText.text = selected
@ -95,7 +105,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun load() { fun load() {
val size = 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) { if (size != null && size >= media!!.selected!!.video) {
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
selected selected
@ -145,6 +160,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
ep.extractorCallback = { ep.extractorCallback = {
scope.launch { scope.launch {
adapter.add(it) adapter.add(it)
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
} }
} }
model.getEpisode().observe(this) { model.getEpisode().observe(this) {
@ -164,6 +182,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} else { } else {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep) media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
adapter.addAll(ep.extractors) adapter.addAll(ep.extractors)
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
binding.selectorProgressBar.visibility = View.GONE binding.selectorProgressBar.visibility = View.GONE
} }
} }
@ -179,7 +200,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
prevEpisode = null prevEpisode = null
dismiss() dismiss()
if (launch!!) { if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList() stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java) val intent = Intent(activity, ExoplayerView::class.java)
ExoplayerView.media = media ExoplayerView.media = media
@ -214,7 +235,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val extractor = links[position] 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.layoutManager = LinearLayoutManager(requireContext())
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor) holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
@ -235,6 +257,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
notifyItemRangeInserted(0, extractors.size) 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) : private inner class StreamViewHolder(val binding: ItemStreamBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }
@ -256,24 +290,107 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) { override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
val video = extractor.videos[position] val video = extractor.videos[position]
binding.urlQuality.text = if (isDownloadMenu == true) {
if (video.quality != null) "${video.quality}p" else "Default Quality" binding.urlDownload.visibility = View.VISIBLE
binding.urlNote.text = video.extraNote ?: "" } else {
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE binding.urlDownload.visibility = View.GONE
binding.urlDownload.visibility = View.VISIBLE }
binding.urlDownload.setSafeOnClickListener { binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position position
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
download( val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
requireActivity(), val selectedVideo =
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!, if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
media!!.userPreferredName
) 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() 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) { if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text = binding.urlSize.text =
@ -281,12 +398,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat( (if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
"#.##" "#.##"
).format(video.size ?: 0).toString() + " MB")) ).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 override fun getItemCount(): Int = extractor.videos.size
@ -295,6 +410,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
if (isDownloadMenu == true) {
binding.urlDownload.performClick()
return@setSafeOnClickListener
}
tryWith(true) { tryWith(true) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
extractor.server.name extractor.server.name
@ -326,13 +445,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun newInstance( fun newInstance(
server: String? = null, server: String? = null,
la: Boolean = true, la: Boolean = true,
prev: String? = null prev: String? = null,
isDownload: Boolean
): SelectorDialogFragment = ): SelectorDialogFragment =
SelectorDialogFragment().apply { SelectorDialogFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putString("server", server) putString("server", server)
putBoolean("launch", la) putBoolean("launch", la)
putString("prev", prev) putString("prev", prev)
putBoolean("isDownload", isDownload)
} }
} }
} }

View file

@ -6,7 +6,9 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.BottomSheetDialogFragment
@ -60,6 +62,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
) )
) )
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
if (position == 0) { if (position == 0) {

View file

@ -141,8 +141,8 @@ class MangaChapterAdapter(
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>() private val activeCoroutines = mutableSetOf<String>()
val typedValue1 = TypedValue() private val typedValue1 = TypedValue()
val typedValue2 = TypedValue() private val typedValue2 = TypedValue()
fun bind(chapterNumber: String, progress: String?) { fun bind(chapterNumber: String, progress: String?) {
if (progress != null) { if (progress != null) {
binding.itemChapterTitle.visibility = View.VISIBLE binding.itemChapterTitle.visibility = View.VISIBLE
@ -167,6 +167,7 @@ class MangaChapterAdapter(
} else { } else {
// Show download icon // Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_circle_add) binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
binding.itemDownload.rotation = 0f
} }
} }
@ -235,7 +236,7 @@ class MangaChapterAdapter(
input.maxValue = itemCount - bindingAdapterPosition input.maxValue = itemCount - bindingAdapterPosition
input.value = 1 input.value = 1
alertDialog.setView(input) alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { dialog, which -> alertDialog.setPositiveButton("OK") { _, _ ->
downloadNChaptersFrom(bindingAdapterPosition, input.value) downloadNChaptersFrom(bindingAdapterPosition, input.value)
} }
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }

View file

@ -5,16 +5,25 @@ import java.util.regex.Pattern
class MangaNameAdapter { class MangaNameAdapter {
companion object { 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? { fun findChapterNumber(text: String): Float? {
val regex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)" val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text) val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) { return if (matcher.find()) {
matcher.group(2)?.toFloat() matcher.group(2)?.toFloat()
} else { } 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
}
} }
} }
} }
} }

View file

@ -2,34 +2,41 @@ package ani.dantotsu.media.manga
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.ImageView import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.NumberPicker import android.widget.NumberPicker
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.media.anime.handleProgress 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.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MangaReadAdapter( class MangaReadAdapter(
private val media: Media, private val media: Media,
private val fragment: MangaReadFragment, private val fragment: MangaReadFragment,
@ -47,6 +54,8 @@ class MangaReadAdapter(
return ViewHolder(bind) return ViewHolder(bind)
} }
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
@ -60,7 +69,17 @@ class MangaReadAdapter(
null 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 //Source Selection
var source = var source =
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
@ -117,7 +136,7 @@ class MangaReadAdapter(
} }
} }
//Subscription //Grids
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.animeSourceSubscribe,
@ -136,98 +155,156 @@ class MangaReadAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id)) openSettings(fragment.requireContext(), getChannelId(true, media.id))
} }
//Icons binding.animeNestedButton.setOnClickListener {
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.animeScanlatorTop.setOnClickListener {
val dialogView = val dialogView =
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer) val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
// Dynamically add checkboxes var run = false
var reversed = media.selected!!.recyclerReversed
options.forEach { option -> var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
val checkBox = CheckBox(currContext()).apply { dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
text = option dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
} dialogBinding.animeSourceTop.setOnClickListener {
//set checked if it's already selected reversed = !reversed
if (media.selected!!.scanlators != null) { dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
scanlatorSelectionListener?.onScanlatorsSelected() run = true
} else {
checkBox.isChecked = true
}
checkboxContainer.addView(checkBox)
} }
// Create AlertDialog //Grids
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup) dialogBinding.animeSourceGrid.visibility = View.GONE
.setView(dialogView) var selected = when (style) {
.setPositiveButton("OK") { dialog, which -> 0 -> dialogBinding.animeSourceList
//add unchecked to hidden 1 -> dialogBinding.animeSourceCompact
hiddenScanlators.clear() else -> dialogBinding.animeSourceList
for (i in 0 until checkboxContainer.childCount) { }
val checkBox = checkboxContainer.getChildAt(i) as CheckBox when (style) {
if (!checkBox.isChecked) { 0 -> dialogBinding.layoutText.text = "List"
hiddenScanlators.add(checkBox.text.toString()) 1 -> dialogBinding.layoutText.text = "Compact"
} else -> dialogBinding.animeSourceList
}
fragment.onScanlatorChange(hiddenScanlators)
scanlatorSelectionListener?.onScanlatorsSelected()
}
.setNegativeButton("Cancel", null)
.show()
dialog.window?.setDimAmount(0.8f)
}
binding.animeDownloadTop.setOnClickListener {
//Alert dialog asking for the number of chapters to download
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
alertDialog.setTitle("Multi Chapter Downloader")
alertDialog.setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext())
input.minValue = 1
input.maxValue = 20
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { dialog, which ->
fragment.multiDownload(input.value)
} }
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
val dialog = alertDialog.show()
dialog.window?.setDimAmount(0.8f)
}
var selected = when (style) {
0 -> binding.animeSourceList
1 -> binding.animeSourceCompact
else -> binding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f selected.alpha = 1f
} fun selected(it: ImageButton) {
binding.animeSourceList.setOnClickListener { selected.alpha = 0.33f
selected(it as ImageView) selected = it
style = 0 selected.alpha = 1f
fragment.onIconPressed(style, reversed) }
} dialogBinding.animeSourceList.setOnClickListener {
binding.animeSourceCompact.setOnClickListener { selected(it as ImageButton)
selected(it as ImageView) style = 0
style = 1 dialogBinding.layoutText.text = "List"
fragment.onIconPressed(style, reversed) 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 //Chapter Handling
handleChapters() handleChapters()
} }
@ -259,6 +336,7 @@ class MangaReadAdapter(
0 0
) )
} }
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)]) val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1]) val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
val startChapterString = if (startChapter != null) { 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 val binding = _binding
if (mangaReadSources is MangaSources) { if (mangaReadSources is MangaSources) {
val parser = mangaReadSources[source] as? DynamicMangaParser val parser = mangaReadSources[source] as? DynamicMangaParser
@ -385,12 +463,16 @@ class MangaReadAdapter(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown" parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
) )
} }
binding?.animeSourceLanguage?.setAdapter( val adapter = ArrayAdapter(
ArrayAdapter( fragment.requireContext(),
fragment.requireContext(), R.layout.item_dropdown,
R.layout.item_dropdown, parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
parser.extension.sources.map { it.lang })
) )
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)
} }
} }

View file

@ -29,7 +29,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton import ani.dantotsu.download.manga.MangaServiceDataSingleton
@ -37,6 +37,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser import ani.dantotsu.parsers.MangaParser
@ -166,7 +167,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
chapterAdapter = chapterAdapter =
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
for (download in downloadManager.mangaDownloads) { for (download in downloadManager.mangaDownloadedTypes) {
chapterAdapter.stopDownload(download.chapter) chapterAdapter.stopDownload(download.chapter)
} }
@ -202,20 +203,25 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val selected = media.userProgress val selected = media.userProgress
val chapters = media.manga?.chapters?.values?.toList() val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language //filter by selected language
val progressChapterIndex = chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected }?:0 val progressChapterIndex = (chapters?.indexOfFirst {
if (progressChapterIndex < 0 || n < 1) return MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
val chaptersToDownload = chapters?.subList( } ?: 0) + 1
progressChapterIndex + 1,
progressChapterIndex + n + 1
)
if (chaptersToDownload != null) {
for (chapter in chaptersToDownload) {
onMangaChapterDownloadClick(chapter.title!!)
}
}
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() { private fun updateChapters() {
val loadedChapters = model.getMangaChapters().value val loadedChapters = model.getMangaChapters().value
if (loadedChapters != null) { 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>() val scanlators = mutableListOf<String>()
if (chap != null) { if (chap != null) {
val chapters = chap.values val chapters = chap.values
@ -355,19 +361,20 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (show) View.GONE else View.VISIBLE if (show) View.GONE else View.VISIBLE
} }
} }
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>() val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
if (allSettings.isNotEmpty()) { if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0] var selectedSetting = allSettings[0]
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray() val names =
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0 var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source") .setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which -> .setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex] selectedSetting = allSettings[selectedIndex]
itemSelected = true
dialog.dismiss() dialog.dismiss()
// Move the fragment transaction here // Move the fragment transaction here
@ -382,10 +389,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
} }
.setNegativeButton("Cancel") { dialog, _ -> .setOnDismissListener {
dialog.cancel() if (!itemSelected) {
changeUIVisibility(true) changeUIVisibility(true)
return@setNegativeButton }
} }
.show() .show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
@ -440,7 +447,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Create a download task // Create a download task
val downloadTask = MangaDownloaderService.DownloadTask( val downloadTask = MangaDownloaderService.DownloadTask(
title = media.nameMAL ?: media.nameRomaji, title = media.mainName(),
chapter = chapter.title!!, chapter = chapter.title!!,
imageData = images, imageData = images,
sourceMedia = media, sourceMedia = media,
@ -481,10 +488,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
fun onMangaChapterRemoveDownloadClick(i: String) { fun onMangaChapterRemoveDownloadClick(i: String) {
downloadManager.removeDownload( downloadManager.removeDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.mainName(),
i, i,
Download.Type.MANGA DownloadedType.Type.MANGA
) )
) )
chapterAdapter.deleteDownload(i) chapterAdapter.deleteDownload(i)
@ -499,10 +506,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Remove the download from the manager and update the UI // Remove the download from the manager and update the UI
downloadManager.removeDownload( downloadManager.removeDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.mainName(),
i, i,
Download.Type.MANGA DownloadedType.Type.MANGA
) )
) )
chapterAdapter.purgeDownload(i) chapterAdapter.purgeDownload(i)

View file

@ -89,7 +89,8 @@ abstract class BaseImageAdapter(
} }
} else { } else {
val detector = GestureDetectorCompat(view.context, object : GesturesListener() { 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 { view.findViewById<View>(R.id.imgProgCover).apply {
setOnTouchListener { _, event -> setOnTouchListener { _, event ->
@ -112,6 +113,9 @@ abstract class BaseImageAdapter(
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) } 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 abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object { companion object {

View file

@ -50,7 +50,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
if (model.loadMangaChapterImages( if (model.loadMangaChapterImages(
chp, chp,
m.selected!!, m.selected!!,
m.nameMAL ?: m.nameRomaji m.mainName()
) )
) { ) {
val activity = currActivity() val activity = currActivity()

View file

@ -91,4 +91,16 @@ open class ImageAdapter(
} }
override fun getItemCount(): Int = images.size 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)
}
} }

View file

@ -3,8 +3,10 @@ package ani.dantotsu.media.manga.mangareader
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -218,7 +220,7 @@ class MangaReaderActivity : AppCompatActivity() {
val mangaSources = MangaSources val mangaSources = MangaSources
val scope = lifecycleScope val scope = lifecycleScope
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow) mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow, this@MangaReaderActivity)
} }
model.mangaReadSources = mangaSources model.mangaReadSources = mangaSources
} else { } else {
@ -253,7 +255,8 @@ class MangaReaderActivity : AppCompatActivity() {
} }
showProgressDialog = 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 //Chapter Change
fun change(index: Int) { fun change(index: Int) {
@ -358,7 +361,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages( model.loadMangaChapterImages(
chapter, chapter,
media.selected!!, media.selected!!,
media.nameMAL ?: media.nameRomaji media.mainName()
) )
} }
} }
@ -701,8 +704,60 @@ class MangaReaderActivity : AppCompatActivity() {
goneTimer.schedule(timerTask, controllerDuration) 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 (!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) { if (!settings.showSystemBars) {
hideBars() hideBars()
checkNotch() checkNotch()
@ -786,7 +841,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages( model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!, chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!, media.selected!!,
media.nameMAL ?: media.nameRomaji, media.mainName(),
false false
) )
loading = false loading = false
@ -795,17 +850,28 @@ class MangaReaderActivity : AppCompatActivity() {
private fun progress(runnable: Runnable) { private fun progress(runnable: Runnable) {
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) { if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
showProgressDialog =
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
?: true else false
if (showProgressDialog) { if (showProgressDialog) {
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null) val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox) val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName) checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
checkbox.setOnCheckedChangeListener { _, isChecked -> checkbox.setOnCheckedChangeListener { _, isChecked ->
saveData("${media.id}_progressDialog", isChecked) saveData("${media.id}_progressDialog", !isChecked)
showProgressDialog = !isChecked showProgressDialog = !isChecked
} }
val incognito =
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("incognito", false) ?: false
AlertDialog.Builder(this, R.style.MyPopup) AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.title_update_progress)) .setTitle(getString(R.string.title_update_progress))
.apply {
if (incognito) {
setMessage(getString(R.string.incognito_will_not_update))
}
}
.setView(dialogView) .setView(dialogView)
.setCancelable(false) .setCancelable(false)
.setPositiveButton(getString(R.string.yes)) { dialog, _ -> .setPositiveButton(getString(R.string.yes)) { dialog, _ ->

View file

@ -22,7 +22,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton import ani.dantotsu.download.novel.NovelServiceDataSingleton
@ -67,7 +67,7 @@ class NovelReadFragment : Fragment(),
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) { override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
Log.e("downloadTrigger", novelDownloadPackage.link) Log.e("downloadTrigger", novelDownloadPackage.link)
val downloadTask = NovelDownloaderService.DownloadTask( val downloadTask = NovelDownloaderService.DownloadTask(
title = media.nameMAL ?: media.nameRomaji, title = media.mainName(),
chapter = novelDownloadPackage.novelName, chapter = novelDownloadPackage.novelName,
downloadLink = novelDownloadPackage.link, downloadLink = novelDownloadPackage.link,
originalLink = novelDownloadPackage.originalLink, originalLink = novelDownloadPackage.originalLink,
@ -92,16 +92,16 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean { override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
if (downloadsManager.queryDownload( if (downloadsManager.queryDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.mainName(),
novel.name, novel.name,
Download.Type.NOVEL DownloadedType.Type.NOVEL
) )
) )
) { ) {
val file = File( val file = File(
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), 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 if (!file.exists()) return false
val fileUri = FileProvider.getUriForFile( val fileUri = FileProvider.getUriForFile(
@ -124,10 +124,10 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheck(novel: ShowResponse): Boolean { override fun downloadedCheck(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
return downloadsManager.queryDownload( return downloadsManager.queryDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.mainName(),
novel.name, novel.name,
Download.Type.NOVEL DownloadedType.Type.NOVEL
) )
) )
} }
@ -135,10 +135,10 @@ class NovelReadFragment : Fragment(),
override fun deleteDownload(novel: ShowResponse) { override fun deleteDownload(novel: ShowResponse) {
val downloadsManager = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.removeDownload( downloadsManager.removeDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.mainName(),
novel.name, novel.name,
Download.Type.NOVEL DownloadedType.Type.NOVEL
) )
) )
} }
@ -247,8 +247,7 @@ class NovelReadFragment : Fragment(),
headerAdapter.progress?.visibility = View.VISIBLE headerAdapter.progress?.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if (auto || query == "") model.autoSearchNovels(media) if (auto || query == "") model.autoSearchNovels(media)
//else model.searchNovels(query, source) else model.searchNovels(query, source)
else model.autoSearchNovels(media) //testing
} }
searching = true searching = true
if (save) { if (save) {

View file

@ -42,7 +42,11 @@ class NovelResponseAdapter(
.into(binding.itemEpisodeImage) .into(binding.itemEpisodeImage)
val typedValue = TypedValue() 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 val color = typedValue.data
binding.itemEpisodeTitle.text = novel.name binding.itemEpisodeTitle.text = novel.name
@ -98,14 +102,19 @@ class NovelResponseAdapter(
} }
binding.root.setOnLongClickListener { 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.setTitle("Delete ${novel.name}?")
builder.setMessage("Are you sure you want to delete ${novel.name}?") builder.setMessage("Are you sure you want to delete ${novel.name}?")
builder.setPositiveButton("Yes") { _, _ -> builder.setPositiveButton("Yes") { _, _ ->
downloadedCheckCallback.deleteDownload(novel) downloadedCheckCallback.deleteDownload(novel)
deleteDownload(novel.link) deleteDownload(novel.link)
snackString("Deleted ${novel.name}") 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 = "" binding.itemEpisodeFiller.text = ""
} }
} }

View file

@ -15,6 +15,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.view.animation.OvershootInterpolator import android.view.animation.OvershootInterpolator
import android.webkit.WebView
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
@ -36,7 +37,7 @@ import ani.dantotsu.saveData
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.CurrentNovelReaderSettings import ani.dantotsu.settings.CurrentNovelReaderSettings
import ani.dantotsu.settings.CurrentReaderSettings import ani.dantotsu.settings.CurrentReaderSettings
import ani.dantotsu.settings.NovelReaderSettings import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
@ -62,7 +63,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
private lateinit var binding: ActivityNovelReaderBinding private lateinit var binding: ActivityNovelReaderBinding
private val scope = lifecycleScope private val scope = lifecycleScope
lateinit var settings: NovelReaderSettings lateinit var settings: ReaderSettings
private lateinit var uiSettings: UserInterfaceSettings private lateinit var uiSettings: UserInterfaceSettings
private var notchHeight: Int? = null private var notchHeight: Int? = null
@ -139,16 +140,31 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
} }
@SuppressLint("WebViewApiAvailability")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//check for supported webview //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() val firstVersion = webViewVersion?.split(".")?.firstOrNull()?.toIntOrNull()
if (webViewVersion == null || firstVersion == null || firstVersion < 87) { 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 //open playstore
val intent = Intent(Intent.ACTION_VIEW) 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) startActivity(intent)
//stop reader //stop reader
finish() finish()
@ -159,9 +175,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityNovelReaderBinding.inflate(layoutInflater) binding = ActivityNovelReaderBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
settings = loadData("reader_settings", this)
settings = loadData("novel_reader_settings", this) ?: ReaderSettings().apply { saveData("reader_settings", this) }
?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) }
uiSettings = loadData("ui_settings", this) uiSettings = loadData("ui_settings", this)
?: UserInterfaceSettings().also { saveData("ui_settings", it) } ?: UserInterfaceSettings().also { saveData("ui_settings", it) }
@ -271,7 +286,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
binding.bookReader.getAppearance { binding.bookReader.getAppearance {
currentTheme = it currentTheme = it
themes.add(0, it) themes.add(0, it)
settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default settings.defaultLN =
loadData("${sanitizedBookId}_current_settings") ?: settings.defaultLN
applySettings() applySettings()
} }
@ -323,7 +339,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
return when (event.keyCode) { return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP) if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP)
if (!settings.default.volumeButtons) if (!settings.defaultLN.volumeButtons)
return false return false
if (event.action == KeyEvent.ACTION_DOWN) { if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeUp?.invoke() onVolumeUp?.invoke()
@ -333,7 +349,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
if (!settings.default.volumeButtons) if (!settings.defaultLN.volumeButtons)
return false return false
if (event.action == KeyEvent.ACTION_DOWN) { if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeDown?.invoke() onVolumeDown?.invoke()
@ -349,13 +365,18 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
fun applySettings() { fun applySettings() {
saveData("${sanitizedBookId}_current_settings", settings.default) saveData("${sanitizedBookId}_current_settings", settings.defaultLN)
hideBars() hideBars()
if (settings.defaultLN.useOledTheme) {
themes.forEach { theme ->
theme.darkBg = Color.parseColor("#000000")
}
}
currentTheme = 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 -> { CurrentNovelReaderSettings.Layouts.PAGED -> {
currentTheme?.flow = ReaderFlow.PAGINATED currentTheme?.flow = ReaderFlow.PAGINATED
} }
@ -366,22 +387,22 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
} }
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
when (settings.default.dualPageMode) { when (settings.defaultLN.dualPageMode) {
CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1 CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1
CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2 CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2
CurrentReaderSettings.DualPageModes.Force -> requestedOrientation = CurrentReaderSettings.DualPageModes.Force -> requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} }
currentTheme?.lineHeight = settings.default.lineHeight currentTheme?.lineHeight = settings.defaultLN.lineHeight
currentTheme?.gap = settings.default.margin currentTheme?.gap = settings.defaultLN.margin
currentTheme?.maxInlineSize = settings.default.maxInlineSize currentTheme?.maxInlineSize = settings.defaultLN.maxInlineSize
currentTheme?.maxBlockSize = settings.default.maxBlockSize currentTheme?.maxBlockSize = settings.defaultLN.maxBlockSize
currentTheme?.useDark = settings.default.useDarkTheme currentTheme?.useDark = settings.defaultLN.useDarkTheme
currentTheme?.let { binding.bookReader.setAppearance(it) } 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) else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }

View file

@ -30,8 +30,7 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as NovelReaderActivity val activity = requireActivity() as NovelReaderActivity
val settings = activity.settings.default val settings = activity.settings.defaultLN
val themeLabels = activity.themes.map { it.name } val themeLabels = activity.themes.map { it.name }
binding.themeSelect.adapter = binding.themeSelect.adapter =
NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels) NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels)
@ -49,7 +48,11 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onNothingSelected(parent: AdapterView<*>?) {}
} }
binding.useOledTheme.isChecked = settings.useOledTheme
binding.useOledTheme.setOnCheckedChangeListener { _, isChecked ->
settings.useOledTheme = isChecked
activity.applySettings()
}
val layoutList = listOf( val layoutList = listOf(
binding.paged, binding.paged,
binding.continuous binding.continuous
@ -173,6 +176,20 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
binding.maxBlockSize.setText(value.toString()) binding.maxBlockSize.setText(value.toString())
activity.applySettings() 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 binding.useDarkTheme.isChecked = settings.useDarkTheme

View file

@ -1,23 +1,29 @@
package ani.dantotsu.media.user package ani.dantotsu.media.user
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ActivityListBinding import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@ -74,13 +80,16 @@ class ListActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN WindowManager.LayoutParams.FLAG_FULLSCREEN
) )
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
} }
setContentView(binding.root) setContentView(binding.root)
val anime = intent.getBooleanExtra("anime", true) val anime = intent.getBooleanExtra("anime", true)
binding.listTitle.text = binding.listTitle.text =
intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List" intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
this@ListActivity.selectedTabIdx = tab?.position ?: 0 this@ListActivity.selectedTabIdx = tab?.position ?: 0
@ -145,7 +154,8 @@ class ListActivity : AppCompatActivity() {
R.id.release -> "release" R.id.release -> "release"
else -> null else -> null
} }
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putString("sort_order", sort)?.apply()
binding.listProgressBar.visibility = View.VISIBLE binding.listProgressBar.visibility = View.VISIBLE
binding.listViewPager.adapter = null binding.listViewPager.adapter = null
scope.launch { scope.launch {

View file

@ -46,7 +46,7 @@ class ListFragment : Fragment() {
binding.listRecyclerView.layoutManager = binding.listRecyclerView.layoutManager =
GridLayoutManager( GridLayoutManager(
requireContext(), requireContext(),
if (grid!!) (screenWidth / 124f).toInt() else 1 if (grid!!) (screenWidth / 120f).toInt() else 1
) )
binding.listRecyclerView.adapter = adapter binding.listRecyclerView.adapter = adapter
} }

View file

@ -1,11 +1,13 @@
package ani.dantotsu.offline package ani.dantotsu.offline
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentOfflineBinding import ani.dantotsu.databinding.FragmentOfflineBinding
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
@ -13,6 +15,7 @@ import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
class OfflineFragment : Fragment() { class OfflineFragment : Fragment() {
private var offline = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -23,6 +26,11 @@ class OfflineFragment : Fragment() {
topMargin = statusBarHeight topMargin = statusBarHeight
bottomMargin = navBarHeight 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 { binding.refreshButton.setOnClickListener {
if (isOnline(requireContext())) { if (isOnline(requireContext())) {
startMainActivity(requireActivity()) startMainActivity(requireActivity())
@ -30,4 +38,10 @@ class OfflineFragment : Fragment() {
} }
return binding.root return binding.root
} }
override fun onResume() {
super.onResume()
offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("offlineMode", false) ?: false
}
} }

View file

@ -1,4 +1,5 @@
package ani.dantotsu.others package ani.dantotsu.others
const val DisabledReports = false 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

View file

@ -6,26 +6,38 @@ import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.anime.Episode
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import com.google.gson.Gson
import com.lagradost.nicehttp.NiceResponse
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
object Kitsu { object Kitsu {
private suspend fun getKitsuData(query: String): KitsuResponse? { private suspend fun getKitsuData(query: String): KitsuResponse? {
val headers = mapOf( val headers = mapOf(
"Content-Type" to "application/json", "Content-Type" to "application/json",
"Accept" 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", "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 { val response = tryWithSuspend {
client.post( val res = client.post(
"https://kitsu.io/api/graphql", "https://kitsu.io/api/graphql",
headers, headers,
data = mapOf("query" to query) 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>? { suspend fun getKitsuEpisodesDetails(media: Media): Map<String, Episode>? {
@ -54,14 +66,14 @@ query {
} }
} }
} }
}""" }""".trimIndent()
val result = getKitsuData(query) ?: return null val result = getKitsuData(query) ?: return null
logger("Kitsu : result=$result", print) logger("Kitsu : result=$result", print)
media.idKitsu = result.data?.lookupMapping?.id media.idKitsu = result.data?.lookupMapping?.id
return (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep -> val a = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
val num = ep?.num?.toString() ?: return@mapNotNull null val num = ep?.number?.toString() ?: return@mapNotNull null
num to Episode( num to Episode(
number = num, number = num,
title = ep.titles?.canonical, title = ep.titles?.canonical,
@ -69,6 +81,25 @@ query {
thumb = FileUrl[ep.thumbnail?.original?.url], thumb = FileUrl[ep.thumbnail?.original?.url],
) )
}.toMap() }.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 @Serializable
@ -93,7 +124,7 @@ query {
@Serializable @Serializable
data class Node( data class Node(
@SerialName("number") val num: Long? = null, @SerialName("number") val number: Int? = null,
@SerialName("titles") val titles: Titles? = null, @SerialName("titles") val titles: Titles? = null,
@SerialName("description") val description: Description? = null, @SerialName("description") val description: Description? = null,
@SerialName("thumbnail") val thumbnail: Thumbnail? = null @SerialName("thumbnail") val thumbnail: Thumbnail? = null

View file

@ -6,25 +6,116 @@ class LanguageMapper {
fun mapLanguageCodeToName(code: String): String { fun mapLanguageCodeToName(code: String): String {
return when (code) { return when (code) {
"all" -> "Multi" "all" -> "Multi"
"af" -> "Afrikaans"
"am" -> "Amharic"
"ar" -> "Arabic" "ar" -> "Arabic"
"as" -> "Assamese"
"az" -> "Azerbaijani"
"be" -> "Belarusian"
"bg" -> "Bulgarian"
"bn" -> "Bengali"
"bs" -> "Bosnian"
"ca" -> "Catalan"
"ceb" -> "Cebuano"
"cs" -> "Czech"
"da" -> "Danish"
"de" -> "German" "de" -> "German"
"el" -> "Greek"
"en" -> "English" "en" -> "English"
"en-Us" -> "English (United States)"
"eo" -> "Esperanto"
"es" -> "Spanish" "es" -> "Spanish"
"es-419" -> "Spanish (Latin America)"
"et" -> "Estonian"
"eu" -> "Basque"
"fa" -> "Persian"
"fi" -> "Finnish"
"fil" -> "Filipino"
"fo" -> "Faroese"
"fr" -> "French" "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" "id" -> "Indonesian"
"ig" -> "Igbo"
"is" -> "Icelandic"
"it" -> "Italian" "it" -> "Italian"
"ja" -> "Japanese" "ja" -> "Japanese"
"jv" -> "Javanese"
"ka" -> "Georgian"
"kk" -> "Kazakh"
"km" -> "Khmer"
"kn" -> "Kannada"
"ko" -> "Korean" "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" "pl" -> "Polish"
"pt" -> "Portuguese"
"pt-BR" -> "Portuguese (Brazil)" "pt-BR" -> "Portuguese (Brazil)"
"pt-PT" -> "Portuguese (Portugal)"
"ps" -> "Pashto"
"ro" -> "Romanian"
"rm" -> "Romansh"
"ru" -> "Russian" "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" "th" -> "Thai"
"ti" -> "Tigrinya"
"tk" -> "Turkmen"
"tl" -> "Tagalog"
"to" -> "Tongan"
"tr" -> "Turkish" "tr" -> "Turkish"
"uk" -> "Ukrainian" "uk" -> "Ukrainian"
"ur" -> "Urdu"
"uz" -> "Uzbek"
"vi" -> "Vietnamese" "vi" -> "Vietnamese"
"yo" -> "Yoruba"
"zh" -> "Chinese" "zh" -> "Chinese"
"zh-Hans" -> "Chinese (Simplified)" "zh-Hans" -> "Chinese (Simplified)"
else -> "" "zh-Hant" -> "Chinese (Traditional)"
"zh-Habt" -> "Chinese (Hakka)"
"zu" -> "Zulu"
else -> code
} }
} }

View file

@ -50,7 +50,7 @@ object MalScraper {
} }
} }
} catch (e: Exception) { } 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))
} }
} }
} }

View file

@ -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)
}

View file

@ -4,9 +4,11 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -14,7 +16,9 @@ import ani.dantotsu.App.Companion.context
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ActivityImageSearchBinding import ani.dantotsu.databinding.ActivityImageSearchBinding
import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast import ani.dantotsu.toast
@ -49,10 +53,13 @@ class ImageSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
initActivity(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityImageSearchBinding.inflate(layoutInflater) binding = ActivityImageSearchBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.uploadImage.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.uploadImage.setOnClickListener { binding.uploadImage.setOnClickListener {
viewModel.clearResults() viewModel.clearResults()
imageSelectionLauncher.launch("image/*") imageSelectionLauncher.launch("image/*")

View file

@ -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)
}
}

View file

@ -11,6 +11,9 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.databinding.BottomSheetWebviewBinding import ani.dantotsu.databinding.BottomSheetWebviewBinding
import ani.dantotsu.defaultHeaders 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() { abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
@ -30,7 +33,8 @@ abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
dismiss() dismiss()
} }
val cookies: CookieManager = CookieManager.getInstance() val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
//CookieManager.getInstance()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,

View file

@ -1,5 +1,6 @@
package ani.dantotsu.parsers package ani.dantotsu.parsers
import android.content.Context
import ani.dantotsu.Lazier import ani.dantotsu.Lazier
import ani.dantotsu.lazyList import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@ -8,24 +9,52 @@ import kotlinx.coroutines.flow.first
object AnimeSources : WatchSources() { object AnimeSources : WatchSources() {
override var list: List<Lazier<BaseParser>> = emptyList() 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 // Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first() val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) list = createParsersFromExtensions(initialExtensions) + Lazier(
{ OfflineAnimeParser() },
"Downloaded"
)
// Update as StateFlow emits new values // Update as StateFlow emits new values
fromExtensions.collect { extensions -> 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>> { private fun createParsersFromExtensions(extensions: List<AnimeExtension.Installed>): List<Lazier<BaseParser>> {
return extensions.map { extension -> return extensions.map { extension ->
val name = extension.name val name = extension.name
Lazier({ DynamicAnimeParser(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
}
} }

View file

@ -9,23 +9,22 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.widget.Toast
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.currContext
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.anime.AnimeNameAdapter import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.media.manga.ImageData import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.snackString import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video 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.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension 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.network.interceptor.CloudflareBypassException
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -40,7 +39,10 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Request
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -81,50 +83,56 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) { } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} } as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
if (source is AnimeCatalogueSource) { ?: return emptyList())
try { try {
val res = source.getEpisodeList(sAnime) val res = source.getEpisodeList(sAnime)
val sortedEpisodes = if (res[0].episode_number == -1f) { val sortedEpisodes = if (res[0].episode_number == -1f) {
// Find the number in the string and sort by that number // Find the number in the string and sort by that number
val sortedByStringNumber = res.sortedBy { val sortedByStringNumber = res.sortedBy {
val matchResult = "\\d+".toRegex().find(it.name) val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
val number = matchResult?.value?.toFloat() ?: Float.MAX_VALUE val number = matchResult ?: Float.MAX_VALUE
it.episode_number = number // Store the found number in episode_number it.episode_number = number // Store the found number in episode_number
number number
} }
// If there is no number, reverse the order and give them an incrementing number // If there is no number, reverse the order and give them an incrementing number
var incrementingNumber = 1f var incrementingNumber = 1f
sortedByStringNumber.map { sortedByStringNumber.map {
if (it.episode_number == Float.MAX_VALUE) { if (it.episode_number == Float.MAX_VALUE) {
it.episode_number = it.episode_number =
incrementingNumber++ // Update episode_number with the incrementing number incrementingNumber++ // Update episode_number with the incrementing number
} }
it it
} }
} else { } else {
var episodeCounter = 1f var episodeCounter = 1f
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is // Group by season, sort within each season, and then renumber while keeping episode number 0 as is
val seasonGroups = val seasonGroups =
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 } res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
seasonGroups.keys.sorted().flatMap { season -> seasonGroups.keys.sortedBy { it.toInt() }
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode -> .flatMap { season ->
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0 seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
episode.episode_number = episodeCounter++ if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
} val potentialNumber =
episode AnimeNameAdapter.findEpisodeNumber(episode.name)
} ?: emptyList() 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( override suspend fun loadVideoServers(
@ -137,7 +145,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) { } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList() } as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
?: return emptyList())
return try { return try {
val videos = source.getVideoList(sEpisode) val videos = source.getVideoList(sEpisode)
@ -159,15 +168,16 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) { } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList() } as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
?: return emptyList())
return try { return try {
val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle() val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle()
logger("query: $query")
convertAnimesPageToShowResponse(res) convertAnimesPageToShowResponse(res)
} catch (e: CloudflareBypassException) { } catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT) snackString( "Failed to bypass Cloudflare")
.show()
} }
emptyList() emptyList()
} catch (e: Exception) { } catch (e: Exception) {
@ -202,7 +212,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} }
return Episode( return Episode(
if (episodeNumberInt.toInt() != -1) { if (episodeNumberInt.toInt() != -1) {
episodeNumberInt.toString() if (sEpisode.episode_number % 1 == 0f) {
episodeNumberInt.toInt().toString()
} else {
sEpisode.episode_number.toString()
}
} else { } else {
sEpisode.name sEpisode.name
}, },
@ -277,7 +291,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
var imageDataList: List<ImageData> = listOf() var imageDataList: List<ImageData> = listOf()
val ret = coroutineScope { val ret = coroutineScope {
try { try {
println("source.name " + source.name) logger("source.name " + source.name)
val res = source.getPageList(sChapter) val res = source.getPageList(sChapter)
val reIndexedPages = val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) } 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) { } catch (e: Exception) {
logger("loadImages Exception: $e") logger("loadImages Exception: $e")
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT) snackString("Failed to load images: $e")
.show()
emptyList() emptyList()
} }
} }
@ -310,29 +323,30 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList() } as? HttpSource ?: return emptyList()
var imageDataList: List<ImageData> = listOf()
coroutineScope { return coroutineScope {
try { try {
println("source.name " + source.name) logger("source.name " + source.name)
val res = source.getPageList(sChapter) val res = source.getPageList(sChapter)
val reIndexedPages = val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) } res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
val semaphore = Semaphore(5)
val deferreds = reIndexedPages.map { page -> val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) { async(Dispatchers.IO) {
imageDataList += ImageData(page, source) semaphore.withPermit {
ImageData(page, source)
}
} }
} }
deferreds.awaitAll() deferreds.awaitAll()
} catch (e: Exception) { } catch (e: Exception) {
logger("loadImages Exception: $e") logger("loadImages Exception: $e")
snackString("Failed to load images: $e") snackString("Failed to load images: $e")
emptyList() emptyList()
} }
} }
return imageDataList
} }
suspend fun fetchAndProcessImage( suspend fun fetchAndProcessImage(
@ -344,8 +358,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
try { try {
// Fetch the image // Fetch the image
val response = httpSource.getImage(page) val response = httpSource.getImage(page)
println("Response: ${response.code}") logger("Response: ${response.code}")
println("Response: ${response.message}") logger("Response: ${response.message}")
// Convert the Response to an InputStream // Convert the Response to an InputStream
val inputStream = response.body.byteStream() val inputStream = response.body.byteStream()
@ -353,7 +367,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
// Convert InputStream to Bitmap // Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream) val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close() inputStream.close()
ani.dantotsu.media.manga.saveImage( ani.dantotsu.media.manga.saveImage(
bitmap, bitmap,
context.contentResolver, context.contentResolver,
@ -365,7 +379,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
return@withContext bitmap return@withContext bitmap
} catch (e: Exception) { } catch (e: Exception) {
// Handle any exceptions // Handle any exceptions
println("An error occurred: ${e.message}") logger("An error occurred: ${e.message}")
return@withContext null return@withContext null
} }
} }
@ -395,10 +409,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
) )
} }
inputStream?.close() inputStream.close()
} catch (e: Exception) { } catch (e: Exception) {
// Handle any exceptions // 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) { } catch (e: Exception) {
// Handle exception here // 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) { } catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT) snackString("Failed to bypass Cloudflare")
.show()
} }
emptyList() emptyList()
} catch (e: Exception) { } catch (e: Exception) {
@ -603,13 +616,18 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
val fileName = queryPairs.find { it.first == "file" }?.second ?: "" val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName) 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) { if (format == null) {
logger("Unknown video format: $videoUrl") logger("Unknown video format: $videoUrl")
FirebaseCrashlytics.getInstance() //FirebaseCrashlytics.getInstance()
.recordException(Exception("Unknown video format: $videoUrl")) // .recordException(Exception("Unknown video format: $videoUrl"))
format = VideoType.CONTAINER format = VideoType.CONTAINER
} }
val headersMap: Map<String, String> = val headersMap: Map<String, String> =
@ -620,12 +638,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
number, number,
format, format,
FileUrl(videoUrl, headersMap), FileUrl(videoUrl, headersMap),
aniVideo.totalContentLength.toDouble() if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
) )
} }
private fun getVideoType(fileName: String): VideoType? { private fun getVideoType(fileName: String): VideoType? {
return when { val type = when {
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith( fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
".mkv", ".mkv",
ignoreCase = true ignoreCase = true
@ -635,6 +653,47 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> null 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 { 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) return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
} }
private fun findSubtitleType(url: String): SubtitleType? { private fun findSubtitleType(url: String): SubtitleType {
// First, try to determine the type based on the URL file extension // First, try to determine the type based on the URL file extension
val type: SubtitleType = when { val type: SubtitleType = when {
url.endsWith(".vtt", true) -> SubtitleType.VTT url.endsWith(".vtt", true) -> SubtitleType.VTT

View file

@ -56,7 +56,7 @@ abstract class BaseParser {
* **/ * **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? { open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response: ShowResponse? = loadSavedShowResponse(mediaObj.id) 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) saveShowResponse(mediaObj.id, response, true)
} else { } else {
setUserText("Searching : ${mediaObj.mainName()}") setUserText("Searching : ${mediaObj.mainName()}")
@ -156,9 +156,9 @@ abstract class BaseParser {
} }
fun checkIfVariablesAreEmpty() { fun checkIfVariablesAreEmpty() {
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `hostUrl` for the Parser") if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
if (name.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `name` for the Parser") if (name.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `saveName` for the Parser") if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
} }
open var showUserText = "" open var showUserText = ""

View file

@ -16,6 +16,9 @@ abstract class WatchSources : BaseSources() {
?: EmptyAnimeParser() ?: EmptyAnimeParser()
} }
fun isDownloadedSource(i: Int): Boolean {
return get(i) is OfflineAnimeParser
}
suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap<String, Episode> { suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap<String, Episode> {
return tryWithSuspend(true) { return tryWithSuspend(true) {
@ -46,6 +49,19 @@ abstract class WatchSources : BaseSources() {
sEpisode = it.sEpisode 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 return map

View file

@ -1,5 +1,6 @@
package ani.dantotsu.parsers package ani.dantotsu.parsers
import android.content.Context
import ani.dantotsu.Lazier import ani.dantotsu.Lazier
import ani.dantotsu.lazyList import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
@ -7,12 +8,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() { object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList() override var list: List<Lazier<BaseParser>> = emptyList()
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 // Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first() val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) + Lazier( list = createParsersFromExtensions(initialExtensions) + Lazier(
@ -22,19 +24,37 @@ object MangaSources : MangaReadSources() {
// Update as StateFlow emits new values // Update as StateFlow emits new values
fromExtensions.collect { extensions -> fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions) + Lazier( list = sortPinnedMangaSources(createParsersFromExtensions(extensions), pinnedMangaSources) + Lazier(
{ OfflineMangaParser() }, { OfflineMangaParser() },
"Downloaded" "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>> { private fun createParsersFromExtensions(extensions: List<MangaExtension.Installed>): List<Lazier<BaseParser>> {
return extensions.map { extension -> return extensions.map { extension ->
val name = extension.name val name = extension.name
Lazier({ DynamicMangaParser(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() { object HMangaSources : MangaReadSources() {

View file

@ -34,7 +34,7 @@ abstract class NovelParser : BaseParser() {
//val query = mediaObj.name ?: mediaObj.nameRomaji //val query = mediaObj.name ?: mediaObj.nameRomaji
//return search(query).sortByVolume(query) //return search(query).sortByVolume(query)
val results: List<ShowResponse> val results: List<ShowResponse>
return if(mediaObj.name != null) { return if (mediaObj.name != null) {
val query = mediaObj.name val query = mediaObj.name
results = search(query).sortByVolume(query) results = search(query).sortByVolume(query)
results.ifEmpty { results.ifEmpty {

View 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
}
}
}

View file

@ -76,7 +76,7 @@ class OfflineMangaParser : MangaParser() {
} }
override suspend fun search(query: String): List<ShowResponse> { 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() val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) { for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {

View file

@ -3,16 +3,13 @@ package ani.dantotsu.parsers
import android.os.Environment import android.os.Environment
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.manga.MangaNameAdapter 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 me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
class OfflineNovelParser: NovelParser() { class OfflineNovelParser : NovelParser() {
private val downloadManager = Injekt.get<DownloadsManager>() private val downloadManager = Injekt.get<DownloadsManager>()
override val hostUrl: String = "Offline" override val hostUrl: String = "Offline"
@ -34,7 +31,7 @@ class OfflineNovelParser: NovelParser() {
if (it.isDirectory) { if (it.isDirectory) {
val chapter = Book( val chapter = Book(
it.name, it.name,
it.absolutePath + "/cover.jpg", it.absolutePath + "/cover.jpg",
null, null,
listOf(it.absolutePath + "/0.epub") listOf(it.absolutePath + "/0.epub")
) )
@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() {
} }
override suspend fun search(query: String): List<ShowResponse> { 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() val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) { for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { 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 { names.forEach {
returnList.add(ShowResponse(it, it, cover)) returnList.add(ShowResponse(it, it, cover))
} }

View file

@ -57,10 +57,13 @@ data class VideoServer(
val name: String, val name: String,
val embed: FileUrl, val embed: FileUrl,
val extraData: Map<String, String>? = null, 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 { ) : Serializable {
constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null) constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null)
: this(name, FileUrl(embedUrl), extraData) : this(name, FileUrl(embedUrl), extraData)
constructor(name: String, offline: Boolean, extraData: Map<String, String>?)
: this(name, FileUrl(""), extraData, null, offline)
constructor( constructor(
name: String, name: String,

View file

@ -22,10 +22,10 @@ class DynamicNovelParser(extension: NovelExtension.Installed) : NovelParser() {
override suspend fun search(query: String): List<ShowResponse> { override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.firstOrNull() val source = extension.sources.firstOrNull()
if (source is NovelInterface) { return if (source is NovelInterface) {
return source.search(query, client) source.search(query, client)
} else { } else {
return emptyList() emptyList()
} }
} }

View file

@ -111,11 +111,12 @@ internal object NovelExtensionLoader {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun getSignatureHash(pkgInfo: PackageInfo): List<String>? { private fun getSignatureHash(pkgInfo: PackageInfo): List<String>? {
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) { val signatures =
pkgInfo.signingInfo.apkContentsSigners if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
} else { pkgInfo.signingInfo.apkContentsSigners
pkgInfo.signatures } else {
} pkgInfo.signatures
}
return if (!signatures.isNullOrEmpty()) { return if (!signatures.isNullOrEmpty()) {
signatures.map { Hash.sha256(it.toByteArray()) } signatures.map { Hash.sha256(it.toByteArray()) }
} else { } else {

View file

@ -11,6 +11,7 @@ data class CurrentNovelReaderSettings(
var justify: Boolean = true, var justify: Boolean = true,
var hyphenation: Boolean = true, var hyphenation: Boolean = true,
var useDarkTheme: Boolean = false, var useDarkTheme: Boolean = false,
var useOledTheme: Boolean = false,
var invert: Boolean = false, var invert: Boolean = false,
var maxInlineSize: Int = 720, var maxInlineSize: Int = 720,
var maxBlockSize: Int = 1440, var maxBlockSize: Int = 1440,

View file

@ -6,11 +6,10 @@ import android.os.Build.VERSION.*
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
@ -23,12 +22,8 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
class ExtensionsActivity : AppCompatActivity() { class ExtensionsActivity : AppCompatActivity() {
private val restartMainActivity = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity)
}
lateinit var binding: ActivityExtensionsBinding lateinit var binding: ActivityExtensionsBinding
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -119,17 +114,18 @@ class ExtensionsActivity : AppCompatActivity() {
initActivity(this) initActivity(this)
/* TODO binding.languageselect.visibility = View.GONE
binding.languageselect.setOnClickListener { /* TODO
val popup = PopupMenu(this, it) binding.languageselect.setOnClickListener {
popup.inflate(R.menu.launguage_selector_menu) val popup = PopupMenu(this, it)
popup.setOnMenuItemClickListener { menuItem -> popup.inflate(R.menu.launguage_selector_menu)
true popup.setOnMenuItemClickListener { menuItem ->
} true
popup.setOnDismissListener { }
} popup.setOnDismissListener {
popup.show() }
}*/ popup.show()
}*/
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight
bottomMargin = navBarHeight bottomMargin = navBarHeight

View file

@ -18,88 +18,83 @@ class FAQActivity : AppCompatActivity() {
Triple( Triple(
R.drawable.ic_round_help_24, R.drawable.ic_round_help_24,
currContext()!!.getString(R.string.question_1), currContext()?.getString(R.string.question_1) ?: "",
currContext()!!.getString(R.string.answer_1) currContext()?.getString(R.string.answer_1) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_auto_awesome_24, R.drawable.ic_round_auto_awesome_24,
currContext()!!.getString(R.string.question_2), currContext()?.getString(R.string.question_2) ?: "",
currContext()!!.getString(R.string.answer_2) currContext()?.getString(R.string.answer_2) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_auto_awesome_24, R.drawable.ic_round_auto_awesome_24,
currContext()!!.getString(R.string.question_17), currContext()?.getString(R.string.question_17) ?: "",
currContext()!!.getString(R.string.answer_17) currContext()?.getString(R.string.answer_17) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_download_24, R.drawable.ic_round_download_24,
currContext()!!.getString(R.string.question_3), currContext()?.getString(R.string.question_3) ?: "",
currContext()!!.getString(R.string.answer_3) currContext()?.getString(R.string.answer_3) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_help_24, R.drawable.ic_round_help_24,
currContext()!!.getString(R.string.question_16), currContext()?.getString(R.string.question_16) ?: "",
currContext()!!.getString(R.string.answer_16) currContext()?.getString(R.string.answer_16) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_dns_24, R.drawable.ic_round_dns_24,
currContext()!!.getString(R.string.question_4), currContext()?.getString(R.string.question_4) ?: "",
currContext()!!.getString(R.string.answer_4) currContext()?.getString(R.string.answer_4) ?: ""
), ),
Triple( Triple(
R.drawable.ic_baseline_screen_lock_portrait_24, R.drawable.ic_baseline_screen_lock_portrait_24,
currContext()!!.getString(R.string.question_5), currContext()?.getString(R.string.question_5) ?: "",
currContext()!!.getString(R.string.answer_5) currContext()?.getString(R.string.answer_5) ?: ""
), ),
Triple( Triple(
R.drawable.ic_anilist, R.drawable.ic_anilist,
currContext()!!.getString(R.string.question_6), currContext()?.getString(R.string.question_6) ?: "",
currContext()!!.getString(R.string.answer_6) currContext()?.getString(R.string.answer_6) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_movie_filter_24, R.drawable.ic_round_movie_filter_24,
currContext()!!.getString(R.string.question_7), currContext()?.getString(R.string.question_7) ?: "",
currContext()!!.getString(R.string.answer_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)
), ),
Triple( Triple(
R.drawable.ic_round_lock_open_24, R.drawable.ic_round_lock_open_24,
currContext()!!.getString(R.string.question_9), currContext()?.getString(R.string.question_9) ?: "",
currContext()!!.getString(R.string.answer_9) currContext()?.getString(R.string.answer_9) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_smart_button_24, R.drawable.ic_round_smart_button_24,
currContext()!!.getString(R.string.question_10), currContext()?.getString(R.string.question_10) ?: "",
currContext()!!.getString(R.string.answer_10) currContext()?.getString(R.string.answer_10) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_smart_button_24, R.drawable.ic_round_smart_button_24,
currContext()!!.getString(R.string.question_11), currContext()?.getString(R.string.question_11) ?: "",
currContext()!!.getString(R.string.answer_11) currContext()?.getString(R.string.answer_11) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_info_24, R.drawable.ic_round_info_24,
currContext()!!.getString(R.string.question_12), currContext()?.getString(R.string.question_12) ?: "",
currContext()!!.getString(R.string.answer_12) currContext()?.getString(R.string.answer_12) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_help_24, R.drawable.ic_round_help_24,
currContext()!!.getString(R.string.question_13), currContext()?.getString(R.string.question_13) ?: "",
currContext()!!.getString(R.string.answer_13) currContext()?.getString(R.string.answer_13) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_art_track_24, R.drawable.ic_round_art_track_24,
currContext()!!.getString(R.string.question_14), currContext()?.getString(R.string.question_14) ?: "",
currContext()!!.getString(R.string.answer_14) currContext()?.getString(R.string.answer_14) ?: ""
), ),
Triple( Triple(
R.drawable.ic_round_video_settings_24, R.drawable.ic_round_video_settings_24,
currContext()!!.getString(R.string.question_15), currContext()?.getString(R.string.question_15) ?: "",
currContext()!!.getString(R.string.answer_15) currContext()?.getString(R.string.answer_15) ?: ""
) )
) )
} }

View file

@ -1,5 +1,6 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
@ -45,90 +46,84 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentAnimeExtensionsBinding? = null private var _binding: FragmentAnimeExtensionsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false private val skipIcons = loadData("skip_extension_icons") ?: false
private val animeExtensionManager: AnimeExtensionManager = Injekt.get() private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
private val extensionsAdapter = AnimeExtensionsAdapter( private val extensionsAdapter = AnimeExtensionsAdapter(
{ pkg -> { 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>() val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) { if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0] var selectedSetting = allSettings[0]
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray() val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
.toTypedArray()
var selectedIndex = 0 var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source") .setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which -> .setSingleChoiceItems(names, selectedIndex) { dialog, which ->
itemSelected = true
selectedIndex = which selectedIndex = which
selectedSetting = allSettings[selectedIndex] selectedSetting = allSettings[selectedIndex]
dialog.dismiss() dialog.dismiss()
// Move the fragment transaction here val fragment =
val eActivity = requireActivity() as ExtensionsActivity AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
eActivity.runOnUiThread { changeUIVisibility(true)
val fragment = }
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
eActivity.findViewById<ViewPager2>(R.id.viewPager).visibility = .replace(R.id.fragmentExtensionsContainer, fragment)
View.VISIBLE .addToBackStack(null)
eActivity.findViewById<TabLayout>(R.id.tabLayout).visibility = .commit()
View.VISIBLE }
eActivity.findViewById<TextInputLayout>(R.id.searchView).visibility = .setOnDismissListener {
View.VISIBLE if (!itemSelected) {
eActivity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = changeUIVisibility(true)
View.GONE
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
} }
} }
.show() .show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
} else { } else {
// If there's only one setting, proceed with the fragment transaction // If there's only one setting, proceed with the fragment transaction
val eActivity = requireActivity() as ExtensionsActivity val fragment =
eActivity.runOnUiThread { AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
val fragment = changeUIVisibility(true)
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { }
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 // Hide ViewPager2 and TabLayout
val activity = requireActivity() as ExtensionsActivity changeUIVisibility(false)
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.GONE
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.GONE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.GONE
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
View.VISIBLE
} else { } else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show() .show()
} }
}, },
{ pkg -> { pkg, forceDelete ->
if (isAdded) { // Check if the fragment is currently added to its activity if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable val context = requireContext() // Store context in a variable
val notificationManager = val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
if (pkg.hasUpdate) { if (pkg.hasUpdate && !forceDelete) {
animeExtensionManager.updateExtension(pkg) animeExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe( .subscribe(
@ -209,7 +204,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private class AnimeExtensionsAdapter( private class AnimeExtensionsAdapter(
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit, private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit, private val onUninstallClicked: (AnimeExtension.Installed, Boolean) -> Unit,
val skipIcons: Boolean val skipIcons: Boolean
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>( ) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED DIFF_CALLBACK_INSTALLED
@ -225,6 +220,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
return ViewHolder(view) return ViewHolder(view)
} }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) "(18+)" else "" 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.setImageResource(R.drawable.ic_round_delete_24)
} }
holder.closeTextView.setOnClickListener { holder.closeTextView.setOnClickListener {
onUninstallClicked(extension) onUninstallClicked(extension, false)
} }
holder.settingsImageView.setOnClickListener { holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension) onSettingsClicked(extension)
} }
holder.card.setOnLongClickListener {
onUninstallClicked(extension, true)
true
}
} }
fun filter(query: String, currentList: List<AnimeExtension.Installed>) { 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 settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView) val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
val card = view.findViewById<View>(R.id.extensionCardView)
} }
companion object { companion object {

View file

@ -1,6 +1,7 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
@ -44,70 +45,84 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentMangaExtensionsBinding? = null private var _binding: FragmentMangaExtensionsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false private val skipIcons = loadData("skip_extension_icons") ?: false
private val mangaExtensionManager: MangaExtensionManager = Injekt.get() private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
private val extensionsAdapter = MangaExtensionsAdapter({ pkg -> private val extensionsAdapter = MangaExtensionsAdapter(
val changeUIVisibility: (Boolean) -> Unit = { show -> { pkg ->
val activity = requireActivity() as ExtensionsActivity val name = pkg.name
val visibility = if (show) View.VISIBLE else View.GONE val changeUIVisibility: (Boolean) -> Unit = { show ->
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility val activity = requireActivity() as ExtensionsActivity
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
if (show) View.GONE else View.VISIBLE activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
} activity.findViewById<ImageView>(R.id.languageselect).visibility = visibility
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>() activity.findViewById<TextView>(R.id.extensions).text =
if (allSettings.isNotEmpty()) { if (show) getString(R.string.extensions) else name
var selectedSetting = allSettings[0] activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (allSettings.size > 1) { if (show) View.GONE else View.VISIBLE
val names = allSettings.map { it.lang }.toTypedArray() }
var selectedIndex = 0 var itemSelected = false
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
.setTitle("Select a Source") if (allSettings.isNotEmpty()) {
.setSingleChoiceItems(names, selectedIndex) { dialog, which -> var selectedSetting = allSettings[0]
selectedIndex = which if (allSettings.size > 1) {
selectedSetting = allSettings[selectedIndex] val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
dialog.dismiss() .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 // Move the fragment transaction here
val fragment = val fragment =
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { 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) changeUIVisibility(true)
} }
parentFragmentManager.beginTransaction() }
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down) .show()
.replace(R.id.fragmentExtensionsContainer, fragment) dialog.window?.setDimAmount(0.8f)
.addToBackStack(null) } else {
.commit() // If there's only one setting, proceed with the fragment transaction
} val fragment =
.show() MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
dialog.window?.setDimAmount(0.8f) changeUIVisibility(true)
} else { }
// If there's only one setting, proceed with the fragment transaction parentFragmentManager.beginTransaction()
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
changeUIVisibility(true) .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 // Hide ViewPager2 and TabLayout
changeUIVisibility(false) changeUIVisibility(false)
} else { } else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show() .show()
} }
}, },
{ pkg -> { pkg: MangaExtension.Installed, forceDelete: Boolean ->
if (isAdded) { // Check if the fragment is currently added to its activity if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable val context = requireContext() // Store context in a variable
val notificationManager = val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
if (pkg.hasUpdate) { if (pkg.hasUpdate && !forceDelete) {
mangaExtensionManager.updateExtension(pkg) mangaExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe( .subscribe(
@ -188,7 +203,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private class MangaExtensionsAdapter( private class MangaExtensionsAdapter(
private val onSettingsClicked: (MangaExtension.Installed) -> Unit, private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
private val onUninstallClicked: (MangaExtension.Installed) -> Unit, private val onUninstallClicked: (MangaExtension.Installed, Boolean) -> Unit,
skipIcons: Boolean skipIcons: Boolean
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>( ) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED DIFF_CALLBACK_INSTALLED
@ -206,6 +221,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
return ViewHolder(view) return ViewHolder(view)
} }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) "(18+)" else "" 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.setImageResource(R.drawable.ic_round_delete_24)
} }
holder.closeTextView.setOnClickListener { holder.closeTextView.setOnClickListener {
onUninstallClicked(extension) onUninstallClicked(extension, false)
} }
holder.settingsImageView.setOnClickListener { holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension) onSettingsClicked(extension)
} }
holder.card.setOnLongClickListener {
onUninstallClicked(extension, true)
true
}
} }
fun filter(query: String, currentList: List<MangaExtension.Installed>) { 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 settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView) val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
val card: View = view.findViewById(R.id.extensionCardView)
} }
companion object { companion object {

View file

@ -39,17 +39,18 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
private lateinit var extensionsRecyclerView: RecyclerView private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false val skipIcons = loadData("skip_extension_icons") ?: false
private val novelExtensionManager: NovelExtensionManager = Injekt.get() private val novelExtensionManager: NovelExtensionManager = Injekt.get()
private val extensionsAdapter = NovelExtensionsAdapter({ pkg -> private val extensionsAdapter = NovelExtensionsAdapter(
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
},
{ pkg -> { 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 if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable val context = requireContext() // Store context in a variable
val notificationManager = val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
if (pkg.hasUpdate) { if (pkg.hasUpdate && !forceDelete) {
novelExtensionManager.updateExtension(pkg) novelExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe( .subscribe(
@ -130,7 +131,7 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
private class NovelExtensionsAdapter( private class NovelExtensionsAdapter(
private val onSettingsClicked: (NovelExtension.Installed) -> Unit, private val onSettingsClicked: (NovelExtension.Installed) -> Unit,
private val onUninstallClicked: (NovelExtension.Installed) -> Unit, private val onUninstallClicked: (NovelExtension.Installed, Boolean) -> Unit,
skipIcons: Boolean skipIcons: Boolean
) : ListAdapter<NovelExtension.Installed, NovelExtensionsAdapter.ViewHolder>( ) : ListAdapter<NovelExtension.Installed, NovelExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED DIFF_CALLBACK_INSTALLED
@ -165,11 +166,15 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24) holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
} }
holder.closeTextView.setOnClickListener { holder.closeTextView.setOnClickListener {
onUninstallClicked(extension) onUninstallClicked(extension, false)
} }
holder.settingsImageView.setOnClickListener { holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension) onSettingsClicked(extension)
} }
holder.card.setOnLongClickListener {
onUninstallClicked(extension, true)
true
}
} }
fun filter(query: String, currentList: List<NovelExtension.Installed>) { 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 settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView) val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
val card = view.findViewById<View>(R.id.extensionCardView)
} }
companion object { companion object {

View file

@ -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

View file

@ -22,7 +22,7 @@ data class PlayerSettings(
//TimeStamps //TimeStamps
var timeStampsEnabled: Boolean = true, var timeStampsEnabled: Boolean = true,
var useProxyForTimeStamps: Boolean = true, var useProxyForTimeStamps: Boolean = false,
var showTimeStampButton: Boolean = true, var showTimeStampButton: Boolean = true,
//Auto //Auto
@ -45,6 +45,6 @@ data class PlayerSettings(
var skipTime: Int = 85, var skipTime: Int = 85,
//Other //Other
var cast: Boolean = false, var cast: Boolean = true,
var pip: Boolean = true var pip: Boolean = true
) : Serializable ) : Serializable

View file

@ -97,7 +97,21 @@ class PlayerSettingsActivity : AppCompatActivity() {
val speeds = 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) 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 curSpeedArr = if (settings.cursedSpeeds) cursedSpeeds else speeds
var speedsName = curSpeedArr.map { "${it}x" }.toTypedArray() var speedsName = curSpeedArr.map { "${it}x" }.toTypedArray()
@ -106,13 +120,14 @@ class PlayerSettingsActivity : AppCompatActivity() {
val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme) val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.default_speed)) .setTitle(getString(R.string.default_speed))
binding.playerSettingsSpeed.setOnClickListener { binding.playerSettingsSpeed.setOnClickListener {
val dialog = speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i -> val dialog =
settings.defaultSpeed = i speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i ->
binding.playerSettingsSpeed.text = settings.defaultSpeed = i
getString(R.string.default_playback_speed, speedsName[i]) binding.playerSettingsSpeed.text =
saveData(player, settings) getString(R.string.default_playback_speed, speedsName[i])
dialog.dismiss() saveData(player, settings)
}.show() dialog.dismiss()
}.show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
} }
@ -256,11 +271,12 @@ class PlayerSettingsActivity : AppCompatActivity() {
val resizeDialog = AlertDialog.Builder(this, R.style.DialogTheme) val resizeDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.default_resize_mode)) .setTitle(getString(R.string.default_resize_mode))
binding.playerResizeMode.setOnClickListener { binding.playerResizeMode.setOnClickListener {
val dialog = resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count -> val dialog =
settings.resize = count resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count ->
saveData(player, settings) settings.resize = count
dialog.dismiss() saveData(player, settings)
}.show() dialog.dismiss()
}.show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
} }
fun restartApp() { fun restartApp() {
@ -382,7 +398,10 @@ class PlayerSettingsActivity : AppCompatActivity() {
val outlineDialog = AlertDialog.Builder(this, R.style.DialogTheme) val outlineDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.outline_type)) .setTitle(getString(R.string.outline_type))
binding.videoSubOutline.setOnClickListener { binding.videoSubOutline.setOnClickListener {
val dialog = outlineDialog.setSingleChoiceItems(typesOutline, settings.outline) { dialog, count -> val dialog = outlineDialog.setSingleChoiceItems(
typesOutline,
settings.outline
) { dialog, count ->
settings.outline = count settings.outline = count
saveData(player, settings) saveData(player, settings)
dialog.dismiss() dialog.dismiss()

View file

@ -8,6 +8,7 @@ data class ReaderSettings(
var autoDetectWebtoon: Boolean = true, var autoDetectWebtoon: Boolean = true,
var default: CurrentReaderSettings = CurrentReaderSettings(), var default: CurrentReaderSettings = CurrentReaderSettings(),
var defaultLN: CurrentNovelReaderSettings = CurrentNovelReaderSettings(),
var askIndividual: Boolean = true, var askIndividual: Boolean = true,
var updateForH: Boolean = false var updateForH: Boolean = false

View file

@ -42,7 +42,7 @@ class ReaderSettingsActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
//General //Manga Settings
binding.readerSettingsSourceName.isChecked = settings.showSource binding.readerSettingsSourceName.isChecked = settings.showSource
binding.readerSettingsSourceName.setOnCheckedChangeListener { _, isChecked -> binding.readerSettingsSourceName.setOnCheckedChangeListener { _, isChecked ->
settings.showSource = isChecked settings.showSource = isChecked
@ -54,14 +54,14 @@ class ReaderSettingsActivity : AppCompatActivity() {
settings.showSystemBars = isChecked settings.showSystemBars = isChecked
saveData(reader, settings) saveData(reader, settings)
} }
//Default Manga
binding.readerSettingsAutoWebToon.isChecked = settings.autoDetectWebtoon binding.readerSettingsAutoWebToon.isChecked = settings.autoDetectWebtoon
binding.readerSettingsAutoWebToon.setOnCheckedChangeListener { _, isChecked -> binding.readerSettingsAutoWebToon.setOnCheckedChangeListener { _, isChecked ->
settings.autoDetectWebtoon = isChecked settings.autoDetectWebtoon = isChecked
saveData(reader, settings) saveData(reader, settings)
} }
//Default
val layoutList = listOf( val layoutList = listOf(
binding.readerSettingsPaged, binding.readerSettingsPaged,
binding.readerSettingsContinuousPaged, binding.readerSettingsContinuousPaged,
@ -185,6 +185,169 @@ class ReaderSettingsActivity : AppCompatActivity() {
saveData(reader, settings) 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 //Update Progress
binding.readerSettingsAskUpdateProgress.isChecked = settings.askIndividual binding.readerSettingsAskUpdateProgress.isChecked = settings.askIndividual
binding.readerSettingsAskUpdateProgress.setOnCheckedChangeListener { _, isChecked -> binding.readerSettingsAskUpdateProgress.setOnCheckedChangeListener { _, isChecked ->

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.os.Build.* import android.os.Build.*
import android.os.Build.VERSION.* import android.os.Build.VERSION.*
@ -11,19 +12,24 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.ActivitySettingsBinding 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.AppUpdater
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
@ -37,7 +43,9 @@ import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText 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.domain.base.BasePreferences
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
@ -50,7 +58,7 @@ import uy.kohesive.injekt.api.get
import kotlin.random.Random import kotlin.random.Random
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListener {
private val restartMainActivity = object : OnBackPressedCallback(false) { private val restartMainActivity = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity) override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
} }
@ -59,6 +67,7 @@ class SettingsActivity : AppCompatActivity() {
private val networkPreferences = Injekt.get<NetworkPreferences>() private val networkPreferences = Injekt.get<NetworkPreferences>()
private var cursedCounter = 0 private var cursedCounter = 0
@OptIn(UnstableApi::class)
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -71,26 +80,7 @@ class SettingsActivity : AppCompatActivity() {
binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME) binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
binding.settingsVersion.setOnLongClickListener { binding.settingsVersion.setOnLongClickListener {
fun getArch(): String { copyToClipboard(getDeviceInfo(), false)
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)
toast(getString(R.string.copied_device_info)) toast(getString(R.string.copied_device_info))
return@setOnLongClickListener true return@setOnLongClickListener true
} }
@ -176,34 +166,30 @@ class SettingsActivity : AppCompatActivity() {
binding.customTheme.setOnClickListener { binding.customTheme.setOnClickListener {
var passedColor: Int = 0 val originalColor = getSharedPreferences("Dantotsu", MODE_PRIVATE).getInt(
val dialogView = layoutInflater.inflate(R.layout.dialog_color_picker, null) "custom_theme_int",
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup) Color.parseColor("#6200EE")
.setTitle("Custom Theme") )
.setView(dialogView)
.setPositiveButton("OK") { dialog, _ -> class CustomColorDialog : SimpleColorDialog() { //idk where to put it
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() override fun onPositiveButtonClick() {
.putInt("custom_theme_int", passedColor).apply()
logger("Custom Theme: $passedColor")
dialog.dismiss()
restartApp() restartApp()
super.onPositiveButtonClick()
} }
.setNegativeButton("Cancel") { dialog, _ -> }
dialog.dismiss()
} val tag = "colorPicker"
.create() CustomColorDialog().title("Custom Theme")
val colorPickerView = .colorPreset(originalColor)
dialogView.findViewById<com.skydoves.colorpickerview.ColorPickerView>(R.id.colorPickerView) .colors(this, SimpleColorDialog.BEIGE_COLOR_PALLET)
colorPickerView.setColorListener(ColorListener { color, fromUser -> .allowCustom(true)
val linearLayout = dialogView.findViewById<LinearLayout>(R.id.linear) .showOutline(0x46000000)
passedColor = color .gridNumColumn(5)
linearLayout.setBackgroundColor(color) .choiceMode(SimpleColorDialog.SINGLE_CHOICE)
}) .neg()
alertDialog.show() .show(this, tag)
alertDialog.window?.setDimAmount(0.8f)
} }
//val animeSource = loadData<Int>("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
val animeSource = getSharedPreferences( val animeSource = getSharedPreferences(
"Dantotsu", "Dantotsu",
Context.MODE_PRIVATE Context.MODE_PRIVATE
@ -228,6 +214,47 @@ class SettingsActivity : AppCompatActivity() {
binding.animeSource.clearFocus() 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 { binding.settingsPlayer.setOnClickListener {
startActivity(Intent(this, PlayerSettingsActivity::class.java)) startActivity(Intent(this, PlayerSettingsActivity::class.java))
} }
@ -237,7 +264,10 @@ class SettingsActivity : AppCompatActivity() {
AlertDialog.Builder(this, R.style.DialogTheme).setTitle("Download Manager") AlertDialog.Builder(this, R.style.DialogTheme).setTitle("Download Manager")
var downloadManager = loadData<Int>("settings_download_manager") ?: 0 var downloadManager = loadData<Int>("settings_download_manager") ?: 0
binding.settingsDownloadManager.setOnClickListener { binding.settingsDownloadManager.setOnClickListener {
val dialog = downloadManagerDialog.setSingleChoiceItems(managers, downloadManager) { dialog, count -> val dialog = downloadManagerDialog.setSingleChoiceItems(
managers,
downloadManager
) { dialog, count ->
downloadManager = count downloadManager = count
saveData("settings_download_manager", downloadManager) saveData("settings_download_manager", downloadManager)
dialog.dismiss() dialog.dismiss()
@ -245,6 +275,62 @@ class SettingsActivity : AppCompatActivity() {
dialog.window?.setDimAmount(0.8f) 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 = binding.settingsForceLegacyInstall.isChecked =
extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY
binding.settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked -> binding.settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked ->
@ -259,9 +345,9 @@ class SettingsActivity : AppCompatActivity() {
binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked -> binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked ->
saveData("skip_extension_icons", isChecked) saveData("skip_extension_icons", isChecked)
} }
binding.NSFWExtension.isChecked = loadData("NFSWExtension") ?: true binding.NSFWExtension.isChecked = loadData("NSFWExtension") ?: true
binding.NSFWExtension.setOnCheckedChangeListener { _, isChecked -> binding.NSFWExtension.setOnCheckedChangeListener { _, isChecked ->
saveData("NFSWExtension", isChecked) saveData("NSFWExtension", isChecked)
} }
@ -278,7 +364,7 @@ class SettingsActivity : AppCompatActivity() {
} }
.setNeutralButton("Reset") { dialog, _ -> .setNeutralButton("Reset") { dialog, _ ->
networkPreferences.defaultUserAgent() 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("") editText.setText("")
dialog.dismiss() dialog.dismiss()
} }
@ -339,6 +425,17 @@ class SettingsActivity : AppCompatActivity() {
binding.settingsRecentlyListOnly.setOnCheckedChangeListener { _, isChecked -> binding.settingsRecentlyListOnly.setOnCheckedChangeListener { _, isChecked ->
saveData("recently_list_only", 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.isChecked = loadData("settings_prefer_dub") ?: false
binding.settingsPreferDub.setOnCheckedChangeListener { _, isChecked -> binding.settingsPreferDub.setOnCheckedChangeListener { _, isChecked ->
saveData("settings_prefer_dub", isChecked) saveData("settings_prefer_dub", isChecked)
@ -370,6 +467,47 @@ class SettingsActivity : AppCompatActivity() {
binding.mangaSource.clearFocus() 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 { binding.settingsReader.setOnClickListener {
startActivity(Intent(this, ReaderSettingsActivity::class.java)) startActivity(Intent(this, ReaderSettingsActivity::class.java))
} }
@ -408,16 +546,6 @@ class SettingsActivity : AppCompatActivity() {
uiTheme(true, it) 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) { var previousStart: View = when (uiSettings.defaultStartUpTab) {
0 -> binding.uiSettingsAnime 0 -> binding.uiSettingsAnime
1 -> binding.uiSettingsHome 1 -> binding.uiSettingsHome
@ -434,6 +562,7 @@ class SettingsActivity : AppCompatActivity() {
initActivity(this) initActivity(this)
} }
binding.uiSettingsAnime.setOnClickListener { binding.uiSettingsAnime.setOnClickListener {
uiTheme(0, it) uiTheme(0, it)
} }
@ -510,10 +639,6 @@ class SettingsActivity : AppCompatActivity() {
lifecycleScope.launch { lifecycleScope.launch {
binding.settingBuyMeCoffee.pop() binding.settingBuyMeCoffee.pop()
} }
binding.settingUPI.visibility = if (checkCountry(this)) View.VISIBLE else View.GONE
lifecycleScope.launch {
binding.settingUPI.pop()
}
binding.loginDiscord.setOnClickListener { binding.loginDiscord.setOnClickListener {
openLinkInBrowser(getString(R.string.discord)) openLinkInBrowser(getString(R.string.discord))
@ -521,7 +646,9 @@ class SettingsActivity : AppCompatActivity() {
binding.loginGithub.setOnClickListener { binding.loginGithub.setOnClickListener {
openLinkInBrowser(getString(R.string.github)) openLinkInBrowser(getString(R.string.github))
} }
binding.loginTelegram.setOnClickListener {
openLinkInBrowser(getString(R.string.telegram))
}
binding.settingsUi.setOnClickListener { binding.settingsUi.setOnClickListener {
startActivity(Intent(this, UserInterfaceSettingsActivity::class.java)) startActivity(Intent(this, UserInterfaceSettingsActivity::class.java))
} }
@ -759,8 +886,7 @@ class SettingsActivity : AppCompatActivity() {
} }
setPositiveButton("denote :)") { setPositiveButton("denote :)") {
if (binding.settingUPI.visibility == View.VISIBLE) binding.settingUPI.performClick() binding.settingBuyMeCoffee.performClick()
else binding.settingBuyMeCoffee.performClick()
dismiss() dismiss()
} }
show(supportFragmentManager, "dialog") 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() { private fun restartApp() {
Snackbar.make( Snackbar.make(
binding.root, binding.root,
@ -788,4 +926,28 @@ class SettingsActivity : AppCompatActivity() {
show() 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"
}
}
}

View file

@ -1,41 +1,43 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.app.DownloadManager import android.content.Context
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.MainActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSettingsBinding 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.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.loadImage
import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.toast
class SettingsDialogFragment : BottomSheetDialogFragment() {
class SettingsDialogFragment() : BottomSheetDialogFragment() {
private var _binding: BottomSheetSettingsBinding? = null private var _binding: BottomSheetSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var pageType: PageType private lateinit var pageType: PageType
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
pageType = arguments?.getSerializable("pageType") as? PageType ?: PageType.HOME pageType = arguments?.getSerializable("pageType") as? PageType ?: PageType.HOME
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, 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 { binding.settingsExtensionSettings.setSafeOnClickListener {
startActivity(Intent(activity, ExtensionsActivity::class.java)) startActivity(Intent(activity, ExtensionsActivity::class.java))
dismiss() dismiss()
@ -88,48 +101,61 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
startActivity(Intent(activity, ImageSearchActivity::class.java)) startActivity(Intent(activity, ImageSearchActivity::class.java))
dismiss() dismiss()
} }
binding.settingsDownloads.setSafeOnClickListener {
binding.settingsDownloads.isChecked =
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("offlineMode", false) ?: false
binding.settingsDownloads.setOnCheckedChangeListener { _, isChecked ->
when (pageType) { when (pageType) {
PageType.MANGA -> { PageType.MANGA -> {
val intent = Intent(activity, DownloadContainerActivity::class.java) val intent = Intent(activity, NoInternet::class.java)
intent.putExtra("FRAGMENT_CLASS_NAME", OfflineMangaFragment::class.java.name) intent.putExtra(
"FRAGMENT_CLASS_NAME",
OfflineMangaFragment::class.java.name
)
startActivity(intent) startActivity(intent)
} }
PageType.ANIME -> { PageType.ANIME -> {
try { val intent = Intent(activity, NoInternet::class.java)
val arrayOfFiles = intent.putExtra(
ContextCompat.getExternalFilesDirs(requireContext(), null) "FRAGMENT_CLASS_NAME",
startActivity( OfflineAnimeFragment::class.java.name
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) { )
val parentDirectory = arrayOfFiles[1].toString() startActivity(intent)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
)
} catch (e: ActivityNotFoundException) {
toast(getString(R.string.file_manager_not_found))
}
} }
PageType.HOME -> { PageType.HOME -> {
try { val intent = Intent(activity, NoInternet::class.java)
val arrayOfFiles = intent.putExtra("FRAGMENT_CLASS_NAME", OfflineFragment::class.java.name)
ContextCompat.getExternalFilesDirs(requireContext(), null) startActivity(intent)
startActivity( }
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString() PageType.OfflineMANGA -> {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(activity, MainActivity::class.java)
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder") intent.putExtra("FRAGMENT_CLASS_NAME", MangaFragment::class.java.name)
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) startActivity(intent)
) }
} catch (e: ActivityNotFoundException) {
toast(getString(R.string.file_manager_not_found)) 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() dismiss()
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putBoolean("offlineMode", isChecked)?.apply()
} }
} }
@ -140,7 +166,7 @@ class SettingsDialogFragment() : BottomSheetDialogFragment() {
companion object { companion object {
enum class PageType { enum class PageType {
MANGA, ANIME, HOME MANGA, ANIME, HOME, OfflineMANGA, OfflineANIME, OfflineHOME
} }
fun newInstance(pageType: PageType): SettingsDialogFragment { fun newInstance(pageType: PageType): SettingsDialogFragment {

View file

@ -44,14 +44,14 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
binding.uiSettingsHomeLayout.setOnClickListener { binding.uiSettingsHomeLayout.setOnClickListener {
val dialog = AlertDialog.Builder(this, R.style.DialogTheme) val dialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.home_layout_show)).apply { .setTitle(getString(R.string.home_layout_show)).apply {
setMultiChoiceItems( setMultiChoiceItems(
views, views,
settings.homeLayoutShow.toBooleanArray() settings.homeLayoutShow.toBooleanArray()
) { _, i, value -> ) { _, i, value ->
settings.homeLayoutShow[i] = value settings.homeLayoutShow[i] = value
saveData(ui, settings) saveData(ui, settings)
} }
}.show() }.show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
} }
@ -68,7 +68,6 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
saveData(ui, settings) saveData(ui, settings)
restartApp() restartApp()
} }
binding.uiSettingsBannerAnimation.isChecked = settings.bannerAnimations binding.uiSettingsBannerAnimation.isChecked = settings.bannerAnimations
binding.uiSettingsBannerAnimation.setOnCheckedChangeListener { _, isChecked -> binding.uiSettingsBannerAnimation.setOnCheckedChangeListener { _, isChecked ->
settings.bannerAnimations = isChecked settings.bannerAnimations = isChecked

View file

@ -10,6 +10,7 @@ import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.forEach import androidx.preference.forEach
import androidx.preference.getOnBindEditTextListener import androidx.preference.getOnBindEditTextListener
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.PreferenceScreen import eu.kanade.tachiyomi.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
@ -21,7 +22,12 @@ import uy.kohesive.injekt.api.get
class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() { class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 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 //set background color
val color = TypedValue() val color = TypedValue()
requireContext().theme.resolveAttribute( requireContext().theme.resolveAttribute(
@ -42,8 +48,8 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
private fun populateAnimePreferenceScreen(): PreferenceScreen { private fun populateAnimePreferenceScreen(): PreferenceScreen {
val sourceId = requireArguments().getLong(SOURCE_ID) val sourceId = requireArguments().getLong(SOURCE_ID)
val source = Injekt.get<AnimeSourceManager>().get(sourceId)!! val source = Injekt.get<AnimeSourceManager>().get(sourceId) as? ConfigurableAnimeSource
check(source is ConfigurableAnimeSource) ?: error("Source with id: $sourceId not found!")
val sharedPreferences = val sharedPreferences =
requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
val dataStore = SharedPreferencesDataStore(sharedPreferences) val dataStore = SharedPreferencesDataStore(sharedPreferences)

View file

@ -1,5 +1,6 @@
package ani.dantotsu.settings.paging package ani.dantotsu.settings.paging
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
@ -91,18 +92,15 @@ class AnimeExtensionPagingSource(
val availableExtensions = val availableExtensions =
availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } availableExtensionsFlow.filterNot { it.pkgName in installedExtensions }
val query = searchQuery val query = searchQuery
val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true
val filteredExtensions = if (query.isEmpty()) { val filteredExtensions = if (query.isEmpty()) {
availableExtensions availableExtensions
} else { } else {
availableExtensions.filter { it.name.contains(query, ignoreCase = true) } availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
} }
val filternfsw = if (isNsfwEnabled) { val filternfsw =
filteredExtensions if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw }
} else {
filteredExtensions.filterNot { it.isNsfw }
}
return try { return try {
val sublist = filternfsw.subList( val sublist = filternfsw.subList(
fromIndex = position, fromIndex = position,
@ -176,6 +174,7 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
init { init {
binding.closeTextView.setOnClickListener { binding.closeTextView.setOnClickListener {
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
val extension = getItem(bindingAdapterPosition) val extension = getItem(bindingAdapterPosition)
if (extension != null) { if (extension != null) {
clickListener.onInstallClick(extension) clickListener.onInstallClick(extension)
@ -198,6 +197,7 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
val extensionIconImageView: ImageView = binding.extensionIconImageView val extensionIconImageView: ImageView = binding.extensionIconImageView
@SuppressLint("SetTextI18n")
fun bind(extension: AnimeExtension.Available) { fun bind(extension: AnimeExtension.Available) {
val nsfw = if (extension.isNsfw) "(18+)" else "" val nsfw = if (extension.isNsfw) "(18+)" else ""
val lang = LanguageMapper.mapLanguageCodeToName(extension.lang) val lang = LanguageMapper.mapLanguageCodeToName(extension.lang)

View file

@ -1,5 +1,6 @@
package ani.dantotsu.settings.paging package ani.dantotsu.settings.paging
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
@ -91,17 +92,14 @@ class MangaExtensionPagingSource(
val availableExtensions = val availableExtensions =
availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } availableExtensionsFlow.filterNot { it.pkgName in installedExtensions }
val query = searchQuery val query = searchQuery
val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true
val filteredExtensions = if (query.isEmpty()) { val filteredExtensions = if (query.isEmpty()) {
availableExtensions availableExtensions
} else { } else {
availableExtensions.filter { it.name.contains(query, ignoreCase = true) } availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
} }
val filternfsw = if (isNsfwEnabled) { val filternfsw =
filteredExtensions if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw }
} else {
filteredExtensions.filterNot { it.isNsfw }
}
return try { return try {
val sublist = filternfsw.subList( val sublist = filternfsw.subList(
fromIndex = position, fromIndex = position,
@ -173,6 +171,7 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
init { init {
binding.closeTextView.setOnClickListener { binding.closeTextView.setOnClickListener {
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
val extension = getItem(bindingAdapterPosition) val extension = getItem(bindingAdapterPosition)
if (extension != null) { if (extension != null) {
clickListener.onInstallClick(extension) clickListener.onInstallClick(extension)
@ -194,6 +193,8 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
} }
val extensionIconImageView: ImageView = binding.extensionIconImageView val extensionIconImageView: ImageView = binding.extensionIconImageView
@SuppressLint("SetTextI18n")
fun bind(extension: MangaExtension.Available) { fun bind(extension: MangaExtension.Available) {
val nsfw = if (extension.isNsfw) "(18+)" else "" val nsfw = if (extension.isNsfw) "(18+)" else ""
val lang = LanguageMapper.mapLanguageCodeToName(extension.lang) val lang = LanguageMapper.mapLanguageCodeToName(extension.lang)

View file

@ -176,6 +176,7 @@ class NovelExtensionAdapter(private val clickListener: OnNovelInstallClickListen
init { init {
binding.closeTextView.setOnClickListener { binding.closeTextView.setOnClickListener {
if (bindingAdapterPosition == RecyclerView.NO_POSITION) return@setOnClickListener
val extension = getItem(bindingAdapterPosition) val extension = getItem(bindingAdapterPosition)
if (extension != null) { if (extension != null) {
clickListener.onInstallClick(extension) clickListener.onInstallClick(extension)

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