diff --git a/.github/workflows/pre_release.yml b/.github/workflows/pre_release.yml new file mode 100644 index 00000000..726732f8 --- /dev/null +++ b/.github/workflows/pre_release.yml @@ -0,0 +1,61 @@ +name: Pre-Release + +# 触发器 +on: + workflow_dispatch: + push: + branches: + - 'master' + paths-ignore: + - '**.md' + - 'doc/**' + - 'screenshot/**' + - 'image/**' + pull_request: + branches: + - 'master' + paths-ignore: + - '**.md' + - 'doc/**' + - 'screenshot/**' + - 'image/**' + +jobs: + build: + + runs-on: ubuntu-latest + # 设置JDK为11 + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '11' + - name: Checkout Secret + uses: actions/checkout@v3 + with: + repository: ${{ secrets.SECRET_REPO }} + token: ${{ secrets.TOKEN }} # 连接仓库的Token + path: secret + # 准备secret文件 + - name: Copy Secret Files + run: | + cd secret/Imomoe + cp key.jks ../.. + cp secret.gradle ../.. + cp notice.html ../../app/src/main/res/raw + # 清理secret文件 + - name: Clean Temp Secret Files + run: | + rm -rf ./secret + # 打包 + - name: Build with Gradle + run: | + bash ./gradlew assembleGithub + # 存档打包的文件 + - name: Upload Pre-Release Apk + uses: actions/upload-artifact@v3 + with: + name: Pre-Release Apk + path: app/build/outputs/apk/Github/release/*.apk diff --git a/.gitignore b/.gitignore index 6e4e9197..1c57c031 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ *.iml .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +/.idea .DS_Store /build /captures @@ -19,3 +14,5 @@ local.properties /.idea/statistic.xml /app/schemas /app/release +/key.jks +/app/Github diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 87d6859b..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -樱花动漫 \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index bf58fcb7..00000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
- - -
-
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c..00000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index fb7f4a8a..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/dictionaries/Sky_D.xml b/.idea/dictionaries/Sky_D.xml deleted file mode 100644 index 8fc5169a..00000000 --- a/.idea/dictionaries/Sky_D.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - anime - bugly - ctiao - danmaku - deps - dlna - filedownloader - gitee - iframe - imomoe - jsoup - mobileqq - sakura - shuyu - sina - skyd - tencent - umeng - umsdk - upnp - wechat - weibo - yhdm - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index c6e6e30f..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 69c43390..00000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 6199cc2a..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 797acea5..00000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 2a596fd9..922f865e 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,11 @@ codebeat badge + + GitHub Workflow Status + - GitHub release (latest by date) + GitHub release (latest by date) GitHub all downloads @@ -22,10 +25,15 @@

樱花动漫第三方安卓Android客户端,免费开源,目的是学习Android开发。(仅支持Android 5及以上版本)

+

+ (Android 5用户请尽快升级到6及以上,后续可能会将最低支持版本提高为Android 6) +

- ---- +## Discord频道:https://discord.gg/MyaRtRGEzr + +### 可以选择将自制数据源共享到[DataSourceRepository](https://github.com/SkyD666/DataSourceRepository)仓库,自制数据源/后端[帮助文档](doc/customdatasource/RV_ITEM.md) ## [>>必看安全说明<<](#安全说明) @@ -36,15 +44,18 @@ 3. 支持**分类查看**动漫 4. 支持**双指缩放**、**移动**、**旋转**视频 5. 支持视频**投屏**到电视 +5. 支持**WebDAV**备份恢复数据 6. 支持部分视频**显示**、**发送弹幕**(需要数据源支持弹幕) -7. 支持**缓存视频**到本地(暂不支持m3u8格式资源缓存) -8. 支持**追番**(数据保存在本地) -9. 支持显示**观看历史**记录 -10. 支持显示**搜索历史**记录 -11. 支持改变视频**播放速度** -12. 支持改变**视频**显示**比例**(16:9, 4:3, 全屏等) -13. [支持**自定义**显示**数据源**](doc/customdatasource/README.md) -14. ...... +7. 支持输入某站弹幕链接播放网络弹幕(例如
https://api.bilibili.com/x/v1/dm/list.so?oid=97495910) +8. 支持**缓存视频**到本地 +9. 支持**追番**(数据保存在本地) +10. 支持显示**观看历史**记录 +11. 支持显示**搜索历史**记录 +12. 支持改变视频**播放速度** +13. 支持改变**视频**显示**比例**(16:9, 4:3, 全屏等) +14. 支持**本地记忆**视频**播放进度** +15. 支持**播放本地音视频** +16. ...... ## 运行截图 @@ -58,11 +69,27 @@ ![favorite](screenshot/favorite.jpg) ![history](screenshot/history.jpg) -![player](screenshot/player.jpg) +player + +## 主要技术栈 + +- MVVM +- ViewModel +- Flow +- Kotlin Coroutine +- Hilt +- Room +- Jetpack Compose +- SplashScreen +- DiffUtil +- Retrofit +- OkHttp +- Jsoup +- ...... ## 安全说明 -**请勿**私自**传播APK**安装包,Github仓库为唯一长期仓库,**请仅在Github仓库下载安装包**,请勿下载来历不明的应用与Jar包,谨防隐私泄露,谨防受骗! +**请勿**私自**传播APK**安装包,GitHub仓库为唯一长期仓库,**请仅在GitHub仓库下载安装包**,请勿下载来历不明的应用与ads包,谨防隐私泄露,谨防受骗! ### 已发现未知来源的APK @@ -88,13 +115,9 @@ 1. 读取存储卡中的内容:缓存动漫功能需要读取本地存储卡中缓存的视频文件 2. 修改或删除存储卡中的内容:缓存动漫功能需要修改记录缓存信息的xml文件 -### 电话 - -1. 读取设备通话状态和识别码:友盟+SDK需要收集您的设备Mac地址、唯一设备识别码以提供统计分析服务 - ### 位置信息 -1. 访问大致、确切位置:友盟+SDK会通过地理位置校准报表数据准确性,提供基础反作弊能力 +1. 访问大致、确切位置:Flurry SDK会通过地理位置校准报表数据准确性,提供基础反作弊能力 ### 其它应用功能 @@ -103,7 +126,7 @@ ## 附加说明 -默认数据源来自http://www.yhdm.io/ +App内不提供默认数据源,需要用户自行导入或从APP内的数据源商店下载使用。 ## 免责声明 @@ -112,6 +135,10 @@ 3. 此软件**仅可用作学习交流**,未经授权,**禁止用于其他用途**,请在下载**24小时内删除**。 4. 因使用此软件产生的版权问题,软件作者概不负责。 +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=SkyD666/Imomoe)](https://star-history.com/) + ## 许可证 使用此软件代码需**遵循以下许可证协议** diff --git a/andresguard.gradle b/andresguard.gradle deleted file mode 100644 index ab0e2b0e..00000000 --- a/andresguard.gradle +++ /dev/null @@ -1,46 +0,0 @@ -def andresguard = [:] - -def whiteList = [ - // skin - "R.drawable.*_skin*", - "R.color.*_skin*", - // for your icon - "R.mipmap.ic_launcher", - "R.mipmap.ic_launcher_round", - // for fabric - "R.string.com.crashlytics.*", - // for google-services - "R.string.google_app_id", - "R.string.gcm_defaultSenderId", - "R.string.default_web_client_id", - "R.string.ga_trackingId", - "R.string.firebase_database_url", - "R.string.google_api_key", - "R.string.google_crash_reporting_api_key", - - // 友盟sdk - "R.anim.umeng*", - "R.string.umeng*", - "R.string.UM*", - "R.string.tb_*", - "R.layout.umeng*", - "R.layout.socialize_*", - "R.layout.*messager*", - "R.layout.tb_*", - "R.color.umeng*", - "R.color.tb_*", - "R.style.*UM*", - "R.style.umeng*", - "R.drawable.umeng*", - "R.drawable.tb_*", - "R.drawable.sina*", - "R.drawable.qq_*", - "R.drawable.tb_*", - "R.id.umeng*", - "R.id.*messager*", - "R.id.progress_bar_parent", - "R.id.socialize_*", - "R.id.webView" -] -andresguard.whiteList = whiteList -ext.andresguard = andresguard diff --git a/app/.gitignore b/app/.gitignore index 52ffe7c1..b53043ca 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,2 @@ /build -/src/main/res/raw/notice.txt +/src/main/res/raw/notice.html diff --git a/app/build.gradle b/app/build.gradle index 11aa080b..9c170445 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,20 +2,26 @@ plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' - id 'AndResGuard' - id 'com.efs.sdk.plugin' + id 'dagger.hilt.android.plugin' + id 'com.google.devtools.ksp' +// id 'com.flurry.android.symbols' } apply from: secret -apply from: andresguard + +def buildTime = new Date().format("YYYY-MM-dd HH:mm:ss", TimeZone.getTimeZone("GMT+8:00")) android.defaultConfig { secret.buildConfigField.forEach({ k, v -> buildConfigField("String", k, "\"${v}\"") }) + buildConfigField("String", "BUILD_TIME", "\"${buildTime}\"") secret.shieldTextList.forEach({ k, v -> buildConfigField("String[]", k, v) }) + secret.dataSource.forEach({ k, v -> + buildConfigField("String", k, "\"${v}\"") + }) } android { @@ -35,19 +41,21 @@ android { renderscriptTargetApi 19 renderscriptSupportModeEnabled true - javaCompileOptions { - annotationProcessorOptions { - arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] - } + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) } ndk { - abiFilters 'armeabi' + abiFilters 'armeabi', 'arm64-v8a' } manifestPlaceholders = secret.manifestPlaceholders resConfigs "xxxhdpi", "anydpi-v26" + + vectorDrawables { + useSupportLibrary true + } } signingConfigs { @@ -60,9 +68,7 @@ android { } productFlavors { - Github { - manifestPlaceholders = [UMENG_CHANNEL_VALUE: "Github"] - } + Github } applicationVariants.all { variant -> @@ -75,21 +81,21 @@ android { debug { minifyEnabled false zipAlignEnabled false - shrinkResources false // 使用keep.xml,keep住皮肤文件 + shrinkResources false // 使用keep.xml,keep住某些资源文件 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' applicationIdSuffix '.debug' // 一台手机debug release共存 ndk { - abiFilters 'armeabi', 'x86', 'x86_64' + abiFilters 'armeabi', 'x86', 'x86_64', 'arm64-v8a' } } release { signingConfig signingConfigs.release //签名 minifyEnabled true zipAlignEnabled true - shrinkResources true // 使用keep.xml,keep住皮肤文件 + shrinkResources true // 使用keep.xml,keep住某些资源文件 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' ndk { - abiFilters 'armeabi' + abiFilters 'armeabi', 'arm64-v8a' } } } @@ -117,11 +123,23 @@ android { exclude 'org/seamless/**' exclude 'org/eclipse/jetty/**' exclude 'org/fourthline/cling/**' - exclude 'okhttp3/internal/**' + exclude 'okhttp3/internal/publicsuffix/NOTICE' + exclude 'com/badlogic/**' + exclude 'XPP3_1.1.3.2_VERSION' + exclude 'XPP3_1.1.3.3_VERSION' + exclude 'kotlin-tooling-metadata.json' + exclude 'build-data.properties' + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } } buildFeatures { viewBinding true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.2.0' } } @@ -131,137 +149,122 @@ dependencies { implementation deps.kotlin.core_ktx implementation deps.support.appcompat implementation deps.support.material + implementation deps.support.recyclerview implementation deps.support.swiperefreshlayout + implementation deps.support.coordinatorlayout implementation deps.support.constraintlayout + implementation deps.support.fragment_ktx + implementation deps.support.viewpager2 + implementation deps.support.preference + implementation deps.support.security_crypto + implementation deps.support.core_splashscreen + implementation deps.support.profileinstaller + implementation deps.compose.ui + implementation deps.compose.constraintlayout_compose + implementation deps.compose.material3 + implementation deps.compose.ui_viewbinding + implementation deps.compose.ui_tooling_preview + implementation deps.compose.runtime_livedata + implementation deps.compose.compose_theme_adapter + implementation deps.compose.material3_window_size_class + implementation deps.accompanist.systemuicontroller + implementation deps.accompanist.swiperefresh implementation deps.jsoup.jsoup - implementation deps.lifecycle.lifecycle_livedata_ktx implementation deps.lifecycle.lifecycle_viewmodel_ktx implementation deps.lifecycle.lifecycle_runtime_ktx - implementation deps.support.viewpager2 + implementation deps.lifecycle.lifecycle_extensions + implementation deps.hilt.hilt_android + implementation deps.hilt.hilt_navigation_compose + kapt deps.hilt.hilt_android_compiler implementation deps.okhttp3.okhttp - implementation deps.shuyu.GSYVideoPlayer + implementation deps.okhttp3.logging_interceptor + implementation deps.shuyu.gsyVideoPlayer_java + implementation deps.shuyu.gsyVideoPlayer_exo2 implementation deps.retrofit2.retrofit implementation deps.retrofit2.converter_gson implementation deps.getActivity.XXPermissions implementation deps.kotlinx.kotlinx_coroutines_android - implementation deps.material_dialogs.core - implementation deps.material_dialogs.input implementation deps.room.room_runtime implementation deps.room.room_ktx - kapt deps.room.room_compiler - implementation deps.filedownloader.library + implementation deps.support.legacy_support_v4 + ksp deps.room.room_compiler + implementation deps.aria.core + kapt deps.aria.compiler + implementation deps.aria.m3u8Component implementation deps.cling.cling_core implementation deps.cling.cling_support implementation deps.jetty.jetty_server implementation deps.jetty.jetty_servlet implementation deps.jetty.jetty_client implementation deps.nanohttpd.nanohttpd - compileOnly files ('libs/cdi-api.jar') // DLNACastService编译需要javax.enterprise.inject.Alternative类 -// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' + compileOnly files('libs/cdi-api.jar') + // DLNACastService编译需要javax.enterprise.inject.Alternative类 +// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' implementation deps.greenrobot.eventbus - implementation deps.umsdk.common - implementation deps.umsdk.asms - implementation deps.umsdk.apm - implementation deps.umsdk.push + implementation deps.flurry.analytics implementation deps.smart.refresh_layout_kernel implementation deps.smart.refresh_header_material implementation deps.smart.refresh_footer_ball - implementation deps.ctiao.DanmakuFlameMaster implementation deps.coil_kt.coil - implementation project(':skin') - implementation project(':skin_blue') - implementation project(':skin_dark') - implementation project(':skin_lemon') - implementation project(':skin_sweat_soybean') -} - -// UM U-APM性能报告 -efs { - //是否对启动过程进程插桩的开关,如果使用自动集成监控则必须开启 - isAutoTrack = true - //您自定义Application的类名称,必填项,如没有自定义则填写系统Application - applicationName = "App" - //您自定义Activity的类名称,必填项,将您所有Activity的类名按如下格式填写 - activityList = [ - "AboutActivity", - "AnimeDetailActivity", - "AnimeDownloadActivity", - "ClassifyActivity", - "CrashActivity", - "DlnaActivity", - "DlnaControlActivity", - "FavoriteActivity", - "HistoryActivity", - "LicenseActivity", - "MainActivity", - "MonthAnimeActivity", - "PlayActivity", - "RankActivity", - "SearchActivity", - "SettingActivity", - "SimplePlayActivity", - "DetailPlayerActivity", - "WebViewActivity", - "SkinActivity", - "NoticeActivity" - ] + implementation deps.coil_kt.coil_compose + implementation deps.kuaishou.akdanmaku + implementation deps.vadiole.colorpicker + implementation deps.commons.commons_text + implementation deps.okhttp3.okhttp_dnsoverhttps + implementation deps.thegrizzlylabs.sardine_android } -//AndResGuard资源混淆工具 -andResGuard { - // 使用mappingFile,防止需要换肤的资源id被替换 - mappingFile = file("./resource_mapping.txt") -// mappingFile = null - use7zip = true - useSign = true - // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字 - keepRoot = false - // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小 - fixedResName = "arg" - // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源 - mergeDuplicatedRes = true - whiteList = andresguard.whiteList - compressFilePattern = [ - "*.png", - "*.jpg", - "*.jpeg", - "*.gif", - "resources.arsc" - ] - sevenzip { - artifact = 'com.tencent.mm:SevenZip:1.2.20' - //path = "/usr/local/bin/7za" - } - - /** - * 可选: 如果不设置则会默认覆盖assemble输出的apk - **/ - // finalApkBackupPath = "${project.rootDir}/final.apk" - - /** - * 可选: 指定v1签名时生成jar文件的摘要算法 - * 默认值为“SHA-1” - **/ - // digestalg = "SHA-256" -} +//flurryCrash { +// apiKey secret.buildConfigField.FLURRY_API_KEY +// useEnvVar false +// token secret.buildConfigField.PROGRAMMATIC_TOKEN +//} -//class转jar -//删除之前打出的包,默认将包打在'build/libs/'下 +/** + * 下面是生成数据源Jar的过程。保证生成了数据源的class文件后,再直接运行makeAds任务即可 + */ +// 1.删除之前打出的包,默认将包打在'build/libs/'下 task deleteOldJar(type: Delete) { delete 'build/libs/CustomDataSource.jar' } -//自定义数据源打包为普通的jar包操作 + +// 2.使用d8将class打包为dex文件 +// 要保证已经将.../Android/Sdk/build-tools/xx.x.x加入环境变量 +task makeDex(type: Exec) { + project.file('build/dex/').mkdirs() + // 根据需求更改 + commandLine 'cmd', "/c", "d8 --output build/dex/ build/tmp/kotlin-classes/GithubDebug/com/skyd/imomoe/model/impls/custom/*.class" +} + +// 3.将dex和CustomInfo文件一起打入jar task makeJar(type: Jar) { //要打成的包的名字 baseName 'CustomDataSource' //选取要打包的文件夹 - from('build\\tmp\\kotlin-classes\\GithubDebug\\com\\skyd\\imomoe\\model\\impls\\custom') - //需要跟实际类的包名路径一样 - into('com/skyd/imomoe/model/impls/custom') - //排除在外的文件 - exclude('BuildConfig.class', 'R.class', 'MainActivity.class', 'TestClass.class') - //排除以R$开头的文件 - exclude { it.name.startsWith('R$') } + from( + 'build/dex/', + 'src/main/java/com/skyd/imomoe/model/impls/custom/CustomInfo' + ) + // dex文件和CustomInfo文件都存放在根目录即可 + into('/') } -//打包~ -makeJar.dependsOn(deleteOldJar) \ No newline at end of file + +// 4.命名为ads +task makeAds(type: Sync) { + from('build/libs/') + into('build/libs/') + include '*.jar' + rename { String filename -> + filename.replace(".jar", ".ads") + } +} + +task openDirectory(type: Exec) { + println project.projectDir + commandLine 'cmd', "/c", "explorer.exe /root,\"${project.projectDir}\\build\\libs\"" +} + +// 打包~ +makeDex.dependsOn(deleteOldJar) +makeJar.dependsOn(makeDex) +makeAds.dependsOn(makeJar) \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 5dfc3fc6..6fbeea46 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -24,48 +24,27 @@ -keep class * implements java.io.Serializable { *;} -keep class * implements android.os.Parcelable { *;} -#-------------------------Umeng --keep class com.umeng.** {*;} --keep class com.uc.** {*;} --keep class com.efs.** { *; } - --keepclassmembers class * { - public (org.json.JSONObject); +#-------------------------Flurry SDK +# Required to preserve the Flurry SDK +-keep class com.flurry.** { *; } +-dontwarn com.flurry.** +-keepattributes *Annotation*,EnclosingMethod,Signature +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet, int); } --keepclassmembers enum * { - public static **[] values(); - public static ** valueOf(java.lang.String); +# Google Play Services library +-keep class * extends java.util.ListResourceBundle { +protected Object[][] getContents(); } --keep public class com.skyd.imomoe.R$*{ -public static final int *; +-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { +public static final *** NULL; } - --dontwarn com.umeng.** --dontwarn com.taobao.** --dontwarn anet.channel.** --dontwarn anetwork.channel.** --dontwarn org.android.** --dontwarn org.apache.thrift.** --dontwarn com.xiaomi.** --dontwarn com.huawei.** --dontwarn com.meizu.** - --keepattributes *Annotation* - --keep class com.taobao.** {*;} --keep class org.android.** {*;} --keep class anet.channel.** {*;} --keep class com.xiaomi.** {*;} --keep class com.huawei.** {*;} --keep class com.meizu.** {*;} --keep class org.apache.thrift.** {*;} - --keep class com.alibaba.sdk.android.** {*;} --keep class com.ut.** {*;} --keep class com.ta.** {*;} - --keep public class **.R$* { - public static final int *; +-keepnames @com.google.android.gms.common.annotation.KeepName class * +-keepclassmembernames class * { +@com.google.android.gms.common.annotation.KeepName *; +} +-keepnames class * implements android.os.Parcelable { +public static final ** CREATOR; } #-------------------------EventBus @@ -80,6 +59,22 @@ public static final int *; (java.lang.Throwable); } +#-------------------------Aria +-dontwarn com.arialyy.aria.** +-keep class com.arialyy.aria.**{*;} +-keep class **$$DownloadListenerProxy{ *; } +-keep class **$$UploadListenerProxy{ *; } +-keep class **$$DownloadGroupListenerProxy{ *; } +-keep class **$$DGSubListenerProxy{ *; } +-keepclasseswithmembernames class * { + @Download.* ; + @Upload.* ; + @DownloadGroup.* ; +} + +#-------------------------okhttp +-keep class okhttp3.internal.publicsuffix.PublicSuffixDatabase + #-------------------------gsyvideoplayer播放器 -keep class com.shuyu.gsyvideoplayer.video.** { *; } -dontwarn com.shuyu.gsyvideoplayer.video.** @@ -96,13 +91,20 @@ public static final int *; # 自定义数据源接口不应被混淆 -keep class com.skyd.imomoe.model.interfaces.** { *; } # 与自定义数据源相关的类不应该被混淆 +-keep class com.skyd.imomoe.adsapi.** { *; } -keep class com.skyd.imomoe.util.Util { *; } -keep class com.skyd.imomoe.bean.** { *; } -keep class com.skyd.imomoe.config.** { *; } +-keep class com.skyd.imomoe.route.** { *; } -keep class com.skyd.imomoe.model.util.** { *; } -keep class com.skyd.imomoe.util.html.source.** { *; } -keep class com.skyd.imomoe.util.eventbus.** { *; } +-keep class com.skyd.imomoe.util.ToastKt { *; } +-keep class com.skyd.imomoe.net.RetrofitManager { *; } +-keep class com.skyd.imomoe.net.RetrofitManager$Companion { *; } +-keep class retrofit2.** { *; } # 与自定义数据源相关的库不应该被混淆 +-keep class com.google.gson.** { *; } -keep class org.jsoup.** { *; } -keep class org.greenrobot.eventbus.** { *; } # kotlin @@ -113,6 +115,7 @@ public static final int *; *** get*(); void set*(***); public (android.content.Context); + public (android.content.Context, java.lang.Boolean); public (android.content.Context, android.util.AttributeSet); public (android.content.Context, android.util.AttributeSet, int); } @@ -155,13 +158,29 @@ public static final int *; -dontwarn org.eclipse.jetty.** -dontwarn org.fourthline.cling.** -dontwarn org.seamless.** --keep class org.fourthline.cling.** {*;} +-keep class org.fourthline.cling.** { *; } -keepattributes Annotation +-keep class org.xmlpull.v1.* { *; } +-dontwarn org.xmlpull.v1.** +-dontwarn javax.xml.namespace.** #for media render state machine #-keep class org.seamless.statemachine.** {;} #-keepclassmembers class * implements org.fourthline.cling.support.avtransport.impl.state.AbstractState {;} +#-------------------------AkDanmaku v1.0.3 +-dontwarn com.badlogic.gdx.backends.android.AndroidFragmentApplication +-dontwarn com.badlogic.gdx.utils.GdxBuild +-dontwarn com.badlogic.gdx.jnigen.BuildTarget* +-dontwarn com.badlogic.gdx.graphics.g2d.freetype.FreetypeBuild +-keep class com.kuaishou.akdanmaku.ecs.system.ActionSystem { *; } +-keep class com.kuaishou.akdanmaku.ecs.system.DanmakuSystem { *; } +-keep class com.kuaishou.akdanmaku.ecs.system.DataSystem { *; } +-keep class com.kuaishou.akdanmaku.ecs.system.RenderSystem { *; } +-keep class com.kuaishou.akdanmaku.ecs.system.layout.LayoutSystem { *; } +# Required if using Gdx-Controllers extension +-keep class com.badlogic.gdx.controllers.android.AndroidControllers + #------------------------- -keep public class * extends android.app.Activity -keep public class * extends android.app.AppCompatActivity diff --git a/app/src/androidTest/java/com/skyd/imomoe/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/skyd/imomoe/ExampleInstrumentedTest.kt deleted file mode 100644 index 3df7c190..00000000 --- a/app/src/androidTest/java/com/skyd/imomoe/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.skyd.imomoe - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.skyd.imomoe", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c09ae051..46c75357 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ @@ -14,13 +13,6 @@ - - - - - - + android:name=".view.activity.UrlMapActivity" + android:exported="false" /> + android:name=".view.activity.DownloadManagerActivity" + android:exported="false" /> + + android:name=".view.activity.ConfigDataSourceActivity" + android:exported="true" + android:label="@string/import_data_source_label" + android:launchMode="singleTask"> + + + + + + + + + + + + + + + + + + - + - - - - - - - - + android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" + android:exported="true" + android:label="@string/play_video_label" + android:screenOrientation="landscape"> + + + + + + + + + + + + + + + + + + + + - - + android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize" /> + + + android:exported="true" + android:theme="@style/Theme.App.Starting" + android:windowSoftInputMode="adjustPan"> + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/assets/aria_config.xml b/app/src/main/assets/aria_config.xml new file mode 100644 index 00000000..0ccfcebf --- /dev/null +++ b/app/src/main/assets/aria_config.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/fonts/BPreplay.otf b/app/src/main/assets/fonts/BPreplay.otf deleted file mode 100644 index 3b67c111..00000000 Binary files a/app/src/main/assets/fonts/BPreplay.otf and /dev/null differ diff --git a/app/src/main/java/com/skyd/imomoe/App.kt b/app/src/main/java/com/skyd/imomoe/App.kt index 9b98bcc7..2ad8f60c 100644 --- a/app/src/main/java/com/skyd/imomoe/App.kt +++ b/app/src/main/java/com/skyd/imomoe/App.kt @@ -1,74 +1,39 @@ package com.skyd.imomoe -import android.annotation.SuppressLint import android.app.Application import android.content.Context -import com.liulishuo.filedownloader.FileDownloader import com.scwang.smart.refresh.footer.BallPulseFooter import com.scwang.smart.refresh.header.MaterialHeader import com.scwang.smart.refresh.layout.SmartRefreshLayout +import com.skyd.imomoe.ext.theme.getAttrColor +import com.skyd.imomoe.ext.theme.initDarkMode import com.skyd.imomoe.util.CrashHandler -import com.skyd.imomoe.util.PushHelper -import com.skyd.imomoe.util.Util -import com.skyd.imomoe.util.Util.getManifestMetaValue -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.Util.getSkinResourceId +import com.skyd.imomoe.util.initHeadsetEventReceiver import com.skyd.imomoe.util.release -import com.skyd.imomoe.util.skin.SkinUtil -import com.skyd.skin.core.attrs.SrlPrimaryColorAttr -import com.umeng.analytics.MobclickAgent -import com.umeng.commonsdk.UMConfigure -import com.umeng.message.PushAgent -import com.umeng.message.UmengNotificationClickHandler -import com.umeng.message.entity.UMessage +import com.skyd.imomoe.view.component.player.PlayerCore +import dagger.hilt.android.HiltAndroidApp +@HiltAndroidApp class App : Application() { + override fun onCreate() { super.onCreate() - context = this + appContext = this + + initDarkMode() release { // Crash提示 CrashHandler.getInstance(this) - - // 友盟 - // 初始化组件化基础库, 所有友盟业务SDK都必须调用此初始化接口。 - UMConfigure.init( - this, - getManifestMetaValue("UMENG_APPKEY"), - getManifestMetaValue("UMENG_CHANNEL"), - UMConfigure.DEVICE_TYPE_PHONE, - BuildConfig.UMENG_MESSAGE_SECRET - ) - UMConfigure.setLogEnabled(BuildConfig.DEBUG) - - // 选择AUTO页面采集模式,统计SDK基础指标无需手动埋点可自动采集。 - MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO) - - PushAgent.getInstance(context).apply { - resourcePackageName = BuildConfig.APPLICATION_ID - notificationClickHandler = object : UmengNotificationClickHandler() { - override fun dealWithCustomAction(context: Context, msg: UMessage) { - super.dealWithCustomAction(context, msg) - Util.process(context, msg.custom) - } - } - } - PushHelper.init(applicationContext) - Thread { PushHelper.init(applicationContext) }.start() } - FileDownloader.setup(this) + initHeadsetEventReceiver() - // 初始化自定义皮肤属性 - SkinUtil.initCustomAttrIds() + PlayerCore.onAppCreate() } companion object { - @SuppressLint("StaticFieldLeak") - lateinit var context: Context - init { // 防止内存泄漏 // 设置全局默认配置(优先级最低,会被其他设置覆盖) @@ -77,31 +42,26 @@ class App : Application() { layout.setFooterHeight(100f) layout.setHeaderTriggerRate(0.5f) layout.setDisableContentWhenLoading(false) + layout.setPrimaryColors(context.getAttrColor(R.attr.colorSurface)) } // 全局设置默认的 Header SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, layout -> //开始设置全局的基本参数(这里设置的属性只跟下面的MaterialHeader绑定,其他Header不会生效,能覆盖DefaultRefreshInitializer的属性和Xml设置的属性) - val colorSchemeResources = R.color.main_color_skin - SrlPrimaryColorAttr.materialHeaderColorSchemeRes = colorSchemeResources layout.setEnableHeaderTranslationContent(true) .setHeaderHeight(70f) .setDragRate(0.6f) - MaterialHeader(context).setColorSchemeResources( - getSkinResourceId( - colorSchemeResources - ) - ) + MaterialHeader(context) + .setColorSchemeColors(context.getAttrColor(R.attr.colorPrimary)) .setShowBezierWave(true) } SmartRefreshLayout.setDefaultRefreshFooterCreator { context, layout -> - val animatingColor = R.color.foreground_main_color_2_skin - SrlPrimaryColorAttr.ballPulseFooterAnimatingColorRes = animatingColor layout.setEnableFooterTranslationContent(true) - BallPulseFooter(context).setAnimatingColor( - context.getResColor(animatingColor) - ) + BallPulseFooter(context) + .setAnimatingColor(context.getAttrColor(R.attr.colorPrimary)) } } } -} \ No newline at end of file +} + +lateinit var appContext: Context \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/adsapi/config/UserAgent.kt b/app/src/main/java/com/skyd/imomoe/adsapi/config/UserAgent.kt new file mode 100644 index 00000000..b6a8a69e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/adsapi/config/UserAgent.kt @@ -0,0 +1,6 @@ +package com.skyd.imomoe.adsapi.config + +import com.skyd.imomoe.config.Const.Request.Companion.randomUserAgent + + +fun randomUserAgent(): String = randomUserAgent() diff --git a/app/src/main/java/com/skyd/imomoe/adsapi/net/OkHttp.kt b/app/src/main/java/com/skyd/imomoe/adsapi/net/OkHttp.kt new file mode 100644 index 00000000..f9966ae7 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/adsapi/net/OkHttp.kt @@ -0,0 +1,5 @@ +package com.skyd.imomoe.adsapi.net + +import okhttp3.OkHttpClient + +val okhttpClient: OkHttpClient = com.skyd.imomoe.net.okhttpClient \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/adsapi/net/Retrofit.kt b/app/src/main/java/com/skyd/imomoe/adsapi/net/Retrofit.kt new file mode 100644 index 00000000..42010c84 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/adsapi/net/Retrofit.kt @@ -0,0 +1,5 @@ +package com.skyd.imomoe.adsapi.net + +import com.skyd.imomoe.net.RetrofitManager + +val retrofitManager: RetrofitManager = RetrofitManager.get() \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/adsapi/websource/JsoupUtil.kt b/app/src/main/java/com/skyd/imomoe/adsapi/websource/JsoupUtil.kt new file mode 100644 index 00000000..7d782034 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/adsapi/websource/JsoupUtil.kt @@ -0,0 +1,21 @@ +package com.skyd.imomoe.adsapi.websource + +import okhttp3.MediaType +import org.jsoup.nodes.Document + +object JsoupUtil { + /** + * 获取没有运行js的html + */ + suspend fun getDocument(url: String): Document = + com.skyd.imomoe.model.util.JsoupUtil.getDocument(url) + + fun getDocumentSynchronously(url: String): Document = + com.skyd.imomoe.model.util.JsoupUtil.getDocumentSynchronously(url) + + /** + * 指定解析类型 + */ + suspend fun getDocument(url: String, mediaType: MediaType): Document = + com.skyd.imomoe.model.util.JsoupUtil.getDocument(url, mediaType) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/adsapi/websource/WebSource.kt b/app/src/main/java/com/skyd/imomoe/adsapi/websource/WebSource.kt new file mode 100644 index 00000000..c8b7ab4d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/adsapi/websource/WebSource.kt @@ -0,0 +1,19 @@ +package com.skyd.imomoe.adsapi.websource + +import com.skyd.imomoe.util.html.source.WebSource + +object WebSource { + suspend fun getWebSource( + url: String, + encoding: String = "UTF-8", + userAgent: String? = null, + timeout: Long = 10000L + ): String { + return WebSource.getWebSource( + url = url, + encoding = encoding, + userAgent = userAgent, + timeout = timeout + ) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/bean/AnimeCoverBean.kt b/app/src/main/java/com/skyd/imomoe/bean/AnimeCoverBean.kt new file mode 100644 index 00000000..dc0eb4a6 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/AnimeCoverBean.kt @@ -0,0 +1,163 @@ +package com.skyd.imomoe.bean + +import com.skyd.imomoe.util.compare.EpisodeTitleCompareUtil +import com.skyd.imomoe.view.adapter.variety.Diff + + +class AnimeCover1Bean( + override var route: String, + // 网页地址 + var url: String, + var title: String, + var cover: ImageBean, + var episode: String +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover1Bean -> false + route == o.route && url == o.url && title == o.title && + cover == o.cover && episode == o.episode -> true + else -> false + } +} + +class AnimeCover2Bean( + override var route: String, + var title: String +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover2Bean -> false + route == o.route && title == o.title -> true + else -> false + } +} + +class AnimeCover3Bean( + override var route: String = "", + // 网页地址 + var url: String? = null, + var title: String? = null, + var cover: ImageBean? = null, + var episode: String? = null, + var describe: String? = null, + var animeType: List? = null +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover3Bean -> false + route == o.route && url == o.url && title == o.title && cover == o.cover && + episode == o.episode && describe == o.describe && animeType == o.animeType -> true + else -> false + } +} + +class AnimeCover4Bean( + override var route: String, + // 网页地址 + var url: String, + var title: String, + var cover: ImageBean +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover4Bean -> false + route == o.route && url == o.url && title == o.title && cover == o.cover -> true + else -> false + } +} + +class AnimeCover5Bean( + override var route: String, + // 网页地址 + var url: String, + var title: String, + var area: AnimeAreaBean, + var date: String, + var episodeClickable: AnimeEpisodeDataBean +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover5Bean -> false + route == o.route && url == o.url && title == o.title && area == o.area && + date == o.date && episodeClickable == o.episodeClickable -> true + else -> false + } +} + +class AnimeCover6Bean( + override var route: String = "", + var title: String? = null, + var cover: ImageBean? = null, + var describe: String? = null, + var episodeClickable: AnimeEpisodeDataBean? = null +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover6Bean -> false + route == o.route && title == o.title && cover == o.cover && + describe == o.describe && episodeClickable == o.episodeClickable -> true + else -> false + } +} + +class AnimeCover7Bean( + override var route: String, + var title: String, + var size: String? = null, //视频大小,如300M + var episodeCount: String? = null, //集数 + var path: String, + // 0:/storage/emulated/0/Android/data/packagename/files + // 1:/storage/emulated/0/ + var pathType: Int = 0, +) : BaseBean, Diff, Comparable { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover7Bean -> false + route == o.route && title == o.title && size == o.size && + episodeCount == o.episodeCount && path == o.path && pathType == o.pathType -> true + else -> false + } + + override fun compareTo(other: AnimeCover7Bean): Int = + EpisodeTitleCompareUtil.compare(title, other.title) +} + +typealias AnimeCover8Bean = FavoriteAnimeBean + +typealias AnimeCover9Bean = HistoryBean + +class AnimeCover10Bean( + override var route: String, + var url: String, + var title: String, + var episodeClickable: AnimeEpisodeDataBean? +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover10Bean -> false + route == o.route && url == o.url && title == o.title && + episodeClickable == o.episodeClickable -> true + else -> false + } +} + +class AnimeCover11Bean( + override var route: String, + // 网页地址 + var url: String, + var title: String +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover11Bean -> false + route == o.route && url == o.url && title == o.title -> true + else -> false + } +} + +class AnimeCover12Bean( + override var route: String, + // 网页地址 + var url: String, + var title: String, + var episodeClickable: AnimeEpisodeDataBean +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeCover12Bean -> false + route == o.route && url == o.url && title == o.title && + episodeClickable == o.episodeClickable -> true + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/AnimeDescribe1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/AnimeDescribe1Bean.kt new file mode 100644 index 00000000..92ca6e87 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/AnimeDescribe1Bean.kt @@ -0,0 +1,14 @@ +package com.skyd.imomoe.bean + +import com.skyd.imomoe.view.adapter.variety.Diff + +class AnimeDescribe1Bean( + override var route: String, + var describe: String +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeDescribe1Bean -> false + route == o.route && describe == o.describe -> true + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/AnimeDetailBean.kt b/app/src/main/java/com/skyd/imomoe/bean/AnimeDetailBean.kt index 1c719803..757c67ac 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/AnimeDetailBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/AnimeDetailBean.kt @@ -1,42 +1,57 @@ package com.skyd.imomoe.bean -class AnimeInfoBean( - override var type: String, - override var actionUrl: String, - var title: String, - var cover: ImageBean, - var alias: String, - var area: String, - var year: String, - var index: String, - var animeType: List, - var tag: List, - var info: String -) : BaseBean - -//番剧详情下方信息rv数据,播放页面下方rv数据 -class AnimeDetailBean( - override var type: String, - override var actionUrl: String, - override var title: String, - override var describe: String?, - override var episodeList: List? = null, - override var animeCoverList: List? = null, - override var headerInfo: AnimeInfoBean? = null -) : IAnimeDetailBean +import com.skyd.imomoe.util.compare.EpisodeTitleCompareUtil +import com.skyd.imomoe.view.adapter.variety.Diff -interface IAnimeDetailBean : BaseBean { - var title: String - var describe: String? - var episodeList: List? - var animeCoverList: List? - var headerInfo: AnimeInfoBean? +class AnimeInfo1Bean( + override var route: String, + var title: String?, + var cover: ImageBean?, + var alias: String?, + var area: String?, + var year: String?, + var index: String?, + var animeType: List?, + var tag: List?, + var info: String? +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeInfo1Bean -> false + route == o.route && title == o.title && cover == o.cover && + alias == o.alias && area == o.area && year == o.year && index == o.index && + animeType == o.animeType && tag == o.tag && info == o.info -> true + else -> false + } } //每一集 class AnimeEpisodeDataBean( - override var type: String, - override var actionUrl: String, + override var route: String, var title: String, var videoUrl: String = "" -) : BaseBean \ No newline at end of file +) : BaseBean, Diff, Comparable { + override fun contentSameAs(o: Any?): Boolean = when { + o !is AnimeEpisodeDataBean -> false + route == o.route && title == o.title && videoUrl == o.videoUrl -> true + else -> false + } + + // actionUrl相同的排序到一块 + override fun compareTo(other: AnimeEpisodeDataBean): Int { + val actionUrlComp = EpisodeTitleCompareUtil.compare(route, other.route) + if (actionUrlComp != 0) return actionUrlComp + return EpisodeTitleCompareUtil.compare(title, other.title) + } + + override fun equals(other: Any?): Boolean { + return if (other !is AnimeEpisodeDataBean) false + else route == other.route && title == other.title && videoUrl == other.videoUrl + } + + override fun hashCode(): Int { + var result = route.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + videoUrl.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/AnimeDownload1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/AnimeDownload1Bean.kt new file mode 100644 index 00000000..b9fc3489 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/AnimeDownload1Bean.kt @@ -0,0 +1,109 @@ +package com.skyd.imomoe.bean + +import com.arialyy.aria.core.download.DownloadEntity +import com.skyd.imomoe.view.adapter.variety.Diff + +class AnimeDownload1Bean( + override var route: String, + var title: String, + var episode: String, + var url: String, + var id: Long, + var peerNum: Int = -1, + var peerIndex: Int = -1, + var percent: Int = 0, + var fileSize: Long = 0, + var state: Int = 0, + var speed: Long = 0, +) : BaseBean, Diff { + val isM3U8: Boolean + get() = peerIndex != -1 && peerNum != -1 + + override fun sameAs(o: Any?): Boolean { + return o is AnimeDownload1Bean && url == o.url && id == o.id + } + + override fun contentSameAs(o: Any?): Boolean { + return when { + o !is AnimeDownload1Bean -> false + route == o.route && title == o.title && episode == o.episode && + url == o.url && id == o.id && + percent == o.percent && + peerNum == o.peerNum && + peerIndex == o.peerIndex && + state == o.state && + speed == o.speed -> true + else -> false + } + } + + override fun diff(o: Any?): Any? { + if (o !is AnimeDownload1Bean) return null + + val list: MutableList = mutableListOf() + if (peerIndex != o.peerIndex) list += PEER_INDEX + if (percent != o.percent) list += PERCENT + if (state != o.state) list += STATE + if (speed != o.speed) list += SPEED + return list.ifEmpty { null } + } + + override fun equals(other: Any?): Boolean { + return (other is AnimeDownload1Bean && url == other.url && id == other.id && + peerIndex == other.peerIndex && peerNum == other.peerNum && + percent == other.percent && fileSize == other.fileSize && + episode == other.episode && title == other.title && + state == other.state && speed == other.speed) || + (other is DownloadEntity && url == other.url && id == other.id && + peerIndex == other.m3U8Entity?.peerIndex && + peerNum == other.m3U8Entity?.peerNum && + percent == other.percent && fileSize == other.fileSize && + state == other.state && speed == other.speed) + } + + override fun hashCode(): Int { + var result = route.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + episode.hashCode() + result = 31 * result + url.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + peerNum + result = 31 * result + peerIndex + result = 31 * result + percent + result = 31 * result + fileSize.hashCode() + result = 31 * result + state + result = 31 * result + speed.hashCode() + return result + } + + companion object { + const val PEER_INDEX = "peerIndex" + const val PERCENT = "percent" + const val STATE = "state" + const val SPEED = "speed" + + fun create( + title: String, + episode: String, + entity: DownloadEntity + ): AnimeDownload1Bean { + return AnimeDownload1Bean( + route = "", + title = title, + episode = episode, + url = entity.url, + id = entity.id, + state = entity.state, + speed = entity.speed + ).apply { + val m3U8Entity = entity.m3U8Entity + if (m3U8Entity != null) { + peerIndex = m3U8Entity.peerIndex + peerNum = m3U8Entity.peerNum + } + fileSize = entity.fileSize + percent = entity.percent + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/AnimeEpisode1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/AnimeEpisode1Bean.kt new file mode 100644 index 00000000..e7b68f8b --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/AnimeEpisode1Bean.kt @@ -0,0 +1,3 @@ +package com.skyd.imomoe.bean + +typealias AnimeEpisode1Bean = AnimeEpisodeDataBean diff --git a/app/src/main/java/com/skyd/imomoe/bean/AnimeShowBean.kt b/app/src/main/java/com/skyd/imomoe/bean/AnimeShowBean.kt index ccdc14a8..9a7419fc 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/AnimeShowBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/AnimeShowBean.kt @@ -2,73 +2,40 @@ package com.skyd.imomoe.bean import com.google.gson.annotations.SerializedName -class AnimeShowBean( - override var type: String, - override var actionUrl: String, - override var url: String, - override var title: String, - override var rTitle: String, //右侧更多等... - override var cover: ImageBean?, - override var episode: String, - override var animeCoverList: List? = null -) : BaseBean, IAnimeShowBean - -interface IAnimeShowBean : BaseBean { - var url: String - var title: String - var rTitle: String //右侧更多等... - var cover: ImageBean? - var episode: String - var animeCoverList: List? -} - -class AnimeCoverBean( - override var type: String, - override var actionUrl: String, - override var url: String, - override var title: String, - override var cover: ImageBean?, - override var episode: String, - var animeType: List? = null, - override var describe: String? = null, - var episodeClickable: AnimeEpisodeDataBean? = null, - var area: AnimeAreaBean? = null, - var date: String? = null, - var size: String? = null, //视频大小,如300M - var episodeCount: String? = null, //集数 - // 0:/storage/emulated/0/Android/data/packname/files - // 1:/storage/emulated/0/ - var path: Int = 0, - override var rTitle: String = "", - override var animeCoverList: List? = null, - override var episodeList: List? = null, - override var headerInfo: AnimeInfoBean? = null -) : BaseBean, IAnimeShowBean, IAnimeDetailBean - class AnimeTypeBean( //番剧类型:包括类型名和链接 - override var type: String, - override var actionUrl: String, - var url: String, - var title: String + override var route: String = "", + var url: String? = null, + var title: String? = null ) : BaseBean class AnimeAreaBean( //番剧地区:包括地区名和链接 - override var type: String, - override var actionUrl: String, - var url: String, - var title: String + override var route: String = "", + var url: String? = null, + var title: String? = null ) : BaseBean class ImageBean( //图片bean,带有referer信息 - @SerializedName("type") - override var type: String, - @SerializedName("actionUrl") - override var actionUrl: String, + override var route: String = "", @SerializedName("url") - var url: String, + var url: String? = null, @SerializedName("referer") - var referer: String -) : BaseBean \ No newline at end of file + var referer: String? = null +) : BaseBean { + override fun equals(other: Any?): Boolean { + return when { + other !is ImageBean -> false + route == other.route && url == other.url && referer == other.referer -> true + else -> false + } + } + + override fun hashCode(): Int { + var result = route.hashCode() + result = 31 * result + url.hashCode() + result = 31 * result + referer.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/Banner1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/Banner1Bean.kt new file mode 100644 index 00000000..89d0c09d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/Banner1Bean.kt @@ -0,0 +1,14 @@ +package com.skyd.imomoe.bean + +import com.skyd.imomoe.view.adapter.variety.Diff + +class Banner1Bean( + override var route: String, + var animeCoverList: List +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is Banner1Bean -> false + route == o.route && animeCoverList == o.animeCoverList -> true + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/BaseBean.kt b/app/src/main/java/com/skyd/imomoe/bean/BaseBean.kt index d27c53de..a5ba3c16 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/BaseBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/BaseBean.kt @@ -3,6 +3,7 @@ package com.skyd.imomoe.bean import java.io.Serializable interface BaseBean : Serializable { - var type: String - var actionUrl: String -} \ No newline at end of file + var route: String +} + +class BaseBeanImpl(override var route: String = "") : BaseBean \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/ClassifyBean.kt b/app/src/main/java/com/skyd/imomoe/bean/ClassifyBean.kt index 31263924..2b7ccc3e 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/ClassifyBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/ClassifyBean.kt @@ -1,20 +1,28 @@ package com.skyd.imomoe.bean +import com.skyd.imomoe.view.adapter.variety.Diff + class ClassifyBean( - override var type: String, - override var actionUrl: String, + override var route: String, var name: String, - var classifyDataList: ArrayList -) : BaseBean { + var classifyDataList: ArrayList +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean { + return when { + o !is ClassifyBean -> false + route == o.route && name == o.name && classifyDataList == o.classifyDataList -> true + else -> false + } + } + override fun toString(): String { return name.replace(":", "").replace(":", "") } } //每个分类子项,如字母的A,地区的大陆 -class ClassifyDataBean( - override var type: String, - override var actionUrl: String, +class ClassifyTab1Bean( + override var route: String, var url: String, var title: String ) : BaseBean //也可以继承TabBean \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/DataSourceBean.kt b/app/src/main/java/com/skyd/imomoe/bean/DataSourceBean.kt new file mode 100644 index 00000000..885ceb45 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/DataSourceBean.kt @@ -0,0 +1,88 @@ +package com.skyd.imomoe.bean + +import com.google.gson.annotations.SerializedName +import com.skyd.imomoe.view.adapter.variety.Diff +import java.io.File +import java.io.Serializable + +typealias DataSource2Bean = DataSourceRepositoryBean + +class DataSource1Bean( + override var route: String, + var file: File, + var selected: Boolean = false, + var name: String, + var versionCode: Int? = null, + var versionName: String? = null, +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is DataSource1Bean -> false + route == o.route && file == o.file && selected == o.selected -> true + else -> false + } +} + +class DataSourceRepositoryBeanWrapper( + @SerializedName("dataSourceList") + val dataSourceList: List +) : Serializable + +class DataSourceRepositoryBean( + override var route: String, + @SerializedName("name") + val name: String?, + @SerializedName("interfaceVersion") + var interfaceVersion: String?, + @SerializedName("versionName") + val versionName: String?, + @SerializedName("versionCode") + val versionCode: Int, + @SerializedName("author") + val author: String?, + @SerializedName("icon") + val icon: String?, + @SerializedName("describe") + val describe: String?, + @SerializedName("publicAt") + val publicAt: Long, // 发布时间戳 + @SerializedName("downloadUrl") + val downloadUrl: String?, + var status: Status? = Status.NONE +) : BaseBean, Diff, Cloneable { + override fun sameAs(o: Any?): Boolean { + return o is DataSourceRepositoryBean && name == o.name && interfaceVersion == o.interfaceVersion + } + + override fun contentSameAs(o: Any?): Boolean = when { + o !is DataSourceRepositoryBean -> false + route == o.route && name == o.name && interfaceVersion == o.interfaceVersion + && versionName == o.versionName && versionCode == o.versionCode && + author == o.author && describe == o.describe && publicAt == o.publicAt && + downloadUrl == o.downloadUrl && icon == o.icon && status == o.status -> true + else -> false + } + + override fun diff(o: Any?): Any? { + if (o !is DataSourceRepositoryBean) return null + + val list: MutableList = mutableListOf() + if (status != o.status) list += STATUS + return list.ifEmpty { null } + } + + public override fun clone(): Any { + return super.clone() + } + + enum class Status { + NONE, // 未安装过 + OUTDATED, // 有新版本 + NEWEST, // 已经是最新版本 + DOWNLOADING,// 正在下载中 + INSTALLING // 正在安装中 + } + + companion object { + const val STATUS = "status" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/FavoriteAnimeBean.kt b/app/src/main/java/com/skyd/imomoe/bean/FavoriteAnimeBean.kt index f8b5f381..0137bbbe 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/FavoriteAnimeBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/FavoriteAnimeBean.kt @@ -3,16 +3,14 @@ package com.skyd.imomoe.bean import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.skyd.imomoe.bean.BaseBean -import com.skyd.imomoe.bean.ImageBean +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.view.adapter.variety.Diff import java.io.Serializable -@Entity(tableName = "favoriteAnimeList") +@Entity(tableName = Const.Database.AppDataBase.FAVORITE_ANIME_TABLE_NAME) class FavoriteAnimeBean( //下面的url都是partUrl - @ColumnInfo(name = "type") - override var type: String, @ColumnInfo(name = "actionUrl") - override var actionUrl: String, + override var route: String, @PrimaryKey @ColumnInfo(name = "animeUrl") var animeUrl: String, @@ -26,4 +24,12 @@ class FavoriteAnimeBean( //下面的url都是partUrl var lastEpisodeUrl: String? = null, //上次看到哪一集 @ColumnInfo(name = "lastEpisode") var lastEpisode: String? = null -) : BaseBean, Serializable +) : BaseBean, Serializable, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is FavoriteAnimeBean -> false + route == o.route && animeUrl == o.animeUrl && animeTitle == o.animeTitle && + time == o.time && cover == o.cover && lastEpisodeUrl == o.lastEpisodeUrl && + lastEpisode == o.lastEpisode -> true + else -> false + } +} diff --git a/app/src/main/java/com/skyd/imomoe/bean/GetDataEnum.kt b/app/src/main/java/com/skyd/imomoe/bean/GetDataEnum.kt deleted file mode 100644 index edcd6d21..00000000 --- a/app/src/main/java/com/skyd/imomoe/bean/GetDataEnum.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.skyd.imomoe.bean - -enum class GetDataEnum { - FAILED, REFRESH, LOAD_MORE -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/GridRecyclerView1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/GridRecyclerView1Bean.kt new file mode 100644 index 00000000..e0111645 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/GridRecyclerView1Bean.kt @@ -0,0 +1,3 @@ +package com.skyd.imomoe.bean + +typealias GridRecyclerView1 = ArrayList \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/Header1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/Header1Bean.kt new file mode 100644 index 00000000..016beb56 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/Header1Bean.kt @@ -0,0 +1,14 @@ +package com.skyd.imomoe.bean + +import com.skyd.imomoe.view.adapter.variety.Diff + +class Header1Bean( + override var route: String, + var title: String +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is Header1Bean -> false + route == o.route && title == o.title -> true + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/HistoryBean.kt b/app/src/main/java/com/skyd/imomoe/bean/HistoryBean.kt index c1b808a0..6eda48ac 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/HistoryBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/HistoryBean.kt @@ -3,14 +3,14 @@ package com.skyd.imomoe.bean import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.view.adapter.variety.Diff import java.io.Serializable -@Entity(tableName = "historyList") +@Entity(tableName = Const.Database.AppDataBase.HISTORY_TABLE_NAME) class HistoryBean( //下面的url都是partUrl - @ColumnInfo(name = "type") - override var type: String, @ColumnInfo(name = "actionUrl") - override var actionUrl: String, + override var route: String, @PrimaryKey @ColumnInfo(name = "animeUrl") var animeUrl: String, @@ -24,4 +24,12 @@ class HistoryBean( //下面的url都是partUrl var lastEpisodeUrl: String? = null, //上次看到哪一集 @ColumnInfo(name = "lastEpisode") var lastEpisode: String? = null -) : BaseBean, Serializable +) : BaseBean, Serializable, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is HistoryBean -> false + route == o.route && animeUrl == o.animeUrl && animeTitle == o.animeTitle && + time == o.time && cover == o.cover && lastEpisodeUrl == o.lastEpisodeUrl && + lastEpisode == o.lastEpisode -> true + else -> false + } +} diff --git a/app/src/main/java/com/skyd/imomoe/bean/HorizontalRecyclerView1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/HorizontalRecyclerView1Bean.kt new file mode 100644 index 00000000..228c3ef8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/HorizontalRecyclerView1Bean.kt @@ -0,0 +1,14 @@ +package com.skyd.imomoe.bean + +import com.skyd.imomoe.view.adapter.variety.Diff + +class HorizontalRecyclerView1Bean( + override var route: String, + var episodeList: List +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is HorizontalRecyclerView1Bean -> false + route == o.route && episodeList == o.episodeList -> true + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/License1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/License1Bean.kt new file mode 100644 index 00000000..1ce7bc54 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/License1Bean.kt @@ -0,0 +1,15 @@ +package com.skyd.imomoe.bean + +import com.skyd.imomoe.view.adapter.variety.Diff + +class License1Bean( + override var route: String, + var title: String, + var license: String +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is License1Bean -> false + route == o.route && title == o.title && license == o.license -> true + else -> false + } +} diff --git a/app/src/main/java/com/skyd/imomoe/bean/LicenseBean.kt b/app/src/main/java/com/skyd/imomoe/bean/LicenseBean.kt deleted file mode 100644 index 42d9b1a9..00000000 --- a/app/src/main/java/com/skyd/imomoe/bean/LicenseBean.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.skyd.imomoe.bean - -class LicenseBean( - override var type: String, - override var actionUrl: String, - var url: String, - var title: String, - var license: String -) : BaseBean diff --git a/app/src/main/java/com/skyd/imomoe/bean/More1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/More1Bean.kt new file mode 100644 index 00000000..62be76d3 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/More1Bean.kt @@ -0,0 +1,17 @@ +package com.skyd.imomoe.bean + +import androidx.annotation.DrawableRes +import com.skyd.imomoe.view.adapter.variety.Diff + +class More1Bean( + override var route: String, + var title: String, + @DrawableRes + var image: Int +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is More1Bean -> false + route == o.route && title == o.title && image == o.image -> true + else -> false + } +} diff --git a/app/src/main/java/com/skyd/imomoe/bean/MoreBean.kt b/app/src/main/java/com/skyd/imomoe/bean/MoreBean.kt deleted file mode 100644 index c7e247b3..00000000 --- a/app/src/main/java/com/skyd/imomoe/bean/MoreBean.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.skyd.imomoe.bean - -import androidx.annotation.DrawableRes - -class MoreBean( - override var type: String, - override var actionUrl: String, - var title: String, - @DrawableRes - var image: Int -) : BaseBean diff --git a/app/src/main/java/com/skyd/imomoe/bean/PageNumberBean.kt b/app/src/main/java/com/skyd/imomoe/bean/PageNumberBean.kt index c9d321c0..bc4fb047 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/PageNumberBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/PageNumberBean.kt @@ -1,8 +1,7 @@ package com.skyd.imomoe.bean class PageNumberBean( - override var type: String, - override var actionUrl: String, + override var route: String, var url: String, var title: String ) : BaseBean diff --git a/app/src/main/java/com/skyd/imomoe/bean/PlayBean.kt b/app/src/main/java/com/skyd/imomoe/bean/PlayBean.kt index 59741918..0eaef217 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/PlayBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/PlayBean.kt @@ -1,16 +1,15 @@ package com.skyd.imomoe.bean class PlayBean( - override var type: String, - override var actionUrl: String, + override var route: String, var title: AnimeTitleBean, var episode: AnimeEpisodeDataBean, - var data: List + var detailPartUrl: String, + var data: List ) : BaseBean //番剧详情下方信息rv数据 class AnimeTitleBean( - override var type: String, - override var actionUrl: String, + override var route: String, var title: String ) : BaseBean \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/SearchHistoryBean.kt b/app/src/main/java/com/skyd/imomoe/bean/SearchHistoryBean.kt index 8dc00836..ab9c3f38 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/SearchHistoryBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/SearchHistoryBean.kt @@ -3,23 +3,38 @@ package com.skyd.imomoe.bean import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.view.adapter.variety.Diff -@Entity(tableName = "searchHistoryList") +typealias SearchHistory1Bean = SearchHistoryBean + +@Entity(tableName = Const.Database.AppDataBase.SEARCH_HISTORY_TABLE_NAME) class SearchHistoryBean( - @ColumnInfo(name = "type") - override var type: String, @ColumnInfo(name = "actionUrl") - override var actionUrl: String, + override var route: String, @PrimaryKey @ColumnInfo(name = "id") var timeStamp: Long, //时间戳作为主键 @ColumnInfo(name = "title") var title: String -) : BaseBean { +) : BaseBean, Diff { override fun equals(other: Any?): Boolean { if (other is SearchHistoryBean) { return other.title == title } return false } + + override fun contentSameAs(o: Any?): Boolean = when { + o !is SearchHistoryBean -> false + route == o.route && timeStamp == o.timeStamp && title == o.title -> true + else -> false + } + + override fun hashCode(): Int { + var result = route.hashCode() + result = 31 * result + timeStamp.hashCode() + result = 31 * result + title.hashCode() + return result + } } diff --git a/app/src/main/java/com/skyd/imomoe/bean/SearchHistoryHeader1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/SearchHistoryHeader1Bean.kt new file mode 100644 index 00000000..4405c976 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/SearchHistoryHeader1Bean.kt @@ -0,0 +1,14 @@ +package com.skyd.imomoe.bean + +import com.skyd.imomoe.view.adapter.variety.Diff + +class SearchHistoryHeader1Bean( + override var route: String, + var title: String +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is SearchHistoryHeader1Bean -> false + route == o.route && title == o.title -> true + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/bean/SendDanmuBean.kt b/app/src/main/java/com/skyd/imomoe/bean/SendDanmuBean.kt deleted file mode 100644 index 2ea5cded..00000000 --- a/app/src/main/java/com/skyd/imomoe/bean/SendDanmuBean.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.skyd.imomoe.bean - -import com.google.gson.annotations.SerializedName -import java.io.Serializable - -class SendDanmuBean( - @SerializedName("author") - var author: String, - @SerializedName("color") - var color: String, - @SerializedName("player") - var player: String, - @SerializedName("referer") - var referer: String, - @SerializedName("size") - var size: String, - @SerializedName("text") - var text: String, - @SerializedName("time") - var time: Double, - @SerializedName("type") - var type: String -) : Serializable diff --git a/app/src/main/java/com/skyd/imomoe/bean/SendDanmuResultBean.kt b/app/src/main/java/com/skyd/imomoe/bean/SendDanmuResultBean.kt deleted file mode 100644 index ab2659de..00000000 --- a/app/src/main/java/com/skyd/imomoe/bean/SendDanmuResultBean.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.skyd.imomoe.bean - -import com.google.gson.annotations.SerializedName - -class SendDanmuResultBean( - override var type: String, - override var actionUrl: String, - @SerializedName("code") - var code: Long, - @SerializedName("msg") - var message: String -) : BaseBean diff --git a/app/src/main/java/com/skyd/imomoe/bean/SkinBean.kt b/app/src/main/java/com/skyd/imomoe/bean/SkinBean.kt deleted file mode 100644 index d76e055b..00000000 --- a/app/src/main/java/com/skyd/imomoe/bean/SkinBean.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.skyd.imomoe.bean - -class SkinBean( - override var type: String, - override var actionUrl: String, - var cover: Any, // Int颜色,或String图片链接 - var title: String, - var using: Boolean, // 正在使用 - var skinPath: String, - var skinSuffix: String -) : BaseBean diff --git a/app/src/main/java/com/skyd/imomoe/bean/SkinCover1Bean.kt b/app/src/main/java/com/skyd/imomoe/bean/SkinCover1Bean.kt new file mode 100644 index 00000000..b675b251 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/SkinCover1Bean.kt @@ -0,0 +1,20 @@ +package com.skyd.imomoe.bean + +import androidx.annotation.StyleRes +import com.skyd.imomoe.view.adapter.variety.Diff + +class SkinCover1Bean( + override var route: String, + var cover: Any, // Int颜色,或String图片链接 + var title: String, + var using: Boolean, // 正在使用 + @StyleRes + var themeRes: Int +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is SkinCover1Bean -> false + route == o.route && cover == o.cover && title == o.title && using == o.using && + themeRes == o.themeRes -> true + else -> false + } +} diff --git a/app/src/main/java/com/skyd/imomoe/bean/TabBean.kt b/app/src/main/java/com/skyd/imomoe/bean/TabBean.kt index 987f9948..6761f260 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/TabBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/TabBean.kt @@ -1,8 +1,15 @@ package com.skyd.imomoe.bean +import com.skyd.imomoe.view.adapter.variety.Diff + class TabBean( - override var type: String, - override var actionUrl: String, - var url: String, - var title: String -) : BaseBean + override var route: String = "", + var partUrl: String = "", + var title: String = "" +) : BaseBean, Diff { + override fun contentSameAs(o: Any?): Boolean = when { + o !is TabBean -> false + route == o.route && partUrl == o.partUrl && title == o.title -> true + else -> false + } +} diff --git a/app/src/main/java/com/skyd/imomoe/bean/UpdateBean.kt b/app/src/main/java/com/skyd/imomoe/bean/UpdateBean.kt index 54e7c595..76b3d02a 100644 --- a/app/src/main/java/com/skyd/imomoe/bean/UpdateBean.kt +++ b/app/src/main/java/com/skyd/imomoe/bean/UpdateBean.kt @@ -3,8 +3,7 @@ package com.skyd.imomoe.bean import com.google.gson.annotations.SerializedName class UpdateBean( - override var type: String, - override var actionUrl: String, + override var route: String, @SerializedName("tag_name") var tagName: String, @@ -24,8 +23,7 @@ class UpdateBean( ) : BaseBean { class AssetsBean( - override var type: String, - override var actionUrl: String, + override var route: String, @SerializedName("name") var name: String, diff --git a/app/src/main/java/com/skyd/imomoe/bean/danmaku/DanmakuData.kt b/app/src/main/java/com/skyd/imomoe/bean/danmaku/DanmakuData.kt new file mode 100644 index 00000000..e00968b5 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/danmaku/DanmakuData.kt @@ -0,0 +1,50 @@ +package com.skyd.imomoe.bean.danmaku + +import com.google.gson.annotations.SerializedName +import java.io.Serializable + +class DanmakuData( + @SerializedName("episode") + val episode: Episode?, + @SerializedName("data") + val data: MutableList?, + @SerializedName("total") + val total: Int, +) : Serializable { + class Data( + @SerializedName("content") + val content: String, + @SerializedName("time") + val time: Double, + @SerializedName("color") + val color: String?, + @SerializedName("type") + val type: String, + @SerializedName("episodeId") + val episodeId: String, + @SerializedName("ip") + val ip: String, + @SerializedName("createdAt") + val createdAt: String, + @SerializedName("updatedAt") + val updatedAt: String, + @SerializedName("id") + val id: String, + ) : Serializable + + class Episode( + @SerializedName("number") + val number: String, + @SerializedName("type") + val type: String, + @SerializedName("goodsId") + val goodsId: String?, + @SerializedName("createdAt") + val createdAt: String, + @SerializedName("updatedAt") + val updatedAt: String, + @SerializedName("id") + val id: String, + ) : Serializable +} + diff --git a/app/src/main/java/com/skyd/imomoe/bean/danmaku/DanmakuWrapper.kt b/app/src/main/java/com/skyd/imomoe/bean/danmaku/DanmakuWrapper.kt new file mode 100644 index 00000000..bb74f16e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/bean/danmaku/DanmakuWrapper.kt @@ -0,0 +1,13 @@ +package com.skyd.imomoe.bean.danmaku + +import com.google.gson.annotations.SerializedName +import java.io.Serializable + +class DanmakuWrapper( + @SerializedName("code") + val code: Int, + @SerializedName("data") + val data: T, + @SerializedName("msg") + val msg: String, +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/config/Api.kt b/app/src/main/java/com/skyd/imomoe/config/Api.kt index 6cf5fa6d..3f78d413 100644 --- a/app/src/main/java/com/skyd/imomoe/config/Api.kt +++ b/app/src/main/java/com/skyd/imomoe/config/Api.kt @@ -4,23 +4,25 @@ import com.skyd.imomoe.model.DataSourceManager interface Api { companion object { - const val DEFAULT_MAIN_URL = "http://www.yhdm.io" - var MAIN_URL = DEFAULT_MAIN_URL - get() = (DataSourceManager.getConst() ?: com.skyd.imomoe.model.impls.Const()) - .MAIN_URL() - // return App.context.sharedPreferences("url") -// .getString("mainUrl", DEFAULT_MAIN_URL) ?: DEFAULT_MAIN_URL - private set/*(value) { - App.context.sharedPreferences("url").editor { - putString("mainUrl", value) - } - }*/ + val MAIN_URL + get() = (DataSourceManager.getConst() ?: com.skyd.imomoe.model.impls.Const()).MAIN_URL - //github + // github const val CHECK_UPDATE_URL = "https://api.github.com/repos/SkyD666/Imomoe/releases/latest" - //弹幕url - const val DANMU_URL = "https://yuan.cuan.la/barrage/api" + // github获取目录API + const val REPO_CONTENT_URL = + "https://api.github.com/repos/SkyD666/DataSourceRepository/contents/" + // github获取data_source_list.json + fun dataSourceListJsonUrl(interfaceVersion: String) = + "${DATA_SOURCE_PREFIX}/datasource/${interfaceVersion}/data_source_list.json" + + // 数据源仓库icon前缀地址 + val DATA_SOURCE_PREFIX = + "https://raw.githubusercontent.com/SkyD666/DataSourceRepository/master" + + // 弹幕url + const val DANMAKU_URL = "https://api.danmu.oyyds.top/api" } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/config/Const.kt b/app/src/main/java/com/skyd/imomoe/config/Const.kt index b0c1abf6..abaa046c 100644 --- a/app/src/main/java/com/skyd/imomoe/config/Const.kt +++ b/app/src/main/java/com/skyd/imomoe/config/Const.kt @@ -1,127 +1,55 @@ package com.skyd.imomoe.config import android.os.Environment -import com.skyd.imomoe.App -import java.io.File +import com.skyd.imomoe.appContext +import kotlin.random.Random interface Const { - interface Common { - companion object { - const val GITHUB_URL = "https://github.com/SkyD666/Imomoe" - const val GITHUB_NEW_ISSUE_URL = "https://github.com/SkyD666/Imomoe/issues/new" - const val USER_NOTICE_VERSION = 1 - } - } - - interface ActionUrl { - companion object { - const val ANIME_CLASSIFY = "/app/classify" //此常量为自己定义,与服务器无关 - const val ANIME_BROWSER = "/app/browser" //此常量为自己定义,与服务器无关 - const val ANIME_ANIME_DOWNLOAD_EPISODE = - "/app/animeDownloadEpisode" //此常量为自己定义,转到下载的每一集 - const val ANIME_ANIME_DOWNLOAD_PLAY = "/app/animeDownloadPlay" //此常量为自己定义,播放这一集 - const val ANIME_ANIME_DOWNLOAD_M3U8 = "/app/animeDownloadM3U8" //此常量为自己定义,m3u8格式 - const val ANIME_LAUNCH_ACTIVITY = "/app/animeLaunchActivity" //此常量为自己定义,启动Activity - const val ANIME_SKIP_BY_WEBSITE = "/app/skipByWebsite" //此常量为自己定义,根据网址跳转 - // 此常量为自己定义,显示通知。要求传入的参数需要经过URL编码!!! - const val ANIME_NOTICE = "/app/notice" - } - } - - interface ShortCuts { - companion object { - const val ID_FAVORITE = "favorite" - const val ID_EVERYDAY = "everyday" - const val ID_DOWNLOAD = "download" - const val ACTION_EVERYDAY = "everyday" - } + object Common { + const val GITHUB_URL = "https://github.com/SkyD666/Imomoe" + const val GITHUB_DATA_SOURCE_URL = "https://github.com/SkyD666/DataSourceRepository" + const val GITHUB_NEW_ISSUE_URL = "https://github.com/SkyD666/Imomoe/issues/new" + const val USER_NOTICE_VERSION = 6 } - interface ViewHolderTypeInt { - companion object { - const val UNKNOWN = -1 //未知类型,使用EmptyViewHolder容错处理。 - const val HEADER_1 = 0 - const val ANIME_COVER_1 = 1 - const val ANIME_COVER_2 = 2 - const val ANIME_COVER_3 = 3 - const val ANIME_COVER_4 = 4 - const val ANIME_COVER_5 = 5 - const val ANIME_COVER_6 = 6 - const val ANIME_COVER_7 = 7 - const val ANIME_COVER_8 = 8 - const val ANIME_COVER_9 = 9 - const val GRID_RECYCLER_VIEW_1 = 20 - const val BANNER_1 = 21 - const val LICENSE_HEADER_1 = 22 - const val LICENSE_1 = 23 - const val SEARCH_HISTORY_HEADER_1 = 24 - const val SEARCH_HISTORY_1 = 25 - const val ANIME_EPISODE_FLOW_LAYOUT_1 = 26 - const val ANIME_EPISODE_FLOW_LAYOUT_2 = 27 - const val ANIME_DESCRIBE_1 = 28 - const val ANIME_INFO_1 = 29 - const val HORIZONTAL_RECYCLER_VIEW_1 = 30 - const val ANIME_EPISODE_2 = 31 - const val UPNP_DEVICE_1 = 32 - const val MORE_1 = 33 - const val SKIN_COVER_1 = 34 - } + object ShortCuts { + const val ID_FAVORITE = "favorite" + const val ID_EVERYDAY = "everyday" + const val ID_DOWNLOAD = "download" + const val ACTION_EVERYDAY = "everyday" } - interface ViewHolderTypeString { - companion object { - const val EMPTY_STRING = "" //未知类型,使用EmptyViewHolder容错处理。 - const val UNKNOWN = "unknown" //未知类型,使用EmptyViewHolder容错处理。 - const val ANIME_COVER_1 = "animeCover1" - const val ANIME_COVER_2 = "animeCover2" - const val ANIME_COVER_3 = "animeCover3" - const val ANIME_COVER_4 = "animeCover4" - const val ANIME_COVER_5 = "animeCover5" - const val ANIME_COVER_6 = "animeCover6" - const val ANIME_COVER_7 = "animeCover7" - const val ANIME_COVER_8 = "animeCover8" - const val ANIME_COVER_9 = "animeCover9" - const val HEADER_1 = "header1" - const val ANIME_EPISODE_FLOW_LAYOUT_1 = "animeEpisodeFlowLayout1" - const val ANIME_EPISODE_FLOW_LAYOUT_2 = "animeEpisodeFlowLayout2" - const val ANIME_DESCRIBE_1 = "animeDescribe1" - const val GRID_RECYCLER_VIEW_1 = "gridRecyclerView1" - const val BANNER_1 = "banner1" - const val LICENSE_HEADER_1 = "licenseHeader1" - const val LICENSE_1 = "license1" - const val SEARCH_HISTORY_HEADER_1 = "searchHistoryHeader1" - const val SEARCH_HISTORY_1 = "searchHistory1" - const val ANIME_INFO_1 = "animeInfo1" - const val HORIZONTAL_RECYCLER_VIEW_1 = "horizontalRecyclerView1" - const val ANIME_EPISODE_2 = "animeEpisode2" - const val UPNP_DEVICE_1 = "upnpDevice1" - const val MORE_1 = "More1" - const val SKIN_COVER_1 = "skinCover1" + object Database { + object AppDataBase { + const val APP_DATA_BASE_FILE_NAME = "app.db" + const val ANIME_DOWNLOAD_TABLE_NAME = "animeDownloadList" + const val FAVORITE_ANIME_TABLE_NAME = "favoriteAnimeList" + const val HISTORY_TABLE_NAME = "historyList" + const val SEARCH_HISTORY_TABLE_NAME = "searchHistoryList" + const val URL_MAP_TABLE_NAME = "urlMapList" } - } - interface DownloadAnime { - companion object { - var new: Boolean = true - val animeFilePath: String - get() { - return if (new) App.context.getExternalFilesDir(null) - .toString() + "/DownloadAnime/" - else Environment.getExternalStorageDirectory() - .toString() + "/Imomoe/DownloadAnime/" - } + object OfflineDataBase { + const val OFFLINE_DATA_BASE_FILE_NAME = "offline_data.db" + const val PLAY_RECORD_TABLE_NAME = "playRecord" } } - interface AnimeDanmu { - companion object { - val animeDanmuFilePath: String = - App.context.getExternalFilesDir(null).toString() + "/AnimeDanmu/danmu.json" - } + object DownloadAnime { + var new: Boolean = true + val animeFilePath: String + get() { + return if (new) appContext.getExternalFilesDir(null) + .toString() + "/DownloadAnime/" + else Environment.getExternalStorageDirectory() + .toString() + "/Imomoe/DownloadAnime/" + } } interface Request { companion object { + fun randomUserAgent(): String = USER_AGENT_ARRAY[Random.nextInt(USER_AGENT_ARRAY.size)] + val USER_AGENT_ARRAY = arrayOf( "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 OPR/26.0.1656.60", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36", diff --git a/app/src/main/java/com/skyd/imomoe/config/UnknownActionUrl.kt b/app/src/main/java/com/skyd/imomoe/config/UnknownActionUrl.kt deleted file mode 100644 index 48b4b228..00000000 --- a/app/src/main/java/com/skyd/imomoe/config/UnknownActionUrl.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.skyd.imomoe.config - -object UnknownActionUrl { - val actionMap: HashMap = HashMap() - - interface Action { - fun action() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/AppDatabase.kt b/app/src/main/java/com/skyd/imomoe/database/AppDatabase.kt index 3a71f306..6c1c0d48 100644 --- a/app/src/main/java/com/skyd/imomoe/database/AppDatabase.kt +++ b/app/src/main/java/com/skyd/imomoe/database/AppDatabase.kt @@ -5,25 +5,27 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.skyd.imomoe.App +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.FavoriteAnimeBean +import com.skyd.imomoe.bean.HistoryBean import com.skyd.imomoe.bean.SearchHistoryBean +import com.skyd.imomoe.config.Const.Database.AppDataBase.APP_DATA_BASE_FILE_NAME import com.skyd.imomoe.database.converter.AnimeDownloadStatusConverter import com.skyd.imomoe.database.converter.ImageBeanConverter -import com.skyd.imomoe.database.dao.AnimeDownloadDao -import com.skyd.imomoe.database.dao.FavoriteAnimeDao -import com.skyd.imomoe.database.dao.SearchHistoryDao +import com.skyd.imomoe.database.dao.* import com.skyd.imomoe.database.entity.AnimeDownloadEntity -import com.skyd.imomoe.bean.FavoriteAnimeBean -import com.skyd.imomoe.bean.HistoryBean -import com.skyd.imomoe.database.dao.HistoryDao +import com.skyd.imomoe.database.entity.UrlMapEntity +import com.skyd.imomoe.database.migration.Migration1To2 +import com.skyd.imomoe.database.migration.Migration2To3 +import com.skyd.imomoe.database.migration.Migration3To4 +import com.skyd.imomoe.database.migration.Migration4To5 @Database( entities = [SearchHistoryBean::class, AnimeDownloadEntity::class, FavoriteAnimeBean::class, - HistoryBean::class], version = 3 + HistoryBean::class, + UrlMapEntity::class], version = 5 ) @TypeConverters( value = [AnimeDownloadStatusConverter::class, @@ -35,22 +37,15 @@ abstract class AppDatabase : RoomDatabase() { abstract fun animeDownloadDao(): AnimeDownloadDao abstract fun favoriteAnimeDao(): FavoriteAnimeDao abstract fun historyDao(): HistoryDao + abstract fun urlMapDao(): UrlMapDao + abstract fun utilDao(): UtilDao companion object { private var instance: AppDatabase? = null - private val migration1To2 = object : Migration(1, 2) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE animeDownloadList ADD fileName TEXT") - } - } - - private val migration2To3 = object : Migration(2, 3) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE favoriteAnimeList(type TEXT NOT NULL, actionUrl TEXT NOT NULL, animeUrl TEXT PRIMARY KEY NOT NULL, animeTitle TEXT NOT NULL, time INTEGER NOT NULL, cover TEXT NOT NULL, lastEpisodeUrl TEXT, lastEpisode TEXT)") - database.execSQL("CREATE TABLE historyList(type TEXT NOT NULL, actionUrl TEXT NOT NULL, animeUrl TEXT PRIMARY KEY NOT NULL, animeTitle TEXT NOT NULL, time INTEGER NOT NULL, cover TEXT NOT NULL, lastEpisodeUrl TEXT, lastEpisode TEXT)") - } - } + private val migrations = arrayOf( + Migration1To2(), Migration2To3(), Migration3To4(), Migration4To5() + ) fun getInstance(context: Context): AppDatabase { if (instance == null) { @@ -59,9 +54,9 @@ abstract class AppDatabase : RoomDatabase() { Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, - "app.db" + APP_DATA_BASE_FILE_NAME ) - .addMigrations(migration1To2, migration2To3) + .addMigrations(*migrations) .build() } } else { @@ -70,11 +65,6 @@ abstract class AppDatabase : RoomDatabase() { } } - - interface DBCallback { - fun success(result: T) - fun fail(throwable: Throwable) - } } -fun getAppDataBase() = AppDatabase.getInstance(App.context) \ No newline at end of file +fun getAppDataBase() = AppDatabase.getInstance(appContext) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/OfflineDatabase.kt b/app/src/main/java/com/skyd/imomoe/database/OfflineDatabase.kt new file mode 100644 index 00000000..b2f0a56a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/OfflineDatabase.kt @@ -0,0 +1,36 @@ +package com.skyd.imomoe.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.skyd.imomoe.appContext +import com.skyd.imomoe.config.Const.Database.OfflineDataBase.OFFLINE_DATA_BASE_FILE_NAME +import com.skyd.imomoe.database.dao.PlayRecordDao +import com.skyd.imomoe.database.entity.PlayRecordEntity + +// 本地数据库,不参与WebDAV备份 +@Database(entities = [PlayRecordEntity::class], version = 1) +abstract class OfflineDatabase : RoomDatabase() { + + abstract fun playRecordDao(): PlayRecordDao + + companion object { + @Volatile + private var INSTANCE: OfflineDatabase? = null + + fun getInstance(context: Context): OfflineDatabase = + INSTANCE ?: synchronized(this) { + INSTANCE ?: buildDatabase(context).also { INSTANCE = it } + } + + private fun buildDatabase(context: Context) = + Room.databaseBuilder( + context, + OfflineDatabase::class.java, + OFFLINE_DATA_BASE_FILE_NAME + ).build() + } +} + +fun getOfflineDatabase() = OfflineDatabase.getInstance(appContext) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/converter/AnimeDownloadStatusConverter.kt b/app/src/main/java/com/skyd/imomoe/database/converter/AnimeDownloadStatusConverter.kt index fc236400..00483320 100644 --- a/app/src/main/java/com/skyd/imomoe/database/converter/AnimeDownloadStatusConverter.kt +++ b/app/src/main/java/com/skyd/imomoe/database/converter/AnimeDownloadStatusConverter.kt @@ -1,14 +1,14 @@ package com.skyd.imomoe.database.converter import androidx.room.TypeConverter -import com.skyd.imomoe.util.downloadanime.AnimeDownloadStatus +import com.skyd.imomoe.util.download.DownloadStatus class AnimeDownloadStatusConverter { @TypeConverter - fun intToEnum(status: Int?): AnimeDownloadStatus? = AnimeDownloadStatus.values()[status ?: 0] + fun intToEnum(status: Int?): DownloadStatus = DownloadStatus.values()[status ?: 0] @TypeConverter - fun enumToInt(animeDownloadStatus: AnimeDownloadStatus?): Int? = animeDownloadStatus?.ordinal + fun enumToInt(animeDownloadStatus: DownloadStatus?): Int? = animeDownloadStatus?.ordinal } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/dao/AnimeDownloadDao.kt b/app/src/main/java/com/skyd/imomoe/database/dao/AnimeDownloadDao.kt index 1f0d5f16..6338845a 100644 --- a/app/src/main/java/com/skyd/imomoe/database/dao/AnimeDownloadDao.kt +++ b/app/src/main/java/com/skyd/imomoe/database/dao/AnimeDownloadDao.kt @@ -1,6 +1,10 @@ package com.skyd.imomoe.database.dao -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.skyd.imomoe.config.Const.Database.AppDataBase.ANIME_DOWNLOAD_TABLE_NAME import com.skyd.imomoe.database.entity.AnimeDownloadEntity @Dao @@ -8,26 +12,26 @@ interface AnimeDownloadDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAnimeDownload(animeDownloadEntity: AnimeDownloadEntity) - @Query(value = "SELECT * FROM animeDownloadList") + @Query(value = "SELECT * FROM $ANIME_DOWNLOAD_TABLE_NAME") fun getAnimeDownloadList(): List - @Query(value = "SELECT * FROM animeDownloadList WHERE md5 = :md5") + @Query(value = "SELECT * FROM $ANIME_DOWNLOAD_TABLE_NAME WHERE md5 = :md5") fun getAnimeDownload(md5: String): AnimeDownloadEntity? // 获取md5列所有内容 - @Query(value = "SELECT md5 FROM animeDownloadList") + @Query(value = "SELECT md5 FROM $ANIME_DOWNLOAD_TABLE_NAME") fun getAnimeDownloadMd5List(): MutableList // 通过md5获得title - @Query(value = "SELECT title FROM animeDownloadList WHERE md5 = :md5") + @Query(value = "SELECT title FROM $ANIME_DOWNLOAD_TABLE_NAME WHERE md5 = :md5") fun getAnimeDownloadTitleByMd5(md5: String): String? - @Query(value = "DELETE FROM animeDownloadList") + @Query(value = "DELETE FROM $ANIME_DOWNLOAD_TABLE_NAME") fun deleteAllAnimeDownload() - @Query(value = "DELETE FROM animeDownloadList WHERE md5 = :md5") + @Query(value = "DELETE FROM $ANIME_DOWNLOAD_TABLE_NAME WHERE md5 = :md5") fun deleteAnimeDownload(md5: String) - @Query(value = "UPDATE animeDownloadList SET fileName = :fileName WHERE md5 = :md5") + @Query(value = "UPDATE $ANIME_DOWNLOAD_TABLE_NAME SET fileName = :fileName WHERE md5 = :md5") fun updateFileNameByMd5(md5: String, fileName: String) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/dao/FavoriteAnimeDao.kt b/app/src/main/java/com/skyd/imomoe/database/dao/FavoriteAnimeDao.kt index 6b40bda7..386b58fc 100644 --- a/app/src/main/java/com/skyd/imomoe/database/dao/FavoriteAnimeDao.kt +++ b/app/src/main/java/com/skyd/imomoe/database/dao/FavoriteAnimeDao.kt @@ -3,6 +3,7 @@ package com.skyd.imomoe.database.dao import androidx.room.* import com.skyd.imomoe.bean.FavoriteAnimeBean import com.skyd.imomoe.bean.ImageBean +import com.skyd.imomoe.config.Const.Database.AppDataBase.FAVORITE_ANIME_TABLE_NAME @Dao interface FavoriteAnimeDao { @@ -10,21 +11,21 @@ interface FavoriteAnimeDao { fun insertFavoriteAnime(favoriteAnimeBean: FavoriteAnimeBean) //按照时间戳顺序,从大到小。最后搜索的元组在最上方(下标0)显示 - @Query(value = "SELECT * FROM favoriteAnimeList ORDER BY time DESC") + @Query(value = "SELECT * FROM $FAVORITE_ANIME_TABLE_NAME ORDER BY time DESC") fun getFavoriteAnimeList(): MutableList - @Query(value = "SELECT * FROM favoriteAnimeList WHERE animeUrl = :animeUrl") + @Query(value = "SELECT * FROM $FAVORITE_ANIME_TABLE_NAME WHERE animeUrl = :animeUrl") fun getFavoriteAnime(animeUrl: String): FavoriteAnimeBean? @Update(onConflict = OnConflictStrategy.REPLACE) fun updateFavoriteAnime(favoriteAnimeBean: FavoriteAnimeBean) - @Query(value = "UPDATE favoriteAnimeList SET cover = :cover WHERE animeUrl = :animeUrl") + @Query(value = "UPDATE $FAVORITE_ANIME_TABLE_NAME SET cover = :cover WHERE animeUrl = :animeUrl") fun updateFavoriteAnimeCover(animeUrl: String, cover: ImageBean) - @Query(value = "UPDATE favoriteAnimeList SET animeTitle = :animeTitle WHERE animeUrl = :animeUrl") + @Query(value = "UPDATE $FAVORITE_ANIME_TABLE_NAME SET animeTitle = :animeTitle WHERE animeUrl = :animeUrl") fun updateFavoriteAnimeTitle(animeUrl: String, animeTitle: String) - @Query(value = "DELETE FROM favoriteAnimeList WHERE animeUrl = :animeUrl") + @Query(value = "DELETE FROM $FAVORITE_ANIME_TABLE_NAME WHERE animeUrl = :animeUrl") fun deleteFavoriteAnime(animeUrl: String) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/dao/HistoryDao.kt b/app/src/main/java/com/skyd/imomoe/database/dao/HistoryDao.kt index bb97b30a..001d77de 100644 --- a/app/src/main/java/com/skyd/imomoe/database/dao/HistoryDao.kt +++ b/app/src/main/java/com/skyd/imomoe/database/dao/HistoryDao.kt @@ -1,8 +1,9 @@ package com.skyd.imomoe.database.dao import androidx.room.* -import com.skyd.imomoe.bean.FavoriteAnimeBean import com.skyd.imomoe.bean.HistoryBean +import com.skyd.imomoe.config.Const.Database.AppDataBase.HISTORY_TABLE_NAME +import kotlinx.coroutines.flow.Flow @Dao interface HistoryDao { @@ -10,21 +11,28 @@ interface HistoryDao { fun insertHistory(historyBean: HistoryBean) //按照时间戳顺序,从大到小。最后搜索的元组在最上方(下标0)显示 - @Query(value = "SELECT * FROM historyList ORDER BY time DESC") + @Query(value = "SELECT * FROM $HISTORY_TABLE_NAME ORDER BY time DESC") fun getHistoryList(): MutableList - @Query(value = "SELECT * FROM historyList WHERE animeUrl = :animeUrl") - fun getHistory(animeUrl: String): FavoriteAnimeBean? + @Query(value = "SELECT * FROM $HISTORY_TABLE_NAME WHERE animeUrl = :animeUrl") + fun getHistory(animeUrl: String): HistoryBean? + + @Query("SELECT * FROM $HISTORY_TABLE_NAME WHERE animeUrl = :animeUrl") + fun getHistoryFlow(animeUrl: String): Flow @Update(onConflict = OnConflictStrategy.REPLACE) - fun updateHistory(favoriteAnimeBean: FavoriteAnimeBean) + fun updateHistory(historyBean: HistoryBean) - @Query(value = "UPDATE historyList SET animeTitle = :animeTitle WHERE animeUrl = :animeUrl") + @Query(value = "UPDATE $HISTORY_TABLE_NAME SET animeTitle = :animeTitle WHERE animeUrl = :animeUrl") fun updateHistoryTitle(animeUrl: String, animeTitle: String) - @Query(value = "DELETE FROM historyList WHERE animeUrl = :animeUrl") + @Query(value = "DELETE FROM $HISTORY_TABLE_NAME WHERE animeUrl = :animeUrl") fun deleteHistory(animeUrl: String) - @Query(value = "DELETE FROM historyList") + @Query(value = "DELETE FROM $HISTORY_TABLE_NAME") fun deleteAllHistory() + + // 获取记录条数 + @Query(value = "SELECT COUNT(1) FROM $HISTORY_TABLE_NAME") + fun getHistoryCount(): Long } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/dao/PlayRecordDao.kt b/app/src/main/java/com/skyd/imomoe/database/dao/PlayRecordDao.kt new file mode 100644 index 00000000..8ba0b41e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/dao/PlayRecordDao.kt @@ -0,0 +1,27 @@ +package com.skyd.imomoe.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.skyd.imomoe.config.Const.Database.OfflineDataBase.PLAY_RECORD_TABLE_NAME +import com.skyd.imomoe.database.entity.PlayRecordEntity + +@Dao +interface PlayRecordDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg record: PlayRecordEntity) + + @Query("SELECT * FROM $PLAY_RECORD_TABLE_NAME WHERE url = :url") + suspend fun query(url: String): PlayRecordEntity? + + @Query("DELETE FROM $PLAY_RECORD_TABLE_NAME WHERE url = :url") + fun delete(url: String) + + @Query(value = "DELETE FROM $PLAY_RECORD_TABLE_NAME") + fun deleteAll() + + // 获取记录条数 + @Query(value = "SELECT COUNT(1) FROM $PLAY_RECORD_TABLE_NAME") + fun getPlayRecordCount(): Long +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/dao/SearchHistoryDao.kt b/app/src/main/java/com/skyd/imomoe/database/dao/SearchHistoryDao.kt index c78f724d..f84bb77d 100644 --- a/app/src/main/java/com/skyd/imomoe/database/dao/SearchHistoryDao.kt +++ b/app/src/main/java/com/skyd/imomoe/database/dao/SearchHistoryDao.kt @@ -2,6 +2,7 @@ package com.skyd.imomoe.database.dao import androidx.room.* import com.skyd.imomoe.bean.SearchHistoryBean +import com.skyd.imomoe.config.Const.Database.AppDataBase.SEARCH_HISTORY_TABLE_NAME @Dao interface SearchHistoryDao { @@ -9,18 +10,22 @@ interface SearchHistoryDao { fun insertSearchHistory(searchHistoryBean: SearchHistoryBean) //按照时间戳顺序,从大到小。最后搜索的元组在最上方(下标0)显示 - @Query(value = "SELECT * FROM searchHistoryList ORDER BY id DESC") + @Query(value = "SELECT * FROM $SEARCH_HISTORY_TABLE_NAME ORDER BY id DESC") fun getSearchHistoryList(): List @Update fun updateSearchHistory(searchHistoryBean: SearchHistoryBean) - @Query(value = "DELETE FROM searchHistoryList WHERE title = :title") + @Query(value = "DELETE FROM $SEARCH_HISTORY_TABLE_NAME WHERE title = :title") fun deleteSearchHistory(title: String) - @Query(value = "DELETE FROM searchHistoryList WHERE id = :timeStamp") + @Query(value = "DELETE FROM $SEARCH_HISTORY_TABLE_NAME WHERE id = :timeStamp") fun deleteSearchHistory(timeStamp: Long) - @Query(value = "DELETE FROM searchHistoryList") + @Query(value = "DELETE FROM $SEARCH_HISTORY_TABLE_NAME") fun deleteAllSearchHistory() + + // 获取记录条数 + @Query(value = "SELECT COUNT(1) FROM $SEARCH_HISTORY_TABLE_NAME") + fun getSearchHistoryCount(): Long } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/dao/UrlMapDao.kt b/app/src/main/java/com/skyd/imomoe/database/dao/UrlMapDao.kt new file mode 100644 index 00000000..99486dfa --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/dao/UrlMapDao.kt @@ -0,0 +1,26 @@ +package com.skyd.imomoe.database.dao + +import androidx.room.* +import com.skyd.imomoe.config.Const.Database.AppDataBase.URL_MAP_TABLE_NAME +import com.skyd.imomoe.database.entity.UrlMapEntity + +@Dao +interface UrlMapDao { + @Query(value = "SELECT * FROM $URL_MAP_TABLE_NAME") + fun getAll(): List + + @Query(value = "SELECT * FROM $URL_MAP_TABLE_NAME WHERE enabled = 1") + fun getAllEnabled(): List + + @Query(value = "SELECT newUrl FROM $URL_MAP_TABLE_NAME WHERE oldUrl = :oldUrl") + fun getNewUrl(oldUrl: String): String? + + @Query(value = "REPLACE INTO $URL_MAP_TABLE_NAME(oldUrl, newUrl, enabled) VALUES(:oldUrl, :newUrl, :enabled)") + fun setNewUrl(oldUrl: String, newUrl: String, enabled: Boolean) + + @Query(value = "DELETE FROM $URL_MAP_TABLE_NAME WHERE oldUrl = :oldUrl") + fun delete(oldUrl: String): Int + + @Query(value = "UPDATE $URL_MAP_TABLE_NAME SET enabled = :enabled WHERE oldUrl = :oldUrl") + fun enabled(oldUrl: String, enabled: Boolean): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/dao/UtilDao.kt b/app/src/main/java/com/skyd/imomoe/database/dao/UtilDao.kt new file mode 100644 index 00000000..5cc79c70 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/dao/UtilDao.kt @@ -0,0 +1,12 @@ +package com.skyd.imomoe.database.dao + +import androidx.room.Dao +import androidx.room.RawQuery +import androidx.sqlite.db.SupportSQLiteQuery + + +@Dao +interface UtilDao { + @RawQuery + fun checkpoint(supportSQLiteQuery: SupportSQLiteQuery): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/entity/AnimeDownloadEntity.kt b/app/src/main/java/com/skyd/imomoe/database/entity/AnimeDownloadEntity.kt index 578fdc27..de4c08a4 100644 --- a/app/src/main/java/com/skyd/imomoe/database/entity/AnimeDownloadEntity.kt +++ b/app/src/main/java/com/skyd/imomoe/database/entity/AnimeDownloadEntity.kt @@ -3,9 +3,10 @@ package com.skyd.imomoe.database.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.skyd.imomoe.config.Const import java.io.Serializable -@Entity(tableName = "animeDownloadList") +@Entity(tableName = Const.Database.AppDataBase.ANIME_DOWNLOAD_TABLE_NAME) class AnimeDownloadEntity( @PrimaryKey @ColumnInfo(name = "md5") @@ -18,4 +19,11 @@ class AnimeDownloadEntity( override fun equals(other: Any?): Boolean { return this.md5 == (other as AnimeDownloadEntity).md5 } + + override fun hashCode(): Int { + var result = md5.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + (fileName?.hashCode() ?: 0) + return result + } } diff --git a/app/src/main/java/com/skyd/imomoe/database/entity/PlayRecordEntity.kt b/app/src/main/java/com/skyd/imomoe/database/entity/PlayRecordEntity.kt new file mode 100644 index 00000000..40e37cf6 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/entity/PlayRecordEntity.kt @@ -0,0 +1,16 @@ +package com.skyd.imomoe.database.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.skyd.imomoe.config.Const + +@Entity(tableName = Const.Database.OfflineDataBase.PLAY_RECORD_TABLE_NAME) +data class PlayRecordEntity( + @PrimaryKey + @ColumnInfo(name = "url") + var url: String, + // 播放进度,单位ms + @ColumnInfo(name = "position") + var position: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/entity/UrlMapEntity.kt b/app/src/main/java/com/skyd/imomoe/database/entity/UrlMapEntity.kt new file mode 100644 index 00000000..946dab0d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/entity/UrlMapEntity.kt @@ -0,0 +1,27 @@ +package com.skyd.imomoe.database.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.view.adapter.variety.Diff +import java.io.Serializable + +@Entity(tableName = Const.Database.AppDataBase.URL_MAP_TABLE_NAME) +class UrlMapEntity( + @PrimaryKey + @ColumnInfo(name = "oldUrl") + var oldUrl: String, // 需要更改的前缀 + @ColumnInfo(name = "newUrl") + var newUrl: String, // 更改为 + @ColumnInfo(name = "enabled") + var enabled: Boolean, // 是否生效 +) : Serializable, Diff { + override fun sameAs(o: Any?): Boolean { + return o is UrlMapEntity && o.newUrl == newUrl && o.oldUrl == oldUrl && o.enabled == enabled + } + + override fun contentSameAs(o: Any?): Boolean { + return o is UrlMapEntity && o.newUrl == newUrl && o.oldUrl == oldUrl && o.enabled == enabled + } +} diff --git a/app/src/main/java/com/skyd/imomoe/database/migration/Migration1To2.kt b/app/src/main/java/com/skyd/imomoe/database/migration/Migration1To2.kt new file mode 100644 index 00000000..888a66c4 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/migration/Migration1To2.kt @@ -0,0 +1,11 @@ +package com.skyd.imomoe.database.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.skyd.imomoe.config.Const.Database.AppDataBase.ANIME_DOWNLOAD_TABLE_NAME + +class Migration1To2 : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE $ANIME_DOWNLOAD_TABLE_NAME ADD fileName TEXT") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/migration/Migration2To3.kt b/app/src/main/java/com/skyd/imomoe/database/migration/Migration2To3.kt new file mode 100644 index 00000000..d29d0c9d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/migration/Migration2To3.kt @@ -0,0 +1,13 @@ +package com.skyd.imomoe.database.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.skyd.imomoe.config.Const.Database.AppDataBase.FAVORITE_ANIME_TABLE_NAME +import com.skyd.imomoe.config.Const.Database.AppDataBase.HISTORY_TABLE_NAME + +class Migration2To3 : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE ${FAVORITE_ANIME_TABLE_NAME}(type TEXT NOT NULL, actionUrl TEXT NOT NULL, animeUrl TEXT PRIMARY KEY NOT NULL, animeTitle TEXT NOT NULL, time INTEGER NOT NULL, cover TEXT NOT NULL, lastEpisodeUrl TEXT, lastEpisode TEXT)") + database.execSQL("CREATE TABLE ${HISTORY_TABLE_NAME}(type TEXT NOT NULL, actionUrl TEXT NOT NULL, animeUrl TEXT PRIMARY KEY NOT NULL, animeTitle TEXT NOT NULL, time INTEGER NOT NULL, cover TEXT NOT NULL, lastEpisodeUrl TEXT, lastEpisode TEXT)") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/migration/Migration3To4.kt b/app/src/main/java/com/skyd/imomoe/database/migration/Migration3To4.kt new file mode 100644 index 00000000..33d26285 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/migration/Migration3To4.kt @@ -0,0 +1,60 @@ +package com.skyd.imomoe.database.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.skyd.imomoe.config.Const.Database.AppDataBase.FAVORITE_ANIME_TABLE_NAME +import com.skyd.imomoe.config.Const.Database.AppDataBase.HISTORY_TABLE_NAME +import com.skyd.imomoe.config.Const.Database.AppDataBase.SEARCH_HISTORY_TABLE_NAME + +class Migration3To4 : Migration(3, 4) { + // 删除type列 + // 不支持删除某一列,只能创建新表 + override fun migrate(database: SupportSQLiteDatabase) { + // searchHistoryList表======================================== + // =========================================================== + // 创建一个新表searchHistoryListTemp,只设定想要的字段 + database.execSQL( + "CREATE TABLE ${SEARCH_HISTORY_TABLE_NAME}Temp " + + "(actionUrl TEXT NOT NULL, id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL)" + ) + // 将原来表中的数据复制过来, + database.execSQL( + "INSERT INTO ${SEARCH_HISTORY_TABLE_NAME}Temp (actionUrl, id, title)" + + "SELECT actionUrl, id, title FROM $SEARCH_HISTORY_TABLE_NAME" + ) + // 删除原表 + database.execSQL("DROP TABLE $SEARCH_HISTORY_TABLE_NAME") + // 将新建的表改名 + database.execSQL("ALTER TABLE ${SEARCH_HISTORY_TABLE_NAME}Temp RENAME to $SEARCH_HISTORY_TABLE_NAME") + + // favoriteAnimeList表======================================== + // =========================================================== + database.execSQL( + "CREATE TABLE ${FAVORITE_ANIME_TABLE_NAME}Temp " + + "(actionUrl TEXT NOT NULL, animeUrl TEXT PRIMARY KEY NOT NULL, " + + "animeTitle TEXT NOT NULL, time INTEGER NOT NULL, cover TEXT NOT NULL, " + + "lastEpisodeUrl TEXT, lastEpisode TEXT)" + ) + database.execSQL( + "INSERT INTO ${FAVORITE_ANIME_TABLE_NAME}Temp (actionUrl, animeUrl, animeTitle, time, cover, lastEpisodeUrl, lastEpisode)" + + "SELECT actionUrl, animeUrl, animeTitle, time, cover, lastEpisodeUrl, lastEpisode FROM $FAVORITE_ANIME_TABLE_NAME" + ) + database.execSQL("DROP TABLE $FAVORITE_ANIME_TABLE_NAME") + database.execSQL("ALTER TABLE ${FAVORITE_ANIME_TABLE_NAME}Temp RENAME to $FAVORITE_ANIME_TABLE_NAME") + + // historyList表============================================== + // =========================================================== + database.execSQL( + "CREATE TABLE ${HISTORY_TABLE_NAME}Temp " + + "(actionUrl TEXT NOT NULL, animeUrl TEXT PRIMARY KEY NOT NULL, " + + "animeTitle TEXT NOT NULL, time INTEGER NOT NULL, cover TEXT NOT NULL, " + + "lastEpisodeUrl TEXT, lastEpisode TEXT)" + ) + database.execSQL( + "INSERT INTO ${HISTORY_TABLE_NAME}Temp (actionUrl, animeUrl, animeTitle, time, cover, lastEpisodeUrl, lastEpisode)" + + "SELECT actionUrl, animeUrl, animeTitle, time, cover, lastEpisodeUrl, lastEpisode FROM $HISTORY_TABLE_NAME" + ) + database.execSQL("DROP TABLE $HISTORY_TABLE_NAME") + database.execSQL("ALTER TABLE ${HISTORY_TABLE_NAME}Temp RENAME to $HISTORY_TABLE_NAME") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/database/migration/Migration4To5.kt b/app/src/main/java/com/skyd/imomoe/database/migration/Migration4To5.kt new file mode 100644 index 00000000..ddcda864 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/database/migration/Migration4To5.kt @@ -0,0 +1,11 @@ +package com.skyd.imomoe.database.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.skyd.imomoe.config.Const.Database.AppDataBase.URL_MAP_TABLE_NAME + +class Migration4To5 : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE ${URL_MAP_TABLE_NAME}(oldUrl TEXT PRIMARY KEY NOT NULL, newUrl TEXT NOT NULL, enabled INTEGER NOT NULL)") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/di/AppModule.kt b/app/src/main/java/com/skyd/imomoe/di/AppModule.kt new file mode 100644 index 00000000..abdac4a7 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/di/AppModule.kt @@ -0,0 +1,18 @@ +package com.skyd.imomoe.di + +import com.skyd.imomoe.util.update.AppUpdateHelper +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Singleton + @Provides + fun provideAppUpdateHelper(): AppUpdateHelper { + return AppUpdateHelper.instance + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/di/ViewModelModule.kt b/app/src/main/java/com/skyd/imomoe/di/ViewModelModule.kt new file mode 100644 index 00000000..571c2735 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/di/ViewModelModule.kt @@ -0,0 +1,84 @@ +package com.skyd.imomoe.di + +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.model.impls.* +import com.skyd.imomoe.model.interfaces.* +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +object ViewModelModule { + @Provides + fun provideAnimeDetailModel(): IAnimeDetailModel { + return DataSourceManager.create(IAnimeDetailModel::class.java) ?: AnimeDetailModel() + } + + @Provides + fun provideAnimeShowModel(): IAnimeShowModel { + return DataSourceManager.create(IAnimeShowModel::class.java) ?: AnimeShowModel() + } + + @Provides + fun provideClassifyModel(): IClassifyModel { + return DataSourceManager.create(IClassifyModel::class.java) ?: ClassifyModel() + } + + @Provides + fun provideConst(): IConst { + return DataSourceManager.getConst() ?: Const() + } + + @Provides + fun provideEverydayAnimeModel(): IEverydayAnimeModel { + return DataSourceManager.create(IEverydayAnimeModel::class.java) ?: EverydayAnimeModel() + } + + @Provides + fun provideEverydayAnimeWidgetModel(): IEverydayAnimeWidgetModel { + return DataSourceManager.create(IEverydayAnimeWidgetModel::class.java) + ?: EverydayAnimeWidgetModel() + } + + @Provides + fun provideHomeModel(): IHomeModel { + return DataSourceManager.create(IHomeModel::class.java) ?: HomeModel() + } + + @Provides + fun provideMonthAnimeModel(): IMonthAnimeModel { + return DataSourceManager.create(IMonthAnimeModel::class.java) ?: MonthAnimeModel() + } + + @Provides + fun providePlayModel(): IPlayModel { + return DataSourceManager.create(IPlayModel::class.java) ?: PlayModel() + } + + @Provides + fun provideRankListModel(): IRankListModel { + return DataSourceManager.create(IRankListModel::class.java) ?: RankListModel() + } + + @Provides + fun provideRankModel(): IRankModel { + return DataSourceManager.create(IRankModel::class.java) ?: RankModel() + } + + @Provides + fun provideRouter(): IRouter { + return DataSourceManager.getRouter() ?: Router() + } + + @Provides + fun provideSearchModel(): ISearchModel { + return DataSourceManager.create(ISearchModel::class.java) ?: SearchModel() + } + + @Provides + fun provideUtil(): IUtil { + return DataSourceManager.getUtil() ?: Util() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/ActivityExt.kt b/app/src/main/java/com/skyd/imomoe/ext/ActivityExt.kt new file mode 100644 index 00000000..831a337b --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/ActivityExt.kt @@ -0,0 +1,30 @@ +package com.skyd.imomoe.ext + +import android.view.WindowManager +import androidx.core.app.ComponentActivity +import com.skyd.imomoe.ext.theme.appThemeRes +import kotlinx.coroutines.flow.MutableSharedFlow + +val recreateAllBaseActivity: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + +var disableScreenshot: Boolean = sharedPreferences().getBoolean("disableScreenshot", false) + set(value) { + if (value == field) return + sharedPreferences().editor { putBoolean("disableScreenshot", value) } + field = value + recreateAllBaseActivity.tryEmit(Unit) + } + +fun ComponentActivity.beforeSetContentView() { + recreateAllBaseActivity.collectWithLifecycle(this) { recreate() } + + // 设置主题 + setTheme(appThemeRes) + + // 是否禁止截图 + if (disableScreenshot) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/CommonExt.kt b/app/src/main/java/com/skyd/imomoe/ext/CommonExt.kt new file mode 100644 index 00000000..482c79d3 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/CommonExt.kt @@ -0,0 +1,35 @@ +package com.skyd.imomoe.ext + +import kotlinx.coroutines.delay +import java.text.SimpleDateFormat +import java.util.* +import kotlin.time.Duration + +/** + * 为空,执行nullAction,否则执行notNullAction + */ +inline fun T?.notNull(notNullAction: (T) -> Unit, nullAction: () -> Unit = {}) { + if (this != null) { + notNullAction.invoke(this) + } else { + nullAction.invoke() + } +} + +fun Long.toTimeString( + pattern: String = "yyyy-MM-dd HH:mm:ss", + locale: Locale = Locale.getDefault() +): String = Date(this).toTimeString(pattern, locale) + +fun Date.toTimeString( + pattern: String = "yyyy-MM-dd HH:mm:ss", + locale: Locale = Locale.getDefault() +): String { + val format = SimpleDateFormat(pattern, locale) + return format.format(this) +} + +suspend fun doAfter(time: Duration, onFinish: () -> Unit) { + delay(time) + onFinish() +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/ContextExt.kt b/app/src/main/java/com/skyd/imomoe/ext/ContextExt.kt new file mode 100644 index 00000000..fedd5909 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/ContextExt.kt @@ -0,0 +1,21 @@ +package com.skyd.imomoe.ext + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Configuration + +val Context.activity: Activity + get() { + var ctx = this + while (ctx is ContextWrapper) { + if (ctx is Activity) { + return ctx + } + ctx = ctx.baseContext + } + error("can't find activity: $this") + } + +val Context.screenIsLand: Boolean + get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/DataSourceExt.kt b/app/src/main/java/com/skyd/imomoe/ext/DataSourceExt.kt new file mode 100644 index 00000000..b7c3cfd2 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/DataSourceExt.kt @@ -0,0 +1,6 @@ +package com.skyd.imomoe.ext + +import kotlinx.coroutines.flow.MutableSharedFlow + +var dataSourceDirectoryChanged: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) diff --git a/app/src/main/java/com/skyd/imomoe/ext/DialogExt.kt b/app/src/main/java/com/skyd/imomoe/ext/DialogExt.kt new file mode 100644 index 00000000..c30906c8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/DialogExt.kt @@ -0,0 +1,267 @@ +package com.skyd.imomoe.ext + +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.view.View +import android.view.Window +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import android.widget.Button +import android.widget.ProgressBar +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AlertDialog +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.skyd.imomoe.R + + +fun Fragment.showMessageDialog( + title: CharSequence = getString(R.string.warning), + message: CharSequence? = null, + @DrawableRes icon: Int = 0, + cancelable: Boolean = true, + negativeText: String = resources.getString(R.string.cancel), + positiveText: String = resources.getString(R.string.ok), + onCancel: ((dialog: DialogInterface) -> Unit)? = null, + onNegative: ((dialog: DialogInterface, which: Int) -> Unit)? = { dialog, _ -> dialog.dismiss() }, + onPositive: ((dialog: DialogInterface, which: Int) -> Unit)? = null, +): AlertDialog? = + requireActivity().showMessageDialog( + onPositive = onPositive, + onNegative = onNegative, + message = message, + icon = icon, + title = title, + cancelable = cancelable, + onCancel = onCancel, + positiveText = positiveText, + negativeText = negativeText + ) + +fun Activity.showMessageDialog( + title: CharSequence = getString(R.string.warning), + message: CharSequence? = null, + @DrawableRes icon: Int = 0, + cancelable: Boolean = true, + negativeText: String = getString(R.string.cancel), + positiveText: String = getString(R.string.ok), + onCancel: ((dialog: DialogInterface) -> Unit)? = null, + onNegative: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onPositive: ((dialog: DialogInterface, which: Int) -> Unit)? = null, +): AlertDialog? { + return MaterialAlertDialogBuilder( + this, + R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered + ) + .setTitle(title) + .setMessage(message) + .apply { onPositive?.let { setPositiveButton(positiveText, it) } } + .apply { onNegative?.let { setNegativeButton(negativeText, it) } } + .setCancelable(cancelable) + .setIcon(icon) + .setOnCancelListener { onCancel?.invoke(it) } + .run { + if (!isFinishing) show() else null + } +} + +fun Activity.showListDialog( + title: CharSequence? = null, + items: List? = null, + checkedItem: Int = -1, + onItemClickListener: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + icon: Drawable? = null, + cancelable: Boolean = true, + negativeText: String = getString(R.string.cancel), + neutralText: String? = null, + positiveText: String = getString(R.string.ok), + onNegative: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onNeutral: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onPositive: ((dialog: DialogInterface, which: Int, itemIndex: Int) -> Unit)? = null, +): AlertDialog? { + var itemIndex = checkedItem + var positionButton: Button? = null + return MaterialAlertDialogBuilder( + this, + R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered + ) + .setTitle(title) + .setSingleChoiceItems(items?.toTypedArray(), checkedItem) { dialog, which -> + itemIndex = which + positionButton?.isEnabled = which != -1 + onItemClickListener?.invoke(dialog, which) + } + .apply { + onPositive?.let { + setPositiveButton(positiveText) { dialog, which -> + onPositive.invoke(dialog, which, itemIndex) + } + } + } + .apply { onNegative?.let { setNegativeButton(negativeText, it) } } + .apply { + if (onNeutral != null && neutralText != null) { + setNeutralButton(neutralText, onNeutral) + } + } + .setCancelable(cancelable) + .setIcon(icon) + .run { + if (!isFinishing) { + show().apply { + positionButton = getButton(AlertDialog.BUTTON_POSITIVE) + positionButton?.isEnabled = itemIndex != -1 + } + } else null + } +} + +fun Fragment.showInputDialog( + title: CharSequence? = null, + hint: String? = null, + prefill: CharSequence? = null, + icon: Drawable? = null, + cancelable: Boolean = true, + empty: Boolean = false, // 是否可以什么都不输入 + multipleLine: Boolean = false, // 是否可以换行 + validator: ((CharSequence?) -> Boolean)? = null, + validatorErrorMessage: String = getString(R.string.input_dialog_error_message), + negativeText: String = getString(R.string.cancel), + neutralText: String? = null, + positiveText: String = getString(R.string.ok), + onNegative: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onNeutral: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onPositive: ((dialog: DialogInterface, which: Int, text: CharSequence) -> Unit)? = null, +): AlertDialog? { + return requireActivity().showInputDialog( + title = title, + hint = hint, + prefill = prefill, + icon = icon, + cancelable = cancelable, + empty = empty, + multipleLine = multipleLine, + validator = validator, + validatorErrorMessage = validatorErrorMessage, + negativeText = negativeText, + neutralText = neutralText, + positiveText = positiveText, + onNegative = onNegative, + onNeutral = onNeutral, + onPositive = onPositive + ) +} + +fun Activity.showInputDialog( + title: CharSequence? = null, + hint: String? = null, + prefill: CharSequence? = null, + icon: Drawable? = null, + cancelable: Boolean = true, + empty: Boolean = false, // 是否可以什么都不输入 + multipleLine: Boolean = false, // 是否可以换行 + validator: ((CharSequence?) -> Boolean)? = null, + validatorErrorMessage: String = getString(R.string.input_dialog_error_message), + negativeText: String = getString(R.string.cancel), + neutralText: String? = null, + positiveText: String = getString(R.string.ok), + onNegative: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onNeutral: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onPositive: ((dialog: DialogInterface, which: Int, text: CharSequence) -> Unit)? = null, +): AlertDialog? { + var text: CharSequence = "" + var positionButton: Button? = null + return MaterialAlertDialogBuilder( + this, + R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered + ) + .setTitle(title) + .apply { + onPositive?.let { + setPositiveButton(positiveText) { dialog, which -> + onPositive.invoke(dialog, which, text) + } + } + } + .apply { onNegative?.let { setNegativeButton(negativeText, it) } } + .apply { + if (onNeutral != null && neutralText != null) { + setNeutralButton(neutralText, onNeutral) + } + } + .setCancelable(cancelable) + .setIcon(icon) + .setView(R.layout.layout_input_dialog) + .run { if (!isFinishing) show() else null } + ?.apply { + findViewById(R.id.til_input_dialog)?.hint = hint + findViewById(R.id.et_input_dialog)?.also { + if (!multipleLine) it.setSingleLine() + it.doOnTextChanged { t, _, _, _ -> + if (t != null && t.isNotEmpty() || empty) { + text = t ?: "" + positionButton?.isEnabled = true + } else { + positionButton?.isEnabled = false + } + + if (validator?.invoke(t) == false) { + it.error = validatorErrorMessage + positionButton?.isEnabled = false + } else { + positionButton?.isEnabled = true + } + } + autoShowKeyboard(this@showInputDialog, it) + window?.fixWindowTranslucentStatus() + if (prefill != null) it.setText(prefill) + it.selectAll() + } + positionButton = getButton(AlertDialog.BUTTON_POSITIVE) + if (!empty) positionButton?.isEnabled = false + } +} + +private var waitingDialog: AlertDialog? = null +fun dismissWaitingDialog() { + waitingDialog?.dismiss() +} + +fun Activity.showWaitingDialog( + message: CharSequence? = null, + cancelable: Boolean = true, + negativeText: String = getString(R.string.cancel), + positiveText: String = getString(R.string.ok), + onNegative: ((dialog: DialogInterface, which: Int) -> Unit)? = null, + onPositive: ((dialog: DialogInterface, which: Int) -> Unit)? = null, +): AlertDialog? { + waitingDialog = MaterialAlertDialogBuilder(this) + .setMessage(message) + .setView(ProgressBar(this)) + .apply { onPositive?.let { setPositiveButton(positiveText, it) } } + .apply { onNegative?.let { setNegativeButton(negativeText, it) } } + .setCancelable(cancelable) + .setOnDismissListener { waitingDialog = null } + .run { if (!isFinishing) show() else null } + return waitingDialog +} + +/** + * 修复theme.xml里面设置windowTranslucentStatus=true时,Dialog无法上弹的问题 + */ +private fun Window.fixWindowTranslucentStatus() { + clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) +} + +private fun autoShowKeyboard(context: Context, v: View) { + v.post { + v.requestFocus() + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/FileExt.kt b/app/src/main/java/com/skyd/imomoe/ext/FileExt.kt new file mode 100644 index 00000000..aedcec72 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/FileExt.kt @@ -0,0 +1,112 @@ +package com.skyd.imomoe.ext + +import android.content.ContentResolver +import android.net.Uri +import android.os.Build +import android.provider.OpenableColumns +import androidx.core.content.FileProvider +import com.skyd.imomoe.appContext +import java.io.File +import java.io.FileInputStream +import java.math.BigDecimal + + +val File.uri: Uri + get() = if (Build.VERSION.SDK_INT >= 24) { + FileProvider.getUriForFile(appContext, "com.skyd.imomoe.fileProvider", this) + } else { + Uri.fromFile(this) + } + +fun String.toFile() = File(this) + +fun File.fileSize(): Long { + var s: Long = 0 + if (this.exists() && this.isFile) { + val fis = FileInputStream(this) + s = fis.available().toLong() + } + return s +} + +fun Uri.fileName(contentResolver: ContentResolver): String { + val name2 = path?.substringAfterLast("/", "") + .orEmpty() + .replaceFirst("primary:", "") + runCatching { + contentResolver.query( + this, null, null, null, null + )?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + return cursor.getString(nameIndex) ?: name2 + } + }.onFailure { + it.printStackTrace() + } + return name2 +} + +fun Uri.fileSuffixName(contentResolver: ContentResolver): String { + return fileName(contentResolver).substringAfterLast(".") +} + +fun File.directorySize(): Long { + var size: Long = 0 + val fList = listFiles() + fList?.let { + for (i in it.indices) { + size += if (it[i].isDirectory) { + it[i].directorySize() + } else { + it[i].fileSize() + } + } + } + return size +} + +/** + * 获取规整的文件大小 + * @param newScale 精确到小数点几位 + */ +fun Double.formatSize(newScale: Int = 2): String { + val kiloByte = this / 1024 + if (kiloByte < 1) { + return this.toString() + "B" + } + val megaByte = kiloByte / 1024 + if (megaByte < 1) { + val result1 = BigDecimal(kiloByte.toString()) + return result1.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() + .toString() + "K" + } + val gigaByte = megaByte / 1024 + if (gigaByte < 1) { + val result2 = BigDecimal(megaByte.toString()) + return result2.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() + .toString() + "M" + } + val teraBytes = gigaByte / 1024 + if (teraBytes < 1) { + val result3 = BigDecimal(gigaByte.toString()) + return result3.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() + .toString() + "G" + } + val result4 = BigDecimal(teraBytes) + return result4.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() + .toString() + "T" +} + +fun Long.formatSize(newScale: Int = 2): String { + return this.toDouble().formatSize(newScale) +} + +/** + * 获取规整的文件大小 + * @param newScale 精确到小数点几位 + */ +fun File.formatSize(newScale: Int = 2): String { + val size: Double = if (isFile) fileSize().toDouble() else directorySize().toDouble() + return size.formatSize(newScale) +} diff --git a/app/src/main/java/com/skyd/imomoe/ext/FlowExt.kt b/app/src/main/java/com/skyd/imomoe/ext/FlowExt.kt new file mode 100644 index 00000000..a3e98f15 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/FlowExt.kt @@ -0,0 +1,19 @@ +package com.skyd.imomoe.ext + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +inline fun Flow.collectWithLifecycle( + lifecycleOwner: LifecycleOwner, + crossinline block: suspend CoroutineScope.(data: T) -> Unit +): Job { + return lifecycleOwner.lifecycleScope.launch { + this@collectWithLifecycle.collect { + block(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/FlurryExt.kt b/app/src/main/java/com/skyd/imomoe/ext/FlurryExt.kt new file mode 100644 index 00000000..c3b4ee8e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/FlurryExt.kt @@ -0,0 +1,18 @@ +package com.skyd.imomoe.ext + +import android.app.Application +import com.flurry.android.FlurryAgent +import com.skyd.imomoe.BuildConfig + +var initializedFlurry: Boolean = false + private set + +fun initializeFlurry(context: Application) { + if (initializedFlurry) return + initializedFlurry = true + if (BuildConfig.DEBUG) return + FlurryAgent.Builder() + .withCaptureUncaughtExceptions(true) + .withLogEnabled(BuildConfig.DEBUG) + .build(context, BuildConfig.FLURRY_API_KEY) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/IOExt.kt b/app/src/main/java/com/skyd/imomoe/ext/IOExt.kt new file mode 100644 index 00000000..7f982865 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/IOExt.kt @@ -0,0 +1,34 @@ +package com.skyd.imomoe.ext + +import android.content.Context +import androidx.annotation.RawRes +import java.io.* +import java.nio.charset.Charset + + +fun InputStream.string(charset: Charset = Charsets.UTF_8): String { + val outputStream = ByteArrayOutputStream() + var len: Int + val buffer = ByteArray(1024) + while (read(buffer).also { len = it } != -1) { + outputStream.write(buffer, 0, len) + } + close() + outputStream.close() + return String(outputStream.toByteArray(), charset) +} + +fun Context.getRawString(@RawRes id: Int, charset: Charset = Charsets.UTF_8): String { + val sb = StringBuffer() + try { + val inputStream = resources.openRawResource(id) + val reader = BufferedReader(InputStreamReader(inputStream, charset)) + var out: String? + while (reader.readLine().also { out = it } != null) { + sb.append(out) + } + } catch (e: IOException) { + e.printStackTrace() + } + return sb.toString() +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/MD5Ext.kt b/app/src/main/java/com/skyd/imomoe/ext/MD5Ext.kt new file mode 100644 index 00000000..75c3d371 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/MD5Ext.kt @@ -0,0 +1,44 @@ +package com.skyd.imomoe.ext + +import okio.ByteString +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.UnsupportedEncodingException +import java.math.BigInteger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + + +fun File.toMD5(): String? { + var bi: BigInteger? = null + try { + val buffer = ByteArray(8192) + var len: Int + val md = MessageDigest.getInstance("MD5") + val fis = FileInputStream(this) + while (fis.read(buffer).also { len = it } != -1) { + md.update(buffer, 0, len) + } + fis.close() + val b = md.digest() + bi = BigInteger(1, b) + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + return bi?.toString(16) +} + +fun String.toMD5(): String { + try { + val messageDigest = MessageDigest.getInstance("MD5") + val md5bytes = messageDigest.digest(toByteArray(charset("UTF-8"))) + return ByteString.of(*md5bytes).hex() + } catch (e: NoSuchAlgorithmException) { + throw AssertionError(e) + } catch (e: UnsupportedEncodingException) { + throw AssertionError(e) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/ext/NumberExt.kt b/app/src/main/java/com/skyd/imomoe/ext/NumberExt.kt new file mode 100644 index 00000000..1600cda2 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/NumberExt.kt @@ -0,0 +1,26 @@ +package com.skyd.imomoe.ext + +import java.math.RoundingMode +import java.text.DecimalFormat + +/** + * 只拼接百分号% + */ +inline val Int.percentage: String + get() = "${this}%" + +/** + * 只拼接百分号% + */ +fun Double.percentage(pattern: String = "0.##"): String { + val format = DecimalFormat(pattern) + format.roundingMode = RoundingMode.FLOOR + return "${format.format(this)}%" +} + +/** + * 乘100后拼接百分号 + */ +fun Int.toPercentage(): String = "${this * 100}%" + +fun Int.toBoolean(): Boolean = this != 0 \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/PaddingValues.kt b/app/src/main/java/com/skyd/imomoe/ext/PaddingValues.kt new file mode 100644 index 00000000..abba80dc --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/PaddingValues.kt @@ -0,0 +1,17 @@ +package com.skyd.imomoe.ext + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalLayoutDirection + +@Composable +operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = PaddingValues( + top = calculateTopPadding() + other.calculateTopPadding(), + bottom = calculateBottomPadding() + other.calculateBottomPadding(), + start = calculateStartPadding(LocalLayoutDirection.current) + + other.calculateStartPadding(LocalLayoutDirection.current), + end = calculateEndPadding(LocalLayoutDirection.current) + + other.calculateEndPadding(LocalLayoutDirection.current) +) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/PermissionExt.kt b/app/src/main/java/com/skyd/imomoe/ext/PermissionExt.kt new file mode 100644 index 00000000..4e3ffbda --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/PermissionExt.kt @@ -0,0 +1,18 @@ +package com.skyd.imomoe.ext + +import android.app.Activity +import androidx.fragment.app.Fragment +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import com.skyd.imomoe.view.listener.dsl.OnSinglePermissionCallback +import com.skyd.imomoe.view.listener.dsl.requestSinglePermission + +fun Activity.requestManageExternalStorage(init: OnSinglePermissionCallback.() -> Unit) { + XXPermissions.with(this).permission(Permission.MANAGE_EXTERNAL_STORAGE) + .requestSinglePermission(init) +} + +fun Fragment.requestManageExternalStorage(init: OnSinglePermissionCallback.() -> Unit) { + XXPermissions.with(this).permission(Permission.MANAGE_EXTERNAL_STORAGE) + .requestSinglePermission(init) +} diff --git a/app/src/main/java/com/skyd/imomoe/ext/PlayerExt.kt b/app/src/main/java/com/skyd/imomoe/ext/PlayerExt.kt new file mode 100644 index 00000000..848aed98 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/PlayerExt.kt @@ -0,0 +1,90 @@ +package com.skyd.imomoe.ext + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import android.net.Uri +import java.lang.Exception + + +enum class SwitchVideoMode { + Once, RepeatOne, Next +} + +var switchVideoMode: SwitchVideoMode + set(value) { + when (value) { + SwitchVideoMode.Once -> { + sharedPreferences().editor { putString("switchVideoMode", "Once") } + } + SwitchVideoMode.RepeatOne -> { + sharedPreferences().editor { putString("switchVideoMode", "RepeatOne") } + } + SwitchVideoMode.Next -> { + sharedPreferences().editor { putString("switchVideoMode", "Next") } + } + } + } + get() { + return when (sharedPreferences().getString("switchVideoMode", "Once")) { + "Once", "StopPlay" -> { + SwitchVideoMode.Once + } + "RepeatOne" -> { + SwitchVideoMode.RepeatOne + } + "Next", "AutoPlayNextEpisode" -> { + SwitchVideoMode.Next + } + else -> { + SwitchVideoMode.Once + } + } + } + +fun Context.getMediaTitle(uri: Uri): String? { + return getMediaStringInfo(uri, MediaMetadataRetriever.METADATA_KEY_TITLE) +} + +fun Context.getMediaMime(uri: Uri): String? { + return getMediaStringInfo(uri, MediaMetadataRetriever.METADATA_KEY_MIMETYPE) +} + +fun Context.getMediaAlbumArt(uri: Uri): Bitmap? { + val image: Bitmap? = try { + val mData = MediaMetadataRetriever() + mData.setDataSource(this, uri) + val art = mData.embeddedPicture!! + BitmapFactory.decodeByteArray(art, 0, art.size) + } catch (e: Exception) { + e.printStackTrace() + null + } + return image +} + +private fun Context.getMediaStringInfo(uri: Uri, keyCode: Int): String? { + val info: String? = try { + val mData = MediaMetadataRetriever() + mData.setDataSource(this, uri) + mData.extractMetadata(keyCode) + } catch (e: Exception) { + e.printStackTrace() + null + } + return info +} + +fun getMediaAlbumArt(path: String): Bitmap? { + val image: Bitmap? = try { + val mData = MediaMetadataRetriever() + mData.setDataSource(path) + val art = mData.embeddedPicture!! + BitmapFactory.decodeByteArray(art, 0, art.size) + } catch (e: Exception) { + e.printStackTrace() + null + } + return image +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/SharedPreferencesExt.kt b/app/src/main/java/com/skyd/imomoe/ext/SharedPreferencesExt.kt new file mode 100644 index 00000000..70aed154 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/SharedPreferencesExt.kt @@ -0,0 +1,36 @@ +package com.skyd.imomoe.ext + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.skyd.imomoe.appContext + + +fun Context.sharedPreferences(name: String = "App"): SharedPreferences = + getSharedPreferences(name, Context.MODE_PRIVATE) + +fun SharedPreferences.editor(editorBuilder: SharedPreferences.Editor.() -> Unit) = + edit().apply(editorBuilder).apply() + +fun SharedPreferences.editor2(editorBuilder: SharedPreferences.Editor.() -> Unit) = + edit().apply(editorBuilder).commit() + +fun sharedPreferences(name: String = "App"): SharedPreferences = appContext.sharedPreferences(name) + +@RequiresApi(Build.VERSION_CODES.M) +fun secretSharedPreferences(name: String = "Secret"): SharedPreferences { + val masterKey = MasterKey.Builder(appContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + return EncryptedSharedPreferences.create( + appContext, + name, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) +} diff --git a/app/src/main/java/com/skyd/imomoe/ext/SnackbarExt.kt b/app/src/main/java/com/skyd/imomoe/ext/SnackbarExt.kt new file mode 100644 index 00000000..111fcf60 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/SnackbarExt.kt @@ -0,0 +1,47 @@ +package com.skyd.imomoe.ext + +import android.app.Activity +import android.content.res.ColorStateList +import android.view.View +import com.google.android.material.snackbar.Snackbar +import com.skyd.imomoe.R + +fun Activity.showSnackbar( + text: CharSequence, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: CharSequence? = getString(R.string.close), + actionCallback: (() -> Unit)? = null, + backgroundTintList: ColorStateList? = null, + textColor: ColorStateList? = null, + actionTextColor: ColorStateList? = null +) { + findViewById(android.R.id.content).showSnackbar( + text = text, + duration = duration, + actionText = actionText, + actionCallback = actionCallback, + backgroundTintList = backgroundTintList, + textColor = textColor, + actionTextColor = actionTextColor + ) +} + + +fun View.showSnackbar( + text: CharSequence, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: CharSequence? = context.getString(R.string.close), + actionCallback: (() -> Unit)? = null, + backgroundTintList: ColorStateList? = null, + textColor: ColorStateList? = null, + actionTextColor: ColorStateList? = null +) { + Snackbar.make(this, text, duration) + .setAction(actionText) { actionCallback?.invoke() } + .apply { + if (backgroundTintList != null) setBackgroundTintList(backgroundTintList) + if (textColor != null) setTextColor(textColor) + if (actionTextColor != null) setActionTextColor(textColor) + } + .show() +} diff --git a/app/src/main/java/com/skyd/imomoe/ext/StringExt.kt b/app/src/main/java/com/skyd/imomoe/ext/StringExt.kt new file mode 100644 index 00000000..adf4c509 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/StringExt.kt @@ -0,0 +1,39 @@ +package com.skyd.imomoe.ext + +import android.annotation.SuppressLint +import android.text.Html +import android.text.Spanned +import com.skyd.imomoe.BuildConfig + +/** + * 屏蔽带有某些关键字的弹幕 + * + * @return 若屏蔽此字符串,则返回true,否则false + */ +fun String.shield(): Boolean { + BuildConfig.SHIELD_TEXT.forEach { + if (this.contains(it, true)) return true + } + return false +} + +fun String.toHtml(@SuppressLint("InlinedApi") flag: Int = Html.FROM_HTML_MODE_LEGACY): Spanned { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Html.fromHtml(this, flag) + } else { + @Suppress("DEPRECATION") + Html.fromHtml(this) + } +} + +fun String.isUrl(): Boolean { + return Regex("\\b(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]") + .matches(this) +} + +fun String.containIn(array: Array): Boolean { + array.forEach { + if (this.contains(it)) return true + } + return false +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/ToolbarExt.kt b/app/src/main/java/com/skyd/imomoe/ext/ToolbarExt.kt new file mode 100644 index 00000000..e5a1a399 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/ToolbarExt.kt @@ -0,0 +1,27 @@ +package com.skyd.imomoe.ext + +import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import com.google.android.material.appbar.AppBarLayout + +/** + * 防止键盘弹出时fitsSystemWindows会将键盘高度也加到Toolbar上 + */ +fun Toolbar.fixKeyboardFitsSystemWindows() { + ViewCompat.setOnApplyWindowInsetsListener(this) { v, ins -> + v.updatePadding(top = ins.getInsets(WindowInsetsCompat.Type.statusBars()).top, bottom = 0) + ins + } +} + +/** + * 透明状态栏界面折叠下,隐藏Toolbar + */ +fun AppBarLayout.hideToolbarWhenCollapsed(v: View) { + addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> + v.alpha = 1 + verticalOffset / appBarLayout.totalScrollRange.toFloat() + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/UriExt.kt b/app/src/main/java/com/skyd/imomoe/ext/UriExt.kt new file mode 100644 index 00000000..96a93b1d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/UriExt.kt @@ -0,0 +1,12 @@ +package com.skyd.imomoe.ext + +import android.net.Uri +import com.skyd.imomoe.appContext +import java.io.File +import java.io.FileOutputStream + + +fun Uri.copyTo(target: File): File { + appContext.contentResolver.openInputStream(this)!!.copyTo(FileOutputStream(target)) + return target +} diff --git a/app/src/main/java/com/skyd/imomoe/ext/ViewExt.kt b/app/src/main/java/com/skyd/imomoe/ext/ViewExt.kt new file mode 100644 index 00000000..40d69662 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/ViewExt.kt @@ -0,0 +1,106 @@ +package com.skyd.imomoe.ext + +import android.app.Activity +import android.content.Context +import android.graphics.Rect +import android.view.View +import android.view.animation.AlphaAnimation +import android.view.inputmethod.InputMethodManager +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import com.skyd.imomoe.appContext + + +fun View.enable() { + if (isEnabled) return + isEnabled = true +} + +fun View.disable() { + if (!isEnabled) return + isEnabled = false +} + +fun View.gone(animate: Boolean = false, dur: Long = 500L) { + if (visibility == View.GONE) return + if (animate) startAnimation(AlphaAnimation(1f, 0f).apply { duration = dur }) + visibility = View.GONE +} + +fun View.visible(animate: Boolean = false, dur: Long = 500L) { + if (visibility == View.VISIBLE) return + visibility = View.VISIBLE + if (animate) startAnimation(AlphaAnimation(0f, 1f).apply { duration = dur }) +} + +fun View.invisible(animate: Boolean = false, dur: Long = 500L) { + if (visibility == View.INVISIBLE) return + visibility = View.INVISIBLE + if (animate) startAnimation(AlphaAnimation(0f, 1f).apply { duration = dur }) +} + +fun View.clickScale(scale: Float = 0.75f, duration: Long = 100) { + animate().scaleX(scale).scaleY(scale).setDuration(duration) + .withEndAction { + animate().scaleX(1f).scaleY(1f).setDuration(duration).start() + }.start() +} + +val View.activity: Activity + get() = context.activity + +fun View.showKeyboard() { + isFocusable = true + isFocusableInTouchMode = true + requestFocus() + val inputManager = + appContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.showSoftInput(this, 0) +} + +fun View.hideKeyboard() { + val inputManager = + appContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.hideSoftInputFromWindow(this.windowToken, 0) +} + +/** + * 判断View和给定的Rect是否重叠(边和点不计入) + * @return true if overlap + */ +fun View.overlap(rect: Rect): Boolean { + val location = IntArray(2) + getLocationOnScreen(location) + val left = location[0] + val right = location[0] + width + val top = location[1] + val bottom = location[1] + height + return !(left > rect.right || right < rect.left || top > rect.bottom || bottom < rect.top) +} + +fun View.addFitsSystemWindows( + top: Boolean = false, + bottom: Boolean = false, + left: Boolean = false, + right: Boolean = false +) { + ViewCompat.setOnApplyWindowInsetsListener(this) { v, ins -> + var newPaddingTop = v.paddingTop + var newPaddingBottom = v.paddingBottom + var newPaddingLeft = v.paddingLeft + var newPaddingRight = v.paddingRight + if (top) newPaddingTop = ins.getInsets(WindowInsetsCompat.Type.statusBars()).top + if (bottom) newPaddingBottom = ins.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom + if (left) newPaddingLeft = ins.getInsets(WindowInsetsCompat.Type.displayCutout()).left + if (right) newPaddingRight = ins.getInsets(WindowInsetsCompat.Type.navigationBars()).right + + v.updatePadding( + top = newPaddingTop, + bottom = newPaddingBottom, + left = newPaddingLeft, + right = newPaddingRight + ) + ins + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/ViewModelExt.kt b/app/src/main/java/com/skyd/imomoe/ext/ViewModelExt.kt new file mode 100644 index 00000000..dfad8126 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/ViewModelExt.kt @@ -0,0 +1,61 @@ +package com.skyd.imomoe.ext + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.skyd.imomoe.state.DataState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +fun ViewModel.request( + request: suspend () -> T, + success: ((T) -> Unit)? = null, + error: ((Throwable) -> Unit)? = null, + // request结束后回调,不论成功还是失败,在success和error后调用。 + // 注意,finish与finally不同,在runCatching中return会导致finish不执行 + finish: (() -> Unit)? = null, + coroutineContext: CoroutineContext = Dispatchers.IO +): Job { + return viewModelScope.launch(coroutineContext) { + runCatching { + request.invoke() + }.onSuccess { + success?.invoke(it) + }.onFailure { + it.printStackTrace() + error?.invoke(it) + }.also { + finish?.invoke() + } + } +} + +fun MutableStateFlow>>.tryEmitLoadMore( + oldData: DataState>, + newData: List +) { + tryEmit( + DataState.Success( + oldData.readOrNull() + .orEmpty() + .toMutableList() + .apply { addAll(newData) } + ) + ) +} + +fun MutableStateFlow>.tryEmitError( + oldData: DataState, + errorMessage: String? = "" +) { + tryEmit( + // 之前有旧数据,则不变化 + if (oldData.readOrNull() == null) { + DataState.Error(errorMessage.orEmpty()) + } else { + oldData + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/ext/theme/DarkExt.kt b/app/src/main/java/com/skyd/imomoe/ext/theme/DarkExt.kt new file mode 100644 index 00000000..17204431 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/theme/DarkExt.kt @@ -0,0 +1,97 @@ +package com.skyd.imomoe.ext.theme + +import android.app.Activity +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.sharedPreferences +import com.skyd.imomoe.ext.showListDialog + +class DarkMode(val name: String, val value: Int) : CharSequence { + override val length: Int + get() = name.length + + override fun get(index: Int): Char = name[index] + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return name.subSequence(startIndex, endIndex) + } + + override fun toString(): String = name + + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + is String -> other == name + is DarkMode -> other.name == this.name && other.value == this.value + else -> false + } + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + value + return result + } +} + +private infix fun String.to(that: Int): DarkMode = DarkMode(this, that) + +val darkModeList: List = mutableListOf( + appContext.getString(R.string.dark_ext_dark_yes) to AppCompatDelegate.MODE_NIGHT_YES, + appContext.getString(R.string.dark_ext_dark_no) to AppCompatDelegate.MODE_NIGHT_NO +).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) add( + 0, + appContext.getString(R.string.dark_ext_dark_follow_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + ) +} + +var darkMode: Int = 0 + get() { + return if (field != AppCompatDelegate.MODE_NIGHT_YES && + field != AppCompatDelegate.MODE_NIGHT_NO && + field != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + ) throw IllegalStateException("call initDarkMode() in app onCreate") + else field + } + set(value) { + if (value != AppCompatDelegate.MODE_NIGHT_YES && + value != AppCompatDelegate.MODE_NIGHT_NO && + value != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + ) { + throw IllegalArgumentException("darkMode value invalid!!!") + } + sharedPreferences().editor { putInt("darkMode", value) } + AppCompatDelegate.setDefaultNightMode(value) + field = value + } + +fun initDarkMode() { + darkMode = sharedPreferences() + .getInt( + "darkMode", if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } else { + AppCompatDelegate.MODE_NIGHT_NO + } + ) + .also { AppCompatDelegate.setDefaultNightMode(it) } +} + +fun Activity.selectDarkMode() { + var initialSelection = 0 + darkModeList.forEachIndexed { index, s -> + if (s.value == darkMode) initialSelection = index + } + showListDialog( + title = getString(R.string.dark_ext_select_dark_mode), + items = darkModeList, + checkedItem = initialSelection, + onNegative = { dialog, _ -> dialog.dismiss() } + ) { _, _, itemIndex -> + darkMode = darkModeList[itemIndex].value + } +} diff --git a/app/src/main/java/com/skyd/imomoe/ext/theme/ThemeExt.kt b/app/src/main/java/com/skyd/imomoe/ext/theme/ThemeExt.kt new file mode 100644 index 00000000..5d8e0bf0 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/ext/theme/ThemeExt.kt @@ -0,0 +1,87 @@ +package com.skyd.imomoe.ext.theme + +import android.content.Context +import android.content.res.Configuration +import android.content.res.TypedArray +import android.graphics.Color +import android.os.Build +import android.util.TypedValue +import android.view.View +import android.view.Window +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.skyd.imomoe.R +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.recreateAllBaseActivity +import com.skyd.imomoe.ext.sharedPreferences + + +private val map = hashMapOf( + "Pink" to R.style.Theme_Anime_Pink, + "Dynamic" to R.style.Theme_Anime_Dynamic, + "Blue" to R.style.Theme_Anime_Blue, + "Lemon" to R.style.Theme_Anime_Lemon, + "Purple" to R.style.Theme_Anime_Purple, + "Green" to R.style.Theme_Anime_Green, +) + +private fun getKeyByValue(v: Int): String? { + for (key in map.keys) { + if (map[key] == v) { + return key + } + } + return null +} + +var appThemeRes: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // getOrDefault method was added in API level 24 + map.getOrDefault( + sharedPreferences().getString("themeRes", null), + R.style.Theme_Anime_Pink + ) +} else { + val v = sharedPreferences().getString("themeRes", null) + map[v] ?: R.style.Theme_Anime_Pink +} + set(value) { + sharedPreferences().editor { + putString("themeRes", getKeyByValue(value)) + } + field = value + recreateAllBaseActivity.tryEmit(Unit) + } + +fun Context.getAttrColor(attr: Int): Int { + val typedValue = TypedValue() + val typedArray: TypedArray = obtainStyledAttributes(typedValue.data, intArrayOf(attr)) + val color = typedArray.getColor(0, 0) + typedArray.recycle() + return color +} + +/** + * 设置状态栏和导航栏透明 + * @param root 根布局,一般传入mBinding.root,或者是window.decorView.findViewById(android.R.id.content) + * @param darkFont 状态栏颜色是不是深色,传入null代表不更改默认颜色 + */ +fun Window.transparentSystemBar( + root: View, + darkFont: Boolean? = context.resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES +) { + WindowCompat.setDecorFitsSystemWindows(this, false) + statusBarColor = Color.TRANSPARENT + navigationBarColor = Color.TRANSPARENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + navigationBarDividerColor = Color.TRANSPARENT + } + + darkFont?.let { + // 状态栏和导航栏字体颜色 + WindowInsetsControllerCompat(this, root).let { controller -> + controller.isAppearanceLightStatusBars = it + controller.isAppearanceLightNavigationBars = it + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/AppUpdateModel.kt b/app/src/main/java/com/skyd/imomoe/model/AppUpdateModel.kt index 017ba9a5..18138e1e 100644 --- a/app/src/main/java/com/skyd/imomoe/model/AppUpdateModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/AppUpdateModel.kt @@ -1,22 +1,22 @@ package com.skyd.imomoe.model -import android.util.Log -import androidx.lifecycle.MutableLiveData -import com.skyd.imomoe.App +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.UpdateBean +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.sharedPreferences import com.skyd.imomoe.net.RetrofitManager import com.skyd.imomoe.net.service.UpdateService -import com.skyd.imomoe.util.Util.isNewVersion -import com.skyd.imomoe.util.editor -import com.skyd.imomoe.util.sharedPreferences +import com.skyd.imomoe.util.Util.isNewVersionByVersionCode +import com.skyd.imomoe.util.logD import com.skyd.imomoe.util.update.AppUpdateHelper import com.skyd.imomoe.util.update.AppUpdateStatus +import kotlinx.coroutines.flow.MutableStateFlow import retrofit2.Call import retrofit2.Callback import retrofit2.Response object AppUpdateModel { - val status: MutableLiveData = MutableLiveData() + val status: MutableStateFlow = MutableStateFlow(AppUpdateStatus.UNCHECK) var updateBean: UpdateBean? = null private set @@ -24,22 +24,18 @@ object AppUpdateModel { set(value) { if (value == field) return field = if (value in AppUpdateHelper.serverName.indices) { - App.context.sharedPreferences("update").editor { + sharedPreferences("update").editor { putInt(AppUpdateHelper.UPDATE_SERVER_SP_KEY, value) } value } else { 0 } - mldUpdateServer.postValue(field) } - var mldUpdateServer: MutableLiveData = MutableLiveData() - init { - status.value = AppUpdateStatus.UNCHECK updateServer = - App.context.sharedPreferences("update").getInt(AppUpdateHelper.UPDATE_SERVER_SP_KEY, 0) + appContext.sharedPreferences("update").getInt(AppUpdateHelper.UPDATE_SERVER_SP_KEY, 0) } fun checkUpdate() { @@ -47,24 +43,25 @@ object AppUpdateModel { return } status.value = AppUpdateStatus.CHECKING - val request = RetrofitManager.instance.create(UpdateService::class.java) - val check = request?.checkUpdate() - check?.enqueue(object : Callback { + val request = RetrofitManager.get().create(UpdateService::class.java) + val check = request.checkUpdate() + check.enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { - Log.d("checkUpdate", t.message ?: "") - status.postValue(AppUpdateStatus.ERROR) + logD("checkUpdate", t.message ?: "") + status.tryEmit(AppUpdateStatus.ERROR) } override fun onResponse(call: Call, response: Response) { updateBean = response.body() updateBean?.let { - status.postValue( - if (isNewVersion(updateBean?.tagName ?: "0")) AppUpdateStatus.DATED + status.tryEmit( + if (isNewVersionByVersionCode(updateBean?.tagName ?: "0")) + AppUpdateStatus.DATED else AppUpdateStatus.VALID ) return } - status.postValue(AppUpdateStatus.ERROR) + status.tryEmit(AppUpdateStatus.ERROR) } }) } diff --git a/app/src/main/java/com/skyd/imomoe/model/DataSourceManager.kt b/app/src/main/java/com/skyd/imomoe/model/DataSourceManager.kt index be95d866..4c735f29 100644 --- a/app/src/main/java/com/skyd/imomoe/model/DataSourceManager.kt +++ b/app/src/main/java/com/skyd/imomoe/model/DataSourceManager.kt @@ -1,41 +1,92 @@ package com.skyd.imomoe.model -import android.util.Log import android.util.LruCache -import com.skyd.imomoe.App +import android.widget.Toast import com.skyd.imomoe.BuildConfig -import com.skyd.imomoe.model.interfaces.IConst -import com.skyd.imomoe.model.interfaces.IRouteProcessor -import com.skyd.imomoe.model.interfaces.IUtil -import com.skyd.imomoe.util.editor -import com.skyd.imomoe.util.sharedPreferences +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.DataSource1Bean +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.model.interfaces.* +import com.skyd.imomoe.util.logE +import com.skyd.imomoe.util.showToast import dalvik.system.DexClassLoader import java.io.File +import java.util.jar.JarFile object DataSourceManager { - var useCustomDataSource: Boolean + + /** + * 测试模式,仅供测试使用,设置为true并在APP内设置默认数据源后 + * 即会使用com.skyd.imomoe.model.impls.custom目录下的数据源 + */ + val testMode: Boolean = false + + const val DEFAULT_DATA_SOURCE = "" + + // 数据源文件名,例如:CustomDataSource1.ads + var dataSourceFileName: String = + sharedPreferences().getString("dataSourceName", DEFAULT_DATA_SOURCE) + ?: DEFAULT_DATA_SOURCE get() { - return App.context.sharedPreferences().getBoolean("customDataSource", false) - } - set(value) { - App.context.sharedPreferences().editor { putBoolean("customDataSource", value) } + return if (field.isBlank() && sharedPreferences() + .getBoolean("customDataSource", false) + ) { + sharedPreferences().editor { putBoolean("customDataSource", false) } + "CustomDataSource.jar" + } else field } + private set + + fun setDataSourceName(value: String) { + sharedPreferences().editor { putString("dataSourceName", value) } + } + + fun setDataSourceNameSynchronously(value: String) { + sharedPreferences().editor2 { putString("dataSourceName", value) } + } + + private var showInterfaceVersionTip: Boolean = false // 第一个是传入的接口,第二个是实现类 private val cache: LruCache, Class<*>> = LruCache(10) private val singletonCache: LruCache, Any> = LruCache(5) + var customDataSourceInfo: HashMap? = null + private set + get() { + if (dataSourceFileName == DEFAULT_DATA_SOURCE) return null + if (field == null) { + field = gerJarInfo(getJarPath()) + } + return field + } + + fun gerJarInfo(jarPath: String): HashMap { + val map: HashMap = HashMap() + runCatching { + val jar = JarFile(jarPath) + jar.getInputStream(jar.getEntry("CustomInfo")) + .string().split("\n").forEach { + it.split("=").let { kv -> + if (kv.size == 2) map[kv[0].trim()] = kv[1].trim() + } + } + }.onFailure { + it.printStackTrace() + } + return map + } + + fun getJarPath(): String = "${getJarDirectory()}/${dataSourceFileName}" - fun getJarPath(): String { -// return "${Environment.getExternalStorageDirectory()}/Download/DataSourceJar/CustomDataSource.jar" - return App.context.getExternalFilesDir(null) - .toString() + "/DataSourceJar/CustomDataSource.jar" + fun getJarDirectory(): String { + return "${appContext.getExternalFilesDir(null).toString()}/DataSourceJar" } fun getBinaryName(clazz: Class): String { return "com.skyd.imomoe.model.impls.custom.Custom${ - clazz.getDeclaredField("implName") - .get(null) + clazz.getDeclaredField("implName").get(null) }" } @@ -48,12 +99,12 @@ object DataSourceManager { } } - fun getRouterProcessor(): IRouteProcessor? { - singletonCache[IRouteProcessor::class.java].let { - if (it != null && it is IRouteProcessor) return it + fun getRouter(): IRouter? { + singletonCache[IRouter::class.java].let { + if (it != null && it is IRouter) return it } - return create(IRouteProcessor::class.java).apply { - if (this != null) singletonCache.put(IRouteProcessor::class.java, this) + return create(IRouter::class.java).apply { + if (this != null) singletonCache.put(IRouter::class.java, this) } } @@ -72,15 +123,34 @@ object DataSourceManager { fun clearCache() { cache.evictAll() singletonCache.evictAll() + showInterfaceVersionTip = false + customDataSourceInfo = null } @Suppress("UNCHECKED_CAST") fun create(clazz: Class): T? { // 如果不使用自定义数据,直接返回null - if (!useCustomDataSource) return null + if (dataSourceFileName == DEFAULT_DATA_SOURCE && !testMode) return null + if (interfaceVersion != customDataSourceInfo?.get("interfaceVersion") && + !coexistentInterfaceVersions.contains(customDataSourceInfo?.get("interfaceVersion")) && + !testMode + ) { + if (!showInterfaceVersionTip) appContext.getString( + R.string.data_source_interface_version_not_match, + customDataSourceInfo?.get("interfaceVersion"), + interfaceVersion + ).showToast(Toast.LENGTH_LONG) + showInterfaceVersionTip = true + return null + } cache[clazz]?.let { return it.newInstance() as T } + return innerCreate(clazz) + } + + @Suppress("UNCHECKED_CAST") + private fun innerCreate(clazz: Class): T? { /** * 参数1 jarPath:待加载的jar文件路径,注意权限。jar必须是含dex的jar(dx --dex --output=dest.jar source.jar) * 参数2 optimizedDirectory:解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写 @@ -89,39 +159,81 @@ object DataSourceManager { */ val jarFile = File(getJarPath()) if (!jarFile.exists() || !jarFile.isFile) { - Log.e("DataSourceManager", "useCustomDataSource but jar doesn't exist") + logE("DataSourceManager", "useCustomDataSource but jar doesn't exist") if (!BuildConfig.DEBUG) return null } val optimizedDirectory = - File(App.context.getExternalFilesDir(null).toString() + "/DataSourceDex") + File(appContext.getExternalFilesDir(null).toString() + "/DataSourceDex") if (!optimizedDirectory.exists() && !optimizedDirectory.mkdirs()) { - Log.e("DataSourceManager", "can't create optimizedDirectory") + logE("DataSourceManager", "can't create optimizedDirectory") return null } val classLoader = - DexClassLoader(jarFile.path, optimizedDirectory.path, null, App.context.classLoader) + DexClassLoader(jarFile.path, optimizedDirectory.path, null, appContext.classLoader) var o: T? = null var clz: Class<*>? = null try { // 该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象 clz = classLoader.loadClass(getBinaryName(clazz)) o = clz.newInstance() as T - } catch (e: Exception) { + } catch (e: Throwable) { e.printStackTrace() -// debug { -// o = getTestClass(clazz) -// } + if (testMode) { + o = getTestClass(clazz) + } } if (clz != null) cache.put(clazz, clz) return o } -// private fun getTestClass(clazz: Class): T? { -// var o: T? = null -// TestClass.classMap[clazz.simpleName].let { -// if (it != null) o = it.newInstance() as T -// } -// return o -// } + @Suppress("UNCHECKED_CAST") + private fun getTestClass(clazz: Class): T? { + var o: T? = null + classMap[clazz.simpleName].let { + if (it != null) { + o = Class + .forName("com.skyd.imomoe.model.impls.$it") + .newInstance() as T + } + } + return o + } + val classMap = hashMapOf( + "IAnimeDetailModel" to "CustomAnimeDetailModel", + "IAnimeShowModel" to "CustomAnimeShowModel", + "IClassifyModel" to "CustomClassifyModel", + "IEverydayAnimeModel" to "CustomEverydayAnimeModel", + "IHomeModel" to "CustomHomeModel", + "IMonthAnimeModel" to "CustomMonthAnimeModel", + "IPlayModel" to "CustomPlayModel", + "IRankModel" to "CustomRankModel", + "ISearchModel" to "CustomSearchModel", + "IConst" to "CustomConst", + "IUtil" to "CustomUtil", + "IRouter" to "CustomRouter", + "IRankListModel" to "CustomRankListModel", + "IEverydayAnimeWidgetModel" to "CustomEverydayAnimeWidgetModel" + ) + + fun getDataSourceList(directoryPath: String): List { + val directory = File(directoryPath) + return if (!directory.isDirectory) { + emptyList() + } else { + val jarList = directory.listFiles { _, name -> + name.endsWith(".ads", true) || + name.endsWith(".jar", true) + } + jarList.orEmpty().map { + val jarInfo = gerJarInfo(it.path) + DataSource1Bean( + route = "", file = it, selected = it.name == dataSourceFileName, + name = jarInfo["name"] ?: it.name.substringBeforeLast("."), + versionCode = jarInfo["versionCode"]?.toIntOrNull(), + versionName = jarInfo["versionName"] + ) + }.toList() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/AnimeDetailModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/AnimeDetailModel.kt index 0e378f73..fb172810 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/AnimeDetailModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/AnimeDetailModel.kt @@ -1,173 +1,12 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.bean.* -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil.parseBotit +import com.skyd.imomoe.bean.ImageBean import com.skyd.imomoe.model.interfaces.IAnimeDetailModel -import org.jsoup.select.Elements class AnimeDetailModel : IAnimeDetailModel { override suspend fun getAnimeDetailData( partUrl: String - ): Triple> { - val animeDetailList: ArrayList = ArrayList() - val cover = ImageBean("", "", "", "") - var title = "" - val url = Api.MAIN_URL + partUrl - val document = JsoupUtil.getDocument(url) - //番剧头部信息 - val area: Elements = document.getElementsByClass("area") - for (i in area.indices) { - val areaChildren = area[i].children() - for (j in areaChildren.indices) { - when (areaChildren[j].className()) { - "fire l" -> { - var alias = "" - var info = "" - var year = "" - var index = "" - var animeArea = "" - val animeType: MutableList = ArrayList() - val tag: MutableList = ArrayList() - - val fireLChildren = - areaChildren[j].select("[class=fire l]")[0].children() - for (k in fireLChildren.indices) { - when (fireLChildren[k].className()) { - "thumb l" -> { - cover.url = fireLChildren[k] - .select("img").attr("src") - cover.referer = url - } - "rate r" -> { - val rateR = fireLChildren[k] - title = rateR.select("h1").text() - val sinfo: Elements = rateR.select("[class=sinfo]") - val span: Elements = sinfo.select("span") - val p: Elements = sinfo.select("p") - if (p.size == 1) { - alias = p[0].text() - } else if (p.size == 2) { - alias = p[0].text() - info = p[1].text() - } - year = span[0].text() - animeArea = span[1].select("a").text() - index = span[3].select("a").text() - val typeElements: Elements = span[2].select("a") - for (l in typeElements.indices) { - animeType.add( - AnimeTypeBean( - "", - typeElements[l].attr("href"), - Api.MAIN_URL + typeElements[l].attr("href"), - typeElements[l].text() - ) - ) - } - val tagElements: Elements = span[4].select("a") - for (l in tagElements.indices) { - tag.add( - AnimeTypeBean( - "", - tagElements[l].attr("href"), - Api.MAIN_URL + tagElements[l].attr("href"), - tagElements[l].text() - ) - ) - } - } - "tabs", "tabs noshow" -> { //播放列表+header - animeDetailList.add( - AnimeDetailBean( - Const.ViewHolderTypeString.HEADER_1, "", - fireLChildren[k].select("[class=menu0]") - .select("li").text(), - "", - null - ) - ) - - animeDetailList.add( - AnimeDetailBean( - Const.ViewHolderTypeString.HORIZONTAL_RECYCLER_VIEW_1, - "", - "", - "", - ParseHtmlUtil.parseMovurls( - fireLChildren[k].select("[class=main0]") - .select("[class=movurl]")[0] - ) - ) - ) - } - "botit" -> { //其它header - animeDetailList.add( - AnimeDetailBean( - Const.ViewHolderTypeString.HEADER_1, "", - parseBotit(fireLChildren[k]), - "", - null - ) - ) - } - "dtit" -> { //其它header - animeDetailList.add( - AnimeDetailBean( - Const.ViewHolderTypeString.HEADER_1, "", - ParseHtmlUtil.parseDtit(fireLChildren[k]), - "", - null - ) - ) - } - "info" -> { //动漫介绍 - animeDetailList.add( - AnimeDetailBean( - Const.ViewHolderTypeString.ANIME_DESCRIBE_1, "", - "", - fireLChildren[k] - .select("[class=info]").text(), - null - ) - ) - } - "img" -> { //系列动漫推荐 - animeDetailList.addAll( - ParseHtmlUtil.parseImg(fireLChildren[k], url) - ) - } - } - } - val animeInfoBean = AnimeInfoBean( - "", - "", - title, - ImageBean("", "", cover.url, url), - alias, - animeArea, - year, - index, - animeType, - tag, - info - ) - animeDetailList.add( - 0, - AnimeDetailBean( - Const.ViewHolderTypeString.ANIME_INFO_1, "", - "", - "", - headerInfo = animeInfoBean - ) - ) - } - } - } - } - return Triple(cover, title, animeDetailList) + ): Triple> { + return Triple(ImageBean("", "", ""), "", ArrayList()) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/AnimeShowModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/AnimeShowModel.kt index ac7a13a4..41f272e3 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/AnimeShowModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/AnimeShowModel.kt @@ -1,133 +1,187 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.bean.AnimeShowBean -import com.skyd.imomoe.bean.IAnimeShowBean -import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.* import com.skyd.imomoe.model.interfaces.IAnimeShowModel -import org.jsoup.select.Elements +import com.skyd.imomoe.route.Router.buildRouteUri +import com.skyd.imomoe.route.processor.ConfigDataSourceActivityProcessor +import com.skyd.imomoe.route.processor.OpenBrowserProcessor class AnimeShowModel : IAnimeShowModel { override suspend fun getAnimeShowData( partUrl: String - ): Pair, PageNumberBean?> { - val url = Api.MAIN_URL + partUrl - var pageNumberBean: PageNumberBean? = null - val document = JsoupUtil.getDocument(url) - val animeShowList: ArrayList = ArrayList() - //banner - val foucsBgElements: Elements = document.getElementsByClass("foucs bg") - for (i in foucsBgElements.indices) { - val foucsBgChildren: Elements = foucsBgElements[i].children() - for (j in foucsBgChildren.indices) { - when (foucsBgChildren[j].className()) { - "hero-wrap" -> { - animeShowList.add( - AnimeShowBean( - Const.ViewHolderTypeString.BANNER_1, "", - "", "", "", null, "", - ParseHtmlUtil.parseHeroWrap( - foucsBgChildren[j], - url + ): Pair, PageNumberBean?> { + return if (partUrl == "/market") { + Pair( + arrayListOf( + Header1Bean( + route = "", + title = "使用方法" + ), + AnimeCover3Bean( + route = ConfigDataSourceActivityProcessor.route.buildRouteUri { + appendQueryParameter("selectPageIndex", "1") + }.toString(), + url = "", + title = "点击这里进入数据源商店", + cover = ImageBean(url = R.drawable.ic_new_use_data_source_step_to_market.toString()), + describe = "数据源商店需要访问GitHub,因此网络连接可能会过慢甚至无法访问,建议使用科学方法或配置URL前缀转换", + animeType = listOf(AnimeTypeBean(title = "步骤1")) + ), + AnimeCover1Bean( + route = "", + url = "", + title = "在商店页面点击下载按钮", + cover = ImageBean(url = R.drawable.ic_new_use_data_source_step_click_download.toString()), + episode = "步骤2" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "等待下载完成", + cover = ImageBean(url = R.drawable.ic_new_use_data_source_step_downloaded.toString()), + episode = "步骤3" + ), + AnimeCover1Bean( + route = ConfigDataSourceActivityProcessor.route.buildRouteUri { + appendQueryParameter("selectPageIndex", "0") + }.toString(), + url = "", + title = "滑动到左侧页面,或点击这里", + cover = ImageBean(url = R.drawable.ic_new_use_data_source_step_left_page.toString()), + episode = "步骤4" + ), + AnimeCover1Bean( + route = ConfigDataSourceActivityProcessor.route.buildRouteUri { + appendQueryParameter("selectPageIndex", "0") + }.toString(), + url = "", + title = "点击要使用的数据源", + cover = ImageBean(url = R.drawable.ic_new_use_data_source_step_use.toString()), + episode = "步骤5" + ), + ), null + ) + } else Pair( + arrayListOf( + Banner1Bean( + "", + arrayListOf( + AnimeCover6Bean( + route = OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter( + "url", + "https://github.com/SkyD666/Imomoe/tree/master/doc/customdatasource/README.md" ) - ) + }.toString(), + title = "请在设置页面选择自定义数据源ads包,以便使用APP", + cover = ImageBean(), + describe = "具体使用方法请点击此处" ) - } - } - } - } - //area - var area: Elements = document.getElementsByClass("area") - if (partUrl == "/") //首页,有右边栏 - area = document.getElementsByClass("area").select("[class=firs l]") - for (i in area.indices) { - val elements: Elements = area[i].children() - for (j in elements.indices) { - when (elements[j].className()) { - "dtit" -> { - val a = elements[j].select("h2").select("a") - if (a.size == 0) { //只有一个标题 - animeShowList.add( - AnimeShowBean( - Const.ViewHolderTypeString.HEADER_1, - "", - "", - elements[j].select("h2").text(), - "", - null, - "" - ) - ) - } else { //有右侧“更多” - animeShowList.add( - AnimeShowBean( - Const.ViewHolderTypeString.HEADER_1, - a.attr("href"), - Api.MAIN_URL + a.attr("href"), - a.text(), - elements[j].select("span").select("a").text(), - null, - "" - ) - ) - } - } - "img", "imgs" -> { - animeShowList.addAll( - ParseHtmlUtil.parseImg( - elements[j], - url - ) - ) - } - "fire l" -> { //右侧前半tab内容 - val firsLChildren = elements[j].children() - for (k in firsLChildren.indices) { - when (firsLChildren[k].className()) { - "lpic" -> { - animeShowList.addAll( - ParseHtmlUtil.parseLpic( - firsLChildren[k], - url - ) - ) - } - "pages" -> { - pageNumberBean = - ParseHtmlUtil.parseNextPages( - firsLChildren[k] - ) - } - } - } - } - "dnews" -> { //右侧后半tab内容,cover4 - animeShowList.addAll( - ParseHtmlUtil.parseDnews( - elements[j], - url - ) - ) - } - "topli" -> { //右侧后半tab内容,cover5 - animeShowList.addAll( - ParseHtmlUtil.parseTopli( - elements[j] - ) - ) - } - "pages" -> { - pageNumberBean = - ParseHtmlUtil.parseNextPages( - elements[j] - ) - } - } - } - } - return Pair(animeShowList, pageNumberBean) + ) + ), + + // 如何导入并使用自定义数据源? + Header1Bean( + route = "", + title = "如何导入并使用自定义数据源?" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "找到ads数据源文件", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_ads_files.toString()), + episode = "步骤1" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "用樱花动漫打开ads文件", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_open_ads.toString()), + episode = "步骤2" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "确认导入数据源", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_import_dialog.toString()), + episode = "步骤3" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "点击已有的数据,选择重启APP", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_set.toString()), + episode = "步骤4" + ), + + // 如何删除自定义数据源? + Header1Bean( + route = "", + title = "如何删除自定义数据源?" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "长按要删除的项目", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_long_click.toString()), + episode = "步骤1" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "点击确定,以删除", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_delete.toString()), + episode = "步骤2" + ), + + // 如何恢复默认数据源? + Header1Bean( + route = "", + title = "如何恢复默认数据源?" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "点击右上角恢复按钮", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_reset_button.toString()), + episode = "步骤1" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "点击重启,以恢复", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_reset_dialog.toString()), + episode = "步骤2" + ), + + // 如何进入自定义数据源界面? + Header1Bean( + route = "", + title = "如何进入自定义数据源界面?" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "点击更多", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_more.toString()), + episode = "步骤1" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "点击设置", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_setting.toString()), + episode = "步骤2" + ), + AnimeCover1Bean( + route = "", + url = "", + title = "点击自定义数据源", + cover = ImageBean(url = R.drawable.ic_use_data_source_step_custom.toString()), + episode = "步骤3" + ) + ), null + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/ClassifyModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/ClassifyModel.kt index 5ebd2552..aaa7832d 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/ClassifyModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/ClassifyModel.kt @@ -1,86 +1,20 @@ package com.skyd.imomoe.model.impls import android.app.Activity -import android.content.Intent -import com.skyd.imomoe.App -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.ClassifyBean import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.config.UnknownActionUrl -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil import com.skyd.imomoe.model.interfaces.IClassifyModel -import com.skyd.imomoe.view.activity.ClassifyActivity -import org.jsoup.select.Elements class ClassifyModel : IClassifyModel { - override suspend fun getClassifyData(partUrl: String): Pair, PageNumberBean?> { - val classifyList: ArrayList = ArrayList() - var pageNumberBean: PageNumberBean? = null - val url = Api.MAIN_URL + partUrl - val document = JsoupUtil.getDocument(url) - val areaElements: Elements = document.getElementsByClass("area") - for (i in areaElements.indices) { - val areaChildren: Elements = areaElements[i].children() - for (j in areaChildren.indices) { - when (areaChildren[j].className()) { - "fire l" -> { - val fireLChildren: Elements = areaChildren[j].children() - for (k in fireLChildren.indices) { - when (fireLChildren[k].className()) { - "lpic" -> { - classifyList.addAll( - ParseHtmlUtil.parseLpic( - fireLChildren[k], - url - ) - ) - } - "pages" -> { - pageNumberBean = ParseHtmlUtil.parseNextPages(fireLChildren[k]) - } - } - } - } - } - } - } - return Pair(classifyList, pageNumberBean) + override suspend fun getClassifyData(partUrl: String): Pair, PageNumberBean?> { + return Pair(ArrayList(), null) } override fun clearActivity() { } override suspend fun getClassifyTabData(): ArrayList { - val classifyTabList: ArrayList = ArrayList() - val document = JsoupUtil.getDocument(Api.MAIN_URL + "/a/") - val areaElements: Elements = document.getElementsByClass("area") - for (i in areaElements.indices) { - val areaChildren: Elements = areaElements[i].children() - for (j in areaChildren.indices) { - when (areaChildren[j].className()) { - "ters" -> { - classifyTabList.addAll(ParseHtmlUtil.parseTers(areaChildren[j])) - } - } - } - } - classifyTabList.forEach { - it.classifyDataList.forEach { item -> - UnknownActionUrl.actionMap[item.actionUrl] = - object : UnknownActionUrl.Action { - override fun action() { - App.context.startActivity( - Intent(App.context, ClassifyActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra("partUrl", item.actionUrl) - ) - } - } - } - } - return classifyTabList + return ArrayList() } override fun setActivity(activity: Activity) { diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/Const.kt b/app/src/main/java/com/skyd/imomoe/model/impls/Const.kt index f9768f5f..cb37f8a3 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/Const.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/Const.kt @@ -3,22 +3,18 @@ package com.skyd.imomoe.model.impls import com.skyd.imomoe.model.interfaces.IConst class Const : IConst { - override val actionUrl = ActionUrl() + override fun versionName(): String = "1.1.0" - class ActionUrl : IConst.IActionUrl { - override fun ANIME_RANK(): String = "/top/" - override fun ANIME_PLAY(): String = "/v/" - override fun ANIME_DETAIL(): String = "/show/" - override fun ANIME_SEARCH(): String = "/search/" - } - - override fun MAIN_URL(): String = "http://www.yhdm.so" - - override fun versionName(): String = "1.0.0" + override fun versionCode(): Int = 5 - override fun versionCode(): Int = 1 + override val MAIN_URL: String + get() { + val url = com.skyd.imomoe.config.Const.Common.GITHUB_URL + return if (url.endsWith("/")) url + else "$url/" + } override fun about(): String { - return "默认数据源\n数据来源:${MAIN_URL()}" + return "默认数据源,不提供任何数据,请在设置界面手动选择自定义数据源!" } } diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeModel.kt index c8688578..57581a8e 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeModel.kt @@ -1,64 +1,10 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.bean.* -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil +import com.skyd.imomoe.bean.TabBean import com.skyd.imomoe.model.interfaces.IEverydayAnimeModel -import org.jsoup.select.Elements class EverydayAnimeModel : IEverydayAnimeModel { - override suspend fun getEverydayAnimeData(): Triple, ArrayList>, AnimeShowBean> { - val tabList = ArrayList() - val header = AnimeShowBean( - "", "", "", "", - "", null, "", null - ) - val everydayAnimeList: ArrayList> = ArrayList() - val document = JsoupUtil.getDocument(Api.MAIN_URL) - val areaChildren: Elements = document.select("[class=area]")[0].children() - for (i in areaChildren.indices) { - when (areaChildren[i].className()) { - "side r" -> { - val sideRChildren = areaChildren[i].children() - out@ for (j in sideRChildren.indices) { - when (sideRChildren[j].className()) { - "bg" -> { - val bgChildren = sideRChildren[j].children() - for (k in bgChildren.indices) { - when (bgChildren[k].className()) { - "dtit" -> { - header.title = ParseHtmlUtil.parseDtit(bgChildren[k]) - } - "tag" -> { - val tagChildren = bgChildren[k].children() - for (l in tagChildren.indices) { - tabList.add( - TabBean( - "", - "", - "", - tagChildren[l].text() - ) - ) - } - } - "tlist" -> { - everydayAnimeList.addAll( - ParseHtmlUtil.parseTlist( - bgChildren[k] - ) - ) - } - } - } - break@out - } - } - } - } - } - } - return Triple(tabList, everydayAnimeList, header) + override suspend fun getEverydayAnimeData(): Triple, ArrayList>, String> { + return Triple(ArrayList(), ArrayList(), "") } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeWidgetModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeWidgetModel.kt index 71562a39..5a0a799c 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeWidgetModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/EverydayAnimeWidgetModel.kt @@ -1,45 +1,10 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.bean.* -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.model.util.JsoupUtil +import com.skyd.imomoe.bean.AnimeCover10Bean import com.skyd.imomoe.model.interfaces.IEverydayAnimeWidgetModel -import com.skyd.imomoe.model.util.ParseHtmlUtil.parseTlist -import org.jsoup.select.Elements class EverydayAnimeWidgetModel : IEverydayAnimeWidgetModel { - override fun getEverydayAnimeData(): ArrayList> { - val list: ArrayList> = ArrayList() - try { - val document = JsoupUtil.getDocument(Api.MAIN_URL) - val areaChildren: Elements = document.select("[class=area]")[0].children() - for (i in areaChildren.indices) { - when (areaChildren[i].className()) { - "side r" -> { - val sideRChildren = areaChildren[i].children() - out@ for (j in sideRChildren.indices) { - when (sideRChildren[j].className()) { - "bg" -> { - val bgChildren = sideRChildren[j].children() - for (k in bgChildren.indices) { - when (bgChildren[k].className()) { - "tlist" -> { - list.addAll( - parseTlist(bgChildren[k]) - ) - } - } - } - break@out - } - } - } - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } - return list + override fun getEverydayAnimeData(): ArrayList> { + return ArrayList() } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/HomeModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/HomeModel.kt index c1d7d1e9..e1806f6b 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/HomeModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/HomeModel.kt @@ -1,39 +1,13 @@ package com.skyd.imomoe.model.impls import com.skyd.imomoe.bean.TabBean -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.config.UnknownActionUrl -import com.skyd.imomoe.model.util.JsoupUtil import com.skyd.imomoe.model.interfaces.IHomeModel -import com.skyd.imomoe.util.eventbus.SelectHomeTabEvent -import org.greenrobot.eventbus.EventBus -import org.jsoup.select.Elements class HomeModel : IHomeModel { override suspend fun getAllTabData(): ArrayList { - return ArrayList().apply { - val document = JsoupUtil.getDocument(Api.MAIN_URL) - val menu: Elements = document.getElementsByClass("menu") - val dmx_l: Elements = menu.select("[class=dmx l]").select("li") - for (i in dmx_l.indices) { - val url = dmx_l[i].select("a").attr("href") - add(TabBean("", url, Api.MAIN_URL + url, dmx_l[i].text())) - UnknownActionUrl.actionMap[url] = object : UnknownActionUrl.Action { - override fun action() { - EventBus.getDefault().post(SelectHomeTabEvent(url)) - } - } - } - val dme_r: Elements = menu.select("[class=dme r]").select("li") - for (i in dme_r.indices) { - val url = dme_r[i].select("a").attr("href") - add(TabBean("", url, Api.MAIN_URL + url, dme_r[i].text())) - UnknownActionUrl.actionMap[url] = object : UnknownActionUrl.Action { - override fun action() { - EventBus.getDefault().post(SelectHomeTabEvent(url)) - } - } - } - } + return arrayListOf( + TabBean("/market", "", "使用方式一:在数据源商店下载数据源"), + TabBean("/manual", "", "使用方式二:手动导入数据源") + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/MonthAnimeModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/MonthAnimeModel.kt index 0b405ca9..32c2c196 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/MonthAnimeModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/MonthAnimeModel.kt @@ -1,29 +1,10 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil import com.skyd.imomoe.model.interfaces.IMonthAnimeModel -import org.jsoup.select.Elements class MonthAnimeModel : IMonthAnimeModel { - override suspend fun getMonthAnimeData(partUrl: String): Pair, PageNumberBean?> { - val monthAnimeList: ArrayList = ArrayList() - val url = Api.MAIN_URL + partUrl - val document = JsoupUtil.getDocument(url) - val areaElements: Elements = document.getElementsByClass("area") - for (i in areaElements.indices) { - val areaChildren: Elements = areaElements[i].children() - for (j in areaChildren.indices) { - when (areaChildren[j].className()) { - "lpic" -> { - monthAnimeList.addAll(ParseHtmlUtil.parseLpic(areaChildren[j], url)) - } - } - } - } - return Pair(monthAnimeList, null) + override suspend fun getMonthAnimeData(partUrl: String): Pair, PageNumberBean?> { + return Pair(ArrayList(), null) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/PlayModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/PlayModel.kt index 91efee2c..ff515caa 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/PlayModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/PlayModel.kt @@ -1,183 +1,44 @@ package com.skyd.imomoe.model.impls import android.app.Activity -import com.skyd.imomoe.bean.* -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil +import com.skyd.imomoe.bean.AnimeEpisodeDataBean +import com.skyd.imomoe.bean.AnimeTitleBean +import com.skyd.imomoe.bean.ImageBean +import com.skyd.imomoe.bean.PlayBean import com.skyd.imomoe.model.interfaces.IPlayModel -import org.jsoup.nodes.Element -import org.jsoup.select.Elements -import java.lang.ref.SoftReference class PlayModel : IPlayModel { - private var mActivity: SoftReference? = null - - private fun getVideoRawUrl(e: Element): String { - val div = e.select("[class=area]").select("[class=bofang]")[0].children() - val rawUrl = div.attr("data-vid") - return when { - rawUrl.endsWith("\$mp4", true) -> rawUrl.replace("\$mp4", "") - rawUrl.endsWith("\$url", true) -> rawUrl.replace("\$url", "") - rawUrl.endsWith("\$hp", true) -> { - JsoupUtil.getDocument("http://tup.yhdm.so/hp.php?url=${rawUrl.substringBefore("\$hp")}") - .body().select("script")[0].toString() - .substringAfter("video: {") - .substringBefore("}") - .split(",")[0] - .substringAfter("url: \"") - .substringBefore("\"") - } - rawUrl.endsWith("\$qzz", true) -> rawUrl - else -> "" - } - } override suspend fun getPlayData( partUrl: String, animeEpisodeDataBean: AnimeEpisodeDataBean - ): Triple, ArrayList, PlayBean> { - val playBeanDataList: ArrayList = ArrayList() - val episodesList: ArrayList = ArrayList() - val title = AnimeTitleBean("", "", "") - val episode = - AnimeEpisodeDataBean( - "", "", - "" + ): Triple, ArrayList, PlayBean> { + return Triple( + ArrayList(), ArrayList(), PlayBean( + "", + AnimeTitleBean("", ""), + AnimeEpisodeDataBean("", ""), + "", + ArrayList() ) - val url = Api.MAIN_URL + partUrl - val document = JsoupUtil.getDocument(url) - val children: Elements = document.allElements - for (i in children.indices) { - when (children[i].className()) { - "play" -> { - animeEpisodeDataBean.videoUrl = getVideoRawUrl(children[i]) - } - "area" -> { - val areaChildren = children[i].children() - for (j in areaChildren.indices) { - when (areaChildren[j].className()) { - "gohome l" -> { //标题 - title.title = areaChildren[j].select("h1") - .select("a").text() - title.actionUrl = areaChildren[j].select("h1") - .select("a").attr("href") - episode.title = areaChildren[j].select("h1") - .select("span").text().replace(":", "") - animeEpisodeDataBean.title = episode.title - } - "botit" -> { - playBeanDataList.add( - AnimeDetailBean( - Const.ViewHolderTypeString.HEADER_1, - "", - ParseHtmlUtil.parseBotit(areaChildren[j]), - "" - ) - ) - } - "movurls" -> { //集数列表 - episodesList.addAll( - ParseHtmlUtil.parseMovurls( - areaChildren[j], - animeEpisodeDataBean - ) - ) - playBeanDataList.add( - AnimeDetailBean( - Const.ViewHolderTypeString.HORIZONTAL_RECYCLER_VIEW_1, - "", - "", - "", - episodesList - ) - ) - } - "imgs" -> { - playBeanDataList.addAll( - ParseHtmlUtil.parseImg(areaChildren[j], url) - ) - } - } - } - } - } - } - val playBean = PlayBean("", "", title, episode, playBeanDataList) - return Triple(playBeanDataList, episodesList, playBean) + ) } - override suspend fun refreshAnimeEpisodeData( - partUrl: String, - animeEpisodeDataBean: AnimeEpisodeDataBean - ): Boolean { - val document = JsoupUtil.getDocument(Api.MAIN_URL + partUrl) - val children: Elements = document.select("body")[0].children() - for (i in children.indices) { - when (children[i].className()) { - "play" -> { - animeEpisodeDataBean.actionUrl = partUrl - animeEpisodeDataBean.videoUrl = getVideoRawUrl(children[i]) - return true - } - } - } - return false + override suspend fun playAnotherEpisode(partUrl: String): AnimeEpisodeDataBean? { + return null } - override suspend fun getAnimeCoverImageBean(detailPartUrl: String): ImageBean? { - try { - val url = Api.MAIN_URL + detailPartUrl - val document = JsoupUtil.getDocument(url) - //番剧头部信息 - val area: Elements = document.getElementsByClass("area") - for (i in area.indices) { - val areaChildren = area[i].children() - for (j in areaChildren.indices) { - when (areaChildren[j].className()) { - "fire l" -> { - val fireLChildren = - areaChildren[j].select("[class=fire l]")[0].children() - for (k in fireLChildren.indices) { - if (fireLChildren[k].className() == "thumb l") { - return ImageBean( - "", "", - ParseHtmlUtil.getCoverUrl( - fireLChildren[k].select("img").attr("src"), - url - ), url - ) - } - } - } - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } + override suspend fun getAnimeCoverImageBean(partUrl: String): ImageBean? { return null } override fun setActivity(activity: Activity) { - mActivity = SoftReference(activity) } override fun clearActivity() { - mActivity = null } - override suspend fun getAnimeEpisodeUrlData(partUrl: String): String? { - val document = JsoupUtil.getDocument(Api.MAIN_URL + partUrl) - val children: Elements = document.select("body")[0].children() - for (i in children.indices) { - when (children[i].className()) { - "play" -> { - return getVideoRawUrl(children[i]) - } - } - } + override suspend fun getAnimeDownloadUrl(partUrl: String): String? { return null } diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/RankListModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/RankListModel.kt index f6b30b4c..f31486c9 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/RankListModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/RankListModel.kt @@ -1,70 +1,10 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil import com.skyd.imomoe.model.interfaces.IRankListModel -import org.jsoup.select.Elements class RankListModel : IRankListModel { - private var bgTimes = 0 - var rankList: MutableList = ArrayList() - - override suspend fun getRankListData(partUrl: String): Pair, PageNumberBean?> { - rankList.clear() - if (partUrl == "/" || partUrl == "") getWeekRankData() - else getAllRankData(partUrl) - return Pair(rankList, null) - } - - private fun getAllRankData(partUrl: String) { - val const = DataSourceManager.getConst() ?: Const() - val document = JsoupUtil.getDocument(Api.MAIN_URL + const.actionUrl.ANIME_RANK()) - val areaChildren: Elements = document.select("[class=area]")[0].children() - for (i in areaChildren.indices) { - when (areaChildren[i].className()) { - "topli" -> { - rankList.addAll(ParseHtmlUtil.parseTopli(areaChildren[i])) - } - } - } - } - - private fun getWeekRankData() { - bgTimes = 0 - val url = Api.MAIN_URL - val document = JsoupUtil.getDocument(url) - val areaChildren: Elements = document.select("[class=area]")[0].children() - for (i in areaChildren.indices) { - when (areaChildren[i].className()) { - "side r" -> { - val sideRChildren = areaChildren[i].children() - for (j in sideRChildren.indices) { - when (sideRChildren[j].className()) { - "bg" -> { - if (bgTimes++ == 0) continue - - val bgChildren = sideRChildren[j].children() - for (k in bgChildren.indices) { - when (bgChildren[k].className()) { - "pics" -> { - rankList.addAll( - ParseHtmlUtil.parsePics( - bgChildren[k], - url - ) - ) - } - } - } - } - } - } - } - } - } + override suspend fun getRankListData(partUrl: String): Pair, PageNumberBean?> { + return Pair(ArrayList(), null) } } diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/RankModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/RankModel.kt index 64a34cee..acb25289 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/RankModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/RankModel.kt @@ -1,79 +1,10 @@ package com.skyd.imomoe.model.impls import com.skyd.imomoe.bean.TabBean -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil import com.skyd.imomoe.model.interfaces.IRankModel -import org.jsoup.select.Elements class RankModel : IRankModel { - private var bgTimes = 0 - private var tabList: ArrayList = ArrayList() - override suspend fun getRankTabData(): ArrayList { - tabList.clear() - getWeekRankData() - getAllRankData() - return tabList - } - - private fun getAllRankData() { - val const = DataSourceManager.getConst() ?: Const() - val document = JsoupUtil.getDocument(Api.MAIN_URL + const.actionUrl.ANIME_RANK()) - val areaChildren: Elements = document.select("[class=area]")[0].children() - for (i in areaChildren.indices) { - when (areaChildren[i].className()) { - "gohome" -> { - tabList.add( - tabList.size, TabBean( - "", - const.actionUrl.ANIME_RANK(), - "", - areaChildren[i].select("h1").select("a").text() - ) - ) - } - } - } - } - - private fun getWeekRankData() { - bgTimes = 0 - val url = Api.MAIN_URL - val document = JsoupUtil.getDocument(url) - val areaChildren: Elements = document.select("[class=area]")[0].children() - for (i in areaChildren.indices) { - when (areaChildren[i].className()) { - "side r" -> { - val sideRChildren = areaChildren[i].children() - for (j in sideRChildren.indices) { - when (sideRChildren[j].className()) { - "bg" -> { - if (bgTimes++ == 0) continue - - val bgChildren = sideRChildren[j].children() - for (k in bgChildren.indices) { - when (bgChildren[k].className()) { - "dtit" -> { - tabList.add( - 0, - TabBean( - "", - "/", - "", - ParseHtmlUtil.parseDtit(bgChildren[k]) - ) - ) - } - } - } - } - } - } - } - } - } + return ArrayList() } } diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/RouteProcessor.kt b/app/src/main/java/com/skyd/imomoe/model/impls/RouteProcessor.kt deleted file mode 100644 index 4b576241..00000000 --- a/app/src/main/java/com/skyd/imomoe/model/impls/RouteProcessor.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.skyd.imomoe.model.impls - -import android.app.Activity -import android.content.Context -import android.content.Intent -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.interfaces.IRouteProcessor -import com.skyd.imomoe.util.Util.getSubString -import com.skyd.imomoe.util.Util.isYearMonth -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.view.activity.* -import java.net.URLDecoder - -class RouteProcessor : IRouteProcessor { - override fun process(context: Context, actionUrl: String): Boolean { - val decodeUrl = URLDecoder.decode(actionUrl, "UTF-8") - val const = DataSourceManager.getConst() ?: Const() - var solved = true - when { - decodeUrl.startsWith(const.actionUrl.ANIME_DETAIL()) -> { //番剧封面点击进入 - context.startActivity( - Intent(context, AnimeDetailActivity::class.java) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra("partUrl", actionUrl) - ) - } - decodeUrl.startsWith(const.actionUrl.ANIME_PLAY()) -> { //番剧每一集点击进入 - val playCode = actionUrl.getSubString("\\/v\\/", "\\.")[0].split("-") - if (playCode.size >= 2) { - var detailPartUrl = actionUrl.substringAfter(const.actionUrl.ANIME_DETAIL(), "") -// if (detailPartUrl.isBlank()) App.context.getString(R.string.error_play_episode).showToast() - detailPartUrl = const.actionUrl.ANIME_DETAIL() + detailPartUrl - context.startActivity( - Intent(context, PlayActivity::class.java) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra( - "partUrl", - actionUrl.substringBefore(const.actionUrl.ANIME_DETAIL()) - ) - .putExtra("detailPartUrl", detailPartUrl) - ) - } else { - App.context.getString(R.string.error_play_episode).showToast() - } - } - else -> solved = false - } - return solved - } - - override fun process(activity: Activity, actionUrl: String): Boolean { - val decodeUrl = URLDecoder.decode(actionUrl, "UTF-8") - val const = DataSourceManager.getConst() ?: Const() - var solved = true - when { - decodeUrl.startsWith(com.skyd.imomoe.config.Const.ActionUrl.ANIME_CLASSIFY) -> { //如进入分类页面 - val paramList = actionUrl.replace(com.skyd.imomoe.config.Const.ActionUrl.ANIME_CLASSIFY, "").split("/") - if (paramList.size == 3) { //例如 /japan/日本 分割后是3个参数:"",japan,日本 - activity.startActivity( - Intent(activity, ClassifyActivity::class.java) - .putExtra("partUrl", "/" + paramList[1] + "/") - .putExtra("classifyTabTitle", "") - .putExtra("classifyTitle", paramList[2]) - ) - } else App.context.resources.getString(R.string.action_url_format_error) - .showToast() - } - decodeUrl.replace("/", "").isYearMonth() -> { //如201907月新番列表 - activity.startActivity( - Intent(activity, MonthAnimeActivity::class.java) - .putExtra("partUrl", actionUrl) - ) - } - decodeUrl.startsWith(const.actionUrl.ANIME_RANK()) -> { // 排行榜 - activity.startActivity(Intent(activity, RankActivity::class.java)) - } - decodeUrl.startsWith(const.actionUrl.ANIME_SEARCH()) -> { // 进入搜索页面 - decodeUrl.replace(const.actionUrl.ANIME_SEARCH(), "").let { - val keyWord = it.replaceFirst(Regex("/.*"), "") - val pageNumber = it.replaceFirst(Regex("($keyWord/)|($keyWord)"), "") - activity.startActivity( - Intent(activity, SearchActivity::class.java) - .putExtra("keyWord", keyWord) - .putExtra("pageNumber", pageNumber) - ) - } - } - decodeUrl.startsWith(const.actionUrl.ANIME_DETAIL()) -> { //番剧封面点击进入 - activity.startActivity( - Intent(activity, AnimeDetailActivity::class.java) - .putExtra("partUrl", actionUrl) - ) - } - decodeUrl.startsWith(const.actionUrl.ANIME_PLAY()) -> { //番剧每一集点击进入 - val playCode = actionUrl.getSubString("\\/v\\/", "\\.")[0].split("-") - if (playCode.size >= 2) { - var detailPartUrl = - actionUrl.substringAfter(const.actionUrl.ANIME_DETAIL(), "") -// if (detailPartUrl.isBlank()) App.context.getString(R.string.error_play_episode).showToast() - detailPartUrl = const.actionUrl.ANIME_DETAIL() + detailPartUrl - activity.startActivity( - Intent(activity, PlayActivity::class.java) - .putExtra( - "partUrl", - actionUrl.substringBefore(const.actionUrl.ANIME_DETAIL()) - ) - .putExtra("detailPartUrl", detailPartUrl) - ) - } else { - App.context.getString(R.string.error_play_episode).showToast() - } - } - else -> solved = false - } - return solved - } -} diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/Router.kt b/app/src/main/java/com/skyd/imomoe/model/impls/Router.kt new file mode 100644 index 00000000..a9f61e55 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/model/impls/Router.kt @@ -0,0 +1,9 @@ +package com.skyd.imomoe.model.impls + +import android.content.Context +import android.net.Uri +import com.skyd.imomoe.model.interfaces.IRouter + +class Router : IRouter { + override fun route(uri: Uri, context: Context?): Boolean = false +} diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/SearchModel.kt b/app/src/main/java/com/skyd/imomoe/model/impls/SearchModel.kt index dcc43ba5..81b92b54 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/SearchModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/SearchModel.kt @@ -1,31 +1,13 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.util.JsoupUtil -import com.skyd.imomoe.model.util.ParseHtmlUtil import com.skyd.imomoe.model.interfaces.ISearchModel -import com.skyd.imomoe.util.Util -import org.jsoup.select.Elements class SearchModel : ISearchModel { override suspend fun getSearchData( keyWord: String, partUrl: String - ): Pair, PageNumberBean?> { - val const = DataSourceManager.getConst() ?: Const() - var pageNumberBean: PageNumberBean? = null - val searchResultList: ArrayList = ArrayList() - val url = - "${Api.MAIN_URL}${const.actionUrl.ANIME_SEARCH()}${Util.getEncodedUrl(keyWord)}/$partUrl" - val document = JsoupUtil.getDocument(url) - val lpic: Elements = document.getElementsByClass("area") - .select("[class=fire l]").select("[class=lpic]") - searchResultList.addAll(ParseHtmlUtil.parseLpic(lpic[0], url)) - val pages = lpic[0].select("[class=pages]") - if (pages.size > 0) pageNumberBean = ParseHtmlUtil.parseNextPages(pages[0]) - return Pair(searchResultList, pageNumberBean) + ): Pair, PageNumberBean?> { + return Pair(ArrayList(), null) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/impls/Util.kt b/app/src/main/java/com/skyd/imomoe/model/impls/Util.kt index 577fe715..987d52b5 100644 --- a/app/src/main/java/com/skyd/imomoe/model/impls/Util.kt +++ b/app/src/main/java/com/skyd/imomoe/model/impls/Util.kt @@ -1,14 +1,5 @@ package com.skyd.imomoe.model.impls -import com.skyd.imomoe.model.DataSourceManager import com.skyd.imomoe.model.interfaces.IUtil -import com.skyd.imomoe.util.Util -class Util : IUtil { - override fun getDetailLinkByEpisodeLink(episodeUrl: String): String { - val const = DataSourceManager.getConst() ?: Const() - return const.actionUrl.ANIME_DETAIL() + episodeUrl - .replaceFirst(const.actionUrl.ANIME_PLAY(), "") - .replaceFirst(Regex("-.*\\.html"), "") + Util.getWebsiteLinkSuffix() - } -} +class Util : IUtil diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeDetailModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeDetailModel.kt index ac361277..31b02247 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeDetailModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeDetailModel.kt @@ -1,8 +1,6 @@ package com.skyd.imomoe.model.interfaces -import com.skyd.imomoe.bean.IAnimeDetailBean import com.skyd.imomoe.bean.ImageBean -import java.util.* /** * 获取番剧详情数据接口 @@ -15,9 +13,9 @@ interface IAnimeDetailModel : IBase { * @return Triple,不可为null * ImageBean:番剧封面图片类,不可为null; * String:番剧名,不可为null; - * ArrayList:详情页数据List,不可为null + * ArrayList:详情页数据List,不可为null */ - suspend fun getAnimeDetailData(partUrl: String): Triple> + suspend fun getAnimeDetailData(partUrl: String): Triple> companion object { const val implName = "AnimeDetailModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeShowModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeShowModel.kt index 75f3ebde..3098ace1 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeShowModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IAnimeShowModel.kt @@ -1,6 +1,5 @@ package com.skyd.imomoe.model.interfaces -import com.skyd.imomoe.bean.IAnimeShowBean import com.skyd.imomoe.bean.PageNumberBean /** @@ -12,10 +11,10 @@ interface IAnimeShowModel : IBase { * * @param partUrl 页面部分url,不为null * @return Pair,不可为null - * ArrayList:数据List,不可为null; + * ArrayList:数据List,不可为null; * PageNumberBean:下一页数据地址Bean,可为null,为空则没有下一页 */ - suspend fun getAnimeShowData(partUrl: String): Pair, PageNumberBean?> + suspend fun getAnimeShowData(partUrl: String): Pair, PageNumberBean?> companion object { const val implName = "AnimeShowModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IClassifyModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IClassifyModel.kt index d12446ed..a487e6a0 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IClassifyModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IClassifyModel.kt @@ -1,7 +1,6 @@ package com.skyd.imomoe.model.interfaces import android.app.Activity -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.ClassifyBean import com.skyd.imomoe.bean.PageNumberBean @@ -9,6 +8,7 @@ import com.skyd.imomoe.bean.PageNumberBean * 获取分类界面数据的接口 */ interface IClassifyModel : IBase { + @Deprecated("This method will cause a memory leak!!!") fun setActivity(activity: Activity) fun clearActivity() @@ -25,10 +25,10 @@ interface IClassifyModel : IBase { * * @param partUrl 页面部分url * @return Pair,不可为null - * ArrayList:数据List,不可为null; + * ArrayList:数据List,不可为null; * PageNumberBean:下一页地址数据,可为null */ - suspend fun getClassifyData(partUrl: String): Pair, PageNumberBean?> + suspend fun getClassifyData(partUrl: String): Pair, PageNumberBean?> companion object { const val implName = "ClassifyModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IConst.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IConst.kt index 038d1604..a4c16078 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IConst.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IConst.kt @@ -1,63 +1,34 @@ package com.skyd.imomoe.model.interfaces /** - * 获取MAIN_URL、页面跳转信息、自定义数据jar包的关于信息等 + * 获取MAIN_URL、版本、自定义数据ads包、关于等信息 */ interface IConst : IBase { - /** - * @return MAIN_URL - */ - fun MAIN_URL(): String + companion object { + const val implName = "Const" + } + + @Suppress("PropertyName") + val MAIN_URL: String /** - * @return jar包的关于信息 + * @return ads包的关于信息 */ fun about(): String { - return MAIN_URL() + "" + return MAIN_URL } /** - * @return jar包的版本名信息 + * @return ads包的版本名信息 */ fun versionName(): String? { return null } /** - * @return jar包的版本号信息 + * @return ads包的版本号信息 */ fun versionCode(): Int { return 0 } - - /** - * @return 跳转类实例 - */ - val actionUrl: IActionUrl - - interface IActionUrl { - /** - * @return 番剧详情界面跳转URL - */ - fun ANIME_DETAIL(): String - - /** - * @return 播放界面跳转URL - */ - fun ANIME_PLAY(): String - - /** - * @return 搜索界面跳转URL - */ - fun ANIME_SEARCH(): String - - /** - * @return 排行榜界面跳转URL - */ - fun ANIME_RANK(): String - } - - companion object { - const val implName = "Const" - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeModel.kt index f9144168..eabcc32c 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeModel.kt @@ -1,7 +1,5 @@ package com.skyd.imomoe.model.interfaces -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.AnimeShowBean import com.skyd.imomoe.bean.TabBean /** @@ -14,9 +12,9 @@ interface IEverydayAnimeModel : IBase { * @return Triple,不可为null * ArrayList:Tab信息ArrayList,不可为null; * ArrayList>:每个Tab内容的ArrayList,不可为null; - * AnimeShowBean:标题,例如:日更动漫,不可为null + * String:标题,例如:日更动漫,不可为null */ - suspend fun getEverydayAnimeData(): Triple, ArrayList>, AnimeShowBean> + suspend fun getEverydayAnimeData(): Triple, ArrayList>, String> companion object { const val implName = "EverydayAnimeModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeWidgetModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeWidgetModel.kt index 2a472021..757ad64c 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeWidgetModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IEverydayAnimeWidgetModel.kt @@ -1,6 +1,6 @@ package com.skyd.imomoe.model.interfaces -import com.skyd.imomoe.bean.AnimeCoverBean +import com.skyd.imomoe.bean.AnimeCover10Bean /** * 获取每日更新番剧桌面小组件的数据接口 @@ -11,7 +11,7 @@ interface IEverydayAnimeWidgetModel : IBase { * * @return ArrayList,不可为null。每天更新番剧的ArrayList,其中共有七条 */ - fun getEverydayAnimeData(): ArrayList> + fun getEverydayAnimeData(): ArrayList> companion object { const val implName = "EverydayAnimeWidgetModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IMonthAnimeModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IMonthAnimeModel.kt index 227df05a..5e3c6dbb 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IMonthAnimeModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IMonthAnimeModel.kt @@ -1,8 +1,6 @@ package com.skyd.imomoe.model.interfaces -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.PageNumberBean -import java.util.* /** * 获取季度番剧数据的接口 @@ -11,10 +9,10 @@ interface IMonthAnimeModel : IBase { /** * @param partUrl 页面部分url,不为null * @return Pair,不可为null - * ArrayList:季度番剧数据ArrayList,不为null; + * ArrayList:季度番剧数据ArrayList,不为null; * PageNumberBean:下一页数据地址,可为null,为空则没有下一页 */ - suspend fun getMonthAnimeData(partUrl: String): Pair, PageNumberBean?> + suspend fun getMonthAnimeData(partUrl: String): Pair, PageNumberBean?> companion object { const val implName = "MonthAnimeModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IPlayModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IPlayModel.kt index d4d517f4..aa0214e0 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IPlayModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IPlayModel.kt @@ -2,7 +2,6 @@ package com.skyd.imomoe.model.interfaces import android.app.Activity import com.skyd.imomoe.bean.AnimeEpisodeDataBean -import com.skyd.imomoe.bean.IAnimeDetailBean import com.skyd.imomoe.bean.ImageBean import com.skyd.imomoe.bean.PlayBean @@ -10,6 +9,7 @@ import com.skyd.imomoe.bean.PlayBean * 获取播放界面数据的接口 */ interface IPlayModel : IBase { + @Deprecated("This function will cause a memory leak!!!") fun setActivity(activity: Activity) fun clearActivity() @@ -26,10 +26,10 @@ interface IPlayModel : IBase { /** * 获取此部番剧封面 * - * @param detailPartUrl 页面部分url,不为null + * @param partUrl 播放页面部分url,不为null * @return ImageBean,可为null。番剧封面 */ - suspend fun getAnimeCoverImageBean(detailPartUrl: String): ImageBean? + suspend fun getAnimeCoverImageBean(partUrl: String): ImageBean? /** * 获取播放页面相关数据 @@ -37,31 +37,30 @@ interface IPlayModel : IBase { * @param partUrl 页面部分url,不为null * @param animeEpisodeDataBean 此集番剧数据,不为null,直接对此引用进行数据设置,不要更改此变量指向的对象 * @return Triple,不可为null - * ArrayList:播放页下方数据ArrayList,不为null; + * ArrayList:播放页下方数据ArrayList,不为null; * ArrayList:番剧集数列表,不为null; * PlayBean:此集番剧数据,不为null */ suspend fun getPlayData( partUrl: String, animeEpisodeDataBean: AnimeEpisodeDataBean - ): Triple, ArrayList, PlayBean> + ): Triple, ArrayList, PlayBean> /** - * 获取当前页面播放视频的地址 + * 获取partUrl页面对应番剧的下载地址 * * @param partUrl 页面部分url,不为null * @return String,可为null。此页面播放的视频地址 */ - suspend fun getAnimeEpisodeUrlData(partUrl: String): String? + suspend fun getAnimeDownloadUrl(partUrl: String): String? /** - * 获取传入partUrl页面对应的视频的数据 + * 播放另一集,获取传入partUrl页面对应的视频的数据 * - * @param partUrl 页面部分url,不为null - * @param animeEpisodeDataBean partUrl页面对应的视频的数据Bean,不为null,直接对此变量设置数据,不要更改此变量指向的对象 - * @return Boolean,不可为null。获取成功true,否则false + * @param partUrl 页面部分url,不为null + * @return AnimeEpisodeDataBean,可为null。partUrl页面对应的视频的数据Bean */ - suspend fun refreshAnimeEpisodeData(partUrl: String, animeEpisodeDataBean: AnimeEpisodeDataBean): Boolean + suspend fun playAnotherEpisode(partUrl: String): AnimeEpisodeDataBean? companion object { const val implName = "PlayModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IRankListModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IRankListModel.kt index b4d8c817..6d67c5c3 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IRankListModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IRankListModel.kt @@ -1,6 +1,5 @@ package com.skyd.imomoe.model.interfaces -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.PageNumberBean /** @@ -12,10 +11,10 @@ interface IRankListModel : IBase { * * @param partUrl 页面部分url,不为null * @return Pair,不可为null - * List:排行榜列表数据List,不为null + * List:排行榜列表数据List,不为null * PageNumberBean:下一页数据地址Bean,可为null,为空则没有下一页 */ - suspend fun getRankListData(partUrl: String): Pair, PageNumberBean?> + suspend fun getRankListData(partUrl: String): Pair, PageNumberBean?> companion object { const val implName = "RankListModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IRouteProcessor.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IRouteProcessor.kt deleted file mode 100644 index 2a4a5c8a..00000000 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IRouteProcessor.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.skyd.imomoe.model.interfaces - -import android.app.Activity -import android.content.Context -import androidx.fragment.app.Fragment - -/** - * 界面跳转处理接口 - */ -interface IRouteProcessor { - /** - * 处理根据actionUrl跳转 - * - * @param context Context - * @param actionUrl 跳转路径 - * @return 成功处理了跳转,则返回true,没处理则返回false - */ - fun process(context: Context, actionUrl: String): Boolean - - /** - * 处理根据actionUrl跳转 - * - * @param activity Activity - * @param actionUrl 跳转路径 - * @return 成功处理了跳转,则返回true,没处理则返回false - */ - fun process(activity: Activity, actionUrl: String): Boolean - - /** - * 处理根据actionUrl跳转 - * - * @param fragment Fragment - * @param actionUrl 跳转路径 - * @return 成功处理了跳转,则返回true,没处理则返回false - */ - fun process(fragment: Fragment, actionUrl: String): Boolean { - val activity: Activity? = fragment.activity - return activity?.let { process(it, actionUrl) } ?: false - } - - companion object { - const val implName = "RouteProcessor" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IRouter.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IRouter.kt new file mode 100644 index 00000000..3d632eb2 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IRouter.kt @@ -0,0 +1,22 @@ +package com.skyd.imomoe.model.interfaces + +import android.content.Context +import android.net.Uri + +/** + * 界面跳转处理接口 + */ +interface IRouter { + /** + * 根据Uri跳转。强烈建议处理“根据网址跳转”功能输入的网址url,否则该功能将不可用!!! + * + * @param uri Uri + * @param context Context + * @return 成功处理了跳转,则返回true,没处理则返回false + */ + fun route(uri: Uri, context: Context?): Boolean = false + + companion object { + const val implName = "Router" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/ISearchModel.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/ISearchModel.kt index 8b5af2c7..b173d707 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/ISearchModel.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/ISearchModel.kt @@ -1,6 +1,5 @@ package com.skyd.imomoe.model.interfaces -import com.skyd.imomoe.bean.AnimeCoverBean import com.skyd.imomoe.bean.PageNumberBean /** @@ -13,10 +12,10 @@ interface ISearchModel : IBase { * @param keyWord 搜索关键词,不为null * @param partUrl 搜索页面部分url,不为null * @return Pair,不可为null - * ArrayList:搜索结果ArrayList,不为null + * ArrayList:搜索结果ArrayList,不为null * PageNumberBean:下一页数据地址Bean,可为null */ - suspend fun getSearchData(keyWord: String, partUrl: String): Pair, PageNumberBean?> + suspend fun getSearchData(keyWord: String, partUrl: String): Pair, PageNumberBean?> companion object { const val implName = "SearchModel" diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/IUtil.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/IUtil.kt index ccb2024f..b61c1b9e 100644 --- a/app/src/main/java/com/skyd/imomoe/model/interfaces/IUtil.kt +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/IUtil.kt @@ -4,14 +4,6 @@ package com.skyd.imomoe.model.interfaces * 工具类 */ interface IUtil : IBase { - /** - * 通过播放页面的网址获取详情页面的网址 - * - * @param episodeUrl 播放页面的网址,不为null - * @return 详情页面的网址,不可为null - */ - fun getDetailLinkByEpisodeLink(episodeUrl: String): String - companion object { const val implName = "Util" } diff --git a/app/src/main/java/com/skyd/imomoe/model/interfaces/Info.kt b/app/src/main/java/com/skyd/imomoe/model/interfaces/Info.kt new file mode 100644 index 00000000..408a1c8a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/model/interfaces/Info.kt @@ -0,0 +1,9 @@ +package com.skyd.imomoe.model.interfaces + +// 接口版本,必须与APP的一致,APP才能应用 +val interfaceVersion = "202206242210" + +// 一般是接口没变,没有减少之前可用的方法,但多加了一些新的工具类或者方法功数据源调用。此时旧的数据源还可以使用 +val coexistentInterfaceVersions: Array = arrayOf( + "202205211312" +) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/model/util/JsoupUtil.kt b/app/src/main/java/com/skyd/imomoe/model/util/JsoupUtil.kt index b3b0b84b..4eb2d9ce 100644 --- a/app/src/main/java/com/skyd/imomoe/model/util/JsoupUtil.kt +++ b/app/src/main/java/com/skyd/imomoe/model/util/JsoupUtil.kt @@ -1,6 +1,12 @@ package com.skyd.imomoe.model.util import com.skyd.imomoe.config.Const +import com.skyd.imomoe.net.RetrofitManager +import com.skyd.imomoe.net.service.HtmlService +import com.skyd.imomoe.util.Util.toEncodedUrl +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.ResponseBody.Companion.toResponseBody import org.jsoup.Jsoup import org.jsoup.nodes.Document import kotlin.random.Random @@ -9,8 +15,45 @@ object JsoupUtil { /** * 获取没有运行js的html */ - fun getDocument(url: String): Document = - Jsoup.connect(url) - .userAgent(Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)]) - .get() -} \ No newline at end of file + suspend fun getDocument(url: String): Document { + return runCatching { + Jsoup.parse( + RetrofitManager.get().create(HtmlService::class.java).getHtml( + url.toEncodedUrl(), + Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)] + ).string() + ) + }.getOrThrow() + } + + fun getDocumentSynchronously(url: String): Document { + return Jsoup.parse( + RetrofitManager.get().create(HtmlService::class.java).getHtmlSynchronously( + url.toEncodedUrl(), + Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)] + ).execute().body()?.string() ?: "" + ) + } + + /** + * 指定解析类型 + */ + suspend fun getDocument(url: String, mediaType: MediaType): Document { + return runCatching { + Jsoup.parse( + RetrofitManager.get().create(HtmlService::class.java).getHtml( + url.toEncodedUrl(), + Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)] + ).byteString().toResponseBody(mediaType).string() + ) + }.getOrThrow() + } + + /** + * 解析GB2312字符编码 + */ + suspend fun getDocumentByGB2312Type(url: String): Document { + val mediaType = "text/html; charset=gb2312".toMediaType() + return getDocument(url, mediaType) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/model/util/ParseHtmlUtil.kt b/app/src/main/java/com/skyd/imomoe/model/util/ParseHtmlUtil.kt deleted file mode 100644 index 569170f9..00000000 --- a/app/src/main/java/com/skyd/imomoe/model/util/ParseHtmlUtil.kt +++ /dev/null @@ -1,386 +0,0 @@ -package com.skyd.imomoe.model.util - -import com.skyd.imomoe.bean.* -import com.skyd.imomoe.config.Api -import org.jsoup.nodes.Element -import org.jsoup.select.Elements -import java.util.ArrayList -import com.skyd.imomoe.config.Const.ViewHolderTypeString -import java.net.URL - -object ParseHtmlUtil { - - fun parseIframeSrc(element: Element): String { - return element.attr("src") - } - - fun parseHeroWrap( //banner - element: Element, - imageReferer: String, - type: String = ViewHolderTypeString.ANIME_COVER_6 - ): List { - val list: MutableList = ArrayList() - val liElements: Elements = element.select("[class=heros]").select("li") - for (i in liElements.indices) { - var episodeTitle = "" - var title = "" - var describe = "" - var url = "" - var cover = "" - val liChildren: Elements = liElements[i].children() - for (j in liChildren.indices) { - when (liChildren[j].tagName()) { - "a" -> { - url = liChildren[j].attr("href") - cover = liChildren[j].select("img").attr("src") - title = liChildren[j].select("p").first().ownText() - describe = liChildren[j].select("p").select("span").text() - } - "em" -> { - episodeTitle = liChildren[j].text() - } - } - } - list.add( - AnimeCoverBean( - type, url, Api.MAIN_URL + url, - title, ImageBean("", "", cover, imageReferer), - "", null, describe, - AnimeEpisodeDataBean("", "", episodeTitle), - null, - null - ) - ) - } - return list - } - - fun parseTers( - element: Element, - type: String = ViewHolderTypeString.EMPTY_STRING - ): List { - val list: MutableList = ArrayList() - val pElements: Elements = element.select("p") - for (i in pElements.indices) { - val pChildren: Elements = pElements[i].children() - for (j in pChildren.indices) { - when (pChildren[j].tagName()) { - "label" -> { - list.add( - ClassifyBean( - type, - "", - pChildren[j].text(), - ArrayList() - ) - ) - } - "a" -> { - if (list.size > 0) { - list[list.size - 1].classifyDataList.add( - ClassifyDataBean( - "", - pChildren[j].attr("href"), - Api.MAIN_URL + pChildren[j].attr("href"), - pChildren[j].text() - ) - ) - } - } - } - } - - - } - return list - } - - fun parseTlist( - element: Element, - type: String = ViewHolderTypeString.ANIME_COVER_5 - ): List> { - val ulList: MutableList> = ArrayList() - val ulElements: Elements = element.select("ul") - for (i in ulElements.indices) { - val liList: MutableList = ArrayList() - val liElements: Elements = ulElements[i].select("li") - for (j in liElements.indices) { - val episodeTitle = liElements[j].select("span").select("a").text() - val title = liElements[j].select("a")[1].text() - val url = liElements[j].select("a")[1].attr("href") - val episodeUrl = liElements[j].select("span").select("a").attr("href") - liList.add( - AnimeCoverBean( - type, url, Api.MAIN_URL + url, - title, null, "", null, null, - AnimeEpisodeDataBean("", episodeUrl, episodeTitle), - null, - null - ) - ) - } - ulList.add(liList) - } - return ulList - } - - fun parseTopli( - element: Element, - type: String = ViewHolderTypeString.ANIME_COVER_5 - ): List { - val animeShowList: MutableList = ArrayList() - val elements: Elements = element.select("ul").select("li") - for (i in elements.indices) { - var url: String - var title: String - if (elements[i].select("a").size >= 2) { //最近更新,显示地区的情况 - url = elements[i].select("a")[1].attr("href") - title = elements[i].select("a")[1].text() - if (elements[i].select("span")[0].children().size == 0) { //最近更新,不显示地区的情况 - url = elements[i].select("a")[0].attr("href") - title = elements[i].select("a")[0].text() - } - } else { //总排行榜 - url = elements[i].select("a")[0].attr("href") - title = elements[i].select("a")[0].text() - } - - val areaUrl = elements[i].select("span").select("a") - .attr("href") - val areaTitle = elements[i].select("span").select("a").text() - var episodeUrl = elements[i].select("b").select("a") - .attr("href") - val episodeTitle = elements[i].select("b").select("a").text() - val date = elements[i].select("em").text() - if (episodeUrl == "") { - episodeUrl = url - } - animeShowList.add( - AnimeCoverBean( - type, url, Api.MAIN_URL + url, - title, null, "", null, null, - AnimeEpisodeDataBean("", episodeUrl, episodeTitle), - AnimeAreaBean("", areaUrl, Api.MAIN_URL + areaUrl, areaTitle), - date - ) - ) - } - return animeShowList - } - - fun parseDnews( - element: Element, - imageReferer: String, - type: String = ViewHolderTypeString.ANIME_COVER_4 - ): List { - val animeShowList: MutableList = ArrayList() - val elements: Elements = element.select("ul").select("li") - for (i in elements.indices) { - val url = elements[i].select("a").attr("href") - var cover = elements[i].select("a").select("img").attr("src") - cover = getCoverUrl( - cover, - imageReferer - ) - val title = elements[i].select("p").select("a").text() - animeShowList.add( - AnimeCoverBean( - type, url, Api.MAIN_URL + url, - title, ImageBean("", "", cover, imageReferer), "" - ) - ) - } - return animeShowList - } - - fun parsePics( //一周动漫排行榜 - element: Element, - imageReferer: String, - type: String = ViewHolderTypeString.ANIME_COVER_3 - ): List { - val animeCover3List: MutableList = ArrayList() - val results: Elements = element.select("ul").select("li") - for (i in results.indices) { - val cover = results[i].select("a") - .select("img").attr("src") - val title = results[i].select("h2") - .select("a").text() - val url = results[i].select("h2") - .select("a").attr("href") - val episode = results[i].select("span") - .select("font").text() - val types = results[i].select("span")[1].select("a") - val animeType: MutableList = ArrayList() - for (j in types.indices) { - animeType.add( - AnimeTypeBean( - type, types[j].attr("href"), - Api.MAIN_URL + types[j].attr("href"), - types[j].text() - ) - ) - } - val describe = results[i].select("p").text() - animeCover3List.add( - AnimeCoverBean( - type, - url, - Api.MAIN_URL + url, - title, - ImageBean("", "", cover, imageReferer), - episode, - animeType, - describe - ) - ) - } - return animeCover3List - } - - fun parseLpic( //搜索 - element: Element, - imageReferer: String, - type: String = ViewHolderTypeString.ANIME_COVER_3 - ): List { - val animeCover3List: MutableList = ArrayList() - val results: Elements = element.select("ul").select("li") - for (i in results.indices) { - var cover = results[i].select("a").select("img").attr("src") - cover = getCoverUrl( - cover, - imageReferer - ) - val title = results[i].select("h2").select("a").attr("title") - val url = results[i].select("h2").select("a").attr("href") - val episode = results[i].select("span").select("font").text() - val types = results[i].select("span")[1].select("a") - val animeType: MutableList = ArrayList() - for (j in types.indices) { - animeType.add( - AnimeTypeBean( - type, types[j].attr("href"), - Api.MAIN_URL + types[j].attr("href"), - types[j].text() - ) - ) - } - val describe = results[i].select("p").text() - animeCover3List.add( - AnimeCoverBean( - type, - url, - Api.MAIN_URL + url, - title, - ImageBean("", "", cover, imageReferer), - episode, - animeType, - describe - ) - ) - } - return animeCover3List - } - - /** - * 只获取下一页的地址,没有下一页则返回null - */ - fun parseNextPages( - element: Element, - type: String = "pageNumber1" - ): PageNumberBean? { - val results: Elements = element.children() - var findCurrentPage = false - for (i in results.indices) { - if (findCurrentPage) { - if (results[i].className() == "a1") return null - val url = results[i].attr("href") - val title = results[i].text() - return PageNumberBean(type, url, Api.MAIN_URL + url, title) - } - if (results[i].tagName() == "span") findCurrentPage = true - } - return null - } - - fun parseDtit( - element: Element - ): String { - return element.children()[0].text() - } - - fun parseBotit( - element: Element - ): String { - return element.select("h2").text() - } - - fun parseMovurls( - element: Element, - selected: AnimeEpisodeDataBean? = null, - type: String = ViewHolderTypeString.ANIME_EPISODE_2 - ): List { - val animeEpisodeList: MutableList = ArrayList() - val elements: Elements = element.select("ul").select("li") - for (k in elements.indices) { - if (selected != null && elements[k].className() == "sel") { - selected.title = elements[k].select("a").text() - selected.actionUrl = elements[k].select("a").attr("href") - } - animeEpisodeList.add( - AnimeEpisodeDataBean( - type, - elements[k].select("a").attr("href"), - elements[k].select("a").text() - ) - ) - } - return animeEpisodeList - } - - fun parseImg( - element: Element, - imageReferer: String, - type: String = ViewHolderTypeString.ANIME_COVER_1 - ): List { - val animeShowList: MutableList = ArrayList() - val elements: Elements = element.select("ul").select("li") - for (i in elements.indices) { - val url = elements[i].select("a").attr("href") - var cover = elements[i].select("a").select("img").attr("src") - cover = getCoverUrl( - cover, - imageReferer - ) - val title = elements[i].select("[class=tname]").select("a").text() - var episode = "" - if (elements[i].select("p").size > 1) { - episode = elements[i].select("p")[1].select("a").text() - } - animeShowList.add( - AnimeCoverBean( - type, url, Api.MAIN_URL + url, - title, ImageBean("", "", cover, imageReferer), episode - ) - ) - } - return animeShowList - } - - fun getCoverUrl(cover: String, imageReferer: String): String { - return when { - cover.startsWith("//") -> { - try { - "${URL(imageReferer).protocol}:$cover" - } catch (e: Exception) { - e.printStackTrace() - cover - } - } - cover.startsWith("/") -> { - //url不全的情况 - Api.MAIN_URL + cover - } - else -> cover - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/net/DnsServer.kt b/app/src/main/java/com/skyd/imomoe/net/DnsServer.kt new file mode 100644 index 00000000..a3bdd319 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/net/DnsServer.kt @@ -0,0 +1,98 @@ +package com.skyd.imomoe.net + +import android.app.Activity +import com.skyd.imomoe.R +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.util.showToast +import okhttp3.HttpUrl.Companion.toHttpUrl + +object DnsServer { + class Dns(val dnsName: String, val dnsServer: String) : CharSequence { + override val length: Int + get() = dnsServer.length + + override fun get(index: Int): Char = dnsServer[index] + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return dnsServer.subSequence(startIndex, endIndex) + } + + override fun toString(): String = + if (dnsServer.isBlank()) dnsName else "$dnsName: $dnsServer" + + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + other === this -> true + is String -> other == dnsServer + is Dns -> other.dnsServer == this.dnsServer && other.dnsName == this.dnsName + else -> false + } + } + + override fun hashCode(): Int { + var result = dnsServer.hashCode() + result = 31 * result + dnsName.hashCode() + return result + } + } + + private infix fun String.to(that: String): Dns = Dns(this, that) + + val defaultDnsServer: List = listOf( + "不使用" to "", + "alidns" to "https://223.5.5.5/dns-query", + "Cloudflare" to "https://1.0.0.1/dns-query", + "Google" to "https://8.8.8.8/dns-query" + ) + + var dnsServer: String? = null + set(value) { + if (value == null || value == field) return + sharedPreferences().editor { putString("dnsServer", value) } + field = value + changeDnsServer(value) + } + get() { + return field ?: sharedPreferences() + .getString("dnsServer", null).apply { field = this } + } + + fun Activity.selectDnsServer() { + var initialSelection = -1 + defaultDnsServer.forEachIndexed { index, s -> + if (s.equals(dnsServer)) initialSelection = index + } + if (dnsServer.isNullOrBlank()) initialSelection = 0 + showListDialog( + title = getString(R.string.select_dns_server), + items = defaultDnsServer, + checkedItem = initialSelection, + neutralText = getString(R.string.custom_dns_server), + onNeutral = { dialog, _ -> + customDnsServer() + dialog.dismiss() + } + ) { _, _, itemIndex -> + dnsServer = defaultDnsServer[itemIndex].dnsServer + } + } + + fun Activity.customDnsServer() { + showInputDialog( + title = getString(R.string.custom_dns_server_dialog_title), + hint = getString(R.string.custom_dns_server_describe), + validator = { it.toString().run { isUrl() && !endsWith("/") } } + ) { _, _, text -> + val url = text.toString() + runCatching { + // 测试url合法性 + url.toHttpUrl() + dnsServer = url + }.onFailure { e -> + e.printStackTrace() + e.message?.showToast() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/net/Okhttp.kt b/app/src/main/java/com/skyd/imomoe/net/Okhttp.kt new file mode 100644 index 00000000..5d778475 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/net/Okhttp.kt @@ -0,0 +1,88 @@ +package com.skyd.imomoe.net + +import android.widget.Toast +import com.skyd.imomoe.BuildConfig +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.database.getAppDataBase +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.sharedPreferences +import com.skyd.imomoe.util.coil.CoilUtil +import com.skyd.imomoe.util.showToast +import okhttp3.Cache +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.dnsoverhttps.DnsOverHttps +import okhttp3.logging.HttpLoggingInterceptor +import java.io.File + +var urlMapEnabled: Boolean = appContext.sharedPreferences().getBoolean("urlMapEnabled", false) + set(value) { + if (field == value) return + appContext.sharedPreferences().editor { + putBoolean("urlMapEnabled", value) + } + field = value + } + +private val okhttpCache = Cache(File("cacheDir", "okhttpcache"), 10 * 1024 * 1024L) +private val bootstrapClient = OkHttpClient.Builder().cache(okhttpCache).apply { + addInterceptor(Interceptor { chain -> + val request: Request = chain.request() + // 不使用URL变换时,直接return + if (!urlMapEnabled) return@Interceptor chain.proceed(request) + + val builder: Request.Builder = request.newBuilder() + runCatching loop@{ + getAppDataBase().urlMapDao().getAllEnabled().forEach { + if (request.url.toString().startsWith(it.oldUrl)) { + builder.url(request.url.toString().replaceFirst(it.oldUrl, it.newUrl)) + return@loop + } + } + }.onFailure { + it.printStackTrace() + appContext.getString(R.string.url_map_error_okhttp, it.message.toString()) + .showToast(Toast.LENGTH_LONG) + } + return@Interceptor chain.proceed(builder.build()) + }) + if (BuildConfig.DEBUG) addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.HEADERS + }) +}.build() + +var dns: DnsOverHttps? = DnsServer.dnsServer.let { + if (it.isNullOrBlank()) null else { + runCatching { + DnsOverHttps.Builder().client(bootstrapClient) + .url(it.toHttpUrl()) + .build() + }.getOrElse { e -> + e.printStackTrace() + e.message?.showToast() + null + } + } +} + +var okhttpClient = bootstrapClient.newBuilder().apply { dns?.let { dns(it) } }.build() + +fun changeDnsServer(server: String) { + dns = if (server.isBlank()) null else { + runCatching { + DnsOverHttps.Builder().client(bootstrapClient) + .url(server.toHttpUrl()) + .build() + }.getOrElse { e -> + e.printStackTrace() + e.message?.showToast() + null + } + } + okhttpClient = bootstrapClient.newBuilder().apply { dns?.let { dns(it) } }.build() + RetrofitManager.get().client(okhttpClient) + CoilUtil.setOkHttpClient(okhttpClient) +} diff --git a/app/src/main/java/com/skyd/imomoe/net/RetrofitManager.kt b/app/src/main/java/com/skyd/imomoe/net/RetrofitManager.kt index 50e45575..eca7a00e 100644 --- a/app/src/main/java/com/skyd/imomoe/net/RetrofitManager.kt +++ b/app/src/main/java/com/skyd/imomoe/net/RetrofitManager.kt @@ -1,26 +1,49 @@ package com.skyd.imomoe.net import com.skyd.imomoe.config.Api +import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class RetrofitManager private constructor() { companion object { - val instance: RetrofitManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { - RetrofitManager() + fun setInstanceNull() { + instance = null + } + + @Volatile + private var instance: RetrofitManager? = null + get() { + if (field == null) { + synchronized(RetrofitManager::class) { + if (field == null) field = RetrofitManager() + } + } + return field + } + + @Synchronized + fun get(): RetrofitManager { + return instance!! } } +// val instance: RetrofitManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { +// RetrofitManager() +// } + + private val builder = Retrofit.Builder() + .baseUrl(Api.MAIN_URL.run { + if (endsWith("/")) this else "$this/" + }) + .addConverterFactory(GsonConverterFactory.create()) //设置数据解析器 - private var mRetrofit: Retrofit? = null + private var mRetrofit: Retrofit = builder.client(okhttpClient).build() - init { - mRetrofit = Retrofit.Builder() - .baseUrl(Api.MAIN_URL) - .addConverterFactory(GsonConverterFactory.create()) //设置数据解析器 - .build() + fun client(client: OkHttpClient) { + mRetrofit = builder.client(client).build() } - fun create(service: Class): T? { - return mRetrofit?.create(service) + fun create(service: Class): T { + return mRetrofit.create(service) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/net/service/DanmakuService.kt b/app/src/main/java/com/skyd/imomoe/net/service/DanmakuService.kt new file mode 100644 index 00000000..0847c29a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/net/service/DanmakuService.kt @@ -0,0 +1,36 @@ +package com.skyd.imomoe.net.service + +import com.skyd.imomoe.bean.danmaku.DanmakuData +import com.skyd.imomoe.bean.danmaku.DanmakuWrapper +import com.skyd.imomoe.config.Api +import com.skyd.imomoe.util.Util.getAppVersionCode +import com.skyd.imomoe.util.Util.getAppVersionName +import com.skyd.imomoe.util.getOsInfo +import okhttp3.ResponseBody +import retrofit2.http.* + +interface DanmakuService { + @FormUrlEncoded + @POST("${Api.DANMAKU_URL}/message/addOne") + suspend fun sendDanmaku( + @Field("content") content: String, + @Field("time") time: Double, // 秒时间戳 + @Field("episodeId") episodeId: String, + @Field("type") type: String, + @Field("color") color: String, + @Header("User-Agent") ua: String = "Imomoe ${getAppVersionName()}/${getAppVersionCode()} (${getOsInfo()})" + ): DanmakuWrapper + + @GET + suspend fun getCustomizeDanmaku(@Url url: String): ResponseBody + + // 查询弹幕 + @GET("${Api.DANMAKU_URL}/message/getSome") + suspend fun getDanmaku( + @Query("name") animeName: String, + @Query("number") episode: String, + @Query("type") type: String = "1", + @Header("User-Agent") ua: String = "Imomoe ${getAppVersionName()}/${getAppVersionCode()} (${getOsInfo()})" + ): DanmakuWrapper + +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/net/service/DanmuService.kt b/app/src/main/java/com/skyd/imomoe/net/service/DanmuService.kt deleted file mode 100644 index e6743c98..00000000 --- a/app/src/main/java/com/skyd/imomoe/net/service/DanmuService.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.skyd.imomoe.net.service - -import com.skyd.imomoe.bean.SendDanmuResultBean -import com.skyd.imomoe.config.Api -import okhttp3.RequestBody -import retrofit2.Call -import retrofit2.http.Body -import retrofit2.http.Headers -import retrofit2.http.POST -import retrofit2.http.Query - -interface DanmuService { - @Headers(value = ["Content-Type: application/json", "Accept: application/json"]) - @POST(Api.DANMU_URL) - fun sendDanmu( - @Query("ac") ac: String, - @Query("key") key: String, - @Body json: RequestBody - ): Call -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/net/service/DataSourceService.kt b/app/src/main/java/com/skyd/imomoe/net/service/DataSourceService.kt new file mode 100644 index 00000000..00cc8812 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/net/service/DataSourceService.kt @@ -0,0 +1,13 @@ +package com.skyd.imomoe.net.service + +import com.skyd.imomoe.bean.DataSourceRepositoryBeanWrapper +import com.skyd.imomoe.config.Api.Companion.dataSourceListJsonUrl +import retrofit2.http.GET +import retrofit2.http.Url + +interface DataSourceService { + @GET + suspend fun getDataSourceJson( + @Url url: String = dataSourceListJsonUrl(com.skyd.imomoe.model.interfaces.interfaceVersion) + ): DataSourceRepositoryBeanWrapper +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/net/service/HtmlService.kt b/app/src/main/java/com/skyd/imomoe/net/service/HtmlService.kt new file mode 100644 index 00000000..b91acbdf --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/net/service/HtmlService.kt @@ -0,0 +1,20 @@ +package com.skyd.imomoe.net.service + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.HEAD +import retrofit2.http.Header +import retrofit2.http.Url + +interface HtmlService { + @GET + suspend fun getHtml(@Url url: String, @Header("User-Agent") ua: String): ResponseBody + + @GET + fun getHtmlSynchronously(@Url url: String, @Header("User-Agent") ua: String): Call + + @HEAD + suspend fun getResponseHeader(@Url url: String): Response +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/Route.kt b/app/src/main/java/com/skyd/imomoe/route/Route.kt new file mode 100644 index 00000000..ef18251a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/Route.kt @@ -0,0 +1,6 @@ +package com.skyd.imomoe.route + + +object Route { + const val SCHEME = "anime" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/Router.kt b/app/src/main/java/com/skyd/imomoe/route/Router.kt new file mode 100644 index 00000000..e46febcb --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/Router.kt @@ -0,0 +1,57 @@ +package com.skyd.imomoe.route + +import android.content.Context +import android.net.Uri +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.route.processor.* +import com.skyd.imomoe.util.showToast + +object Router { + fun String.buildRouteUri(block: Uri.Builder.() -> Unit = {}): Uri { + return Uri.parse(this).buildUpon().apply { block() }.build() + } + + fun String.route(context: Context?) = buildRouteUri().route(context) + + fun Uri.route(context: Context?) { + try { + // 数据源路由优先 + if (DataSourceManager.getRouter()?.route(this, context) == true) return + val prefix = "$scheme://$authority" + var consumed = false + processorMap.forEach { + if (it.key == prefix) { + it.value.process(this, context) + consumed = true + return@forEach + } + } + if (!consumed) { + appContext.getString(R.string.unknown_route, this.toString()).showToast() + } + } catch (e: Exception) { + e.printStackTrace() + e.message?.showToast() + } + } + + val processorMap: HashMap = hashMapOf( + ClassifyActivityProcessor.route to ClassifyActivityProcessor, + DetailActivityProcessor.route to DetailActivityProcessor, + EpisodeDownloadProcessor.route to EpisodeDownloadProcessor, + JumpByUrlProcessor.route to JumpByUrlProcessor, + MonthAnimeActivityProcessor.route to MonthAnimeActivityProcessor, + OpenBrowserProcessor.route to OpenBrowserProcessor, + PlayActivityProcessor.route to PlayActivityProcessor, + PlayDownloadProcessor.route to PlayDownloadProcessor, + PlayDownloadM3U8Processor.route to PlayDownloadM3U8Processor, + RankActivityProcessor.route to RankActivityProcessor, + SearchActivityProcessor.route to SearchActivityProcessor, + StartActivityProcessor.route to StartActivityProcessor, + ConfigDataSourceActivityProcessor.route to ConfigDataSourceActivityProcessor, + UrlMapActivityProcessor.route to UrlMapActivityProcessor, + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/ClassifyActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/ClassifyActivityProcessor.kt new file mode 100644 index 00000000..d80d2003 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/ClassifyActivityProcessor.kt @@ -0,0 +1,35 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.ClassifyActivity + +/** + * 跳转到分类页面对应项目 + */ +object ClassifyActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("context isn't Activity") + val partUrl = uri.getQueryParameter("partUrl") + val classifyTitle = uri.getQueryParameter("classifyTitle") + val classifyTabTitle = uri.getQueryParameter("classifyTabTitle") + context.startActivity( + Intent(context, ClassifyActivity::class.java) + .putExtra("partUrl", partUrl) + .putExtra("classifyTitle", classifyTitle) + .putExtra("classifyTabTitle", classifyTabTitle) + ) + } + + /** + * query: + * partUrl 目标网址 + * classifyTabTitle 分类标题,可选 + * classifyTitle 分类子项标题,可选 + */ + override val route: String + get() = "${Route.SCHEME}://classify.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/ConfigDataSourceActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/ConfigDataSourceActivityProcessor.kt new file mode 100644 index 00000000..223c906e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/ConfigDataSourceActivityProcessor.kt @@ -0,0 +1,29 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.ConfigDataSourceActivity + +/** + * 跳转到配置数据源 + */ +object ConfigDataSourceActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("context isn't Activity") + val selectPageIndex = uri.getQueryParameter("selectPageIndex")?.toIntOrNull() ?: 0 + context.startActivity( + Intent(context, ConfigDataSourceActivity::class.java) + .putExtra(ConfigDataSourceActivity.SELECT_PAGE_INDEX, selectPageIndex) + ) + } + + /** + * query: + * selectPageIndex 0表示默认显示本地数据页面;1表示显示数据源商店页面 + */ + override val route: String + get() = "${Route.SCHEME}://datasource.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/DetailActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/DetailActivityProcessor.kt new file mode 100644 index 00000000..ad49f5d6 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/DetailActivityProcessor.kt @@ -0,0 +1,34 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.AnimeDetailActivity + +/** + * 跳转到分类页面对应项目 + */ +object DetailActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + val partUrl = uri.getQueryParameter("partUrl") + if (context is Activity) { + context.startActivity( + Intent(context, AnimeDetailActivity::class.java) + .putExtra("partUrl", partUrl) + ) + } else context?.startActivity( + Intent(context, AnimeDetailActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra("partUrl", partUrl) + ) + } + + /** + * query: + * partUrl 目标网址 + */ + override val route: String + get() = "${Route.SCHEME}://detail.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/EpisodeDownloadProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/EpisodeDownloadProcessor.kt new file mode 100644 index 00000000..3e0a7b7d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/EpisodeDownloadProcessor.kt @@ -0,0 +1,36 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.AnimeDownloadActivity + +/** + * 转到缓存的番剧页面(显示每一部) + */ +object EpisodeDownloadProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("context isn't Activity") + val type = uri.getQueryParameter("type")?.toInt() + val animeTitle = uri.getQueryParameter("animeTitle") + val directoryName = uri.getQueryParameter("directoryName") + context.startActivity( + Intent(context, AnimeDownloadActivity::class.java) + .putExtra("mode", 1) + .putExtra("actionBarTitle", animeTitle) + .putExtra("directoryName", directoryName) + .putExtra("path", type) + ) + } + + /** + * query: + * type 0存储在内部 or 1存储在外部 + * animeTitle 动漫标题 + * directoryName 动漫文件夹(不是完整路径)名称 + */ + override val route: String + get() = "${Route.SCHEME}://episode.download.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/JumpByUrlProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/JumpByUrlProcessor.kt new file mode 100644 index 00000000..46394031 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/JumpByUrlProcessor.kt @@ -0,0 +1,56 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.net.Uri +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.config.Api +import com.skyd.imomoe.ext.showInputDialog +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.showToast +import java.net.URL + +/** + * 根据网址跳转 + */ +object JumpByUrlProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("JumpByUrlProcessor: context isn't Activity") + var destinationUrl = uri.getQueryParameter("url").orEmpty() + if (destinationUrl.isBlank() || destinationUrl == "/") { + context.showInputDialog( + hint = context.getString(R.string.input_a_website) + ) { _, _, text -> + try { + var url = text.toString() + if (!url.matches(Regex("^.+://.*"))) url = "http://$url" + if (url.startsWith(Api.MAIN_URL)) { + url.replace(Api.MAIN_URL, "").route(context) + } else URL(url).file.route(context) + } catch (e: Exception) { + appContext.getString(R.string.website_format_error).showToast() + e.printStackTrace() + } + } + } else { + try { + if (!destinationUrl.matches(Regex("^.+://.*"))) { + destinationUrl = "http://$destinationUrl" + } + URL(destinationUrl).file.route(context) + } catch (e: Exception) { + appContext.getString(R.string.website_format_error).showToast() + e.printStackTrace() + } + } + } + + /** + * query: + * url 网页url。可选,若不设置,则弹出对话框输入 + */ + override val route: String + get() = "${Route.SCHEME}://jumpByUrl.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/MonthAnimeActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/MonthAnimeActivityProcessor.kt new file mode 100644 index 00000000..eb85891d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/MonthAnimeActivityProcessor.kt @@ -0,0 +1,29 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.MonthAnimeActivity + +/** + * 跳转到搜索界面 + */ +object MonthAnimeActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("context isn't Activity") + val partUrl = uri.getQueryParameter("partUrl") + context.startActivity( + Intent(context, MonthAnimeActivity::class.java) + .putExtra("partUrl", partUrl) + ) + } + + /** + * query: + * partUrl 目标网址 + */ + override val route: String + get() = "${Route.SCHEME}://monthAnime.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/OpenAppProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/OpenAppProcessor.kt new file mode 100644 index 00000000..c2cd08ae --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/OpenAppProcessor.kt @@ -0,0 +1,22 @@ +package com.skyd.imomoe.route.processor + +import android.content.Context +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.route.Router.route + +/** + * 启动app + */ +object OpenAppProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + uri.getQueryParameter("route")?.route(context) + } + + /** + * query: + * route app内route 可选 + */ + override val route: String + get() = "${Route.SCHEME}://open.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/OpenBrowserProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/OpenBrowserProcessor.kt new file mode 100644 index 00000000..48e91a46 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/OpenBrowserProcessor.kt @@ -0,0 +1,24 @@ +package com.skyd.imomoe.route.processor + +import android.content.Context +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.util.Util + +/** + * 打开浏览器 + */ +object OpenBrowserProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + val url = uri.getQueryParameter("url") + ?: error("can't get \"url\" parameter from Route.ROUTE_OPEN_BROWSER") + Util.openBrowser(url) + } + + /** + * query: + * url 目标网址 + */ + override val route: String + get() = "${Route.SCHEME}://openBrowser.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/PlayActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/PlayActivityProcessor.kt new file mode 100644 index 00000000..340a7387 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/PlayActivityProcessor.kt @@ -0,0 +1,32 @@ +package com.skyd.imomoe.route.processor + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.PlayActivity + +/** + * 跳转到播放界面 + */ +object PlayActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + context ?: error("context is null") + val partUrl = uri.getQueryParameter("partUrl") + val detailPartUrl = uri.getQueryParameter("detailPartUrl") + context.startActivity( + Intent(context, PlayActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra("partUrl",partUrl) + .putExtra("detailPartUrl", detailPartUrl) + ) + } + + /** + * query: + * partUrl 目标网址。 + * detailPartUrl 目标网址。可选 + */ + override val route: String + get() = "${Route.SCHEME}://play.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/PlayDownloadM3U8Processor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/PlayDownloadM3U8Processor.kt new file mode 100644 index 00000000..3b328060 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/PlayDownloadM3U8Processor.kt @@ -0,0 +1,19 @@ +package com.skyd.imomoe.route.processor + +import android.content.Context +import android.net.Uri +import android.widget.Toast +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.util.showToast + +/** + * 播放缓存的视频(m3u8格式) + */ +object PlayDownloadM3U8Processor : Processor() { + override fun process(uri: Uri, context: Context?) { + "暂不支持m3u8格式 :(".showToast(Toast.LENGTH_LONG) + } + + override val route: String + get() = "${Route.SCHEME}://m3u8.play.download.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/PlayDownloadProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/PlayDownloadProcessor.kt new file mode 100644 index 00000000..7548e278 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/PlayDownloadProcessor.kt @@ -0,0 +1,35 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.SimplePlayActivity + +/** + * 播放缓存的视频(非m3u8格式) + */ +object PlayDownloadProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("context isn't Activity") + val filePath = uri.getQueryParameter("filePath") + val animeTitle = uri.getQueryParameter("animeTitle") + val episodeTitle = uri.getQueryParameter("episodeTitle") + context.startActivity( + Intent(context, SimplePlayActivity::class.java) + .putExtra(SimplePlayActivity.URL, "file://$filePath") + .putExtra(SimplePlayActivity.ANIME_TITLE, animeTitle) + .putExtra(SimplePlayActivity.EPISODE_TITLE, episodeTitle) + ) + } + + /** + * query: + * filePath 视频文件路径 + * animeTitle 动漫标题 + * episodeTitle 当前集标题 + */ + override val route: String + get() = "${Route.SCHEME}://play.download.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/Processor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/Processor.kt new file mode 100644 index 00000000..5b51c4d3 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/Processor.kt @@ -0,0 +1,9 @@ +package com.skyd.imomoe.route.processor + +import android.content.Context +import android.net.Uri + +abstract class Processor { + abstract val route: String + abstract fun process(uri: Uri, context: Context?) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/RankActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/RankActivityProcessor.kt new file mode 100644 index 00000000..64e52d79 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/RankActivityProcessor.kt @@ -0,0 +1,23 @@ +package com.skyd.imomoe.route.processor + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.RankActivity + +/** + * 跳转到排行榜界面 + */ +object RankActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + context!! + context.startActivity(Intent(context, RankActivity::class.java)) + } + + /** + * query: + */ + override val route: String + get() = "${Route.SCHEME}://rank.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/SearchActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/SearchActivityProcessor.kt new file mode 100644 index 00000000..5b50f4da --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/SearchActivityProcessor.kt @@ -0,0 +1,32 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.SearchActivity + +/** + * 跳转到搜索界面 + */ +object SearchActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("context isn't Activity") + val keyword = uri.getQueryParameter("keyword") + val pageNumber = uri.getQueryParameter("pageNumber") + context.startActivity( + Intent(context, SearchActivity::class.java) + .putExtra("keyword", keyword) + .putExtra("pageNumber", pageNumber) + ) + } + + /** + * query: + * keyword 关键词 + * pageNumber 第几页 + */ + override val route: String + get() = "${Route.SCHEME}://search.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/StartActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/StartActivityProcessor.kt new file mode 100644 index 00000000..a0851b98 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/StartActivityProcessor.kt @@ -0,0 +1,31 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route + +/** + * 启动activity + */ +object StartActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + val cls = Class.forName(uri.getQueryParameter("cls").orEmpty()) + context ?: error("StartActivityProcessor: activity is null") + if (context is Activity) { + context.startActivity(Intent(context, cls)) + } else { + context.startActivity( + Intent(context, cls).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } + } + + /** + * query: + * cls 目标Activity qualifiedName + */ + override val route: String + get() = "${Route.SCHEME}://startActivity.anime.app" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/route/processor/UrlMapActivityProcessor.kt b/app/src/main/java/com/skyd/imomoe/route/processor/UrlMapActivityProcessor.kt new file mode 100644 index 00000000..5976f4ab --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/route/processor/UrlMapActivityProcessor.kt @@ -0,0 +1,47 @@ +package com.skyd.imomoe.route.processor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.skyd.imomoe.route.Route +import com.skyd.imomoe.view.activity.UrlMapActivity + +/** + * 跳转到URL前缀替换页面 + */ +object UrlMapActivityProcessor : Processor() { + override fun process(uri: Uri, context: Context?) { + if (context !is Activity) error("context isn't Activity") + val jsonData = uri.getQueryParameter(JSON_DATA) + val autoAdd = uri.getQueryParameter(AUTO_ADD) + val autoAddAndFinish = uri.getQueryParameter(AUTO_ADD_AND_FINISH) + val enabled = uri.getQueryParameter(ENABLED) + context.startActivity( + Intent(context, UrlMapActivity::class.java) + .putExtra(UrlMapActivity.JSON_DATA, jsonData) + .putExtra(UrlMapActivity.AUTO_ADD, autoAdd?.toBooleanStrictOrNull() ?: false) + .putExtra(UrlMapActivity.ENABLED, enabled?.toBooleanStrictOrNull() ?: false) + .putExtra( + UrlMapActivity.AUTO_ADD_AND_FINISH, + autoAddAndFinish?.toBooleanStrictOrNull() ?: false + ) + ) + } + + fun startActivityForResult() { + + } + + /** + * query: + * jsonData 批量添加URL替换的json数据 + */ + override val route: String + get() = "${Route.SCHEME}://urlMap.anime.app" + + const val ENABLED = "enabled" + const val JSON_DATA = "jsonData" + const val AUTO_ADD = "autoAdd" + const val AUTO_ADD_AND_FINISH = "autoAddAndFinish" +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/state/DataState.kt b/app/src/main/java/com/skyd/imomoe/state/DataState.kt new file mode 100644 index 00000000..0481bb64 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/state/DataState.kt @@ -0,0 +1,19 @@ +package com.skyd.imomoe.state + +sealed class DataState { + object Empty : DataState() + object Refreshing : DataState() + object Loading : DataState() + + data class Error( + val message: String = "" + ) : DataState() + + data class Success( + val data: T + ) : DataState() + + fun read(): T = (this as Success).data + + fun readOrNull(): T? = if (this is Success) read() else null +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/CrashHandler.kt b/app/src/main/java/com/skyd/imomoe/util/CrashHandler.kt index 3767ce2e..eb5d551c 100644 --- a/app/src/main/java/com/skyd/imomoe/util/CrashHandler.kt +++ b/app/src/main/java/com/skyd/imomoe/util/CrashHandler.kt @@ -1,7 +1,7 @@ package com.skyd.imomoe.util +import android.annotation.SuppressLint import android.content.Context -import android.util.Log import com.skyd.imomoe.view.activity.CrashActivity import java.io.PrintWriter import java.io.StringWriter @@ -17,6 +17,8 @@ class CrashHandler private constructor(val context: Context) : Thread.UncaughtEx */ override fun uncaughtException(thread: Thread, ex: Throwable) { try { + // flurry使用UncaughtExceptionHandler捕获异常,与原有的冲突,因此要再调用一次flurry的uncaughtException + com.flurry.sdk.n.a().f.uncaughtException(thread, ex) val stringWriter = StringWriter() val printWriter = PrintWriter(stringWriter) ex.printStackTrace(printWriter) @@ -27,7 +29,7 @@ class CrashHandler private constructor(val context: Context) : Thread.UncaughtEx } printWriter.close() val unCaughtException = stringWriter.toString() //详细错误日志 - Log.e("crash info", unCaughtException) + logE("crash info", unCaughtException) CrashActivity.start(context, unCaughtException) exitProcess(0) } catch (e: Exception) { @@ -37,6 +39,7 @@ class CrashHandler private constructor(val context: Context) : Thread.UncaughtEx } companion object { + @SuppressLint("StaticFieldLeak") private var instance: CrashHandler? = null /** diff --git a/app/src/main/java/com/skyd/imomoe/util/Debug.kt b/app/src/main/java/com/skyd/imomoe/util/Debug.kt index 86e7d13d..665665fb 100644 --- a/app/src/main/java/com/skyd/imomoe/util/Debug.kt +++ b/app/src/main/java/com/skyd/imomoe/util/Debug.kt @@ -5,7 +5,7 @@ import com.skyd.imomoe.BuildConfig /** * 只有debug包才会执行表达式 */ -fun debug(lambda: () -> Unit) { +inline fun debug(lambda: () -> Unit) { if (BuildConfig.DEBUG) { lambda.invoke() } @@ -14,7 +14,7 @@ fun debug(lambda: () -> Unit) { /** * 只有release包才会执行表达式 */ -fun release(lambda: () -> Unit) { +inline fun release(lambda: () -> Unit) { if (!BuildConfig.DEBUG) { lambda.invoke() } diff --git a/app/src/main/java/com/skyd/imomoe/util/File.kt b/app/src/main/java/com/skyd/imomoe/util/File.kt deleted file mode 100644 index 3a0c4246..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/File.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.skyd.imomoe.util - -import android.net.Uri -import android.os.Build -import androidx.core.content.FileProvider -import com.skyd.imomoe.App -import java.io.File - -val File.uri: Uri - get() = if (Build.VERSION.SDK_INT >= 24) { - FileProvider.getUriForFile(App.context, "com.skyd.imomoe.fileProvider", this) - } else { - Uri.fromFile(this) - } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/HeadsetEventUtil.kt b/app/src/main/java/com/skyd/imomoe/util/HeadsetEventUtil.kt new file mode 100644 index 00000000..559fd072 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/HeadsetEventUtil.kt @@ -0,0 +1,91 @@ +package com.skyd.imomoe.util + +import android.content.* +import android.media.AudioManager +import com.shuyu.gsyvideoplayer.GSYVideoManager + +//lateinit var mediaSession: MediaSession + +// 断开耳机后暂停播放视频、点击耳机按钮控制播放 +fun Context.initHeadsetEventReceiver() { + registerReceiver( + HeadsetEventReceiver(), + IntentFilter().apply { + addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) + } + ) + +// mediaSession = MediaSession(this, packageName) +// val mediaComponent = ComponentName(packageName, MediaButtonReceiver::class.java.name) +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { +// mediaSession.setMediaButtonBroadcastReceiver(mediaComponent) +// } else { +// (getSystemService(AUDIO_SERVICE) as? AudioManager) +// ?.registerMediaButtonEventReceiver(mediaComponent) +// } +// /* set flags to handle media buttons */ +// mediaSession.setFlags( +// MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or +// MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS +// ) +// mediaSession.setPlaybackState( +// PlaybackState +// .Builder() +// .setState( +// playerState2PlaybackState(GSYVideoManager.instance().lastState), +// GSYVideoManager.instance().currentPosition, +// 1f +// ) +// .build() +// ) +// mediaSession.setCallback(object : MediaSession.Callback() { +// override fun onMediaButtonEvent(intent: Intent): Boolean { +// if (Intent.ACTION_MEDIA_BUTTON != intent.action) { +// return super.onMediaButtonEvent(intent) +// } +// val event: KeyEvent? = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) +// return if (event == null || event.action != KeyEvent.ACTION_UP) { +// super.onMediaButtonEvent(intent) +// } else true +// } +// +// override fun onPlay() { +// super.onPlay() +// GSYVideoManager.onResume() +// } +// +// override fun onPause() { +// super.onPause() +// GSYVideoManager.onPause() +// } +// }) +// +// if (!mediaSession.isActive) { +// mediaSession.isActive = true +// } +} +// +//private fun playerState2PlaybackState(playerState: Int): Int { +// return when (playerState) { +// GSYVideoView.CURRENT_STATE_PLAYING -> PlaybackState.STATE_PLAYING +// GSYVideoView.CURRENT_STATE_ERROR -> PlaybackState.STATE_ERROR +// GSYVideoView.CURRENT_STATE_PAUSE -> PlaybackState.STATE_PAUSED +// GSYVideoView.CURRENT_STATE_PREPAREING -> PlaybackState.STATE_CONNECTING +// GSYVideoView.CURRENT_STATE_NORMAL -> PlaybackState.STATE_NONE +// GSYVideoView.CURRENT_STATE_PLAYING_BUFFERING_START -> PlaybackState.STATE_BUFFERING +// GSYVideoView.CURRENT_STATE_AUTO_COMPLETE -> PlaybackState.STATE_STOPPED +// else -> PlaybackState.STATE_NONE +// } +//} + +// 使用代码动态注册 +class HeadsetEventReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent ?: return + when (intent.action) { + AudioManager.ACTION_AUDIO_BECOMING_NOISY -> { + GSYVideoManager.onPause() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/Log.kt b/app/src/main/java/com/skyd/imomoe/util/Log.kt new file mode 100644 index 00000000..beeaf0ff --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/Log.kt @@ -0,0 +1,70 @@ +package com.skyd.imomoe.util + +import android.util.Log + + +//class LogManager { +// companion object { +// val instance: LogManager by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { LogManager() } +// +// const val VERBOSE = "V" +// const val DEBUG = "D" +// const val INFO = "I" +// const val WARN = "W" +// const val ERROR = "E" +// +// val CACHE_PATH = appContextgetExternalFilesDir(null).toString() + "/Logs/" +// } +// +// private val dateFormat = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.getDefault()) +// +// private val fileName: String = dateFormat.format(Date(System.currentTimeMillis())) +// +// private val diskLruCache: DiskLruCache = DiskLruCache.open(File(CACHE_PATH), 1, 1, 7 * 1024) +// +// fun add(priority: String, tag: String, msg: String) { +// val editor: DiskLruCache.Editor? = diskLruCache.edit(fileName) +// if (editor != null) { +// val outputStream = OutputStreamWriter(editor.newOutputStream(0), Charsets.UTF_8) +// FileWriter(outputStream) +// outputStream.write("${dateFormat.format(Date(System.currentTimeMillis()))} ${priority}/${tag}: $msg") +// editor.commit() +// } +// diskLruCache.flush() +// } +// +// fun get(): InputStream? { +// val snapShot: DiskLruCache.Snapshot? = diskLruCache.get(fileName) +// return snapShot?.getInputStream(0) +// } +//} + +fun logV(tag: String, msg: String) { + Log.v(tag, msg) +} + +fun logD(tag: String, msg: String) { + Log.d(tag, msg) +} + +fun logI(tag: String, msg: String) { + Log.i(tag, msg) +} + +fun logW(tag: String, msg: String) { + Log.w(tag, msg) +} + +fun logE(tag: String, msg: String) { + Log.e(tag, msg) +} + +fun Any.logV(msg: String) = logV(javaClass.simpleName, msg) + +fun Any.logD(msg: String) = logD(javaClass.simpleName, msg) + +fun Any.logI(msg: String) = logI(javaClass.simpleName, msg) + +fun Any.logW(msg: String) = logW(javaClass.simpleName, msg) + +fun Any.logE(msg: String) = logE(javaClass.simpleName, msg) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/MD5.kt b/app/src/main/java/com/skyd/imomoe/util/MD5.kt deleted file mode 100644 index 9e2879ea..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/MD5.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.skyd.imomoe.util - -import okio.ByteString -import java.io.File -import java.io.FileInputStream -import java.io.IOException -import java.io.UnsupportedEncodingException -import java.math.BigInteger -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException - - -object MD5 { - fun getMD5(f: File): String? { - var bi: BigInteger? = null - try { - val buffer = ByteArray(8192) - var len = 0 - val md = MessageDigest.getInstance("MD5") - val fis = FileInputStream(f) - while (fis.read(buffer).also { len = it } != -1) { - md.update(buffer, 0, len) - } - fis.close() - val b = md.digest() - bi = BigInteger(1, b) - } catch (e: NoSuchAlgorithmException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } - return bi?.toString(16) - } - - fun getMD5(s: String): String { - try { - val messageDigest = MessageDigest.getInstance("MD5") - val md5bytes = messageDigest.digest(s.toByteArray(charset("UTF-8"))) - return ByteString.of(*md5bytes).hex() - } catch (e: NoSuchAlgorithmException) { - throw AssertionError(e) - } catch (e: UnsupportedEncodingException) { - throw AssertionError(e) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/OSInfo.kt b/app/src/main/java/com/skyd/imomoe/util/OSInfo.kt new file mode 100644 index 00000000..3a7f883d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/OSInfo.kt @@ -0,0 +1,50 @@ +package com.skyd.imomoe.util + +import android.annotation.SuppressLint +import android.os.Build +import java.lang.reflect.Method + + +fun getOsInfo(): String { + return if (isHarmonyOs()) { + "Harmony " + getHarmonyVersion() + } else { + "Android " + Build.VERSION.RELEASE + } +} + +/** + * 是否为鸿蒙系统 + */ +fun isHarmonyOs(): Boolean { + return try { + val buildExClass = Class.forName("com.huawei.system.BuildEx") + val osBrand: Any = buildExClass.getMethod("getOsBrand").invoke(buildExClass)!! + "Harmony".equals(osBrand.toString(), ignoreCase = true) + } catch (t: Throwable) { + false + } +} + +/** + * 获取鸿蒙系统版本号 + * + * @return 版本号 + */ +fun getHarmonyVersion(): String { + return getProp("hw_sc.build.platform.version", "") +} + +@Suppress("SameParameterValue") +@SuppressLint("PrivateApi") +private fun getProp(property: String, defaultValue: String): String { + try { + val spClz: Class<*> = Class.forName("android.os.SystemProperties") + val method: Method = spClz.getDeclaredMethod("get", String::class.java) + val value = method.invoke(spClz, property) as? String + return if (value.isNullOrEmpty()) defaultValue else value + } catch (e: Throwable) { + e.printStackTrace() + } + return defaultValue +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/PushHelper.kt b/app/src/main/java/com/skyd/imomoe/util/PushHelper.kt deleted file mode 100644 index bea1df78..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/PushHelper.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.skyd.imomoe.util - -import android.content.Context -import android.util.Log -import com.umeng.message.PushAgent -import com.umeng.message.api.UPushRegisterCallback - -object PushHelper { - fun init(context: Context) { - //获取消息推送实例 - val pushAgent = PushAgent.getInstance(context) - //注册推送服务,每次调用register方法都会回调该接口 - pushAgent.register(object : UPushRegisterCallback { - override fun onSuccess(deviceToken: String) { - //注册成功会返回deviceToken deviceToken是推送消息的唯一标志 - Log.i("PushHelper", "注册成功:deviceToken:--> $deviceToken") - } - - override fun onFailure(errCode: String, errDesc: String) { - Log.e("PushHelper", "注册失败:--> code:$errCode, desc:$errDesc") - } - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/RvAdapter.kt b/app/src/main/java/com/skyd/imomoe/util/RvAdapter.kt deleted file mode 100644 index 1160d5cf..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/RvAdapter.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.skyd.imomoe.util - -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.bean.GetDataEnum - -/** - * 根据GetDataEnum状态自动更新 - * @param type 刷新/加载更多/加载失败 - * @param deltaDataSet 新数据集 - * @param dataSet 构造adapter时传入的list - */ -fun RecyclerView.Adapter.smartNotifyDataSetChanged( - type: GetDataEnum, - deltaDataSet: MutableList, - dataSet: MutableList -) { - when (type) { - GetDataEnum.REFRESH -> { - val count = dataSet.size - dataSet.clear() - notifyItemRangeRemoved(0, count) - dataSet.addAll(deltaDataSet) - notifyItemRangeInserted(0, deltaDataSet.size) - } - GetDataEnum.LOAD_MORE -> { - val index = dataSet.size - dataSet.addAll(deltaDataSet) - notifyItemRangeInserted(index, deltaDataSet.size) - } - GetDataEnum.FAILED -> { - val count = dataSet.size - dataSet.clear() - notifyItemRangeRemoved(0, count) - } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/Share.kt b/app/src/main/java/com/skyd/imomoe/util/Share.kt index d6cf39ad..36a91750 100644 --- a/app/src/main/java/com/skyd/imomoe/util/Share.kt +++ b/app/src/main/java/com/skyd/imomoe/util/Share.kt @@ -4,10 +4,9 @@ import android.app.Activity import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageManager -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.util.Util.copyText2Clipboard -import com.skyd.imomoe.util.Util.showToast +import com.skyd.imomoe.appContext +import com.skyd.imomoe.util.Util.copy2Clipboard object Share { const val SHARE_QQ = 1 @@ -17,7 +16,7 @@ object Share { fun isInstalled(packageName: String): Boolean { val packageInfo: PackageInfo? = try { - App.context.packageManager.getPackageInfo(packageName, 0) + appContext.packageManager.getPackageInfo(packageName, 0) } catch (e: PackageManager.NameNotFoundException) { null } @@ -65,7 +64,7 @@ object Share { ) } SHARE_LINK -> { - copyText2Clipboard(activity, shareContent) + shareContent.copy2Clipboard(activity) activity.resources.getString(R.string.already_copy_to_clipboard).showToast() } } diff --git a/app/src/main/java/com/skyd/imomoe/util/SharedPreferences.kt b/app/src/main/java/com/skyd/imomoe/util/SharedPreferences.kt deleted file mode 100644 index 38d382a0..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/SharedPreferences.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.skyd.imomoe.util - -import android.content.Context -import android.content.SharedPreferences - -fun Context.sharedPreferences(name: String = "App"): SharedPreferences = getSharedPreferences(name, Context.MODE_PRIVATE) -fun SharedPreferences.editor(editorBuilder: SharedPreferences.Editor.() -> Unit) = edit().apply(editorBuilder).apply() \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/Shortcut.kt b/app/src/main/java/com/skyd/imomoe/util/Shortcut.kt new file mode 100644 index 00000000..0532a19a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/Shortcut.kt @@ -0,0 +1,59 @@ +package com.skyd.imomoe.util + +import android.app.Activity +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.os.Build +import com.skyd.imomoe.R +import com.skyd.imomoe.config.Const.ShortCuts.ACTION_EVERYDAY +import com.skyd.imomoe.config.Const.ShortCuts.ID_DOWNLOAD +import com.skyd.imomoe.config.Const.ShortCuts.ID_EVERYDAY +import com.skyd.imomoe.config.Const.ShortCuts.ID_FAVORITE +import com.skyd.imomoe.view.activity.AnimeDownloadActivity +import com.skyd.imomoe.view.activity.FavoriteActivity +import com.skyd.imomoe.view.activity.MainActivity + +/** + * 设置app图标快捷菜单 + */ +fun Activity.registerShortcuts() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + val mShortcutManager = getSystemService(ShortcutManager::class.java) + val shortcutInfoList = listOf( + ShortcutInfo.Builder(this, ID_FAVORITE) + .setShortLabel(getString(R.string.shortcuts_favorite_short)) + .setLongLabel(getString(R.string.shortcuts_favorite_long)) + .setIcon( + Icon.createWithResource(this, R.drawable.layerlist_shortcuts_favorite_24) + ) + .setIntent( + Intent(this, FavoriteActivity::class.java).setAction(Intent.ACTION_VIEW) + ) + .build(), + ShortcutInfo.Builder(this, ID_EVERYDAY) + .setShortLabel(getString(R.string.shortcuts_everyday_short)) + .setLongLabel(getString(R.string.shortcuts_everyday_long)) + .setIcon( + Icon.createWithResource(this, R.drawable.layerlist_shortcuts_everyday_24) + ) + .setIntent( + Intent(this, MainActivity::class.java).setAction(ACTION_EVERYDAY) + ) + .build(), + ShortcutInfo.Builder(this, ID_DOWNLOAD) + .setShortLabel(getString(R.string.shortcuts_download_short)) + .setLongLabel(getString(R.string.shortcuts_download_long)) + .setIcon( + Icon.createWithResource(this, R.drawable.layerlist_shortcuts_download_24) + ) + .setIntent( + Intent(this, AnimeDownloadActivity::class.java) + .setAction(Intent.ACTION_VIEW) + ) + .build() + ) + mShortcutManager.dynamicShortcuts = shortcutInfoList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/System.kt b/app/src/main/java/com/skyd/imomoe/util/System.kt new file mode 100644 index 00000000..bda2e8e8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/System.kt @@ -0,0 +1,29 @@ +package com.skyd.imomoe.util + +import android.app.ActivityManager +import android.content.Context +import android.os.Process +import com.skyd.imomoe.ext.toTimeString +import java.util.* +import kotlin.system.exitProcess + + +fun currentTimeSecond() = System.currentTimeMillis() / 1000 + +fun currentDate( + pattern: String = "yyyy-MM-dd HH:mm:ss", + locale: Locale = Locale.getDefault() +) = System.currentTimeMillis().toTimeString(pattern, locale) + +fun Context.killApplicationProcess() { + // 注意:不能先杀掉主进程,否则逻辑代码无法继续执行,需先杀掉相关进程最后杀掉主进程 + val mActivityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val mList: List = mActivityManager.runningAppProcesses + for (runningAppProcessInfo in mList) { + if (runningAppProcessInfo.pid != Process.myPid()) { + Process.killProcess(runningAppProcessInfo.pid) + } + } + Process.killProcess(Process.myPid()) + exitProcess(0) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/Text.kt b/app/src/main/java/com/skyd/imomoe/util/Text.kt deleted file mode 100644 index cf21e062..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/Text.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.skyd.imomoe.util - -import com.skyd.imomoe.BuildConfig - -object Text { - /** - * 屏蔽带有某些关键字的弹幕 - * - * @return 若屏蔽此字符串,则返回true,否则false - */ - fun String.shield(): Boolean { - BuildConfig.SHIELD_TEXT.forEach { - if (this.contains(it, true)) return true - } - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/Toast.kt b/app/src/main/java/com/skyd/imomoe/util/Toast.kt new file mode 100644 index 00000000..4acd78ab --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/Toast.kt @@ -0,0 +1,31 @@ +package com.skyd.imomoe.util + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext + +private var uiThreadHandler: Handler = Handler(Looper.getMainLooper()) + +fun CharSequence.showToast(duration: Int = Toast.LENGTH_SHORT) { + uiThreadHandler.post { + val toast = Toast(appContext) + val view: View = + (appContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater) + .inflate(R.layout.toast_1, null) + view.findViewById(R.id.tv_toast_1).also { + it.text = this + it.setTextColor(ContextCompat.getColor(appContext, R.color.on_primary_pink)) + } + view.setBackgroundResource(R.drawable.shape_fill_circle_corner_50) + toast.view = view + toast.duration = duration + toast.show() + } +} diff --git a/app/src/main/java/com/skyd/imomoe/util/Util.kt b/app/src/main/java/com/skyd/imomoe/util/Util.kt index 4a161846..80eebbd5 100644 --- a/app/src/main/java/com/skyd/imomoe/util/Util.kt +++ b/app/src/main/java/com/skyd/imomoe/util/Util.kt @@ -7,119 +7,90 @@ import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.res.Resources -import android.graphics.Color import android.graphics.Point -import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.provider.Settings import android.util.TypedValue -import android.view.View import android.view.Window import android.view.WindowManager -import android.view.inputmethod.InputMethodManager -import android.widget.EditText import android.widget.Toast -import androidx.annotation.AnyRes import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.FileProvider -import androidx.core.graphics.drawable.DrawableCompat -import androidx.fragment.app.Fragment -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.input.input -import com.skyd.imomoe.App +import androidx.core.view.WindowInsetsControllerCompat import com.skyd.imomoe.R -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.config.UnknownActionUrl -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.RouteProcessor -import com.skyd.imomoe.view.activity.* -import com.skyd.imomoe.view.component.AnimeToast -import com.skyd.skin.SkinManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.io.* -import java.math.BigDecimal +import com.skyd.imomoe.appContext +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.getRawString +import com.skyd.imomoe.ext.sharedPreferences +import java.io.File +import java.io.IOException import java.net.HttpURLConnection import java.net.URL -import java.net.URLDecoder import java.util.* import java.util.regex.Matcher import java.util.regex.Pattern -import kotlin.collections.ArrayList object Util { fun openBrowser(url: String) { - val uri: Uri = Uri.parse(url) - val intent = Intent(Intent.ACTION_VIEW, uri) - intent.flags = FLAG_ACTIVITY_NEW_TASK - App.context.startActivity(intent) + try { + val uri: Uri = Uri.parse(url) + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.flags = FLAG_ACTIVITY_NEW_TASK + appContext.startActivity(intent) + } catch (e: ActivityNotFoundException) { + e.printStackTrace() + appContext.getString(R.string.no_browser_found, url).showToast(Toast.LENGTH_LONG) + } } - fun getEncodedUrl(url: String): String { - return Uri.encode(url, ":/-![].,%?&=") + fun String.toEncodedUrl(): String { + return Uri.encode(this, ":/-![].,%?&=") } - /** - * 通过播放页面的网址获取详情页面的网址 - * - * @param episodeUrl 播放页面的网址 - * @return 详情页面的网址 - */ - fun getDetailLinkByEpisodeLink(episodeUrl: String): String { - return (DataSourceManager.getUtil() - ?: com.skyd.imomoe.model.impls.Util()).getDetailLinkByEpisodeLink(episodeUrl) - } + @Deprecated( + "use String.toEncodedUrl()", + ReplaceWith("url.toEncodedUrl()", "com.skyd.imomoe.util.Util.toEncodedUrl") + ) + fun getEncodedUrl(url: String): String = url.toEncodedUrl() fun restartApp() { - val i = App.context.packageManager.getLaunchIntentForPackage(App.context.packageName) + val i = appContext.packageManager.getLaunchIntentForPackage(appContext.packageName) i?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - App.context.startActivity(i) + appContext.startActivity(i) + + // 杀死原进程 + android.os.Process.killProcess(android.os.Process.myPid()) } /** * 上次读过的用户须知的版本号 */ - fun lastReadUserNoticeVersion(): Int = App.context.sharedPreferences().getInt("userNotice", 0) + fun lastReadUserNoticeVersion(): Int = sharedPreferences().getInt("userNotice", 0) /** * @param version 用户须知版本号 */ - fun setReadUserNoticeVersion(version: Int) = App.context.sharedPreferences().editor { + fun setReadUserNoticeVersion(version: Int) = sharedPreferences().editor { putInt("userNotice", version) } /** * 获取用户须知String */ - fun getUserNoticeContent(): String { - val sb = StringBuffer() - try { - val inputStream = App.context.resources.openRawResource(R.raw.notice) - val reader = BufferedReader(InputStreamReader(inputStream, "UTF-8")) - var out: String? - while (reader.readLine().also { out = it } != null) { - sb.append(out) - } - } catch (e: IOException) { - e.printStackTrace() - } - return sb.toString() - } + fun getUserNoticeContent(): String = appContext.getRawString(R.raw.notice) fun getWebsiteLinkSuffix(): String { - return App.context.sharedPreferences().getString("websiteLinkSuffix", ".html") ?: ".html" + return sharedPreferences().getString("websiteLinkSuffix", ".html") ?: ".html" } fun setWebsiteLinkSuffix(suffix: String) { - App.context.sharedPreferences().editor { - putString("websiteLinkSuffix", suffix) - } + sharedPreferences().editor { putString("websiteLinkSuffix", suffix) } } fun openVideoByExternalPlayer(context: Context, url: String): Boolean { @@ -187,15 +158,6 @@ object Util { null } - /** - * 更改Drawable颜色 - */ - fun tintDrawable(drawable: Drawable, color: Int): Drawable { - val wrappedDrawable: Drawable = DrawableCompat.wrap(drawable) - DrawableCompat.setTint(wrappedDrawable, color) - return wrappedDrawable - } - /** * 获取重定向最终的地址 * @param path @@ -222,25 +184,15 @@ object Util { } } - /** - * 通过原始id获取当前皮肤的id - */ - fun getSkinResourceId(@AnyRes id: Int) = SkinManager.getSkinResourceId(id) - /** * 通过id获取drawable */ - fun getResDrawable(@DrawableRes id: Int) = SkinManager.getDrawableOrMipMap(id) + fun getResDrawable(@DrawableRes id: Int) = AppCompatResources.getDrawable(appContext, id) /** * 通过id获取颜色 */ - fun Context.getResColor(@ColorRes id: Int) = SkinManager.getColor(id) - - /** - * 通过id获取颜色,不随皮肤更改,使用默认的 - */ - fun Context.getDefaultResColor(@ColorRes id: Int) = ContextCompat.getColor(this, id) + fun getResColor(@ColorRes id: Int) = ContextCompat.getColor(appContext, id) /** * 计算距今时间 @@ -272,24 +224,23 @@ object Util { return result } - fun copyText2Clipboard(context: Context, text: String) { + fun String.copy2Clipboard(context: Context) { try { val systemService: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - systemService.setPrimaryClip(ClipData.newPlainText("text", text)) + systemService.setPrimaryClip(ClipData.newPlainText("text", this)) } catch (e: Exception) { e.printStackTrace() } } - fun isNewVersion(version: String): Boolean { - val currentVersion = getAppVersionName() + fun isNewVersionByVersionCode(version: String): Boolean { + val currentVersion = getAppVersionCode().toString() return try { - version != currentVersion && - version.replaceFirst("v", "", true) != currentVersion + version != currentVersion } catch (e: Exception) { e.printStackTrace() - "检查版本号失败,建议手动到Github查看是否有更新\n当前版本:$currentVersion".showToast(Toast.LENGTH_LONG) + "检查版本号失败,建议手动到GitHub查看是否有更新\n当前版本代码:$currentVersion".showToast(Toast.LENGTH_LONG) false } } @@ -297,9 +248,9 @@ object Util { fun getAppVersionCode(): Long { var appVersionCode: Long = 0 try { - val packageInfo = App.context.applicationContext + val packageInfo = appContext.applicationContext .packageManager - .getPackageInfo(App.context.packageName, 0) + .getPackageInfo(appContext.packageName, 0) appVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { packageInfo.longVersionCode } else { @@ -314,9 +265,9 @@ object Util { fun getAppVersionName(): String { var appVersionName = "" try { - val packageInfo = App.context.applicationContext + val packageInfo = appContext.applicationContext .packageManager - .getPackageInfo(App.context.packageName, 0) + .getPackageInfo(appContext.packageName, 0) appVersionName = packageInfo.versionName } catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() @@ -326,12 +277,12 @@ object Util { fun getAppName(): String? { return try { - val packageManager = App.context.packageManager + val packageManager = appContext.packageManager val packageInfo: PackageInfo = packageManager.getPackageInfo( - App.context.packageName, 0 + appContext.packageName, 0 ) val labelRes: Int = packageInfo.applicationInfo.labelRes - App.context.resources.getString(labelRes) + appContext.getString(labelRes) } catch (e: Exception) { e.printStackTrace() null @@ -341,11 +292,11 @@ object Util { fun getManifestMetaValue(name: String): String { var metaValue = "" try { - val packageManager = App.context.packageManager + val packageManager = appContext.packageManager if (packageManager != null) { // 注意此处为ApplicationInfo 而不是 ActivityInfo,因为友盟设置的meta-data是在application标签中,而不是某activity标签中,所以用ApplicationInfo val applicationInfo = packageManager.getApplicationInfo( - App.context.packageName, + appContext.packageName, PackageManager.GET_META_DATA ) if (applicationInfo.metaData != null) { @@ -358,23 +309,8 @@ object Util { return metaValue } - fun EditText.showKeyboard() { - isFocusable = true - isFocusableInTouchMode = true - requestFocus() - val inputManager = - App.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.showSoftInput(this, 0) - } - - fun EditText.hideKeyboard() { - val inputManager = - App.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.hideSoftInputFromWindow(this.windowToken, 0) - } - fun String.getSubString(s: String, e: String): List { - val regex = s + "(.*?)" + e + val regex = "$s(.*?)$e" val p: Pattern = Pattern.compile(regex) val m: Matcher = p.matcher(this) val list: MutableList = ArrayList() @@ -412,26 +348,6 @@ object Util { Resources.getSystem().displayMetrics ).toInt() - fun setTransparentStatusBar( - window: Window, - isDark: Boolean = true - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (isDark) window.decorView.systemUiVisibility = - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR //实现状态栏图标和文字颜色为暗色 - window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.clearFlags( - WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION - ) - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_LAYOUT_STABLE) - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - window.statusBarColor = Color.TRANSPARENT - } - } - fun setFullScreen(window: Window) { window.setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, @@ -445,9 +361,12 @@ object Util { darkTextColor: Boolean = false ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - window.statusBarColor = statusBarColor //设置状态栏颜色 - if (darkTextColor) window.decorView.systemUiVisibility = - View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + val decorView = window.decorView + val wic = WindowInsetsControllerCompat(window, decorView) + wic.isAppearanceLightStatusBars = darkTextColor + window.statusBarColor = statusBarColor } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) @@ -456,28 +375,17 @@ object Util { } fun getStatusBarHeight(): Int { - val resourceId: Int = - App.context.resources - .getIdentifier("status_bar_height", "dimen", "android") + val resourceId: Int = appContext.resources + .getIdentifier("status_bar_height", "dimen", "android") if (resourceId > 0) { - return App.context.resources.getDimensionPixelSize(resourceId) + return appContext.resources.getDimensionPixelSize(resourceId) } return 0 } - fun CharSequence.showToast(duration: Int = Toast.LENGTH_SHORT) { - AnimeToast.makeText(App.context, this, duration).show() - } - - fun CharSequence.showToastOnIOThread(duration: Int = Toast.LENGTH_SHORT) { - GlobalScope.launch(Dispatchers.Main) { - this@showToastOnIOThread.showToast(duration) - } - } - fun getScreenHeight(includeVirtualKey: Boolean): Int { val display = - (App.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + (appContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay val outPoint = Point() // 可能有虚拟按键的情况 if (includeVirtualKey) display.getRealSize(outPoint) @@ -487,7 +395,7 @@ object Util { fun getScreenWidth(includeVirtualKey: Boolean): Int { val display = - (App.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + (appContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay val outPoint = Point() // 可能有虚拟按键的情况 if (includeVirtualKey) display.getRealSize(outPoint) @@ -495,208 +403,7 @@ object Util { return outPoint.x } - fun getFileSize(f: File): Long { - var s: Long = 0 - if (f.exists() && f.isFile) { - val fis = FileInputStream(f) - s = fis.available().toLong() - } - return s - } - - fun getDirectorySize(f: File): Long { - var size: Long = 0 - val fList = f.listFiles() - fList?.let { - for (i in it.indices) { - size += if (it[i].isDirectory) { - getDirectorySize(it[i]) - } else { - getFileSize(it[i]) - } - } - } - return size - } - - /** - * 获取规整的文件大小 - * @param size 文件大小 - * @param newScale 精确到小数点几位 - */ - fun getFormatSize(size: Double, newScale: Int = 2): String { - val kiloByte = size / 1024 - if (kiloByte < 1) { - return size.toString() + "B" - } - val megaByte = kiloByte / 1024 - if (megaByte < 1) { - val result1 = BigDecimal(kiloByte.toString()) - return result1.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() - .toString() + "K" - } - val gigaByte = megaByte / 1024 - if (gigaByte < 1) { - val result2 = BigDecimal(megaByte.toString()) - return result2.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() - .toString() + "M" - } - val teraBytes = gigaByte / 1024 - if (teraBytes < 1) { - val result3 = BigDecimal(gigaByte.toString()) - return result3.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() - .toString() + "G" - } - val result4 = BigDecimal(teraBytes) - return result4.setScale(newScale, BigDecimal.ROUND_HALF_UP).toPlainString() - .toString() + "T" - } - fun String.isYearMonth(): Boolean { return Pattern.compile("[1-9][0-9]{3}(0[1-9]|1[0-2])").matcher(this).matches() } - - fun process(fragment: Fragment, actionUrl: String?, toastTitle: String = "") { - val activity = fragment.activity - if (activity != null) - process(activity, actionUrl, toastTitle) - } - - fun process(activity: Activity, actionUrl: String?, toastTitle: String = "") { - actionUrl ?: return - val decodeUrl = URLDecoder.decode(actionUrl, "UTF-8") - val routerProcessor = DataSourceManager.getRouterProcessor() ?: RouteProcessor() - // 没有处理跳转,则进入if体 - if (!routerProcessor.process(activity, actionUrl)) { - when { - decodeUrl.startsWith(Const.ActionUrl.ANIME_BROWSER) -> { //打开浏览器 - openBrowser(actionUrl.replaceFirst(Const.ActionUrl.ANIME_BROWSER, "")) - } - decodeUrl.startsWith(Const.ActionUrl.ANIME_ANIME_DOWNLOAD_EPISODE) -> { //缓存的每一集列表 - var directoryName: String - var path: Int - actionUrl.replaceFirst(Const.ActionUrl.ANIME_ANIME_DOWNLOAD_EPISODE, "") - .split("/").let { - directoryName = it[0] + "/" + it[1] - path = it.last().toInt() - } - activity.startActivity( - Intent(activity, AnimeDownloadActivity::class.java) - .putExtra("mode", 1) - .putExtra("actionBarTitle", directoryName.replace("/", "")) - .putExtra("directoryName", directoryName) - .putExtra("path", path) - ) - } - decodeUrl.startsWith(Const.ActionUrl.ANIME_ANIME_DOWNLOAD_PLAY) -> { //播放缓存的每一集 - val filePath = - actionUrl.replaceFirst(Const.ActionUrl.ANIME_ANIME_DOWNLOAD_PLAY + "/", "") - .replace(Regex("/\\d+$"), "") - val fileName = filePath.split("/").last() - val title = fileName - activity.startActivity( - Intent(activity, SimplePlayActivity::class.java) - .putExtra("url", "file://$filePath") - .putExtra("title", title) - ) - } - decodeUrl.startsWith(Const.ActionUrl.ANIME_ANIME_DOWNLOAD_M3U8) -> { //播放缓存的每一集M3U8 - "暂不支持m3u8格式 :(".showToast(Toast.LENGTH_LONG) - } - decodeUrl.startsWith(Const.ActionUrl.ANIME_LAUNCH_ACTIVITY) -> { // 启动Activity - val cls = Class.forName( - actionUrl.replaceFirst(Const.ActionUrl.ANIME_LAUNCH_ACTIVITY, "") - .split("/").last() - ) - activity.startActivity(Intent(activity, cls)) - } - decodeUrl.startsWith(Const.ActionUrl.ANIME_SKIP_BY_WEBSITE) -> { // 根据网址跳转 - var website = decodeUrl.replaceFirst(Const.ActionUrl.ANIME_SKIP_BY_WEBSITE, "") - if (website.isBlank() || website == "/") { - MaterialDialog(activity).show { - input(hintRes = R.string.input_a_website) { dialog, text -> - try { - var url = text.toString() - if (!url.matches(Regex("^.+://.*"))) url = "http://$url" - process(activity, URL(url).file) - } catch (e: Exception) { - App.context.resources.getString(R.string.website_format_error) - .showToast() - e.printStackTrace() - } - } - positiveButton(R.string.ok) - } - } else { - try { - if (!website.matches(Regex("^.+://.*"))) website = "http://$website" - process(activity, URL(website).file) - } catch (e: Exception) { - App.context.resources.getString(R.string.website_format_error) - .showToast() - e.printStackTrace() - } - } - } - else -> { - val action = UnknownActionUrl.actionMap[decodeUrl] - if (action != null) { - action.action() - } else { - // 空内容 - if (decodeUrl.isBlank()) return - App.context.resources.getString( - R.string.unknown_route, - if (toastTitle.isBlank()) actionUrl else toastTitle - ).showToast() - } - } - } - } - } - - fun process(context: Context, actionUrl: String?, toastTitle: String = "") { - actionUrl ?: return - val decodeUrl = URLDecoder.decode(actionUrl, "UTF-8") - val routerProcessor = DataSourceManager.getRouterProcessor() ?: RouteProcessor() - // 没有处理跳转,则进入if体 - if (!routerProcessor.process(context, actionUrl)) { - when { - decodeUrl.startsWith(Const.ActionUrl.ANIME_BROWSER) -> { //打开浏览器 - openBrowser(actionUrl.replaceFirst(Const.ActionUrl.ANIME_BROWSER, "")) - } - decodeUrl.startsWith(Const.ActionUrl.ANIME_LAUNCH_ACTIVITY) -> { // 启动Activity - val cls = Class.forName( - actionUrl.replaceFirst(Const.ActionUrl.ANIME_LAUNCH_ACTIVITY, "") - .split("/").last() - ) - context.startActivity(Intent(context, cls).addFlags(FLAG_ACTIVITY_NEW_TASK)) - } - decodeUrl.startsWith(Const.ActionUrl.ANIME_NOTICE) -> { // 显示通知 - val paramString: String = - actionUrl.replaceFirst(Const.ActionUrl.ANIME_NOTICE, "") - .split("?").run { - if (!isEmpty()) last() - else "" - } - if (paramString.isBlank()) { - App.context.getString(R.string.notice_activity_error_param).showToast() - return - } - context.startActivity( - Intent(context, NoticeActivity::class.java) - .putExtra(NoticeActivity.PARAM, paramString) - .addFlags(FLAG_ACTIVITY_NEW_TASK) - ) - } - else -> { - if (decodeUrl.isBlank()) return - App.context.resources.getString( - R.string.unknown_route, - if (toastTitle.isBlank()) actionUrl else toastTitle - ).showToast() - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/View.kt b/app/src/main/java/com/skyd/imomoe/util/View.kt deleted file mode 100644 index 3901ada0..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/View.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.skyd.imomoe.util - -import android.view.View -import android.view.animation.AlphaAnimation - - -fun View.gone(animate: Boolean = false, dur: Long = 500L) { - if (animate) startAnimation(AlphaAnimation(1f, 0f).apply { duration = dur }) - visibility = View.GONE -} - -fun View.visible(animate: Boolean = false, dur: Long = 500L) { - visibility = View.VISIBLE - if (animate) startAnimation(AlphaAnimation(0f, 1f).apply { duration = dur }) -} - -fun View.invisible(animate: Boolean = false, dur: Long = 500L) { - visibility = View.INVISIBLE - if (animate) startAnimation(AlphaAnimation(0f, 1f).apply { duration = dur }) -} - -fun View.clickScale(scale: Float = 0.75f, duration: Long = 100) { - animate().scaleX(scale).scaleY(scale).setDuration(duration) - .withEndAction { - animate().scaleX(1f).scaleY(1f).setDuration(duration).start() - }.start() -} diff --git a/app/src/main/java/com/skyd/imomoe/util/ViewHolderUtil.kt b/app/src/main/java/com/skyd/imomoe/util/ViewHolderUtil.kt index b8567307..1e0b54f0 100644 --- a/app/src/main/java/com/skyd/imomoe/util/ViewHolderUtil.kt +++ b/app/src/main/java/com/skyd/imomoe/util/ViewHolderUtil.kt @@ -1,158 +1,33 @@ package com.skyd.imomoe.util -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import android.widget.Button import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.progressindicator.LinearProgressIndicator import com.skyd.imomoe.R -import com.skyd.imomoe.bean.BaseBean -import com.skyd.imomoe.view.component.bannerview.BannerView -import com.skyd.imomoe.view.component.textview.TypefaceTextView import com.skyd.imomoe.view.component.FlowLayout -import com.skyd.imomoe.config.Const.ViewHolderTypeInt -import com.skyd.imomoe.config.Const.ViewHolderTypeString - -class ViewHolderUtil { - companion object { +import com.skyd.imomoe.view.component.bannerview.BannerView - fun getItemViewType(item: BaseBean): Int = when (item.type) { - ViewHolderTypeString.HEADER_1 -> ViewHolderTypeInt.HEADER_1 - ViewHolderTypeString.ANIME_COVER_1 -> ViewHolderTypeInt.ANIME_COVER_1 - ViewHolderTypeString.ANIME_COVER_2 -> ViewHolderTypeInt.ANIME_COVER_2 - ViewHolderTypeString.ANIME_COVER_3 -> ViewHolderTypeInt.ANIME_COVER_3 - ViewHolderTypeString.ANIME_COVER_4 -> ViewHolderTypeInt.ANIME_COVER_4 - ViewHolderTypeString.ANIME_COVER_5 -> ViewHolderTypeInt.ANIME_COVER_5 - ViewHolderTypeString.ANIME_COVER_6 -> ViewHolderTypeInt.ANIME_COVER_6 - ViewHolderTypeString.ANIME_COVER_7 -> ViewHolderTypeInt.ANIME_COVER_7 - ViewHolderTypeString.ANIME_COVER_8 -> ViewHolderTypeInt.ANIME_COVER_8 - ViewHolderTypeString.ANIME_COVER_9 -> ViewHolderTypeInt.ANIME_COVER_9 - ViewHolderTypeString.ANIME_EPISODE_2 -> ViewHolderTypeInt.ANIME_EPISODE_2 - ViewHolderTypeString.ANIME_EPISODE_FLOW_LAYOUT_1 -> ViewHolderTypeInt.ANIME_EPISODE_FLOW_LAYOUT_1 - ViewHolderTypeString.ANIME_EPISODE_FLOW_LAYOUT_2 -> ViewHolderTypeInt.ANIME_EPISODE_FLOW_LAYOUT_2 - ViewHolderTypeString.ANIME_DESCRIBE_1 -> ViewHolderTypeInt.ANIME_DESCRIBE_1 - ViewHolderTypeString.GRID_RECYCLER_VIEW_1 -> ViewHolderTypeInt.GRID_RECYCLER_VIEW_1 - ViewHolderTypeString.BANNER_1 -> ViewHolderTypeInt.BANNER_1 - ViewHolderTypeString.LICENSE_HEADER_1 -> ViewHolderTypeInt.LICENSE_HEADER_1 - ViewHolderTypeString.LICENSE_1 -> ViewHolderTypeInt.LICENSE_1 - ViewHolderTypeString.SEARCH_HISTORY_HEADER_1 -> ViewHolderTypeInt.SEARCH_HISTORY_HEADER_1 - ViewHolderTypeString.SEARCH_HISTORY_1 -> ViewHolderTypeInt.SEARCH_HISTORY_1 - ViewHolderTypeString.ANIME_INFO_1 -> ViewHolderTypeInt.ANIME_INFO_1 - ViewHolderTypeString.HORIZONTAL_RECYCLER_VIEW_1 -> ViewHolderTypeInt.HORIZONTAL_RECYCLER_VIEW_1 - ViewHolderTypeString.UPNP_DEVICE_1 -> ViewHolderTypeInt.UPNP_DEVICE_1 - ViewHolderTypeString.MORE_1 -> ViewHolderTypeInt.MORE_1 - ViewHolderTypeString.SKIN_COVER_1 -> ViewHolderTypeInt.SKIN_COVER_1 - else -> ViewHolderTypeInt.UNKNOWN - } +//UP_TODO 2022/1/22 12:31 0 ViewHolder直接使用ViewBinding +class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view) - fun getViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - ViewHolderTypeInt.HEADER_1 -> Header1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_header_1, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_1 -> AnimeCover1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_1, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_2 -> AnimeCover2ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_2, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_3 -> AnimeCover3ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_3, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_4 -> AnimeCover4ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_4, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_5 -> AnimeCover5ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_5, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_6 -> AnimeCover6ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_6, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_7 -> AnimeCover7ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_7, parent, false) - ) - ViewHolderTypeInt.ANIME_EPISODE_FLOW_LAYOUT_1 -> AnimeEpisodeFlowLayout1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_episode_flow_layout_1, parent, false) - ) - ViewHolderTypeInt.ANIME_EPISODE_FLOW_LAYOUT_2 -> AnimeEpisodeFlowLayout2ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_episode_flow_layout_2, parent, false) - ) - ViewHolderTypeInt.ANIME_DESCRIBE_1 -> AnimeDescribe1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_describe_1, parent, false) - ) - ViewHolderTypeInt.GRID_RECYCLER_VIEW_1 -> GridRecyclerView1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_grid_recycler_view_1, parent, false) - ) - ViewHolderTypeInt.BANNER_1 -> Banner1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_banner_1, parent, false) - ) - ViewHolderTypeInt.LICENSE_HEADER_1 -> LicenseHeader1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_license_header_1, parent, false) - ) - ViewHolderTypeInt.LICENSE_1 -> License1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_license_1, parent, false) - ) - ViewHolderTypeInt.SEARCH_HISTORY_HEADER_1 -> SearchHistoryHeader1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_search_history_header_1, parent, false) - ) - ViewHolderTypeInt.SEARCH_HISTORY_1 -> SearchHistory1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_search_history_1, parent, false) - ) - ViewHolderTypeInt.ANIME_INFO_1 -> AnimeInfo1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_info_1, parent, false) - ) - ViewHolderTypeInt.HORIZONTAL_RECYCLER_VIEW_1 -> HorizontalRecyclerView1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_horizontal_recycler_view_1, parent, false) - ) - ViewHolderTypeInt.ANIME_EPISODE_2 -> AnimeEpisode2ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_episode_2, parent, false) - ) - ViewHolderTypeInt.UPNP_DEVICE_1 -> UpnpDevice1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_dlna_device_1, parent, false) - ) - ViewHolderTypeInt.MORE_1 -> More1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_more_1, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_8 -> AnimeCover8ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_8, parent, false) - ) - ViewHolderTypeInt.ANIME_COVER_9 -> AnimeCover9ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_anime_cover_9, parent, false) - ) - ViewHolderTypeInt.SKIN_COVER_1 -> SkinCover1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_skin_cover_1, parent, false) - ) - else -> EmptyViewHolder(View(parent.context)) - } - } +class DataSource1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val tvDataSource1Name: TextView = view.findViewById(R.id.tv_data_source_1_name) + val tvDataSource1Size: TextView = view.findViewById(R.id.tv_data_source_1_size) + val ivDataSource1Selected: ImageView = view.findViewById(R.id.iv_data_source_1_selected) } -class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view) +class DataSource2ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val tvDataSource2Name: TextView = view.findViewById(R.id.tv_data_source_2_name) + val tvDataSource2Author: TextView = view.findViewById(R.id.tv_data_source_2_author) + val ivDataSource2Icon: ImageView = view.findViewById(R.id.iv_data_source_2_icon) + val tvDataSource2Describe: TextView = view.findViewById(R.id.tv_data_source_2_describe) + val tvDataSource2PublishAt: TextView = view.findViewById(R.id.tv_data_source_2_publish_at) + val tvDataSource2Version: TextView = view.findViewById(R.id.tv_data_source_2_version) + val btnDataSource2Action: Button = view.findViewById(R.id.btn_data_source_2_action) +} class GridRecyclerView1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rvGridRecyclerView1: RecyclerView = view.findViewById(R.id.rv_grid_recycler_view_1) @@ -166,7 +41,6 @@ class AnimeCover1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val ivAnimeCover1Cover: ImageView = view.findViewById(R.id.iv_anime_cover_1_cover) val tvAnimeCover1Title: TextView = view.findViewById(R.id.tv_anime_cover_1_title) val tvAnimeCover1Episode: TextView = view.findViewById(R.id.tv_anime_cover_1_episode) - val viewAnimeCover1Night: View = view.findViewById(R.id.view_anime_cover_1_night) } class AnimeCover2ViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -181,13 +55,11 @@ class AnimeCover3ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val flAnimeCover3Type: FlowLayout = view.findViewById(R.id.fl_anime_cover_3_type) val tvAnimeCover3Describe: TextView = view.findViewById(R.id.tv_anime_cover_3_describe) val tvAnimeCover3Alias: TextView = view.findViewById(R.id.tv_anime_cover_3_alias) - val viewAnimeCover3Night: View = view.findViewById(R.id.view_anime_cover_3_night) } class AnimeCover4ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val ivAnimeCover4Cover: ImageView = view.findViewById(R.id.iv_anime_cover_4_cover) val tvAnimeCover4Title: TextView = view.findViewById(R.id.tv_anime_cover_4_title) - val viewAnimeCover4Night: View = view.findViewById(R.id.view_anime_cover_4_night) } class AnimeCover5ViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -195,7 +67,6 @@ class AnimeCover5ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val tvAnimeCover5Area: TextView = view.findViewById(R.id.tv_anime_cover_5_area) val tvAnimeCover5Date: TextView = view.findViewById(R.id.tv_anime_cover_5_date) val tvAnimeCover5Episode: TextView = view.findViewById(R.id.tv_anime_cover_5_episode) - val tvAnimeCover5Rank: TextView = view.findViewById(R.id.tv_anime_cover_5_rank) } class AnimeCover6ViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -203,30 +74,16 @@ class AnimeCover6ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val tvAnimeCover6Title: TextView = view.findViewById(R.id.tv_anime_cover_6_title) val tvAnimeCover6Episode: TextView = view.findViewById(R.id.tv_anime_cover_6_episode) val tvAnimeCover6Describe: TextView = view.findViewById(R.id.tv_anime_cover_6_describe) - val tvAnimeCover6Night: View = view.findViewById(R.id.view_anime_cover_6_night) -} - -class AnimeCover7ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val tvAnimeCover7Title: TypefaceTextView = view.findViewById(R.id.tv_anime_cover_7_title) - val tvAnimeCover7Size: TypefaceTextView = view.findViewById(R.id.tv_anime_cover_7_size) - val tvAnimeCover7Episodes: TextView = view.findViewById(R.id.tv_anime_cover_7_episodes) - val tvAnimeCover7OldPath: TextView = view.findViewById(R.id.tv_anime_cover_7_old_path) } -class AnimeCover8ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val tvAnimeCover8Title: TypefaceTextView = view.findViewById(R.id.tv_anime_cover_8_title) - val tvAnimeCover8Episodes: TypefaceTextView = view.findViewById(R.id.tv_anime_cover_8_episode) - val ivAnimeCover8Cover: ImageView = view.findViewById(R.id.iv_anime_cover_8_cover) +class AnimeCover11ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val tvAnimeCover11Title: TextView = view.findViewById(R.id.tv_anime_cover_11_title) + val tvAnimeCover11Rank: TextView = view.findViewById(R.id.tv_anime_cover_11_rank) } -class AnimeCover9ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val tvAnimeCover9Title: TypefaceTextView = view.findViewById(R.id.tv_anime_cover_9_title) - val tvAnimeCover9Episodes: TypefaceTextView = view.findViewById(R.id.tv_anime_cover_9_episode) - val tvAnimeCover9Time: TypefaceTextView = view.findViewById(R.id.tv_anime_cover_9_time) - val tvAnimeCover9DetailPage: TypefaceTextView = - view.findViewById(R.id.tv_anime_cover_9_detail_page) - val ivAnimeCover9Cover: ImageView = view.findViewById(R.id.iv_anime_cover_9_cover) - val ivAnimeCover9Delete: ImageView = view.findViewById(R.id.iv_anime_cover_9_delete) +class AnimeCover12ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val tvAnimeCover12Title: TextView = view.findViewById(R.id.tv_anime_cover_12_title) + val tvAnimeCover12Episode: TextView = view.findViewById(R.id.tv_anime_cover_12_episode) } class AnimeEpisodeFlowLayout1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -245,16 +102,6 @@ class Banner1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val banner1: BannerView = view.findViewById(R.id.banner_1) } -class LicenseHeader1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val tvLicenseHeader1Name: TextView = view.findViewById(R.id.tv_license_header_1_name) - val tvLicenseHeader1License: TextView = view.findViewById(R.id.tv_license_header_1_license) -} - -class License1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val tvLicense1Name: TextView = view.findViewById(R.id.tv_license_1_name) - val tvLicense1License: TextView = view.findViewById(R.id.tv_license_1_license) -} - class SearchHistoryHeader1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val tvSearchHistoryHeader1Title: TextView = view.findViewById(R.id.tv_search_history_header_1_title) @@ -277,6 +124,7 @@ class AnimeInfo1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val tvAnimeInfo1Tag: TextView = view.findViewById(R.id.tv_anime_info_1_tag) val flAnimeInfo1Tag: FlowLayout = view.findViewById(R.id.fl_anime_info_1_tag) val tvAnimeInfo1Info: TextView = view.findViewById(R.id.tv_anime_info_1_info) + val tvAnimeInfoContinuePlay: TextView = view.findViewById(R.id.tv_anime_info_continue_play) } class HorizontalRecyclerView1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -286,12 +134,8 @@ class HorizontalRecyclerView1ViewHolder(view: View) : RecyclerView.ViewHolder(vi view.findViewById(R.id.iv_horizontal_recycler_view_1_more) } -class AnimeEpisode2ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val tvAnimeEpisode2: TextView = view.findViewById(R.id.tv_anime_episode_2) -} - -class UpnpDevice1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val tvUpnpDevice1Title: TextView = view.findViewById(R.id.tv_upnp_device_1_title) +class AnimeEpisode1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val tvAnimeEpisode1: TextView = view.findViewById(R.id.tv_anime_episode_1) } class More1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -299,8 +143,25 @@ class More1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val tvMore1: TextView = view.findViewById(R.id.tv_more_1) } -class SkinCover1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val ivSkinCover1Cover: ImageView = view.findViewById(R.id.iv_skin_cover_1_cover) - val tvSkinCover1Title: TypefaceTextView = view.findViewById(R.id.tv_skin_cover_1_title) - val ivSkinCover1Selected: ImageView = view.findViewById(R.id.iv_skin_cover_1_selected) +class ClassifyTab1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val textView: TextView = view.findViewById(R.id.text_view_1) +} + +class RestoreFile1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val ivRestoreFile1Icon: ImageView = view.findViewById(R.id.iv_restore_file_1_icon) + val tvRestoreFile1Title: TextView = view.findViewById(R.id.tv_restore_file_1_title) + val tvRestoreFile1Size: TextView = view.findViewById(R.id.tv_restore_file_1_size) + val tvRestoreFile1LastModified: TextView = + view.findViewById(R.id.tv_restore_file_1_last_modified) +} + +class AnimeDownload1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val tvAnimeDownload1Title: TextView = view.findViewById(R.id.tv_anime_download_1_title) + val tvAnimeDownload1Episode: TextView = view.findViewById(R.id.tv_anime_download_1_episode) + val tvAnimeDownload1Size: TextView = view.findViewById(R.id.tv_anime_download_1_size) + val tvAnimeDownload1Percent: TextView = view.findViewById(R.id.tv_anime_download_1_percent) + val tvAnimeDownload1Speed: TextView = view.findViewById(R.id.tv_anime_download_1_speed) + val pbAnimeDownload1: LinearProgressIndicator = view.findViewById(R.id.pb_anime_download_1) + val ivAnimeDownload1State: ImageView = view.findViewById(R.id.iv_anime_download_1_state) + val ivAnimeDownload1Cancel: ImageView = view.findViewById(R.id.iv_anime_download_1_cancel) } diff --git a/app/src/main/java/com/skyd/imomoe/util/coil/CoilUtil.kt b/app/src/main/java/com/skyd/imomoe/util/coil/CoilUtil.kt index d36c0f12..d60496de 100644 --- a/app/src/main/java/com/skyd/imomoe/util/coil/CoilUtil.kt +++ b/app/src/main/java/com/skyd/imomoe/util/coil/CoilUtil.kt @@ -1,81 +1,235 @@ package com.skyd.imomoe.util.coil import android.widget.ImageView +import androidx.annotation.ColorInt import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import coil.Coil import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter import coil.imageLoader import coil.load import coil.request.ImageRequest -import coil.util.CoilUtils import coil.util.DebugLogger -import com.skyd.imomoe.App import com.skyd.imomoe.R +import com.skyd.imomoe.appContext import com.skyd.imomoe.config.Api.Companion.MAIN_URL import com.skyd.imomoe.config.Const -import com.skyd.imomoe.util.Util.showToastOnIOThread +import com.skyd.imomoe.net.okhttpClient +import com.skyd.imomoe.util.Util.toEncodedUrl import com.skyd.imomoe.util.debug +import com.skyd.imomoe.util.logE import okhttp3.OkHttpClient import java.net.URL import kotlin.random.Random object CoilUtil { + private val imageLoaderBuilder = ImageLoader.Builder(appContext) + .crossfade(400) + .apply { debug { logger(DebugLogger()) } } + + internal lateinit var imageLoader: ImageLoader + init { - ImageLoader.Builder(App.context) - .crossfade(400) - .okHttpClient { - OkHttpClient.Builder() - .cache(CoilUtils.createDefaultCache(App.context)) - .build() - } - .apply { debug { logger(DebugLogger()) } } - .build().apply { - Coil.setImageLoader(this) - } + setOkHttpClient(okhttpClient) + } + + fun setOkHttpClient(okHttpClient: OkHttpClient) { + imageLoaderBuilder.okHttpClient( + okHttpClient.newBuilder().build() + ).build().apply { + imageLoader = this + Coil.setImageLoader(this) + } } fun ImageView.loadImage( - url: String, + url: String?, builder: ImageRequest.Builder.() -> Unit = {}, ) { - if (url.isEmpty()) { - "cover image url must not be null or empty".showToastOnIOThread() + if (url.isNullOrBlank()) { + logE("loadImage", "cover image url must not be null or empty") return } - this.load(url, builder = builder) + val newUrl = if (url.startsWith("//")) url.replaceFirst("//", "https://") else url + + this.load(newUrl, builder = builder) } fun ImageView.loadImage( - url: String, + res: Int, + ) { + loadImage(res.toString(), referer = null) + } + + fun ImageView.loadImage( + url: String?, referer: String? = null, @DrawableRes placeholder: Int = 0, - @DrawableRes error: Int = R.drawable.ic_warning_main_color_3_24_skin + @DrawableRes error: Int = R.drawable.ic_warning_24 ) { - var amendReferer = referer + if (url.isNullOrBlank()) { + logE("loadImage", "cover image url must not be null or empty") + return + } + + // 是本地drawable + url.toIntOrNull()?.let { drawableResId -> + load(drawableResId) { + placeholder(placeholder) + error(error) + } + return + } + + val newUrl = if (url.startsWith("//")) url.replaceFirst("//", "https://") else url + + // 是网络图片 + var amendReferer: String? = referer if (amendReferer?.startsWith(MAIN_URL) == false) amendReferer = MAIN_URL//"http://www.yhdm.io/" - if (referer == MAIN_URL || referer == MAIN_URL) amendReferer += "/" - - loadImage(url) { - placeholder(placeholder) - error(error) - addHeader("Referer", amendReferer ?: MAIN_URL) - addHeader("Host", URL(url).host) - addHeader("Accept", "*/*") - addHeader("Accept-Encoding", "gzip, deflate") - addHeader("Connection", "keep-alive") - addHeader( - "User-Agent", - Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)] - ) + if (referer == MAIN_URL) amendReferer += "/" + + runCatching { + loadImage(newUrl) { + placeholder(placeholder) + error(error) + amendReferer?.let { ref -> + addHeader("Referer", ref.toEncodedUrl()) + } + addHeader("Host", URL(newUrl).host) + addHeader("Accept", "*/*") + addHeader("Accept-Encoding", "gzip, deflate") + addHeader("Connection", "keep-alive") + addHeader( + "User-Agent", + Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)] + ) + } + }.onFailure { + it.printStackTrace() } } - + @OptIn(ExperimentalCoilApi::class) fun clearMemoryDiskCache() { - App.context.imageLoader.memoryCache.clear() - CoilUtils.createDefaultCache(App.context).delete() + appContext.imageLoader.memoryCache?.clear() + Coil.imageLoader(appContext).diskCache?.clear() + } +} + +@Composable +fun AnimeAsyncImage( + url: String?, + referer: String? = null, + contentDescription: String? = null, + imageLoader: ImageLoader = CoilUtil.imageLoader, + modifier: Modifier = Modifier, + placeholder: Painter? = null, + error: Painter? = painterResource(id = R.drawable.ic_warning_24), + fallback: Painter? = error, + onLoading: ((AsyncImagePainter.State.Loading) -> Unit)? = null, + onSuccess: ((AsyncImagePainter.State.Success) -> Unit)? = null, + onError: ((AsyncImagePainter.State.Error) -> Unit)? = null, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, +) { + val context = LocalContext.current + + if (url.isNullOrBlank()) { + logE("loadImage", "cover image url must not be null or empty") + return } + + val newUrl = if (url.startsWith("//")) url.replaceFirst("//", "https://") else url + + // 是网络图片 + var amendReferer: String? = referer + if (amendReferer?.startsWith(MAIN_URL) == false) + amendReferer = MAIN_URL//"http://www.yhdm.io/" + if (referer == MAIN_URL) amendReferer += "/" + + imageLoader.enqueue(ImageRequest.Builder(context).apply { + amendReferer?.let { ref -> + addHeader("Referer", ref.toEncodedUrl()) + } + addHeader("Host", URL(newUrl).host) + addHeader("Accept", "*/*") + addHeader("Accept-Encoding", "gzip, deflate") + addHeader("Connection", "keep-alive") + addHeader( + "User-Agent", + Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)] + ) + }.build()) + + AsyncImage( + model = newUrl, + contentDescription = contentDescription, + imageLoader = imageLoader, + modifier = modifier, + placeholder = placeholder, + error = fallback, + onLoading = onLoading, + onSuccess = onSuccess, + onError = onError, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) +} + +@Composable +fun AnimeAsyncImage( + @ColorInt color: Int, + contentDescription: String?, + imageLoader: ImageLoader = CoilUtil.imageLoader, + modifier: Modifier = Modifier, + placeholder: Painter? = null, + error: Painter? = painterResource(id = R.drawable.ic_warning_24), + fallback: Painter? = error, + onLoading: ((AsyncImagePainter.State.Loading) -> Unit)? = null, + onSuccess: ((AsyncImagePainter.State.Success) -> Unit)? = null, + onError: ((AsyncImagePainter.State.Error) -> Unit)? = null, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, +) { + AsyncImage( + model = color, + contentDescription = contentDescription, + imageLoader = imageLoader, + modifier = modifier, + placeholder = placeholder, + error = fallback, + onLoading = onLoading, + onSuccess = onSuccess, + onError = onError, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/coil/DarkBlurTransformation.kt b/app/src/main/java/com/skyd/imomoe/util/coil/DarkBlurTransformation.kt index f8c52814..8b175f49 100644 --- a/app/src/main/java/com/skyd/imomoe/util/coil/DarkBlurTransformation.kt +++ b/app/src/main/java/com/skyd/imomoe/util/coil/DarkBlurTransformation.kt @@ -1,17 +1,17 @@ package com.skyd.imomoe.util.coil -import coil.transform.Transformation - import android.content.Context -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint import android.renderscript.Allocation import android.renderscript.Element import android.renderscript.RenderScript import android.renderscript.ScriptIntrinsicBlur -import androidx.annotation.RequiresApi import androidx.core.graphics.applyCanvas -import coil.bitmap.BitmapPool import coil.size.Size +import coil.transform.Transformation /** * A [Transformation] that applies a Gaussian blur to an image. @@ -21,7 +21,6 @@ import coil.size.Size * @param sampling The sampling multiplier used to scale the image. Values > 1 * will downscale the image. Values between 0 and 1 will upscale the image. */ -@RequiresApi(18) class DarkBlurTransformation @JvmOverloads constructor( private val context: Context, private val radius: Float = DEFAULT_RADIUS, @@ -35,14 +34,14 @@ class DarkBlurTransformation @JvmOverloads constructor( require(dark > 0) { "dark must be > 0." } } - override fun key(): String = "${DarkBlurTransformation::class.java.name}-$radius-$sampling" + override val cacheKey: String = "${DarkBlurTransformation::class.java.name}-$radius-$sampling" - override suspend fun transform(pool: BitmapPool, input: Bitmap, size: Size): Bitmap { + override suspend fun transform(input: Bitmap, size: Size): Bitmap { val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) val scaledWidth = (input.width / sampling).toInt() val scaledHeight = (input.height / sampling).toInt() - val output = pool.get(scaledWidth, scaledHeight, input.safeConfig) + val output = Bitmap.createBitmap(scaledWidth, scaledHeight, input.safeConfig) output.applyCanvas { scale(1 / sampling, 1 / sampling) val f = ColorMatrixColorFilter(ColorMatrix().apply { diff --git a/app/src/main/java/com/skyd/imomoe/util/comparator/EpisodeTitleComparator.kt b/app/src/main/java/com/skyd/imomoe/util/comparator/EpisodeTitleComparator.kt deleted file mode 100644 index 69c49a84..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/comparator/EpisodeTitleComparator.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.skyd.imomoe.util.comparator - -import com.skyd.imomoe.bean.AnimeCoverBean - -/** - * 比较集数名称(title)的字典序,数字按照从大到小,例如90小于100 - * 例:第10集<第11集<第11.5集<第90集<第100集 - */ -class EpisodeTitleComparator : Comparator { - - // 计算出数字的结尾下标,可以包括一个小数点 - private fun findDigitEndIndex(arrChar: String, at: Int): Int { - var k = at - var c: Char - var hasDot = false - while (k < arrChar.length) { - c = arrChar[k] - if (c == '.' && !hasDot) hasDot = true - else if (c > '9' || c < '0') break - k++ - } - return k - } - - override fun compare(o1: AnimeCoverBean, o2: AnimeCoverBean): Int { - val a: String = o1.title - val b: String = o2.title - var aIndex = 0 - var bIndex = 0 - var aComparedUnitEndIndex: Int - var bComparedUnitEndIndex: Int - while (aIndex < a.length && bIndex < b.length) { - // 找a串的数字结束下标+1 - aComparedUnitEndIndex = findDigitEndIndex(a, aIndex) - // 找b串的数字结束下标+1 - bComparedUnitEndIndex = findDigitEndIndex(b, bIndex) - // 如果a和b数字的结束下标都增加了,则说明之前开始找的地方是数字,开始比较数字 - if (aComparedUnitEndIndex > aIndex && bComparedUnitEndIndex > bIndex) { - // 用BigDecimal比较,防止浮点数出现精度问题 - val aDigit = a.substring(aIndex, aComparedUnitEndIndex).toBigDecimal() // a数 - val bDigit = b.substring(bIndex, bComparedUnitEndIndex).toBigDecimal() // b数 - // 如果a数!=b数,则返回其差值 - aDigit.compareTo(bDigit).let { if (it != 0) return it } - // 如果a数==b数,则继续比较 - aIndex = aComparedUnitEndIndex - bIndex = bComparedUnitEndIndex - } else { - if (a[aIndex] != b[bIndex]) return a[aIndex] - b[bIndex] - aIndex++ - bIndex++ - } - } - return a.length - b.length - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/compare/EpisodeTitleCompareUtil.kt b/app/src/main/java/com/skyd/imomoe/util/compare/EpisodeTitleCompareUtil.kt new file mode 100644 index 00000000..0c2d489c --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/compare/EpisodeTitleCompareUtil.kt @@ -0,0 +1,58 @@ +package com.skyd.imomoe.util.compare + +/** + * 比较集数名称(title)的字典序,数字按照从大到小,例如90小于100 + * 例:第10集<第11集<第11.5集<第90集<第100集 + */ +object EpisodeTitleCompareUtil { + + var asc: Boolean = true + + // 计算出数字的结尾下标,最多可以包括一个小数点。不能只包含小数点 + private fun findDigitEndIndex(arrChar: String, at: Int): Int { + var k = at + var c: Char + var hasDot = false + var hasNumber = false + while (k < arrChar.length) { + c = arrChar[k] + if (c == '.' && !hasDot) hasDot = true + else if (c > '9' || c < '0') break + else if ((c <= '9' || c >= '0') && !hasNumber) hasNumber = true + k++ + } + return if (hasNumber) k else at + } + + fun compare(a: String, b: String): Int { + var aIndex = 0 + var bIndex = 0 + var aComparedUnitEndIndex: Int + var bComparedUnitEndIndex: Int + while (aIndex < a.length && bIndex < b.length) { + // 找a串的数字结束下标+1 + aComparedUnitEndIndex = findDigitEndIndex(a, aIndex) + // 找b串的数字结束下标+1 + bComparedUnitEndIndex = findDigitEndIndex(b, bIndex) + // 如果a和b数字的结束下标都增加了,则说明之前开始找的地方是数字,开始比较数字 + if (aComparedUnitEndIndex > aIndex && bComparedUnitEndIndex > bIndex) { + // 用BigDecimal比较,防止浮点数出现精度问题 + val aDigit = a.substring(aIndex, aComparedUnitEndIndex).toBigDecimal() // a数 + val bDigit = b.substring(bIndex, bComparedUnitEndIndex).toBigDecimal() // b数 + // 如果a数!=b数,则返回其差值 + aDigit.compareTo(bDigit).let { if (it != 0) return asc(it) } + // 如果a数==b数,则继续比较 + aIndex = aComparedUnitEndIndex + bIndex = bComparedUnitEndIndex + } else { + if (a[aIndex] != b[bIndex]) return asc(a[aIndex] - b[bIndex]) + aIndex++ + bIndex++ + } + } + return asc(a.length - b.length) + } + + // 如果是升序,则返回自身,否则返回相反数 + private fun asc(a: Int): Int = if (asc) a else -a +} diff --git a/app/src/main/java/com/skyd/imomoe/util/compare/EpisodeTitleSort.kt b/app/src/main/java/com/skyd/imomoe/util/compare/EpisodeTitleSort.kt new file mode 100644 index 00000000..396cf06f --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/compare/EpisodeTitleSort.kt @@ -0,0 +1,96 @@ +package com.skyd.imomoe.util.compare + +import android.app.Activity +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.sharedPreferences +import com.skyd.imomoe.ext.showListDialog + + +object EpisodeTitleSort { + + sealed class EpisodeTitleSortMode : CharSequence { + object Ascending : EpisodeTitleSortMode() + object Descending : EpisodeTitleSortMode() + object Default : EpisodeTitleSortMode() + + companion object { + fun getFromKey(s: String): EpisodeTitleSortMode = when (s) { + "Ascending" -> Ascending + "Descending" -> Descending + "Default" -> Default + else -> Ascending + } + } + + override fun toString(): String = name + + override fun get(index: Int): Char = name[index] + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = + name.subSequence(startIndex, endIndex) + + override val length: Int + get() = name.length + + val name by lazy { + when (this) { + is Ascending -> appContext.getString(R.string.episode_title_sort_ascending) + is Descending -> appContext.getString(R.string.episode_title_sort_descending) + is Default -> appContext.getString(R.string.episode_title_sort_default) + } + } + + val key by lazy { + when (this) { + is Ascending -> "Ascending" + is Descending -> "Descending" + is Default -> "Default" + } + } + } + + fun > MutableList.sortEpisodeTitle(mode: EpisodeTitleSortMode = episodeTitleSortMode): List { + when (mode) { + is EpisodeTitleSortMode.Ascending -> { + EpisodeTitleCompareUtil.asc = true + sort() + } + is EpisodeTitleSortMode.Descending -> { + EpisodeTitleCompareUtil.asc = false + sort() + } + is EpisodeTitleSortMode.Default -> {} + } + return this + } + + var episodeTitleSortMode: EpisodeTitleSortMode = + EpisodeTitleSortMode.getFromKey( + sharedPreferences().getString("episodeTitleSortMode", null).orEmpty() + ) + set(value) { + if (value == field) return + sharedPreferences().editor { putString("episodeTitleSortMode", value.key) } + field = value + } + + fun Activity.selectEpisodeTitleSortMode(onPositive: ((EpisodeTitleSortMode) -> Unit)? = null) { + var initialSelection = 0 + val items = listOf( + EpisodeTitleSortMode.Default, + EpisodeTitleSortMode.Ascending, + EpisodeTitleSortMode.Descending + ) + items.forEachIndexed { index, s -> if (s == episodeTitleSortMode) initialSelection = index } + showListDialog( + title = getString(R.string.select_episode_title_sort_mode), + items = items, + checkedItem = initialSelection + ) { _, _, itemIndex -> + episodeTitleSortMode = items[itemIndex] + onPositive?.invoke(items[itemIndex]) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/CastObject.java b/app/src/main/java/com/skyd/imomoe/util/dlna/CastObject.java deleted file mode 100644 index dd033c15..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/CastObject.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.skyd.imomoe.util.dlna; - -import androidx.annotation.NonNull; - -import com.skyd.imomoe.util.dlna.dmc.ICast; - -public class CastObject { - private CastObject() { - } - - public static ICast newInstance(String url, String id, String name) { - if (url.endsWith(".mp4")) { - return CastVideo.newInstance(url, id, name); - } else if (url.endsWith(".mp3")) { - return CastAudio.newInstance(url, id, name); - } else if (url.endsWith(".jpg")) { - return CastImage.newInstance(url, id, name); - } else { - return null; - } - } - - /** - * - */ - public static class CastAudio implements ICast.ICastVideo { - public static CastAudio newInstance(String url, String id, String name) { - return new CastAudio(url, id, name); - } - - public final String url; - - public final String id; - - public final String name; - - private long duration; - - public CastAudio(String url, String id, String name) { - this.url = url; - this.id = id; - this.name = name; - } - - /** - * @param duration the total time of video (ms) - */ - public CastAudio setDuration(long duration) { - this.duration = duration; - return this; - } - - @NonNull - @Override - public String getId() { - return id; - } - - @NonNull - @Override - public String getUri() { - return url; - } - - @Override - public String getName() { - return name; - } - - @Override - public long getDurationMillSeconds() { - return duration; - } - - @Override - public long getSize() { - return 0; - } - - @Override - public long getBitrate() { - return 0; - } - } - - /** - * - */ - public static class CastImage implements ICast.ICastVideo { - public static CastImage newInstance(String url, String id, String name) { - return new CastImage(url, id, name); - } - - public final String url; - - public final String id; - - public final String name; - - public CastImage(String url, String id, String name) { - this.url = url; - this.id = id; - this.name = name; - } - - @NonNull - @Override - public String getId() { - return id; - } - - @NonNull - @Override - public String getUri() { - return url; - } - - @Override - public String getName() { - return name; - } - - @Override - public long getDurationMillSeconds() { - return -1L; - } - - @Override - public long getSize() { - return 0; - } - - @Override - public long getBitrate() { - return 0; - } - } - - /** - * - */ - public static class CastVideo implements ICast.ICastVideo { - public static CastVideo newInstance(String url, String id, String name) { - return new CastVideo(url, id, name); - } - - public final String url; - - public final String id; - - public final String name; - - private long duration; - - public CastVideo(String url, String id, String name) { - this.url = url; - this.id = id; - this.name = name; - } - - /** - * @param duration the total time of video (ms) - */ - public CastVideo setDuration(long duration) { - this.duration = duration; - return this; - } - - @NonNull - @Override - public String getId() { - return id; - } - - @NonNull - @Override - public String getUri() { - return url; - } - - @Override - public String getName() { - return name; - } - - @Override - public long getDurationMillSeconds() { - return duration; - } - - @Override - public long getSize() { - return 0; - } - - @Override - public long getBitrate() { - return 0; - } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/CastObject.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/CastObject.kt new file mode 100644 index 00000000..bec1d17a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/CastObject.kt @@ -0,0 +1,90 @@ +package com.skyd.imomoe.util.dlna + +import com.skyd.imomoe.util.dlna.dmc.ICast +import com.skyd.imomoe.util.dlna.dmc.ICast.ICastVideo + +object CastObject { + fun newInstance(url: String, id: String, name: String?): ICast? { + return when { + url.endsWith(".mp4") -> CastVideo.newInstance(url, id, name) + url.endsWith(".mp3") -> CastAudio.newInstance(url, id, name) + url.endsWith(".jpg") -> CastImage.newInstance(url, id, name) + else -> null + } + } + + class CastAudio( + override val uri: String, + override val id: String, + override val name: String? + ) : ICastVideo { + override var durationMillSeconds: Long = 0 + private set + + /** + * @param duration the total time of video (ms) + */ + fun setDuration(duration: Long): CastAudio { + durationMillSeconds = duration + return this + } + + override val size: Long + get() = 0 + override val bitrate: Long + get() = 0 + + companion object { + @JvmStatic + fun newInstance(url: String, id: String, name: String?): CastAudio { + return CastAudio(url, id, name) + } + } + } + + class CastImage( + override val uri: String, + override val id: String, + override val name: String? + ) : ICastVideo { + override val durationMillSeconds: Long + get() = -1L + override val size: Long + get() = 0 + override val bitrate: Long + get() = 0 + + companion object { + @JvmStatic + fun newInstance(url: String, id: String, name: String?): CastImage { + return CastImage(url, id, name) + } + } + } + + class CastVideo(override val uri: String, override val id: String, override val name: String?) : + ICastVideo { + override var durationMillSeconds: Long = 0 + private set + + /** + * @param duration the total time of video (ms) + */ + fun setDuration(duration: Long): CastVideo { + durationMillSeconds = duration + return this + } + + override val size: Long + get() = 0 + override val bitrate: Long + get() = 0 + + companion object { + @JvmStatic + fun newInstance(url: String, id: String, name: String?): CastVideo { + return CastVideo(url, id, name) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/Utils.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/Utils.kt index cfe3a522..61133f3e 100644 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/Utils.kt +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/Utils.kt @@ -2,7 +2,9 @@ package com.skyd.imomoe.util.dlna import android.content.Context import android.net.wifi.WifiManager -import android.text.TextUtils +import com.skyd.imomoe.util.dlna.dmc.DLNACastManager +import java.io.UnsupportedEncodingException +import java.net.URLEncoder import java.util.* object Utils { @@ -17,6 +19,32 @@ object Utils { } else "unknown" } + fun String.isLocalMediaAddress(): Boolean { + return (isNotBlank() && !startsWith("http://") && !startsWith("https://") + && !startsWith("ftp://") + /*&& startsWith(Environment.getExternalStorageDirectory().absolutePath)*/) || + startsWith("file://") + } + + fun String.toLocalHttpServerAddress(): String { + if (isBlank() || !isLocalMediaAddress()) return this + val mediaServer = DLNACastManager.instance.mediaServer ?: return this + val prefix = if (startsWith("file:///")) "file:///" + else if (startsWith("content://")) "content://" else "" + var newSourceUrl: String = mediaServer.baseUrl + replace(prefix, "/") + try { + val urlSplits = newSourceUrl.split("/").toTypedArray() + val originFileName = urlSplits[urlSplits.size - 1] + var fileName = originFileName + fileName = URLEncoder.encode(fileName, "UTF-8") + fileName = fileName.replace("\\+".toRegex(), "%20") + newSourceUrl = newSourceUrl.replace(originFileName, fileName) + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + } + return newSourceUrl + } + private fun getSystemService( context: Context, name: String @@ -47,7 +75,7 @@ object Utils { * @return 时间戳(毫秒) */ fun getIntTime(formatTime: String): Long { - if (!TextUtils.isEmpty(formatTime)) { + if (formatTime.isNotEmpty()) { val tmp = formatTime.split(":".toRegex()).toTypedArray() if (tmp.size < 3) return 0 val second = tmp[0].toInt() * 3600 + tmp[1].toInt() * 60 + tmp[2].toInt() diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastManager.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastManager.java deleted file mode 100644 index dad97172..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastManager.java +++ /dev/null @@ -1,360 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import android.app.Activity; -import android.app.Application; -import android.app.Service; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.skyd.imomoe.util.dlna.dmc.control.ControlImpl; -import com.skyd.imomoe.util.dlna.dmc.control.ICastInterface; -import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction; -import com.skyd.imomoe.util.dlna.dms.MediaServer; - -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.android.AndroidUpnpService; -import org.fourthline.cling.model.message.header.STAllHeader; -import org.fourthline.cling.model.message.header.UDADeviceTypeHeader; -import org.fourthline.cling.model.meta.Device; -import org.fourthline.cling.model.types.DeviceType; -import org.fourthline.cling.model.types.ServiceType; -import org.fourthline.cling.model.types.UDADeviceType; -import org.fourthline.cling.model.types.UDAServiceType; -import org.fourthline.cling.registry.Registry; -import org.fourthline.cling.registry.RegistryListener; -import org.fourthline.cling.support.model.MediaInfo; -import org.fourthline.cling.support.model.PositionInfo; -import org.fourthline.cling.support.model.TransportInfo; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * - */ -public final class DLNACastManager implements ICastInterface.IControl, OnDeviceRegistryListener { - - public static final DeviceType DEVICE_TYPE_DMR = new UDADeviceType("MediaRenderer"); - public static final ServiceType SERVICE_AV_TRANSPORT = new UDAServiceType("AVTransport"); - public static final ServiceType SERVICE_RENDERING_CONTROL = new UDAServiceType("RenderingControl"); - public static final ServiceType SERVICE_CONNECTION_MANAGER = new UDAServiceType("ConnectionManager"); - - private static class Holder { - private static final DLNACastManager INSTANCE = new DLNACastManager(); - } - - public static DLNACastManager getInstance() { - return Holder.INSTANCE; - } - - private AndroidUpnpService mDLNACastService; - private final ILogger mLogger = new ILogger.DefaultLoggerImpl(this); - private final DeviceRegistryImpl mDeviceRegistryImpl = new DeviceRegistryImpl(this); - private final Handler mMainHandler = new Handler(Looper.getMainLooper()); - private final Map>> mTagMap = new HashMap<>(); - private final Map> mActionEventCallbackMap = new LinkedHashMap<>(); - - private DeviceType mSearchDeviceType; - private ControlImpl mControlImpl; - - private DLNACastManager() { - } - - public void bindCastService(@NonNull Context context) { - if (context instanceof Application || context instanceof Activity) { - context.bindService(new Intent(context, DLNACastService.class), mServiceConnection, Service.BIND_AUTO_CREATE); - } else { - mLogger.e("bindCastService only support Application or Activity implementation."); - } - } - - public void unbindCastService(@NonNull Context context) { - if (context instanceof Application || context instanceof Activity) { - context.unbindService(mServiceConnection); - } else { - mLogger.e("bindCastService only support Application or Activity implementation."); - } - } - - private final ServiceConnection mServiceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName componentName, IBinder iBinder) { - AndroidUpnpService upnpService = (AndroidUpnpService) iBinder; - if (mDLNACastService != upnpService) { - mDLNACastService = upnpService; - Utils.logServiceConnected(mLogger, upnpService, componentName, iBinder); - Registry registry = upnpService.getRegistry(); - // add registry listener - Collection collection = registry.getListeners(); - if (collection == null || !collection.contains(mDeviceRegistryImpl)) { - registry.addListener(mDeviceRegistryImpl); - } - // Now add all devices to the list we already know about - mDeviceRegistryImpl.setDevices(upnpService.getRegistry().getDevices()); - } - if (_mediaServer != null && _mediaServer.getDevice() != null) { - mDLNACastService.getRegistry().addDevice(_mediaServer.getDevice()); - } - _mediaServer = null; - } - - @Override - public void onServiceDisconnected(ComponentName componentName) { - mLogger.w(String.format("[%s] onServiceDisconnected", componentName != null ? componentName.getShortClassName() : "NULL")); - removeRegistryListener(); - } - - @Override - public void onBindingDied(ComponentName componentName) { - mLogger.e(String.format("[%s] onBindingDied", componentName.getClassName())); - removeRegistryListener(); - } - - private void removeRegistryListener() { - if (mDLNACastService != null) { - mDLNACastService.getRegistry().removeListener(mDeviceRegistryImpl); - } - mDLNACastService = null; - } - }; - - @Nullable - public AndroidUpnpService getService() { - return mDLNACastService; - } - - // ----------------------------------------------------------------------------------------- - // ---- register or unregister device listener - // ----------------------------------------------------------------------------------------- - private final byte[] mLock = new byte[0]; - private final List mRegisterDeviceListeners = new ArrayList<>(); - - public void registerDeviceListener(OnDeviceRegistryListener listener) { - if (listener == null) return; - if (mDLNACastService != null) { - @SuppressWarnings("rawtypes") Collection devices; - - if (mSearchDeviceType == null) { - devices = mDLNACastService.getRegistry().getDevices(); - } else { - devices = mDLNACastService.getRegistry().getDevices(mSearchDeviceType); - } - - // if some devices has been found, notify first. - if (devices != null && devices.size() > 0) { - exeActionInUIThread(() -> { - for (Device device : devices) listener.onDeviceAdded(device); - }); - } - } - - synchronized (mLock) { - if (!mRegisterDeviceListeners.contains(listener)) { - mRegisterDeviceListeners.add(listener); - } - } - } - - private void exeActionInUIThread(Runnable action) { - if (action != null) { - if (Thread.currentThread() != Looper.getMainLooper().getThread()) { - mMainHandler.post(action); - } else { - action.run(); - } - } - } - - public void unregisterListener(OnDeviceRegistryListener listener) { - synchronized (mLock) { - mRegisterDeviceListeners.remove(listener); - } - } - - @Override - public void onDeviceAdded(Device device) { - if (checkDeviceType(device)) { - synchronized (mLock) { - for (OnDeviceRegistryListener listener : mRegisterDeviceListeners) listener.onDeviceAdded(device); - } - } - } - - @Override - public void onDeviceUpdated(Device device) { - if (checkDeviceType(device)) { - synchronized (mLock) { - for (OnDeviceRegistryListener listener : mRegisterDeviceListeners) listener.onDeviceUpdated(device); - } - } - } - - @Override - public void onDeviceRemoved(Device device) { - if (checkDeviceType(device)) { - // if this device is casting, disconnect first! - if (mControlImpl != null && mControlImpl.isCasting(device)) { - //TODO - // mControlImpl.release(); - } - synchronized (mLock) { - for (OnDeviceRegistryListener listener : mRegisterDeviceListeners) listener.onDeviceRemoved(device); - } - } - } - - private boolean checkDeviceType(Device device) { - return mSearchDeviceType == null || mSearchDeviceType.equals(device.getType()); - } - - // ----------------------------------------------------------------------------------------- - // ---- MediaServer - // ----------------------------------------------------------------------------------------- - private MediaServer _mediaServer; - - public void addMediaServer(@NonNull MediaServer mediaServer) { - if (mDLNACastService != null) { - if (mDLNACastService.getRegistry().getDevice(mediaServer.getDevice().getIdentity().getUdn(), true) == null) { - mDLNACastService.getRegistry().addDevice(mediaServer.getDevice()); - } - } else { - _mediaServer = mediaServer; - } - } - - // ----------------------------------------------------------------------------------------- - // ---- search - // ----------------------------------------------------------------------------------------- - public void search(DeviceType type, int maxSeconds) { - mSearchDeviceType = type; - - if (mDLNACastService != null) { - UpnpService upnpService = mDLNACastService.get(); - //TODO: clear all devices first? check!!! - upnpService.getRegistry().removeAllRemoteDevices(); - upnpService.getControlPoint().search(type == null ? new STAllHeader() : new UDADeviceTypeHeader(type), maxSeconds); - } - } - - public void search(DeviceType type) { - mSearchDeviceType = type; - - if (mDLNACastService != null) { - UpnpService upnpService = mDLNACastService.get(); - //TODO: clear all devices first? check!!! - upnpService.getRegistry().removeAllRemoteDevices(); - upnpService.getControlPoint().search(type == null ? new STAllHeader() : new UDADeviceTypeHeader(type)); - } - } - - // ----------------------------------------------------------------------------------------- - // ---- action - // ----------------------------------------------------------------------------------------- - @Override - public void cast(Device device, ICast object) { - // check device has been connected. - if (mControlImpl != null) mControlImpl.stop(); - //FIXME: cast same video should not stop and restart! - mControlImpl = new ControlImpl(mDLNACastService.getControlPoint(), device, mActionEventCallbackMap); - mControlImpl.cast(device, object); - } - - @Override - public void play() { - if (mControlImpl != null) mControlImpl.play(); - } - - @Override - public void pause() { - if (mControlImpl != null) mControlImpl.pause(); - } - - @Override - public boolean isCasting(Device device) { - return mControlImpl != null && mControlImpl.isCasting(device); - } - - @Override - public void stop() { - if (mControlImpl != null) mControlImpl.stop(); - } - - @Override - public void seekTo(long position) { - if (mControlImpl != null) mControlImpl.seekTo(position); - } - - @Override - public void setVolume(int percent) { - if (mControlImpl != null) mControlImpl.setVolume(percent); - } - - @Override - public void setMute(boolean mute) { - if (mControlImpl != null) mControlImpl.setMute(mute); - } - - @Override - public void setBrightness(int percent) { - if (mControlImpl != null) mControlImpl.setBrightness(percent); - } - - public void registerActionCallbacks(IServiceAction.IServiceActionCallback... callbacks) { - _innerRegisterActionCallback(callbacks); - } - - public void unregisterActionCallbacks() { - if (mActionEventCallbackMap.size() > 0) { - mActionEventCallbackMap.clear(); - } - } - - private void _innerRegisterActionCallback(IServiceAction.IServiceActionCallback... callbacks) { - if (callbacks != null && callbacks.length > 0) { - for (IServiceAction.IServiceActionCallback callback : callbacks) { - if (callback instanceof ICastInterface.CastEventListener) { - mActionEventCallbackMap.put(IServiceAction.ServiceAction.CAST.name(), callback); - } else if (callback instanceof ICastInterface.PlayEventListener) { - mActionEventCallbackMap.put(IServiceAction.ServiceAction.PLAY.name(), callback); - } else if (callback instanceof ICastInterface.PauseEventListener) { - mActionEventCallbackMap.put(IServiceAction.ServiceAction.PAUSE.name(), callback); - } else if (callback instanceof ICastInterface.StopEventListener) { - mActionEventCallbackMap.put(IServiceAction.ServiceAction.STOP.name(), callback); - } else if (callback instanceof ICastInterface.SeekToEventListener) { - mActionEventCallbackMap.put(IServiceAction.ServiceAction.SEEK_TO.name(), callback); - } - } - } - } - - // ----------------------------------------------------------------------------------------- - // ---- query - // ----------------------------------------------------------------------------------------- - public void getMediaInfo(Device device, ICastInterface.GetInfoListener listener) { - new QueryRequest.MediaInfoRequest(device.findService(SERVICE_AV_TRANSPORT)).execute(mDLNACastService.getControlPoint(), listener); - } - - public void getPositionInfo(Device device, ICastInterface.GetInfoListener listener) { - new QueryRequest.PositionInfoRequest(device.findService(SERVICE_AV_TRANSPORT)).execute(mDLNACastService.getControlPoint(), listener); - } - - public void getTransportInfo(Device device, ICastInterface.GetInfoListener listener) { - new QueryRequest.TransportInfoRequest(device.findService(SERVICE_AV_TRANSPORT)).execute(mDLNACastService.getControlPoint(), listener); - } - - public void getVolumeInfo(Device device, ICastInterface.GetInfoListener listener) { - new QueryRequest.VolumeInfoRequest(device.findService(SERVICE_RENDERING_CONTROL)).execute(mDLNACastService.getControlPoint(), listener); - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastManager.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastManager.kt new file mode 100644 index 00000000..67b0557a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastManager.kt @@ -0,0 +1,342 @@ +package com.skyd.imomoe.util.dlna.dmc + +import android.app.Activity +import android.app.Application +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import com.skyd.imomoe.util.dlna.dmc.ILogger.DefaultLoggerImpl +import com.skyd.imomoe.util.dlna.dmc.QueryRequest.* +import com.skyd.imomoe.util.dlna.dmc.control.ControlImpl +import com.skyd.imomoe.util.dlna.dmc.control.ICastInterface.* +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction.IServiceActionCallback +import com.skyd.imomoe.util.dlna.dms.MediaServer +import com.skyd.imomoe.util.showToast +import org.fourthline.cling.android.AndroidUpnpService +import org.fourthline.cling.model.message.header.STAllHeader +import org.fourthline.cling.model.message.header.UDADeviceTypeHeader +import org.fourthline.cling.model.meta.Device +import org.fourthline.cling.model.types.DeviceType +import org.fourthline.cling.model.types.ServiceType +import org.fourthline.cling.model.types.UDADeviceType +import org.fourthline.cling.model.types.UDAServiceType +import org.fourthline.cling.support.model.MediaInfo +import org.fourthline.cling.support.model.PositionInfo +import org.fourthline.cling.support.model.TransportInfo + +class DLNACastManager private constructor() : IControl, OnDeviceRegistryListener { + var dlnaCastService: AndroidUpnpService? = null + private set + private val mLogger: ILogger = DefaultLoggerImpl(this) + private val mDeviceRegistryImpl = DeviceRegistryImpl(this) + private val mMainHandler = Handler(Looper.getMainLooper()) + private val mActionEventCallbackMap: MutableMap> = + LinkedHashMap() + private var mSearchDeviceType: DeviceType? = null + private var mControlImpl: ControlImpl? = null + + fun bindCastService(context: Context) { + if (context is Application || context is Activity) { + context.bindService( + Intent(context, DLNACastService::class.java), + mServiceConnection, + Service.BIND_AUTO_CREATE + ) + } else { + mLogger.e("bindCastService only support Application or Activity implementation.") + } + } + + fun unbindCastService(context: Context) { + if (context is Application || context is Activity) { + context.unbindService(mServiceConnection) + } else { + mLogger.e("bindCastService only support Application or Activity implementation.") + } + } + + private val mServiceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) { + val upnpService = iBinder as AndroidUpnpService + if (dlnaCastService !== upnpService) { + dlnaCastService = upnpService + Utils.logServiceConnected(mLogger, upnpService, componentName, iBinder) + val registry = upnpService.registry + // add registry listener + val collection = registry.listeners + if (collection == null || !collection.contains(mDeviceRegistryImpl)) { + registry.addListener(mDeviceRegistryImpl) + } + // Now add all devices to the list we already know about + mDeviceRegistryImpl.setDevices(upnpService.registry.devices as Collection>) + } + val device = _mediaServer?.device + if (device != null) { + dlnaCastService?.registry?.addDevice(device) + } + _mediaServer = null + } + + override fun onServiceDisconnected(componentName: ComponentName) { + mLogger.w(String.format("[%s] onServiceDisconnected", componentName.shortClassName)) + removeRegistryListener() + } + + override fun onBindingDied(componentName: ComponentName) { + mLogger.e(String.format("[%s] onBindingDied", componentName.className)) + removeRegistryListener() + } + + private fun removeRegistryListener() { + dlnaCastService?.registry?.removeListener(mDeviceRegistryImpl) + dlnaCastService = null + } + } + + // ----------------------------------------------------------------------------------------- + // ---- register or unregister device listener + // ----------------------------------------------------------------------------------------- + private val mLock = ByteArray(0) + private val mRegisterDeviceListeners: MutableList = ArrayList() + fun registerDeviceListener(listener: OnDeviceRegistryListener) { +// if (listener == null) return + dlnaCastService?.let { + val devices: Collection> = if (mSearchDeviceType == null) { + it.registry.devices + } else { + it.registry.getDevices(mSearchDeviceType) + } + + // if some devices has been found, notify first. + if (devices.isNotEmpty()) { + exeActionInUIThread { for (device in devices) listener.onDeviceAdded(device) } + } + } + synchronized(mLock) { + if (!mRegisterDeviceListeners.contains(listener)) { + mRegisterDeviceListeners.add(listener) + } + } + } + + private fun exeActionInUIThread(action: Runnable) { + if (Thread.currentThread() !== Looper.getMainLooper().thread) { + mMainHandler.post(action) + } else { + action.run() + } + } + + fun unregisterListener(listener: OnDeviceRegistryListener) { + synchronized(mLock) { mRegisterDeviceListeners.remove(listener) } + } + + override fun onDeviceAdded(device: Device<*, *, *>) { + if (checkDeviceType(device)) { + synchronized(mLock) { + for (listener in mRegisterDeviceListeners) listener.onDeviceAdded(device) + } + } + } + + + override fun onDeviceUpdated(device: Device<*, *, *>) { + if (checkDeviceType(device)) { + synchronized(mLock) { + for (listener in mRegisterDeviceListeners) listener.onDeviceUpdated(device) + } + } + } + + override fun onDeviceRemoved(device: Device<*, *, *>) { + if (checkDeviceType(device)) { + // if this device is casting, disconnect first! + if (mControlImpl?.isCasting(device) == true) { + //TODO + // mControlImpl.release(); + } + synchronized(mLock) { + for (listener in mRegisterDeviceListeners) listener.onDeviceRemoved(device) + } + } + } + + private fun checkDeviceType(device: Device<*, *, *>): Boolean { + return mSearchDeviceType == null || mSearchDeviceType == device.type + } + + // ----------------------------------------------------------------------------------------- + // ---- MediaServer + // ----------------------------------------------------------------------------------------- + // _mediaServer会被设置为null + private var _mediaServer: MediaServer? = null + + // mMediaServer不会被设置为null + var mediaServer: MediaServer? = null + private set + + fun addMediaServer(mediaServer: MediaServer) { + val service = dlnaCastService + if (service != null) { + if (service.registry.getDevice(mediaServer.device.identity.udn, true) == null) { + service.registry.addDevice(mediaServer.device) + } + } else { + _mediaServer = mediaServer + } + this.mediaServer = mediaServer + } + + // ----------------------------------------------------------------------------------------- + // ---- search + // ----------------------------------------------------------------------------------------- + fun search(type: DeviceType?, maxSeconds: Int) { + mSearchDeviceType = type + dlnaCastService?.let { service -> + val upnpService = service.get() + //TODO: clear all devices first? check!!! + upnpService.registry.removeAllRemoteDevices() + upnpService.controlPoint.search(type?.let { UDADeviceTypeHeader(it) } + ?: STAllHeader(), maxSeconds) + } + } + + fun search(type: DeviceType?) { + mSearchDeviceType = type + dlnaCastService?.let { service -> + val upnpService = service.get() + //TODO: clear all devices first? check!!! + upnpService.registry.removeAllRemoteDevices() + upnpService.controlPoint.search(type?.let { UDADeviceTypeHeader(it) } ?: STAllHeader()) + } + } + + // ----------------------------------------------------------------------------------------- + // ---- action + // ----------------------------------------------------------------------------------------- + override fun cast(device: Device<*, *, *>, `object`: ICast) { + // check device has been connected. + mControlImpl?.stop() + //FIXME: cast same video should not stop and restart! + try { + ControlImpl(dlnaCastService!!.controlPoint, device, mActionEventCallbackMap).let { + mControlImpl = it + it.cast(device, `object`) + } + } catch (e: NullPointerException) { + e.printStackTrace() + e.message?.showToast() + } + } + + override fun play() { + mControlImpl?.play() + } + + override fun pause() { + mControlImpl?.pause() + } + + override fun isCasting(device: Device<*, *, *>): Boolean { + return mControlImpl.let { it != null && it.isCasting(device) } + } + + override fun stop() { + mControlImpl?.stop() + } + + override fun seekTo(position: Long) { + mControlImpl?.seekTo(position) + } + + override fun setVolume(percent: Int) { + mControlImpl?.setVolume(percent) + } + + override fun setMute(mute: Boolean) { + mControlImpl?.setMute(mute) + } + + override fun setBrightness(percent: Int) { + mControlImpl?.setBrightness(percent) + } + + fun registerActionCallbacks(vararg callbacks: IServiceActionCallback<*>) { + innerRegisterActionCallback(*callbacks) + } + + fun unregisterActionCallbacks() { + if (mActionEventCallbackMap.isNotEmpty()) { + mActionEventCallbackMap.clear() + } + } + + private fun innerRegisterActionCallback(vararg callbacks: IServiceActionCallback<*>) { + if (callbacks.isNotEmpty()) { + for (c in callbacks) { + when (c) { + is CastEventListener -> + mActionEventCallbackMap[IServiceAction.ServiceAction.CAST.name] = c + is PlayEventListener -> + mActionEventCallbackMap[IServiceAction.ServiceAction.PLAY.name] = c + is PauseEventListener -> + mActionEventCallbackMap[IServiceAction.ServiceAction.PAUSE.name] = c + is StopEventListener -> + mActionEventCallbackMap[IServiceAction.ServiceAction.STOP.name] = c + is SeekToEventListener -> + mActionEventCallbackMap[IServiceAction.ServiceAction.SEEK_TO.name] = c + } + } + } + } + + // ----------------------------------------------------------------------------------------- + // ---- query + // ----------------------------------------------------------------------------------------- + fun getMediaInfo(device: Device<*, *, *>, listener: GetInfoListener) { + dlnaCastService?.apply { + MediaInfoRequest(device.findService(SERVICE_AV_TRANSPORT)).execute( + controlPoint, listener + ) + } + } + + fun getPositionInfo(device: Device<*, *, *>, listener: GetInfoListener) { + dlnaCastService?.apply { + PositionInfoRequest(device.findService(SERVICE_AV_TRANSPORT)).execute( + controlPoint, listener + ) + } + } + + fun getTransportInfo(device: Device<*, *, *>, listener: GetInfoListener) { + dlnaCastService?.apply { + TransportInfoRequest(device.findService(SERVICE_AV_TRANSPORT)).execute( + controlPoint, listener + ) + } + } + + fun getVolumeInfo(device: Device<*, *, *>, listener: GetInfoListener) { + dlnaCastService?.apply { + VolumeInfoRequest(device.findService(SERVICE_RENDERING_CONTROL)).execute( + controlPoint, listener + ) + } + } + + companion object { + val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { DLNACastManager() } + + val DEVICE_TYPE_DMR: DeviceType = UDADeviceType("MediaRenderer") + val SERVICE_AV_TRANSPORT: ServiceType = UDAServiceType("AVTransport") + val SERVICE_RENDERING_CONTROL: ServiceType = UDAServiceType("RenderingControl") + val SERVICE_CONNECTION_MANAGER: ServiceType = UDAServiceType("ConnectionManager") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastService.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastService.java deleted file mode 100644 index 778e0113..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastService.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import android.content.Intent; - -import org.fourthline.cling.UpnpServiceConfiguration; -import org.fourthline.cling.android.AndroidUpnpServiceConfiguration; -import org.fourthline.cling.android.AndroidUpnpServiceImpl; -import org.fourthline.cling.android.FixedAndroidLogHandler; - -/** - * - */ -public class DLNACastService extends AndroidUpnpServiceImpl { - private final ILogger mLogger = new ILogger.DefaultLoggerImpl(this); - - @Override - public void onCreate() { - mLogger.i(String.format("[%s] onCreate", getClass().getName())); - org.seamless.util.logging.LoggingUtil.resetRootHandler(new FixedAndroidLogHandler()); - super.onCreate(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - mLogger.i(String.format("[%s] onStartCommand: %s , %s", getClass().getName(), intent, flags)); - return super.onStartCommand(intent, flags, startId); - } - - @Override - public void onDestroy() { - mLogger.w(String.format("[%s] onDestroy", getClass().getName())); - super.onDestroy(); - } - - @Override - protected UpnpServiceConfiguration createConfiguration() { - return new DLNACastServiceConfiguration(); - } - - // ---------------------------------------------------------------- - // ---- configuration - // ---------------------------------------------------------------- - private static final class DLNACastServiceConfiguration extends AndroidUpnpServiceConfiguration { - - @Override - public int getRegistryMaintenanceIntervalMillis() { - return 5000; //default is 3000! - } - - // @Override - // public ServiceType[] getExclusiveServiceTypes() { - // return new ServiceType[]{ - // DLNACastManager.SERVICE_RENDERING_CONTROL, - // DLNACastManager.SERVICE_AV_TRANSPORT, - // DLNACastManager.SERVICE_CONNECTION_MANAGER}; - // } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastService.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastService.kt new file mode 100644 index 00000000..10ec24db --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DLNACastService.kt @@ -0,0 +1,50 @@ +package com.skyd.imomoe.util.dlna.dmc + +import android.content.Intent +import com.skyd.imomoe.util.dlna.dmc.ILogger.DefaultLoggerImpl +import org.fourthline.cling.UpnpServiceConfiguration +import org.fourthline.cling.android.AndroidUpnpServiceConfiguration +import org.fourthline.cling.android.AndroidUpnpServiceImpl +import org.fourthline.cling.android.FixedAndroidLogHandler +import org.seamless.util.logging.LoggingUtil + + +class DLNACastService : AndroidUpnpServiceImpl() { + private val mLogger: ILogger = DefaultLoggerImpl(this) + override fun onCreate() { + mLogger.i(String.format("[%s] onCreate", javaClass.name)) + LoggingUtil.resetRootHandler(FixedAndroidLogHandler()) + super.onCreate() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + mLogger.i(String.format("[%s] onStartCommand: %s , %s", javaClass.name, intent, flags)) + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + mLogger.w(String.format("[%s] onDestroy", javaClass.name)) + super.onDestroy() + } + + override fun createConfiguration(): UpnpServiceConfiguration { + return DLNACastServiceConfiguration() + } + + // ---------------------------------------------------------------- + // ---- configuration + // ---------------------------------------------------------------- + private class DLNACastServiceConfiguration : AndroidUpnpServiceConfiguration() { + override fun getRegistryMaintenanceIntervalMillis(): Int { + return 5000 //default is 3000! + } + + // @Override + // public ServiceType[] getExclusiveServiceTypes() { + // return new ServiceType[]{ + // DLNACastManager.SERVICE_RENDERING_CONTROL, + // DLNACastManager.SERVICE_AV_TRANSPORT, + // DLNACastManager.SERVICE_CONNECTION_MANAGER}; + // } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DeviceRegistryImpl.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DeviceRegistryImpl.java deleted file mode 100644 index 59136b04..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DeviceRegistryImpl.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import android.os.Handler; -import android.os.Looper; - -import androidx.annotation.NonNull; - -import org.fourthline.cling.model.meta.Device; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.meta.RemoteDevice; -import org.fourthline.cling.registry.DefaultRegistryListener; -import org.fourthline.cling.registry.Registry; - -import java.util.Collection; - -/** - * - */ -final class DeviceRegistryImpl extends DefaultRegistryListener { - - private final OnDeviceRegistryListener mOnDeviceRegistryListener; - private final ILogger mLogger = new ILogger.DefaultLoggerImpl(this); - private final Handler mHandler = new Handler(Looper.getMainLooper()); - private volatile boolean mIgnoreUpdate = true; - - public DeviceRegistryImpl(@NonNull OnDeviceRegistryListener listener) { - mOnDeviceRegistryListener = listener; - setIgnoreUpdateEvent(true); - } - - public void setIgnoreUpdateEvent(boolean ignoreUpdate) { - mIgnoreUpdate = ignoreUpdate; - } - - public void setDevices(@SuppressWarnings("rawtypes") Collection collection) { - if (collection != null && collection.size() > 0) { - for (Device device : collection) { - notifyDeviceAdd(device); - } - } - } - - // Discovery performance optimization for very slow Android devices! - // This function will called early than 'remoteDeviceAdded',but the device services maybe not entirely. - @Override - public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { - mLogger.i(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - mLogger.i(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - mLogger.i(String.format("[%s] discovery started...", device.getDetails().getFriendlyName())); - } - - //End of optimization, you can remove the whole block if your Android handset is fast (>= 600 Mhz) - @Override - public void remoteDeviceDiscoveryFailed(Registry registry, final RemoteDevice device, final Exception ex) { - mLogger.e("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); - mLogger.e("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); - mLogger.e(String.format("[%s] discovery failed...", device.getDetails().getFriendlyName())); - mLogger.e(ex.toString()); - } - - // remote device - @Override - public void remoteDeviceAdded(Registry registry, RemoteDevice device) { - mLogger.i("remoteDeviceAdded: " + Utils.parseDeviceInfo(device)); - mLogger.i(Utils.parseDeviceService(device)); - notifyDeviceAdd(device); - } - - @Override - public void remoteDeviceUpdated(Registry registry, RemoteDevice device) { - if (!mIgnoreUpdate) { - mLogger.d("remoteDeviceUpdated: " + Utils.parseDeviceInfo(device)); - notifyDeviceUpdate(device); - } - } - - @Override - public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { - mLogger.w("remoteDeviceRemoved: " + Utils.parseDeviceInfo(device)); - notifyDeviceRemove(device); - } - - // local device - @Override - public void localDeviceAdded(Registry registry, LocalDevice device) { - super.localDeviceAdded(registry, device); - } - - @Override - public void localDeviceRemoved(Registry registry, LocalDevice device) { - super.localDeviceRemoved(registry, device); - } - - private void notifyDeviceAdd(final Device device) { - mHandler.post(() -> mOnDeviceRegistryListener.onDeviceAdded(device)); - } - - private void notifyDeviceUpdate(final Device device) { - mHandler.post(() -> mOnDeviceRegistryListener.onDeviceUpdated(device)); - } - - private void notifyDeviceRemove(final Device device) { - mHandler.post(() -> mOnDeviceRegistryListener.onDeviceRemoved(device)); - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DeviceRegistryImpl.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DeviceRegistryImpl.kt new file mode 100644 index 00000000..cdb4d703 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/DeviceRegistryImpl.kt @@ -0,0 +1,111 @@ +package com.skyd.imomoe.util.dlna.dmc + +import android.os.Handler +import android.os.Looper +import com.skyd.imomoe.util.dlna.dmc.ILogger.DefaultLoggerImpl +import org.fourthline.cling.model.meta.Device +import org.fourthline.cling.model.meta.LocalDevice +import org.fourthline.cling.model.meta.RemoteDevice +import org.fourthline.cling.registry.DefaultRegistryListener +import org.fourthline.cling.registry.Registry + + +internal class DeviceRegistryImpl(private val mOnDeviceRegistryListener: OnDeviceRegistryListener) : + DefaultRegistryListener() { + private val mLogger: ILogger = DefaultLoggerImpl(this) + private val mHandler = Handler(Looper.getMainLooper()) + + @Volatile + private var mIgnoreUpdate = true + + fun setIgnoreUpdateEvent(ignoreUpdate: Boolean) { + mIgnoreUpdate = ignoreUpdate + } + + fun setDevices(collection: Collection>?) { + if (collection != null && collection.isNotEmpty()) { + for (device in collection) { + notifyDeviceAdd(device) + } + } + } + + // Discovery performance optimization for very slow Android devices! + // This function will called early than 'remoteDeviceAdded',but the device services maybe not entirely. + override fun remoteDeviceDiscoveryStarted(registry: Registry?, device: RemoteDevice?) { + mLogger.i(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + mLogger.i(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + mLogger.i(String.format("[%s] discovery started...", device?.details?.friendlyName)) + } + + //End of optimization, you can remove the whole block if your Android handset is fast (>= 600 Mhz) + override fun remoteDeviceDiscoveryFailed( + registry: Registry?, + device: RemoteDevice?, + ex: Exception? + ) { + mLogger.e("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + mLogger.e("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + mLogger.e(String.format("[%s] discovery failed...", device?.details?.friendlyName)) + mLogger.e(ex.toString()) + } + + // remote device + override fun remoteDeviceAdded(registry: Registry?, device: RemoteDevice?) { + mLogger.i( + "remoteDeviceAdded: ${ + if (device == null) "device is null!!!" else Utils.parseDeviceInfo(device) + }" + ) + mLogger.i(if (device == null) "device is null!!!" else Utils.parseDeviceService(device)) + device ?: return + notifyDeviceAdd(device) + } + + override fun remoteDeviceUpdated(registry: Registry?, device: RemoteDevice?) { + if (!mIgnoreUpdate) { + mLogger.d( + "remoteDeviceUpdated: ${ + if (device == null) "device is null!!!" else Utils.parseDeviceInfo(device) + }" + ) + device ?: return + notifyDeviceUpdate(device) + } + } + + override fun remoteDeviceRemoved(registry: Registry?, device: RemoteDevice?) { + mLogger.w( + "remoteDeviceRemoved: ${ + if (device == null) "device is null!!!" else Utils.parseDeviceInfo(device) + }" + ) + device ?: return + notifyDeviceRemove(device) + } + + // local device + override fun localDeviceAdded(registry: Registry?, device: LocalDevice?) { + super.localDeviceAdded(registry, device) + } + + override fun localDeviceRemoved(registry: Registry?, device: LocalDevice?) { + super.localDeviceRemoved(registry, device) + } + + private fun notifyDeviceAdd(device: Device<*, *, *>) { + mHandler.post { mOnDeviceRegistryListener.onDeviceAdded(device) } + } + + private fun notifyDeviceUpdate(device: Device<*, *, *>) { + mHandler.post { mOnDeviceRegistryListener.onDeviceUpdated(device) } + } + + private fun notifyDeviceRemove(device: Device<*, *, *>) { + mHandler.post { mOnDeviceRegistryListener.onDeviceRemoved(device) } + } + + init { + setIgnoreUpdateEvent(true) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ICast.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ICast.java deleted file mode 100644 index 57ebb927..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ICast.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import androidx.annotation.NonNull; - -public interface ICast { - - @NonNull - String getId(); - - @NonNull - String getUri(); - - String getName(); - - interface ICastVideo extends ICast { - - /** - * @return video duration, ms - */ - long getDurationMillSeconds(); - - long getSize(); - - long getBitrate(); - } - - interface ICastAudio extends ICast { - /** - * @return audio duration, ms - */ - long getDurationMillSeconds(); - - long getSize(); - } - - interface ICastImage extends ICast { - long getSize(); - } - -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ICast.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ICast.kt new file mode 100644 index 00000000..c6e0b593 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ICast.kt @@ -0,0 +1,28 @@ +package com.skyd.imomoe.util.dlna.dmc + +interface ICast { + val id: String + val uri: String + val name: String? + + interface ICastVideo : ICast { + /** + * @return video duration, ms + */ + val durationMillSeconds: Long + val size: Long + val bitrate: Long + } + + interface ICastAudio : ICast { + /** + * @return audio duration, ms + */ + val durationMillSeconds: Long + val size: Long + } + + interface ICastImage : ICast { + val size: Long + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ILogger.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ILogger.java deleted file mode 100644 index c01ec4b9..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ILogger.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import android.text.TextUtils; -import android.util.Log; - -import com.skyd.imomoe.BuildConfig; - -public interface ILogger { - String PREFIX_TAG = "DLNACast_"; - - void v(String msg); - - void d(String msg); - - void i(String msg); - - void w(String msg); - - void e(String msg); - - class DefaultLoggerImpl implements ILogger { - private final String TAG; - private final boolean DEBUG; - - public DefaultLoggerImpl(Object object) { - this(object, BuildConfig.DEBUG); - } - - public DefaultLoggerImpl(Object object, boolean debug) { - String className = object.getClass().getSimpleName(); - if (TextUtils.isEmpty(className)) { - if (object.getClass().getSuperclass() != null) { - className = object.getClass().getSuperclass().getSimpleName(); - } else { - className = "$1"; - } - } - TAG = PREFIX_TAG + className; - DEBUG = debug; - } - - @Override - public void v(String msg) { - if (DEBUG) { - Log.v(TAG, msg); - } - } - - @Override - public void d(String msg) { - if (DEBUG) { - Log.d(TAG, msg); - } - } - - @Override - public void i(String msg) { - if (DEBUG) { - Log.i(TAG, msg); - } - } - - @Override - public void w(String msg) { - if (DEBUG) { - Log.w(TAG, msg); - } - } - - @Override - public void e(String msg) { - if (DEBUG) { - Log.e(TAG, msg); - } - } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ILogger.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ILogger.kt new file mode 100644 index 00000000..cef20ebd --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/ILogger.kt @@ -0,0 +1,57 @@ +package com.skyd.imomoe.util.dlna.dmc + +import com.skyd.imomoe.BuildConfig +import com.skyd.imomoe.util.* + +interface ILogger { + fun v(msg: String?) + fun d(msg: String?) + fun i(msg: String?) + fun w(msg: String?) + fun e(msg: String?) + + class DefaultLoggerImpl @JvmOverloads constructor( + `object`: Any, + debug: Boolean = BuildConfig.DEBUG + ) : ILogger { + private val TAG: String + private val DEBUG: Boolean + + override fun v(msg: String?) { + if (DEBUG) logV(TAG, msg.toString()) + } + + override fun d(msg: String?) { + if (DEBUG) logD(TAG, msg.toString()) + } + + override fun i(msg: String?) { + if (DEBUG) logI(TAG, msg.toString()) + } + + override fun w(msg: String?) { + if (DEBUG) logW(TAG, msg.toString()) + } + + override fun e(msg: String?) { + if (DEBUG) logE(TAG, msg.toString()) + } + + init { + var className = `object`.javaClass.simpleName + if (className.isNullOrEmpty()) { + className = if (`object`.javaClass.superclass != null) { + `object`.javaClass.superclass.simpleName + } else { + "$1" + } + } + TAG = PREFIX_TAG + className + DEBUG = debug + } + } + + companion object { + const val PREFIX_TAG = "DLNACast_" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/OnDeviceRegistryListener.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/OnDeviceRegistryListener.java deleted file mode 100644 index ce358dd3..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/OnDeviceRegistryListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import org.fourthline.cling.model.meta.Device; - -/** - * this listener call in UI thread. - */ -public interface OnDeviceRegistryListener { - void onDeviceAdded(Device device); - - void onDeviceUpdated(Device device); - - void onDeviceRemoved(Device device); -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/OnDeviceRegistryListener.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/OnDeviceRegistryListener.kt new file mode 100644 index 00000000..1962d5a4 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/OnDeviceRegistryListener.kt @@ -0,0 +1,62 @@ +package com.skyd.imomoe.util.dlna.dmc + +import org.fourthline.cling.model.meta.Device + +/** + * this listener call in UI thread. + */ +interface OnDeviceRegistryListener { + fun onDeviceAdded(device: Device<*, *, *>) + fun onDeviceUpdated(device: Device<*, *, *>) + fun onDeviceRemoved(device: Device<*, *, *>) +} + +// use kotlin dsl to replace OnDeviceRegistryListener interface +private val listeners: HashMap Unit, OnDeviceRegistryListenerDsl> + by lazy { HashMap() } + +fun DLNACastManager.registerDeviceListener(init: OnDeviceRegistryListenerDsl.() -> Unit) { + val listener = OnDeviceRegistryListenerDsl() + listeners[init] = listener + listener.init() + this.registerDeviceListener(listener) +} + +fun DLNACastManager.unregisterListener(init: OnDeviceRegistryListenerDsl.() -> Unit) { + val listener = listeners[init] + if (listener != null) this.unregisterListener(listener) +} + +private typealias DeviceAdded = (device: Device<*, *, *>) -> Unit +private typealias DeviceUpdated = (device: Device<*, *, *>) -> Unit +private typealias DeviceRemoved = (device: Device<*, *, *>) -> Unit + +class OnDeviceRegistryListenerDsl : OnDeviceRegistryListener { + private var deviceAdded: DeviceAdded? = null + private var deviceUpdated: DeviceUpdated? = null + private var deviceRemoved: DeviceRemoved? = null + + fun onDeviceAdded(deviceAdded: DeviceAdded?) { + this.deviceAdded = deviceAdded + } + + fun onDeviceUpdated(deviceUpdated: DeviceUpdated?) { + this.deviceUpdated = deviceUpdated + } + + fun onDeviceRemoved(deviceRemoved: DeviceRemoved?) { + this.deviceRemoved = deviceRemoved + } + + override fun onDeviceAdded(device: Device<*, *, *>) { + deviceAdded?.invoke(device) + } + + override fun onDeviceUpdated(device: Device<*, *, *>) { + deviceUpdated?.invoke(device) + } + + override fun onDeviceRemoved(device: Device<*, *, *>) { + deviceRemoved?.invoke(device) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/QueryRequest.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/QueryRequest.java deleted file mode 100644 index f4a9f8b4..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/QueryRequest.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.skyd.imomoe.util.dlna.dmc.control.ICastInterface; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.avtransport.callback.GetMediaInfo; -import org.fourthline.cling.support.avtransport.callback.GetPositionInfo; -import org.fourthline.cling.support.avtransport.callback.GetTransportInfo; -import org.fourthline.cling.support.model.MediaInfo; -import org.fourthline.cling.support.model.PositionInfo; -import org.fourthline.cling.support.model.TransportInfo; -import org.fourthline.cling.support.renderingcontrol.callback.GetVolume; - -abstract class QueryRequest { - - @Nullable - private final Service service; - private ICastInterface.GetInfoListener listener; - private final ILogger logger = new ILogger.DefaultLoggerImpl(this); - private final Handler mainHandler = new Handler(Looper.getMainLooper()); - - public QueryRequest(@Nullable Service service) { - this.service = service; - } - - @Nullable - protected final Service getService() { - return service; - } - - protected abstract String getActionName(); - - protected abstract ActionCallback getAction(); - - protected void setResult(T t) { - if (listener != null) { - if (Thread.currentThread() != Looper.getMainLooper().getThread()) { - mainHandler.post(() -> listener.onGetInfoResult(t, null)); - } else { - listener.onGetInfoResult(t, null); - } - } - } - - protected void setError(String errorMsg) { - logger.e(errorMsg != null ? errorMsg : "error"); - if (listener != null) { - if (Thread.currentThread() != Looper.getMainLooper().getThread()) { - mainHandler.post(() -> listener.onGetInfoResult(null, errorMsg != null ? errorMsg : "error")); - } else { - listener.onGetInfoResult(null, errorMsg != null ? errorMsg : "error"); - } - } - } - - public final void execute(ControlPoint point, ICastInterface.GetInfoListener listener) { - this.listener = listener; - if (TextUtils.isEmpty(getActionName())) { - setError("not find action name!"); - return; - } else if (getService() == null) { - setError("the service is NULL!"); - return; - } else if (getService().getAction(getActionName()) == null) { - setError(String.format("this service not support '%s' action.", getActionName())); - return; - } - ActionCallback actionCallback = getAction(); - if (actionCallback != null) { - point.execute(getAction()); - } else { - setError("this service action is NULL!"); - } - } - - // --------------------------------------------------------------------------------------------- - // ---- MediaInfo - // --------------------------------------------------------------------------------------------- - static class MediaInfoRequest extends QueryRequest { - - public MediaInfoRequest(Service service) { - super(service); - } - - @Override - protected String getActionName() { - return "GetMediaInfo"; - } - - @Override - protected ActionCallback getAction() { - return getService() != null ? - new GetMediaInfo(getService()) { - @Override - public void received(ActionInvocation invocation, MediaInfo mediaInfo) { - setResult(mediaInfo); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - setError(defaultMsg); - } - } : - null; - } - - } - - // --------------------------------------------------------------------------------------------- - // ---- PositionInfo - // --------------------------------------------------------------------------------------------- - static final class PositionInfoRequest extends QueryRequest { - - public PositionInfoRequest(@NonNull Service service) { - super(service); - } - - @Override - protected String getActionName() { - return "GetPositionInfo"; - } - - @Override - protected ActionCallback getAction() { - return getService() != null ? - new GetPositionInfo(getService()) { - @Override - public void received(ActionInvocation invocation, PositionInfo positionInfo) { - setResult(positionInfo); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - setError(defaultMsg); - } - } : - null; - } - - } - - // --------------------------------------------------------------------------------------------- - // ---- TransportInfo - // --------------------------------------------------------------------------------------------- - static final class TransportInfoRequest extends QueryRequest { - - public TransportInfoRequest(@NonNull Service service) { - super(service); - } - - @Override - protected String getActionName() { - return "GetTransportInfo"; - } - - @Override - protected ActionCallback getAction() { - return getService() != null ? - new GetTransportInfo(getService()) { - @Override - public void received(ActionInvocation invocation, TransportInfo transportInfo) { - setResult(transportInfo); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - setError(defaultMsg); - } - } : - null; - } - - } - - // --------------------------------------------------------------------------------------------- - // ---- VolumeInfo - // --------------------------------------------------------------------------------------------- - static final class VolumeInfoRequest extends QueryRequest { - - public VolumeInfoRequest(@NonNull Service service) { - super(service); - } - - @Override - protected String getActionName() { - return "GetVolume"; - } - - @Override - protected ActionCallback getAction() { - return getService() != null ? - new GetVolume(getService()) { - @Override - public void received(ActionInvocation actionInvocation, int currentVolume) { - setResult(currentVolume); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - setError(defaultMsg); - } - } : - null; - } - - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/QueryRequest.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/QueryRequest.kt new file mode 100644 index 00000000..a1c7f4eb --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/QueryRequest.kt @@ -0,0 +1,151 @@ +package com.skyd.imomoe.util.dlna.dmc + +import android.os.Handler +import android.os.Looper +import com.skyd.imomoe.util.dlna.dmc.ILogger.DefaultLoggerImpl +import com.skyd.imomoe.util.dlna.dmc.control.ICastInterface.GetInfoListener +import org.fourthline.cling.controlpoint.ActionCallback +import org.fourthline.cling.controlpoint.ControlPoint +import org.fourthline.cling.model.action.ActionInvocation +import org.fourthline.cling.model.message.UpnpResponse +import org.fourthline.cling.model.meta.Service +import org.fourthline.cling.support.avtransport.callback.GetMediaInfo +import org.fourthline.cling.support.avtransport.callback.GetPositionInfo +import org.fourthline.cling.support.avtransport.callback.GetTransportInfo +import org.fourthline.cling.support.model.MediaInfo +import org.fourthline.cling.support.model.PositionInfo +import org.fourthline.cling.support.model.TransportInfo +import org.fourthline.cling.support.renderingcontrol.callback.GetVolume + +internal abstract class QueryRequest(protected val service: Service<*, *>) { + private var listener: GetInfoListener? = null + private val logger: ILogger by lazy { DefaultLoggerImpl(this) } + private val mainHandler by lazy { Handler(Looper.getMainLooper()) } + + protected abstract val actionName: String + protected abstract val action: ActionCallback + + protected fun setResult(t: T) { + listener?.let { + if (Thread.currentThread() !== Looper.getMainLooper().thread) { + mainHandler.post { it.onGetInfoResult(t, null) } + } else { + it.onGetInfoResult(t, null) + } + } + } + + protected fun setError(errorMsg: String?) { + logger.e(errorMsg ?: "error") + listener?.let { + if (Thread.currentThread() !== Looper.getMainLooper().thread) { + mainHandler.post { it.onGetInfoResult(null, errorMsg ?: "error") } + } else { + it.onGetInfoResult(null, errorMsg ?: "error") + } + } + } + + fun execute(point: ControlPoint, listener: GetInfoListener) { + this.listener = listener + when { + actionName.isBlank() -> { + setError("not find action name!") + return + } + service.getAction(actionName) == null -> { + setError(String.format("this service not support '%s' action.", actionName)) + return + } + else -> { + point.execute(action) + } + } + } + + // --------------------------------------------------------------------------------------------- + // ---- MediaInfo + // --------------------------------------------------------------------------------------------- + internal class MediaInfoRequest(service: Service<*, *>) : QueryRequest(service) { + override val actionName: String + get() = "GetMediaInfo" + override val action: ActionCallback + get() = object : GetMediaInfo(service) { + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) = setError(defaultMsg) + + override fun received( + invocation: ActionInvocation>?, + mediaInfo: MediaInfo? + ) = setResult(mediaInfo) + } + } + + // --------------------------------------------------------------------------------------------- + // ---- PositionInfo + // --------------------------------------------------------------------------------------------- + internal class PositionInfoRequest(service: Service<*, *>) : + QueryRequest(service) { + override val actionName: String + get() = "GetPositionInfo" + override val action: ActionCallback + get() = object : GetPositionInfo(service) { + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) = setError(defaultMsg) + + override fun received( + invocation: ActionInvocation>?, + positionInfo: PositionInfo? + ) = setResult(positionInfo) + } + } + + // --------------------------------------------------------------------------------------------- + // ---- TransportInfo + // --------------------------------------------------------------------------------------------- + internal class TransportInfoRequest(service: Service<*, *>) : + QueryRequest(service) { + override val actionName: String + get() = "GetTransportInfo" + override val action: ActionCallback + get() = object : GetTransportInfo(service) { + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) = setError(defaultMsg) + + override fun received( + invocation: ActionInvocation>?, + transportInfo: TransportInfo? + ) = setResult(transportInfo) + } + } + + // --------------------------------------------------------------------------------------------- + // ---- VolumeInfo + // --------------------------------------------------------------------------------------------- + internal class VolumeInfoRequest(service: Service<*, *>) : QueryRequest(service) { + override val actionName: String + get() = "GetVolume" + override val action: ActionCallback + get() = object : GetVolume(service) { + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) = setError(defaultMsg) + + override fun received( + actionInvocation: ActionInvocation>?, + currentVolume: Int + ) = setResult(currentVolume) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/Utils.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/Utils.java deleted file mode 100644 index 28c3754e..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/Utils.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc; - -import android.content.ComponentName; -import android.os.IBinder; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import org.fourthline.cling.android.AndroidUpnpService; -import org.fourthline.cling.model.meta.Action; -import org.fourthline.cling.model.meta.RemoteDevice; -import org.fourthline.cling.model.meta.RemoteService; -import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; -import org.fourthline.cling.support.model.item.AudioItem; -import org.fourthline.cling.support.model.item.ImageItem; -import org.fourthline.cling.support.model.item.VideoItem; -import org.seamless.util.MimeType; - -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.Formatter; -import java.util.List; -import java.util.Locale; - -/** - * - */ -final public class Utils { - - private static final String DIDL_LITE_FOOTER = ""; - private static final String DIDL_LITE_HEADER = "" + ""; - private static final String CAST_PARENT_ID = "1"; - private static final String CAST_CREATOR = "NLUpnpCast"; - private static final String CAST_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(CAST_DATE_FORMAT, Locale.US); - - /** - * 把时间戳转换成 00:00:00 格式 - * - * @param timeMs 时间戳 - * @return 00:00:00 时间格式 - */ - public static String getStringTime(long timeMs) { - StringBuilder formatBuilder = new StringBuilder(); - Formatter formatter = new Formatter(formatBuilder, Locale.US); - - long totalSeconds = timeMs / 1000; - long seconds = totalSeconds % 60; - long minutes = (totalSeconds / 60) % 60; - long hours = totalSeconds / 3600; - - return formatter.format("%02d:%02d:%02d", hours, minutes, seconds).toString(); - } - - /** - * 把 00:00:00 格式转成时间戳 - * - * @param formatTime 00:00:00 时间格式 - * @return 时间戳(毫秒) - */ - public static long getIntTime(String formatTime) { - if (!TextUtils.isEmpty(formatTime)) { - String[] tmp = formatTime.split(":"); - - if (tmp.length < 3) { - return 0; - } - - int second = Integer.valueOf(tmp[0]) * 3600 + Integer.valueOf(tmp[1]) * 60 + Integer.valueOf(tmp[2]); - - return second * 1000L; - } - - return 0; - } - - public static long parseTime(String s) { - try { - return Long.parseLong(s); - } catch (Exception e) { - e.printStackTrace(); - } - - return 0L; - } - - public static String getMetadata(ICast cast) { - if (cast instanceof ICast.ICastVideo) { - ICast.ICastVideo castObj = (ICast.ICastVideo) cast; - Res castRes = new Res(new MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), castObj.getSize(), cast.getUri()); - castRes.setBitrate(castObj.getBitrate()); - castRes.setDuration(getStringTime(castObj.getDurationMillSeconds())); - String resolution = "description"; - VideoItem item = new VideoItem(cast.getId(), CAST_PARENT_ID, cast.getName(), CAST_CREATOR, castRes); - item.setDescription(resolution); - return createItemMetadata(item); - } - if (cast instanceof ICast.ICastAudio) { - ICast.ICastAudio castObj = (ICast.ICastAudio) cast; - Res castRes = new Res(new MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), castObj.getSize(), cast.getUri()); - castRes.setDuration(getStringTime(castObj.getDurationMillSeconds())); - String resolution = "description"; - AudioItem item = new AudioItem(cast.getId(), CAST_PARENT_ID, cast.getName(), CAST_CREATOR, castRes); - item.setDescription(resolution); - return createItemMetadata(item); - } else if (cast instanceof ICast.ICastImage) { - ICast.ICastImage castObj = (ICast.ICastImage) cast; - Res castRes = new Res(new MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), castObj.getSize(), cast.getUri()); - String resolution = "description"; - ImageItem item = new ImageItem(cast.getId(), CAST_PARENT_ID, cast.getName(), CAST_CREATOR, castRes); - item.setDescription(resolution); - return createItemMetadata(item); - } else { - return ""; - } - } - - private static String createItemMetadata(DIDLObject item) { - StringBuilder metadata = new StringBuilder(); - metadata.append(DIDL_LITE_HEADER); - metadata.append(String.format("", item.getId(), item.getParentID(), item.isRestricted() ? "1" : "0")); - metadata.append(String.format("%s", item.getTitle())); - String creator = item.getCreator(); - if (creator != null) { - creator = creator.replaceAll("<", "_"); - creator = creator.replaceAll(">", "_"); - } - metadata.append(String.format("%s", creator)); - metadata.append(String.format("%s", item.getClazz().getValue())); - metadata.append(String.format("%s", DATE_FORMAT.format(new Date()))); - - // metadata.append(String.format("%s",item.get); - // http://192.168.1.104:8088/Music/07.我醒著做夢.mp3 - - Res res = item.getFirstResource(); - if (res != null) { - // protocol info - String protocolInfo = ""; - ProtocolInfo pi = res.getProtocolInfo(); - if (pi != null) { - protocolInfo = String.format("protocolInfo=\"%s:%s:%s:%s\"", pi.getProtocol(), pi.getNetwork(), pi.getContentFormatMimeType(), pi.getAdditionalInfo()); - } - - // resolution, extra info, not adding yet - String resolution = ""; - if (!TextUtils.isEmpty(res.getResolution())) { - resolution = String.format("resolution=\"%s\"", res.getResolution()); - } - - // duration - String duration = ""; - if (!TextUtils.isEmpty(res.getDuration())) { - duration = String.format("duration=\"%s\"", res.getDuration()); - } - - // res begin - // metadata.append(String.format("", protocolInfo)); // no resolution & duration yet - metadata.append(String.format("", protocolInfo, resolution, duration)); - - // url - metadata.append(res.getValue()); - - // res end - metadata.append(""); - } - metadata.append(""); - - metadata.append(DIDL_LITE_FOOTER); - - return metadata.toString(); - } - - /** - * @return device information like: [deviceType][name][manufacturer][udn] - */ - public static String parseDeviceInfo(@NonNull RemoteDevice device) { - return String.format("[%s][%s][%s][%s]", - device.getType().getType(), - device.getDetails().getFriendlyName(), - device.getDetails().getManufacturerDetails().getManufacturer(), - device.getIdentity().getUdn()); - } - - public static String parseDeviceService(@NonNull RemoteDevice device) { - StringBuilder builder = new StringBuilder(device.getDetails().getFriendlyName()); - builder.append(":"); - for (RemoteService service : device.getServices()) { - builder.append("\nservice:").append(service.getServiceType().getType()); - if (service.hasActions()) { - builder.append("\nactions: "); - List> list = Arrays.asList(service.getActions()); - Collections.sort(list, (o1, o2) -> o1.getName().compareTo(o2.getName())); - for (Action action : list) { - builder.append(action.getName()).append(", "); - } - } - } - return builder.toString(); - } - - public static void logServiceConnected(ILogger mLogger, AndroidUpnpService upnpService, ComponentName componentName, IBinder iBinder) { - mLogger.i("---------------------------------------------------------------------------"); - mLogger.i("---------------------------------------------------------------------------"); - mLogger.i(String.format("[%s] connected %s", componentName.getShortClassName(), iBinder.getClass().getName())); - mLogger.i(String.format("[UpnpService]: %s@0x%s", upnpService.get().getClass().getName(), toHexString(upnpService.get().hashCode()))); - mLogger.i(String.format("[Registry]: listener=%s, devices=%s", upnpService.getRegistry().getListeners().size(), upnpService.getRegistry().getDevices().size())); - mLogger.i("---------------------------------------------------------------------------"); - mLogger.i("---------------------------------------------------------------------------"); - } - - private static String toHexString(int hashCode) { - return Integer.toHexString(hashCode).toUpperCase(Locale.US); - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/Utils.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/Utils.kt new file mode 100644 index 00000000..7e78f4f8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/Utils.kt @@ -0,0 +1,241 @@ +package com.skyd.imomoe.util.dlna.dmc + +import android.content.ComponentName +import android.os.IBinder +import com.skyd.imomoe.util.dlna.dmc.ICast.* +import okhttp3.internal.toHexString +import org.fourthline.cling.android.AndroidUpnpService +import org.fourthline.cling.model.meta.Action +import org.fourthline.cling.model.meta.RemoteDevice +import org.fourthline.cling.support.model.DIDLObject +import org.fourthline.cling.support.model.ProtocolInfo +import org.fourthline.cling.support.model.Res +import org.fourthline.cling.support.model.item.AudioItem +import org.fourthline.cling.support.model.item.ImageItem +import org.fourthline.cling.support.model.item.VideoItem +import org.seamless.util.MimeType +import java.text.SimpleDateFormat +import java.util.* + +object Utils { + private const val DIDL_LITE_FOOTER = "" + private const val DIDL_LITE_HEADER = + "" + "" + private const val CAST_PARENT_ID = "1" + private const val CAST_CREATOR = "NLUpnpCast" + private const val CAST_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" + private val DATE_FORMAT = SimpleDateFormat(CAST_DATE_FORMAT, Locale.US) + + /** + * 把时间戳转换成 00:00:00 格式 + * + * @param timeMs 时间戳 + * @return 00:00:00 时间格式 + */ + fun getStringTime(timeMs: Long): String { + val formatBuilder = StringBuilder() + val formatter = Formatter(formatBuilder, Locale.US) + val totalSeconds = timeMs / 1000 + val seconds = totalSeconds % 60 + val minutes = totalSeconds / 60 % 60 + val hours = totalSeconds / 3600 + return formatter.format("%02d:%02d:%02d", hours, minutes, seconds).toString() + } + + /** + * 把 00:00:00 格式转成时间戳 + * + * @param formatTime 00:00:00 时间格式 + * @return 时间戳(毫秒) + */ + fun getIntTime(formatTime: String): Long { + if (formatTime.isNotEmpty()) { + val tmp = formatTime.split(":").toTypedArray() + if (tmp.size < 3) return 0 + val second = tmp[0].toInt() * 3600 + tmp[1].toInt() * 60 + tmp[2].toInt() + return second * 1000L + } + return 0 + } + + fun parseTime(s: String): Long { + return try { + s.toLong() + } catch (e: Exception) { + e.printStackTrace() + 0L + } + } + + fun getMetadata(cast: ICast): String { + return when (cast) { + is ICastVideo -> { + val castRes = + Res(MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), cast.size, cast.uri) + castRes.bitrate = cast.bitrate + castRes.duration = getStringTime(cast.durationMillSeconds) + val resolution = "description" + val item = VideoItem(cast.id, CAST_PARENT_ID, cast.name, CAST_CREATOR, castRes) + item.description = resolution + createItemMetadata(item) + } + is ICastAudio -> { + val castRes = Res( + MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), cast.size, cast.uri + ) + castRes.duration = getStringTime(cast.durationMillSeconds) + val resolution = "description" + val item = AudioItem(cast.id, CAST_PARENT_ID, cast.name, CAST_CREATOR, castRes) + item.description = resolution + createItemMetadata(item) + } + is ICastImage -> { + val castRes = Res( + MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), cast.size, cast.uri + ) + val resolution = "description" + val item = ImageItem(cast.id, CAST_PARENT_ID, cast.name, CAST_CREATOR, castRes) + item.description = resolution + createItemMetadata(item) + } + else -> "" + } + } + + private fun createItemMetadata(item: DIDLObject): String { + val metadata = StringBuilder() + metadata.append(DIDL_LITE_HEADER) + metadata.append( + String.format( + "", + item.id, + item.parentID, + if (item.isRestricted) "1" else "0" + ) + ) + metadata.append(String.format("%s", item.title)) + var creator = item.creator + if (creator != null) { + creator = creator.replace("<".toRegex(), "_") + creator = creator.replace(">".toRegex(), "_") + } + metadata.append(String.format("%s", creator)) + metadata.append(String.format("%s", item.clazz.value)) + metadata.append(String.format("%s", DATE_FORMAT.format(Date()))) + + // metadata.append(String.format("%s",item.get); + // http://192.168.1.104:8088/Music/07.我醒著做夢.mp3 + val res = item.firstResource + if (res != null) { + // protocol info + var protocolInfo = "" + val pi = res.protocolInfo + if (pi != null) { + protocolInfo = String.format( + "protocolInfo=\"%s:%s:%s:%s\"", + pi.protocol, + pi.network, + pi.contentFormatMimeType, + pi.additionalInfo + ) + } + + // resolution, extra info, not adding yet + var resolution = "" + if (!res.resolution.isNullOrEmpty()) { + resolution = String.format("resolution=\"%s\"", res.resolution) + } + + // duration + var duration = "" + if (!res.duration.isNullOrEmpty()) { + duration = String.format("duration=\"%s\"", res.duration) + } + + // res begin + // metadata.append(String.format("", protocolInfo)); // no resolution & duration yet + metadata.append(String.format("", protocolInfo, resolution, duration)) + + // url + metadata.append(res.value) + + // res end + metadata.append("") + } + metadata.append("") + metadata.append(DIDL_LITE_FOOTER) + return metadata.toString() + } + + /** + * @return device information like: [deviceType][name][manufacturer][udn] + */ + fun parseDeviceInfo(device: RemoteDevice): String { + return String.format( + "[%s][%s][%s][%s]", + device.type.type, + device.details.friendlyName, + device.details.manufacturerDetails.manufacturer, + device.identity.udn + ) + } + + fun parseDeviceService(device: RemoteDevice): String { + val builder = StringBuilder(device.details.friendlyName) + builder.append(":") + for (service in device.services) { + builder.append("\nservice:").append(service.serviceType.type) + if (service.hasActions()) { + builder.append("\nactions: ") + val list: MutableList> = mutableListOf(*service.actions) + list.sortWith { o1: Action<*>, o2: Action<*> -> o1.name.compareTo(o2.name) } + for (action in list) { + builder.append(action.name).append(", ") + } + } + } + return builder.toString() + } + + fun logServiceConnected( + mLogger: ILogger, + upnpService: AndroidUpnpService, + componentName: ComponentName, + iBinder: IBinder + ) { + mLogger.i("---------------------------------------------------------------------------") + mLogger.i("---------------------------------------------------------------------------") + mLogger.i( + String.format( + "[%s] connected %s", + componentName.shortClassName, + iBinder.javaClass.name + ) + ) + mLogger.i( + String.format( + "[UpnpService]: %s@0x%s", + upnpService.get().javaClass.name, + toHexString(upnpService.get().hashCode()) + ) + ) + mLogger.i( + String.format( + "[Registry]: listener=%s, devices=%s", + upnpService.registry.listeners.size, + upnpService.registry.devices.size + ) + ) + mLogger.i("---------------------------------------------------------------------------") + mLogger.i("---------------------------------------------------------------------------") + } + + private fun toHexString(hashCode: Int): String { + return hashCode.toHexString().uppercase(Locale.US) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/GetBrightness.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/GetBrightness.java deleted file mode 100644 index e709a430..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/GetBrightness.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc.action; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionException; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.ErrorCode; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; - -/** - * - */ -public abstract class GetBrightness extends ActionCallback { - @SuppressWarnings("WeakerAccess") - public GetBrightness(Service service) { - super(new ActionInvocation<>(service.getAction("GetBrightness"))); - getActionInvocation().setInput("InstanceID", new UnsignedIntegerFourBytes(0)); - //getActionInvocation().setInput("Channel", Channel.Master.toString()); - } - - public void success(ActionInvocation invocation) { - boolean ok = true; - int brightness = 0; - try { - brightness = Integer.parseInt(invocation.getOutput("CurrentBrightness").getValue().toString()); // UnsignedIntegerTwoBytes... - } catch (Exception ex) { - invocation.setFailure(new ActionException(ErrorCode.ACTION_FAILED, "Can't parse ProtocolInfo response: " + ex, ex)); - - failure(invocation, null); - - ok = false; - } - if (ok) { - received(invocation, brightness); - } - } - - public abstract void received(ActionInvocation actionInvocation, int brightness); - -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/GetBrightness.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/GetBrightness.kt new file mode 100644 index 00000000..f5028ed6 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/GetBrightness.kt @@ -0,0 +1,36 @@ +package com.skyd.imomoe.util.dlna.dmc.action + +import org.fourthline.cling.controlpoint.ActionCallback +import org.fourthline.cling.model.action.ActionException +import org.fourthline.cling.model.action.ActionInvocation +import org.fourthline.cling.model.meta.Service +import org.fourthline.cling.model.types.ErrorCode +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes + +abstract class GetBrightness(service: Service<*, *>?) : + ActionCallback(ActionInvocation(service?.getAction("GetBrightness"))) { + + override fun success(invocation: ActionInvocation>?) { + invocation ?: return + var ok = true + var brightness = 0 + try { + brightness = invocation.getOutput("CurrentBrightness").value.toString() + .toInt() // UnsignedIntegerTwoBytes... + } catch (ex: Exception) { + invocation.failure = ActionException( + ErrorCode.ACTION_FAILED, "Can't parse ProtocolInfo response: $ex", ex + ) + failure(invocation, null) + ok = false + } + if (ok) received(invocation, brightness) + } + + abstract fun received(actionInvocation: ActionInvocation>?, brightness: Int) + + init { + super.getActionInvocation().setInput("InstanceID", UnsignedIntegerFourBytes(0)) + //getActionInvocation().setInput("Channel", Channel.Master.toString()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/SetBrightness.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/SetBrightness.java deleted file mode 100644 index 02a36d91..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/SetBrightness.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc.action; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes; - -/** - * - */ -public abstract class SetBrightness extends ActionCallback { - @SuppressWarnings({"WeakerAccess"}) - public SetBrightness(Service service, long newBrightness) { - super(new ActionInvocation<>(service.getAction("SetBrightness"))); - getActionInvocation().setInput("InstanceID", new UnsignedIntegerFourBytes(0)); - //getActionInvocation().setInput("Channel", Channel.Master.toString()); - getActionInvocation().setInput("DesiredBrightness", new UnsignedIntegerTwoBytes(newBrightness)); - } - - @Override - public void success(ActionInvocation invocation) { - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/SetBrightness.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/SetBrightness.kt new file mode 100644 index 00000000..2dc52a2d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/action/SetBrightness.kt @@ -0,0 +1,20 @@ +package com.skyd.imomoe.util.dlna.dmc.action + +import org.fourthline.cling.controlpoint.ActionCallback +import org.fourthline.cling.model.action.ActionInvocation +import org.fourthline.cling.model.meta.Service +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes +import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes + +abstract class SetBrightness(service: Service<*, *>?, newBrightness: Long) : + ActionCallback(ActionInvocation(service?.getAction("SetBrightness"))) { + + override fun success(invocation: ActionInvocation>?) {} + + init { + super.getActionInvocation().setInput("InstanceID", UnsignedIntegerFourBytes(0)) + //getActionInvocation().setInput("Channel", Channel.Master.toString()); + super.getActionInvocation() + .setInput("DesiredBrightness", UnsignedIntegerTwoBytes(newBrightness)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/BaseServiceExecutor.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/BaseServiceExecutor.java deleted file mode 100644 index 0b284329..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/BaseServiceExecutor.java +++ /dev/null @@ -1,332 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc.control; - - -import android.os.Handler; -import android.os.Looper; - -import androidx.annotation.Nullable; - -import com.skyd.imomoe.util.dlna.dmc.Utils; -import com.skyd.imomoe.util.dlna.dmc.action.GetBrightness; -import com.skyd.imomoe.util.dlna.dmc.action.SetBrightness; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.avtransport.callback.GetMediaInfo; -import org.fourthline.cling.support.avtransport.callback.GetPositionInfo; -import org.fourthline.cling.support.avtransport.callback.GetTransportInfo; -import org.fourthline.cling.support.avtransport.callback.Pause; -import org.fourthline.cling.support.avtransport.callback.Play; -import org.fourthline.cling.support.avtransport.callback.Seek; -import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI; -import org.fourthline.cling.support.avtransport.callback.Stop; -import org.fourthline.cling.support.model.MediaInfo; -import org.fourthline.cling.support.model.PositionInfo; -import org.fourthline.cling.support.model.TransportInfo; -import org.fourthline.cling.support.renderingcontrol.callback.GetMute; -import org.fourthline.cling.support.renderingcontrol.callback.GetVolume; -import org.fourthline.cling.support.renderingcontrol.callback.SetMute; -import org.fourthline.cling.support.renderingcontrol.callback.SetVolume; - -/** - * - */ -abstract class BaseServiceExecutor { - private final ControlPoint mControlPoint; - private final Service mService; - private final Handler mHandler = new Handler(Looper.getMainLooper()); - - protected BaseServiceExecutor(ControlPoint controlPoint, Service service) { - mControlPoint = controlPoint; - mService = service; - } - - protected Service getService() { - return mService; - } - - protected boolean invalidServiceAction(String actionName) { - return mService == null || mService.getAction(actionName) == null; - } - - protected void execute(ActionCallback actionCallback) { - mControlPoint.execute(actionCallback); - } - - protected final void notifySuccess(@Nullable IServiceAction.IServiceActionCallback listener, T t) { - if (listener != null) notify(() -> listener.onSuccess(t)); - } - - protected final void notifyFailure(@Nullable IServiceAction.IServiceActionCallback listener, String errMsg) { - if (listener != null) notify(() -> listener.onFailed(errMsg != null ? errMsg : "error")); - } - - private void notify(Runnable runnable) { - if (Looper.myLooper() != Looper.getMainLooper()) { - mHandler.post(runnable); - } else { - runnable.run(); - } - } - - // --------------------------------------------------------------------------------------------------------- - // Implement - // --------------------------------------------------------------------------------------------------------- - static final class AVServiceExecutorImpl extends BaseServiceExecutor implements IServiceAction.IAVServiceAction { - - public AVServiceExecutorImpl(ControlPoint controlPoint, Service service) { - super(controlPoint, service); - } - - @Override - public void cast(IServiceAction.IServiceActionCallback listener, String uri, String metadata) { - if (invalidServiceAction("SetAVTransportURI")) return; - - execute(new SetAVTransportURI(getService(), uri, metadata) { - @Override - public void success(final ActionInvocation invocation) { - notifySuccess(listener, uri); - } - - @Override - public void failure(final ActionInvocation invocation, final UpnpResponse operation, final String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void play(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("Play")) return; - - execute(new Play(getService()) { - @Override - public void success(final ActionInvocation invocation) { - notifySuccess(listener, null); - } - - @Override - public void failure(final ActionInvocation invocation, final UpnpResponse operation, final String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void pause(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("Pause")) return; - - execute(new Pause(getService()) { - @Override - public void success(final ActionInvocation invocation) { - notifySuccess(listener, null); - } - - @Override - public void failure(final ActionInvocation invocation, final UpnpResponse operation, final String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void stop(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("Stop")) return; - - execute(new Stop(getService()) { - @Override - public void success(final ActionInvocation invocation) { - notifySuccess(listener, null); - } - - @Override - public void failure(final ActionInvocation invocation, final UpnpResponse operation, final String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void seek(IServiceAction.IServiceActionCallback listener, long position) { - if (invalidServiceAction("Seek")) return; - - execute(new Seek(getService(), Utils.getStringTime(position)) { - @Override - public void success(final ActionInvocation invocation) { - notifySuccess(listener, position); - } - - @Override - public void failure(final ActionInvocation invocation, final UpnpResponse operation, final String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void getPositionInfo(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("GetPositionInfo")) return; - - execute(new GetPositionInfo(getService()) { - @Override - public void received(ActionInvocation invocation, final PositionInfo positionInfo) { - notifySuccess(listener, positionInfo); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void getMediaInfo(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("GetMediaInfo")) return; - - execute(new GetMediaInfo(getService()) { - @Override - public void received(ActionInvocation invocation, final MediaInfo mediaInfo) { - notifySuccess(listener, mediaInfo); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void getTransportInfo(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("GetTransportInfo")) return; - - execute(new GetTransportInfo(getService()) { - @Override - public void received(ActionInvocation invocation, TransportInfo transportInfo) { - notifySuccess(listener, transportInfo); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - } - - // --------------------------------------------------------------------------------------------------------- - // Implement - // --------------------------------------------------------------------------------------------------------- - static class RendererServiceExecutorImpl extends BaseServiceExecutor implements IServiceAction.IRendererServiceAction { - - public RendererServiceExecutorImpl(ControlPoint controlPoint, Service service) { - super(controlPoint, service); - } - - @Override - public void setVolume(IServiceAction.IServiceActionCallback listener, int volume) { - if (invalidServiceAction("SetVolume")) return; - - execute(new SetVolume(getService(), volume) { - @Override - public void success(ActionInvocation invocation) { - notifySuccess(listener, volume); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void getVolume(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("GetVolume")) return; - - execute(new GetVolume(getService()) { - @Override - public void received(ActionInvocation invocation, final int currentVolume) { - notifySuccess(listener, currentVolume); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void setMute(IServiceAction.IServiceActionCallback listener, boolean mute) { - if (invalidServiceAction("SetMute")) return; - - execute(new SetMute(getService(), mute) { - @Override - public void success(ActionInvocation invocation) { - notifySuccess(listener, mute); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void isMute(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("GetMute")) return; - - execute(new GetMute(getService()) { - @Override - public void received(ActionInvocation invocation, final boolean currentMute) { - notifySuccess(listener, currentMute); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void setBrightness(IServiceAction.IServiceActionCallback listener, int percent) { - if (invalidServiceAction("SetBrightness")) return; - - execute(new SetBrightness(getService(), percent) { - @Override - public void success(final ActionInvocation invocation) { - notifySuccess(listener, percent); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - - @Override - public void getBrightness(IServiceAction.IServiceActionCallback listener) { - if (invalidServiceAction("GetBrightness")) return; - - execute(new GetBrightness(getService()) { - @Override - public void received(ActionInvocation invocation, final int brightness) { - notifySuccess(listener, brightness); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - notifyFailure(listener, defaultMsg); - } - }); - } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/BaseServiceExecutor.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/BaseServiceExecutor.kt new file mode 100644 index 00000000..41ceaec4 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/BaseServiceExecutor.kt @@ -0,0 +1,335 @@ +package com.skyd.imomoe.util.dlna.dmc.control + +import android.os.Handler +import android.os.Looper +import com.skyd.imomoe.util.dlna.dmc.Utils +import com.skyd.imomoe.util.dlna.dmc.action.GetBrightness +import com.skyd.imomoe.util.dlna.dmc.action.SetBrightness +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction.* +import com.skyd.imomoe.util.showToast +import org.fourthline.cling.controlpoint.ActionCallback +import org.fourthline.cling.controlpoint.ControlPoint +import org.fourthline.cling.model.action.ActionInvocation +import org.fourthline.cling.model.message.UpnpResponse +import org.fourthline.cling.model.meta.Service +import org.fourthline.cling.support.avtransport.callback.* +import org.fourthline.cling.support.model.MediaInfo +import org.fourthline.cling.support.model.PositionInfo +import org.fourthline.cling.support.model.TransportInfo +import org.fourthline.cling.support.renderingcontrol.callback.GetMute +import org.fourthline.cling.support.renderingcontrol.callback.GetVolume +import org.fourthline.cling.support.renderingcontrol.callback.SetMute +import org.fourthline.cling.support.renderingcontrol.callback.SetVolume + +internal abstract class BaseServiceExecutor protected constructor( + private val mControlPoint: ControlPoint, + protected val service: Service<*, *>? +) { + private val mHandler = Handler(Looper.getMainLooper()) + + protected fun invalidServiceAction(actionName: String): Boolean { + return service?.getAction(actionName) == null + } + + protected fun execute(actionCallback: ActionCallback) { + mControlPoint.execute(actionCallback) + } + + protected fun notifySuccess(listener: IServiceActionCallback?, t: T) { + if (listener != null) notify { listener.onSuccess(t) } + } + + protected fun notifyFailure(listener: IServiceActionCallback<*>?, errMsg: String?) { + if (listener != null) notify { listener.onFailed(errMsg ?: "error") } + } + + private fun notify(runnable: Runnable) { + if (Looper.myLooper() != Looper.getMainLooper()) { + mHandler.post(runnable) + } else { + runnable.run() + } + } + + // --------------------------------------------------------------------------------------------------------- + // Implement + // --------------------------------------------------------------------------------------------------------- + internal class AVServiceExecutorImpl(controlPoint: ControlPoint, service: Service<*, *>?) : + BaseServiceExecutor(controlPoint, service), IAVServiceAction { + override fun cast(listener: IServiceActionCallback?, uri: String, metadata: String) { + if (invalidServiceAction("SetAVTransportURI")) return + execute(object : SetAVTransportURI(service, uri, metadata) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, uri) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun play(listener: IServiceActionCallback?) { + if (invalidServiceAction("Play")) return + execute(object : Play(service) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, null) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun pause(listener: IServiceActionCallback?) { + if (invalidServiceAction("Pause")) return + execute(object : Pause(service) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, null) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun stop(listener: IServiceActionCallback?) { + if (invalidServiceAction("Stop")) return + execute(object : Stop(service) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, null) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun seek(listener: IServiceActionCallback?, position: Long) { + if (invalidServiceAction("Seek")) return + execute(object : Seek(service, Utils.getStringTime(position)) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, position) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun getPositionInfo(listener: IServiceActionCallback?) { + if (invalidServiceAction("GetPositionInfo")) return + execute(object : GetPositionInfo(service) { + override fun received( + invocation: ActionInvocation>?, + positionInfo: PositionInfo? + ) { + notifySuccess(listener, positionInfo) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun getMediaInfo(listener: IServiceActionCallback?) { + if (invalidServiceAction("GetMediaInfo")) return + execute(object : GetMediaInfo(service) { + override fun received( + invocation: ActionInvocation>?, + mediaInfo: MediaInfo? + ) { + notifySuccess(listener, mediaInfo) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun getTransportInfo(listener: IServiceActionCallback?) { + if (invalidServiceAction("GetTransportInfo")) return + execute(object : GetTransportInfo(service) { + override fun received( + invocation: ActionInvocation>?, + transportInfo: TransportInfo? + ) { + notifySuccess(listener, transportInfo) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + } + + // --------------------------------------------------------------------------------------------------------- + // Implement + // --------------------------------------------------------------------------------------------------------- + internal class RendererServiceExecutorImpl( + controlPoint: ControlPoint, + service: Service<*, *>? + ) : BaseServiceExecutor(controlPoint, service), IRendererServiceAction { + override fun setVolume(listener: IServiceActionCallback?, volume: Int) { + if (invalidServiceAction("SetVolume")) return + execute(object : SetVolume(service, volume.toLong()) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, volume) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun getVolume(listener: IServiceActionCallback?) { + if (invalidServiceAction("GetVolume")) return + execute(object : GetVolume(service) { + override fun received( + actionInvocation: ActionInvocation>?, + currentVolume: Int + ) { + notifySuccess(listener, currentVolume) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun setMute(listener: IServiceActionCallback?, mute: Boolean) { + if (invalidServiceAction("SetMute")) return + execute(object : SetMute(service, mute) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, mute) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun isMute(listener: IServiceActionCallback?) { + if (invalidServiceAction("GetMute")) return + execute(object : GetMute(service) { + override fun received( + actionInvocation: ActionInvocation>?, + currentMute: Boolean + ) { + notifySuccess(listener, currentMute) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } + + override fun setBrightness(listener: IServiceActionCallback?, percent: Int) { + if (invalidServiceAction("SetBrightness")) return + try { + execute(object : SetBrightness(service, percent.toLong()) { + override fun success(invocation: ActionInvocation>?) { + notifySuccess(listener, percent) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } catch (e: IllegalArgumentException) { + // service is null: ActionInvocation -> Action can not be null + e.printStackTrace() + e.message?.showToast() + } + } + + override fun getBrightness(listener: IServiceActionCallback?) { + if (invalidServiceAction("GetBrightness")) return + try { + execute(object : GetBrightness(service) { + override fun received( + actionInvocation: ActionInvocation>?, + brightness: Int + ) { + notifySuccess(listener, brightness) + } + + override fun failure( + invocation: ActionInvocation>?, + operation: UpnpResponse?, + defaultMsg: String? + ) { + notifyFailure(listener, defaultMsg) + } + }) + } catch (e: IllegalArgumentException) { + // service is null: ActionInvocation -> Action can not be null + e.printStackTrace() + e.message?.showToast() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ControlImpl.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ControlImpl.java deleted file mode 100644 index 50ccd4e2..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ControlImpl.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc.control; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.skyd.imomoe.util.dlna.dmc.ICast; -import com.skyd.imomoe.util.dlna.dmc.Utils; - -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.meta.Device; - -import java.util.Map; - -public class ControlImpl implements ICastInterface.IControl { - - private final IServiceFactory mServiceFactory; - private final Device mDevice; - private final Map> mCallbackMap; - - public ControlImpl(@NonNull ControlPoint controlPoint, @NonNull Device device, Map> map) { - mDevice = device; - mCallbackMap = map; - mServiceFactory = new IServiceFactory.ServiceFactoryImpl(controlPoint, device); - } - - @Override - public void cast(Device device, ICast object) { - mServiceFactory.getAvService().cast(new ICastInterface.CastEventListener() { - @Override - public void onSuccess(String result) { - IServiceAction.IServiceActionCallback listener = getCallback(IServiceAction.ServiceAction.CAST); - if (listener != null) listener.onSuccess(result); - } - - @Override - public void onFailed(String errMsg) { - IServiceAction.IServiceActionCallback listener = getCallback(IServiceAction.ServiceAction.CAST); - if (listener != null) listener.onFailed(errMsg); - } - }, object.getUri(), Utils.getMetadata(object)); - } - - @Override - public boolean isCasting(Device device) { - return mDevice != null && mDevice.equals(device); - } - - @Override - public void stop() { - mServiceFactory.getAvService().stop(getCallback(IServiceAction.ServiceAction.STOP)); - } - - @Override - public void play() { - mServiceFactory.getAvService().play(getCallback(IServiceAction.ServiceAction.PLAY)); - } - - @Override - public void pause() { - mServiceFactory.getAvService().pause(getCallback(IServiceAction.ServiceAction.PAUSE)); - } - - @Override - public void seekTo(long position) { - mServiceFactory.getAvService().seek(getCallback(IServiceAction.ServiceAction.SEEK_TO), position); - } - - @Override - public void setVolume(int percent) { - mServiceFactory.getRenderService().setVolume(getCallback(IServiceAction.ServiceAction.SET_VOLUME), percent); - } - - @Override - public void setMute(boolean mute) { - mServiceFactory.getRenderService().setMute(getCallback(IServiceAction.ServiceAction.SET_MUTE), mute); - } - - @Override - public void setBrightness(int percent) { - mServiceFactory.getRenderService().setBrightness(getCallback(IServiceAction.ServiceAction.SET_BRIGHTNESS), percent); - } - - @SuppressWarnings("unchecked") - @Nullable - private IServiceAction.IServiceActionCallback getCallback(IServiceAction.ServiceAction action) { - Object result = mCallbackMap.get(action.name()); - if (result == null) return null; - return (IServiceAction.IServiceActionCallback) result; - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ControlImpl.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ControlImpl.kt new file mode 100644 index 00000000..9ceaca54 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ControlImpl.kt @@ -0,0 +1,75 @@ +package com.skyd.imomoe.util.dlna.dmc.control + +import com.skyd.imomoe.util.dlna.dmc.ICast +import com.skyd.imomoe.util.dlna.dmc.Utils +import com.skyd.imomoe.util.dlna.dmc.control.ICastInterface.CastEventListener +import com.skyd.imomoe.util.dlna.dmc.control.ICastInterface.IControl +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction.IServiceActionCallback +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction.ServiceAction +import com.skyd.imomoe.util.dlna.dmc.control.IServiceFactory.ServiceFactoryImpl +import org.fourthline.cling.controlpoint.ControlPoint +import org.fourthline.cling.model.meta.Device + +class ControlImpl( + val controlPoint: ControlPoint, + val device: Device<*, *, *>, + val callbackMap: Map> +) : IControl { + private val serviceFactory: IServiceFactory + + override fun cast(device: Device<*, *, *>, `object`: ICast) { + serviceFactory.avService.cast(object : CastEventListener { + override fun onSuccess(result: String) { + val listener = getCallback(ServiceAction.CAST) + listener?.onSuccess(result) + } + + override fun onFailed(errMsg: String) { + val listener = getCallback(ServiceAction.CAST) + listener?.onFailed(errMsg) + } + }, `object`.uri, Utils.getMetadata(`object`)) + } + + override fun isCasting(device: Device<*, *, *>): Boolean { + return this.device == device + } + + override fun stop() { + serviceFactory.avService.stop(getCallback(ServiceAction.STOP)) + } + + override fun play() { + serviceFactory.avService.play(getCallback(ServiceAction.PLAY)) + } + + override fun pause() { + serviceFactory.avService.pause(getCallback(ServiceAction.PAUSE)) + } + + override fun seekTo(position: Long) { + serviceFactory.avService.seek(getCallback(ServiceAction.SEEK_TO), position) + } + + override fun setVolume(percent: Int) { + serviceFactory.renderService.setVolume(getCallback(ServiceAction.SET_VOLUME), percent) + } + + override fun setMute(mute: Boolean) { + serviceFactory.renderService.setMute(getCallback(ServiceAction.SET_MUTE), mute) + } + + override fun setBrightness(percent: Int) { + serviceFactory.renderService + .setBrightness(getCallback(ServiceAction.SET_BRIGHTNESS), percent) + } + + private fun getCallback(action: ServiceAction): IServiceActionCallback? { + val result = callbackMap[action.name] ?: return null + return result as IServiceActionCallback + } + + init { + serviceFactory = ServiceFactoryImpl(controlPoint, device) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ICastInterface.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ICastInterface.java deleted file mode 100644 index 594239d2..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ICastInterface.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc.control; - -import androidx.annotation.Nullable; - -import com.skyd.imomoe.util.dlna.dmc.ICast; - -import org.fourthline.cling.model.gena.GENASubscription; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Device; - -public interface ICastInterface { - - // ------------------------------------------------------------------ - // ---- control - // ------------------------------------------------------------------ - interface IControl { - void cast(Device device, ICast object); - - boolean isCasting(Device device); - - void stop(); - - void play(); - - void pause(); - - /** - * @param position, current watch time(ms) - */ - void seekTo(long position); - - void setVolume(int percent); - - void setMute(boolean mute); - - void setBrightness(int percent); - } - - // ------------------------------------------------------------------ - // ---- subscription - // ------------------------------------------------------------------ - interface ISubscriptionListener { - void onSubscriptionEstablished(GENASubscription subscription); - - void onSubscriptionEventReceived(GENASubscription subscription); - - void onSubscriptionFinished(GENASubscription subscription, UpnpResponse responseStatus, String defaultMsg); - } - - // ------------------------------------------------------------------ - // ---- GetInfo Listener - // ------------------------------------------------------------------ - interface GetInfoListener { - void onGetInfoResult(@Nullable T t, @Nullable String errMsg); - } - - // ------------------------------------------------------------------ - // ---- Event Listener - // ------------------------------------------------------------------ - interface CastEventListener extends IServiceAction.IServiceActionCallback { - } - - interface PlayEventListener extends IServiceAction.IServiceActionCallback { - } - - interface PauseEventListener extends IServiceAction.IServiceActionCallback { - } - - interface StopEventListener extends IServiceAction.IServiceActionCallback { - } - - interface SeekToEventListener extends IServiceAction.IServiceActionCallback { - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ICastInterface.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ICastInterface.kt new file mode 100644 index 00000000..ebf03a72 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/ICastInterface.kt @@ -0,0 +1,77 @@ +package com.skyd.imomoe.util.dlna.dmc.control + +import com.skyd.imomoe.util.dlna.dmc.ICast +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction.IServiceActionCallback +import org.fourthline.cling.model.gena.GENASubscription +import org.fourthline.cling.model.message.UpnpResponse +import org.fourthline.cling.model.meta.Device + +interface ICastInterface { + // ------------------------------------------------------------------ + // ---- control + // ------------------------------------------------------------------ + interface IControl { + fun cast(device: Device<*, *, *>, `object`: ICast) + fun isCasting(device: Device<*, *, *>): Boolean + fun stop() + fun play() + fun pause() + + /** + * @param position, current watch time(ms) + */ + fun seekTo(position: Long) + fun setVolume(percent: Int) + fun setMute(mute: Boolean) + fun setBrightness(percent: Int) + } + + // ------------------------------------------------------------------ + // ---- subscription + // ------------------------------------------------------------------ + interface ISubscriptionListener { + fun onSubscriptionEstablished(subscription: GENASubscription<*>) + fun onSubscriptionEventReceived(subscription: GENASubscription<*>) + fun onSubscriptionFinished( + subscription: GENASubscription<*>, + responseStatus: UpnpResponse, + defaultMsg: String + ) + } + + // ------------------------------------------------------------------ + // ---- GetInfo Listener + // ------------------------------------------------------------------ + interface GetInfoListener { + fun onGetInfoResult(t: T?, errMsg: String?) + } + + // ------------------------------------------------------------------ + // ---- Event Listener + // ------------------------------------------------------------------ + interface CastEventListener : IServiceActionCallback + interface PlayEventListener : IServiceActionCallback + interface PauseEventListener : IServiceActionCallback + interface StopEventListener : IServiceActionCallback + interface SeekToEventListener : IServiceActionCallback +} + +// use kotlin dsl to replace ICastInterface.GetInfoListener interface +// 由于使用此接口的方法不止一个,因此下面的方法与平常的用法不同 +fun newGetInfoListener(onDeviceAdded: ((t: T?, errMsg: String?) -> Unit)): GetInfoListenerDsl { + val listener = GetInfoListenerDsl() + listener.onGetInfoResult(onDeviceAdded) + return listener +} + +class GetInfoListenerDsl : ICastInterface.GetInfoListener { + private var getInfoResult: ((t: T?, errMsg: String?) -> Unit)? = null + + fun onGetInfoResult(getInfoResult: ((t: T?, errMsg: String?) -> Unit)?) { + this.getInfoResult = getInfoResult + } + + override fun onGetInfoResult(t: T?, errMsg: String?) { + getInfoResult?.invoke(t, errMsg) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceAction.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceAction.java deleted file mode 100644 index b4a551ba..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceAction.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc.control; - -import org.fourthline.cling.support.model.MediaInfo; -import org.fourthline.cling.support.model.PositionInfo; -import org.fourthline.cling.support.model.TransportInfo; - -public interface IServiceAction { - - enum ServiceAction { - CAST("cast"), - PLAY("play"), - PAUSE("pause"), - STOP("stop"), - SEEK_TO("seekTo"), - SET_VOLUME("setVolume"), - SET_MUTE("setMute"), - SET_BRIGHTNESS("setBrightness"); - - String action; - - ServiceAction(String action) { - this.action = action; - } - } - - interface IServiceActionCallback { - void onSuccess(T result); - - void onFailed(String errMsg); - } - - // -------------------------------------------------------------------------------- - // ---- AvService - // -------------------------------------------------------------------------------- - @SuppressWarnings("unused") - interface IAVServiceAction { - - void cast(IServiceActionCallback listener, String uri, String metadata); - - void play(IServiceActionCallback listener); - - void pause(IServiceActionCallback listener); - - void stop(IServiceActionCallback listener); - - void seek(IServiceActionCallback listener, final long position); - - void getPositionInfo(IServiceActionCallback listener); - - void getMediaInfo(IServiceActionCallback listener); - - void getTransportInfo(IServiceActionCallback listener); - } - - // -------------------------------------------------------------------------------- - // ---- RendererService - // -------------------------------------------------------------------------------- - @SuppressWarnings("unused") - interface IRendererServiceAction { - void setVolume(IServiceActionCallback listener, final int volume); - - void getVolume(IServiceActionCallback listener); - - void setMute(IServiceActionCallback listener, boolean mute); - - void isMute(IServiceActionCallback listener); - - void setBrightness(IServiceActionCallback listener, final int percent); - - void getBrightness(IServiceActionCallback listener); - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceAction.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceAction.kt new file mode 100644 index 00000000..d7c6cc30 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceAction.kt @@ -0,0 +1,49 @@ +package com.skyd.imomoe.util.dlna.dmc.control + +import org.fourthline.cling.support.model.MediaInfo +import org.fourthline.cling.support.model.PositionInfo +import org.fourthline.cling.support.model.TransportInfo + +interface IServiceAction { + enum class ServiceAction(var action: String) { + CAST("cast"), + PLAY("play"), + PAUSE("pause"), + STOP("stop"), + SEEK_TO("seekTo"), + SET_VOLUME("setVolume"), + SET_MUTE("setMute"), + SET_BRIGHTNESS("setBrightness"); + } + + interface IServiceActionCallback { + fun onSuccess(result: T) + fun onFailed(errMsg: String) + } + + // -------------------------------------------------------------------------------- + // ---- AvService + // -------------------------------------------------------------------------------- + interface IAVServiceAction { + fun cast(listener: IServiceActionCallback?, uri: String, metadata: String) + fun play(listener: IServiceActionCallback?) + fun pause(listener: IServiceActionCallback?) + fun stop(listener: IServiceActionCallback?) + fun seek(listener: IServiceActionCallback?, position: Long) + fun getPositionInfo(listener: IServiceActionCallback?) + fun getMediaInfo(listener: IServiceActionCallback?) + fun getTransportInfo(listener: IServiceActionCallback?) + } + + // -------------------------------------------------------------------------------- + // ---- RendererService + // -------------------------------------------------------------------------------- + interface IRendererServiceAction { + fun setVolume(listener: IServiceActionCallback?, volume: Int) + fun getVolume(listener: IServiceActionCallback?) + fun setMute(listener: IServiceActionCallback?, mute: Boolean) + fun isMute(listener: IServiceActionCallback?) + fun setBrightness(listener: IServiceActionCallback?, percent: Int) + fun getBrightness(listener: IServiceActionCallback?) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceFactory.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceFactory.java deleted file mode 100644 index 6e9c0419..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.skyd.imomoe.util.dlna.dmc.control; - -import com.skyd.imomoe.util.dlna.dmc.DLNACastManager; - -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.meta.Device; -import org.fourthline.cling.model.meta.Service; - -/** - * - */ -interface IServiceFactory { - IServiceAction.IAVServiceAction getAvService(); - - IServiceAction.IRendererServiceAction getRenderService(); - - // ------------------------------------------------------------------------------------------ - // Implement - // ------------------------------------------------------------------------------------------ - final class ServiceFactoryImpl implements IServiceFactory { - private final IServiceAction.IAVServiceAction mAvAction; - private final IServiceAction.IRendererServiceAction mRenderAction; - - public ServiceFactoryImpl(ControlPoint controlPoint, Device device) { - Service avService = device.findService(DLNACastManager.SERVICE_AV_TRANSPORT); - mAvAction = new BaseServiceExecutor.AVServiceExecutorImpl(controlPoint, avService); - Service rendererService = device.findService(DLNACastManager.SERVICE_RENDERING_CONTROL); - mRenderAction = new BaseServiceExecutor.RendererServiceExecutorImpl(controlPoint, rendererService); - } - - @Override - public IServiceAction.IAVServiceAction getAvService() { - return mAvAction; - } - - @Override - public IServiceAction.IRendererServiceAction getRenderService() { - return mRenderAction; - } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceFactory.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceFactory.kt new file mode 100644 index 00000000..2f3d357d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dmc/control/IServiceFactory.kt @@ -0,0 +1,35 @@ +package com.skyd.imomoe.util.dlna.dmc.control + +import com.skyd.imomoe.util.dlna.dmc.DLNACastManager +import com.skyd.imomoe.util.dlna.dmc.control.BaseServiceExecutor.AVServiceExecutorImpl +import com.skyd.imomoe.util.dlna.dmc.control.BaseServiceExecutor.RendererServiceExecutorImpl +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction.IAVServiceAction +import com.skyd.imomoe.util.dlna.dmc.control.IServiceAction.IRendererServiceAction +import org.fourthline.cling.controlpoint.ControlPoint +import org.fourthline.cling.model.meta.Device + +internal interface IServiceFactory { + val avService: IAVServiceAction + val renderService: IRendererServiceAction + + // ------------------------------------------------------------------------------------------ + // Implement + // ------------------------------------------------------------------------------------------ + class ServiceFactoryImpl(controlPoint: ControlPoint, device: Device<*, *, *>) : + IServiceFactory { + private val mAvAction: IAVServiceAction + private val mRenderAction: IRendererServiceAction + + init { + val avService = device.findService(DLNACastManager.SERVICE_AV_TRANSPORT) + mAvAction = AVServiceExecutorImpl(controlPoint, avService) + val rendererService = device.findService(DLNACastManager.SERVICE_RENDERING_CONTROL) + mRenderAction = RendererServiceExecutorImpl(controlPoint, rendererService) + } + + override val avService: IAVServiceAction + get() = mAvAction + override val renderService: IRendererServiceAction + get() = mRenderAction + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentDirectoryService.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentDirectoryService.java deleted file mode 100644 index ee476aee..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentDirectoryService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.skyd.imomoe.util.dlna.dms; - -import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService; -import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode; -import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; -import org.fourthline.cling.support.contentdirectory.DIDLParser; -import org.fourthline.cling.support.model.BrowseFlag; -import org.fourthline.cling.support.model.BrowseResult; -import org.fourthline.cling.support.model.DIDLContent; -import org.fourthline.cling.support.model.SortCriterion; -import org.fourthline.cling.support.model.container.Container; -import org.fourthline.cling.support.model.item.Item; - -public class ContentDirectoryService extends AbstractContentDirectoryService { - - @Override - public BrowseResult browse(String objectID, - BrowseFlag browseFlag, - String filter, - long firstResult, - long maxResults, - SortCriterion[] orderBy) throws ContentDirectoryException { - Container container = ContentFactory.getInstance().getContent(objectID); - DIDLContent didlContent = new DIDLContent(); - for (Container c : container.getContainers()) { - didlContent.addContainer(c); - } - for (Item item : container.getItems()) { - didlContent.addItem(item); - } - int count = container.getChildCount(); - String result; - try { - result = new DIDLParser().generate(didlContent); - } catch (Exception e) { - throw new ContentDirectoryException(ContentDirectoryErrorCode.CANNOT_PROCESS, e.toString()); - } - return new BrowseResult(result, count, count); - } - - @Override - public BrowseResult search(String containerId, String searchCriteria, - String filter, long firstResult, long maxResults, - SortCriterion[] orderBy) throws ContentDirectoryException { - // You can override this method to implement searching! - return super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy); - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentDirectoryService.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentDirectoryService.kt new file mode 100644 index 00000000..48d0590a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentDirectoryService.kt @@ -0,0 +1,52 @@ +package com.skyd.imomoe.util.dlna.dms + +import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService +import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode +import org.fourthline.cling.support.contentdirectory.ContentDirectoryException +import org.fourthline.cling.support.contentdirectory.DIDLParser +import org.fourthline.cling.support.model.BrowseFlag +import org.fourthline.cling.support.model.BrowseResult +import org.fourthline.cling.support.model.DIDLContent +import org.fourthline.cling.support.model.SortCriterion + +class ContentDirectoryService : AbstractContentDirectoryService() { + + @Throws(ContentDirectoryException::class) + override fun browse( + objectID: String?, + browseFlag: BrowseFlag?, + filter: String?, + firstResult: Long, + maxResults: Long, + orderby: Array? + ): BrowseResult { + val container = ContentFactory.instance.getContent(objectID) + val didlContent = DIDLContent() + for (c in container.containers) { + didlContent.addContainer(c) + } + for (item in container.items) { + didlContent.addItem(item) + } + val count = container.childCount + val result: String = try { + DIDLParser().generate(didlContent) + } catch (e: Exception) { + throw ContentDirectoryException(ContentDirectoryErrorCode.CANNOT_PROCESS, e.toString()) + } + return BrowseResult(result, count.toLong(), count.toLong()) + } + + @Throws(ContentDirectoryException::class) + override fun search( + containerId: String?, + searchCriteria: String?, + filter: String?, + firstResult: Long, + maxResults: Long, + orderBy: Array? + ): BrowseResult { + // You can override this method to implement searching! + return super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentFactory.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentFactory.java deleted file mode 100644 index a648c8f7..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentFactory.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.skyd.imomoe.util.dlna.dms; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import org.fourthline.cling.support.model.container.Container; -import org.fourthline.cling.support.model.item.Item; - -import java.util.List; - -public class ContentFactory { - private static ContentFactory sInstance; - - public static void initInstance(Context context, String baseUrl) { - if (sInstance == null || sInstance.checkBaseUrl(baseUrl)) { - sInstance = new ContentFactory(context.getApplicationContext(), baseUrl); - } - } - - public static ContentFactory getInstance() { - return sInstance; - } - - private final Context context; - private final String baseUrl; - - private ContentFactory(Context context, String baseUrl) { - this.context = context.getApplicationContext(); - this.baseUrl = baseUrl; - } - - boolean checkBaseUrl(String baseUrl) { - return this.baseUrl != null && !this.baseUrl.equals(baseUrl); - } - - @NonNull - public Container getContent(String containerId) { - Container result = new Container(); - result.setChildCount(0); - if (MediaItem.ROOT_ID.equals(containerId)) { - // 定义音频资源 - Container audioContainer = new Container(); - audioContainer.setId(MediaItem.AUDIO_ID); - audioContainer.setParentID(MediaItem.ROOT_ID); - audioContainer.setClazz(MediaItem.AUDIO_CLASS); - audioContainer.setTitle("Audios"); - - result.addContainer(audioContainer); - result.setChildCount(result.getChildCount() + 1); - - // 定义图片资源 - Container imageContainer = new Container(); - imageContainer.setId(MediaItem.IMAGE_ID); - imageContainer.setParentID(MediaItem.ROOT_ID); - imageContainer.setClazz(MediaItem.IMAGE_CLASS); - imageContainer.setTitle("Images"); - - result.addContainer(imageContainer); - result.setChildCount(result.getChildCount() + 1); - - // 定义视频资源 - Container videoContainer = new Container(); - videoContainer.setId(MediaItem.VIDEO_ID); - videoContainer.setParentID(MediaItem.ROOT_ID); - videoContainer.setClazz(MediaItem.VIDEO_CLASS); - videoContainer.setTitle("Videos"); - - result.addContainer(videoContainer); - result.setChildCount(result.getChildCount() + 1); - } else if (MediaItem.IMAGE_ID.equals(containerId)) { - MediaContentDao contentDao = new MediaContentDao(baseUrl); - //Get image items - List items = contentDao.getImageItems(context); - for (Item item : items) { - result.addItem(item); - result.setChildCount(result.getChildCount() + 1); - } - } else if (MediaItem.AUDIO_ID.equals(containerId)) { - MediaContentDao contentDao = new MediaContentDao(baseUrl); - //Get audio items - List items = contentDao.getAudioItems(context); - for (Item item : items) { - result.addItem(item); - result.setChildCount(result.getChildCount() + 1); - } - } else if (MediaItem.VIDEO_ID.equals(containerId)) { - MediaContentDao contentDao = new MediaContentDao(baseUrl); - //Get video items - List items = contentDao.getVideoItems(context); - for (Item item : items) { - result.addItem(item); - result.setChildCount(result.getChildCount() + 1); - } - } - return result; - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentFactory.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentFactory.kt new file mode 100644 index 00000000..84a7440d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentFactory.kt @@ -0,0 +1,88 @@ +package com.skyd.imomoe.util.dlna.dms + +import android.annotation.SuppressLint +import android.content.Context +import org.fourthline.cling.support.model.container.Container + +class ContentFactory private constructor(context: Context, private val baseUrl: String) { + private val context: Context = context.applicationContext + fun checkBaseUrl(baseUrl: String): Boolean { + return this.baseUrl != baseUrl + } + + fun getContent(containerId: String?): Container { + val result = Container() + result.childCount = 0 + when (containerId) { + MediaItem.ROOT_ID -> { + // 定义音频资源 + val audioContainer = Container() + audioContainer.id = MediaItem.AUDIO_ID + audioContainer.parentID = MediaItem.ROOT_ID + audioContainer.clazz = MediaItem.AUDIO_CLASS + audioContainer.title = "Audios" + result.addContainer(audioContainer) + result.childCount = result.childCount + 1 + + // 定义图片资源 + val imageContainer = Container() + imageContainer.id = MediaItem.IMAGE_ID + imageContainer.parentID = MediaItem.ROOT_ID + imageContainer.clazz = MediaItem.IMAGE_CLASS + imageContainer.title = "Images" + result.addContainer(imageContainer) + result.childCount = result.childCount + 1 + + // 定义视频资源 + val videoContainer = Container() + videoContainer.id = MediaItem.VIDEO_ID + videoContainer.parentID = MediaItem.ROOT_ID + videoContainer.clazz = MediaItem.VIDEO_CLASS + videoContainer.title = "Videos" + result.addContainer(videoContainer) + result.childCount = result.childCount + 1 + } + MediaItem.IMAGE_ID -> { + val contentDao = MediaContentDao(baseUrl) + //Get image items + val items = contentDao.getImageItems(context) + for (item in items) { + result.addItem(item) + result.childCount = result.childCount + 1 + } + } + MediaItem.AUDIO_ID -> { + val contentDao = MediaContentDao(baseUrl) + //Get audio items + val items = contentDao.getAudioItems(context) + for (item in items) { + result.addItem(item) + result.childCount = result.childCount + 1 + } + } + MediaItem.VIDEO_ID -> { + val contentDao = MediaContentDao(baseUrl) + //Get video items + val items = contentDao.getVideoItems(context) + for (item in items) { + result.addItem(item) + result.childCount = result.childCount + 1 + } + } + } + return result + } + + companion object { + @SuppressLint("StaticFieldLeak") + lateinit var instance: ContentFactory + private set + + @JvmStatic + fun initInstance(context: Context, baseUrl: String) { + if (!this::instance.isInitialized || instance.checkBaseUrl(baseUrl)) { + instance = ContentFactory(context.applicationContext, baseUrl) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentResourceServlet.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentResourceServlet.java deleted file mode 100644 index 83cf1320..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/ContentResourceServlet.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2014 Kevin Shen - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.skyd.imomoe.util.dlna.dms; - -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.util.resource.FileResource; -import org.eclipse.jetty.util.resource.Resource; - -import java.io.File; - -public class ContentResourceServlet extends DefaultServlet { - - @Override - public Resource getResource(String pathInContext) { - // String id = Utils.parseResourceId(pathInContext); - // content://media/external/video/media/1611127029319529 - // Uri uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, Long.parseLong(id)); -// Logger.i("ContentResourceServlet, path: %s", pathInContext); - try { - File file = new File(pathInContext); - if (file.exists()) return FileResource.newResource(file); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - public static class VideoResourceServlet extends ContentResourceServlet { - } - - public static class AudioResourceServlet extends ContentResourceServlet { - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IMediaContentDao.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IMediaContentDao.java deleted file mode 100644 index 6f3581c9..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IMediaContentDao.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.skyd.imomoe.util.dlna.dms; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import org.fourthline.cling.support.model.item.Item; - -import java.util.List; - -interface IMediaContentDao { - @NonNull - List getImageItems(@NonNull Context context); - - @NonNull - List getAudioItems(@NonNull Context context); - - @NonNull - List getVideoItems(@NonNull Context context); -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IMediaContentDao.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IMediaContentDao.kt new file mode 100644 index 00000000..0a0d7c35 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IMediaContentDao.kt @@ -0,0 +1,10 @@ +package com.skyd.imomoe.util.dlna.dms + +import android.content.Context +import org.fourthline.cling.support.model.item.Item + +internal interface IMediaContentDao { + fun getImageItems(context: Context): List + fun getAudioItems(context: Context): List + fun getVideoItems(context: Context): List +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServer.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServer.java deleted file mode 100644 index 53ba4c95..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServer.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.skyd.imomoe.util.dlna.dms; - -interface IResourceServer { - void startServer(); - - void stopServer(); -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServer.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServer.kt new file mode 100644 index 00000000..a75e9432 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServer.kt @@ -0,0 +1,6 @@ +package com.skyd.imomoe.util.dlna.dms + +interface IResourceServer { + fun startServer() + fun stopServer() +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServerFactory.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServerFactory.java deleted file mode 100644 index 2969322b..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServerFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.skyd.imomoe.util.dlna.dms; - -interface IResourceServerFactory { - int getPort(); - - IResourceServer getInstance(); - - // ---------------------------------------------------------------------------- - // ---- implement - // ---------------------------------------------------------------------------- - final class DefaultResourceServerFactoryImpl implements IResourceServerFactory { - private final int port; - - public DefaultResourceServerFactoryImpl(int port) { - this.port = port; - } - - @Override - public int getPort() { - return port; - } - - @Override - public IResourceServer getInstance() { - // TODO: - // return new JettyHttpServer(port); - return new NanoHttpServer(port); - } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServerFactory.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServerFactory.kt new file mode 100644 index 00000000..70f52cbf --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/IResourceServerFactory.kt @@ -0,0 +1,17 @@ +package com.skyd.imomoe.util.dlna.dms + + +interface IResourceServerFactory { + val port: Int + val instance: IResourceServer + + // ---------------------------------------------------------------------------- + // ---- implement + // ---------------------------------------------------------------------------- + class DefaultResourceServerFactoryImpl(override val port: Int) : IResourceServerFactory { + override val instance: IResourceServer + // TODO: + // return new JettyHttpServer(port); + get() = NanoHttpServer(port) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaContentDao.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaContentDao.kt index 11a68235..bbb169e2 100644 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaContentDao.kt +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaContentDao.kt @@ -1,5 +1,6 @@ package com.skyd.imomoe.util.dlna.dms +import android.annotation.SuppressLint import android.content.Context import android.provider.MediaStore.* import org.fourthline.cling.support.model.PersonWithRole @@ -9,9 +10,9 @@ import org.fourthline.cling.support.model.item.Item import org.fourthline.cling.support.model.item.Movie import org.fourthline.cling.support.model.item.MusicTrack import java.io.File -import java.util.* internal class MediaContentDao(private val mBaseUrl: String) : IMediaContentDao { + @SuppressLint("Range") override fun getImageItems(context: Context): List { val items: MutableList = ArrayList() context.contentResolver.query( @@ -39,6 +40,7 @@ internal class MediaContentDao(private val mBaseUrl: String) : IMediaContentDao return items } + @SuppressLint("Range") override fun getAudioItems(context: Context): List { val items: MutableList = ArrayList() context.contentResolver.query( @@ -75,6 +77,7 @@ internal class MediaContentDao(private val mBaseUrl: String) : IMediaContentDao return items } + @SuppressLint("Range") override fun getVideoItems(context: Context): List { val items: MutableList = ArrayList() context.contentResolver.query( diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaItem.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaItem.java deleted file mode 100644 index b034c42e..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaItem.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.skyd.imomoe.util.dlna.dms; - -import org.fourthline.cling.support.model.DIDLObject; - -public interface MediaItem { - String ROOT_ID = "0"; - String AUDIO_ID = "10"; - String VIDEO_ID = "20"; - String IMAGE_ID = "30"; - - DIDLObject.Class AUDIO_CLASS = new DIDLObject.Class("object.container.audio"); - DIDLObject.Class IMAGE_CLASS = new DIDLObject.Class("object.item.imageItem"); - DIDLObject.Class VIDEO_CLASS = new DIDLObject.Class("object.container.video"); -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaItem.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaItem.kt new file mode 100644 index 00000000..8d3d7058 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaItem.kt @@ -0,0 +1,15 @@ +package com.skyd.imomoe.util.dlna.dms + +import org.fourthline.cling.support.model.DIDLObject + +interface MediaItem { + companion object { + const val ROOT_ID = "0" + const val AUDIO_ID = "10" + const val VIDEO_ID = "20" + const val IMAGE_ID = "30" + val AUDIO_CLASS = DIDLObject.Class("object.container.audio") + val IMAGE_CLASS = DIDLObject.Class("object.item.imageItem") + val VIDEO_CLASS = DIDLObject.Class("object.container.video") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaServer.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaServer.java deleted file mode 100644 index 83912eb4..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaServer.java +++ /dev/null @@ -1,114 +0,0 @@ - -package com.skyd.imomoe.util.dlna.dms; - -import android.content.Context; - -import androidx.annotation.Nullable; -import com.skyd.imomoe.util.dlna.*; - -import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder; -import org.fourthline.cling.model.DefaultServiceManager; -import org.fourthline.cling.model.ValidationException; -import org.fourthline.cling.model.meta.DeviceDetails; -import org.fourthline.cling.model.meta.DeviceIdentity; -import org.fourthline.cling.model.meta.Icon; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.meta.LocalService; -import org.fourthline.cling.model.meta.ManufacturerDetails; -import org.fourthline.cling.model.meta.ModelDetails; -import org.fourthline.cling.model.types.DeviceType; -import org.fourthline.cling.model.types.UDADeviceType; -import org.fourthline.cling.model.types.UDN; - -import java.io.IOException; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.util.UUID; - -public final class MediaServer { - - //TODO:remove local device field? - private LocalDevice mDevice; - private IResourceServer mResourceServer; - private final String mInetAddress; - private final String mBaseUrl; - - public MediaServer(Context context) { - this(context, new IResourceServerFactory.DefaultResourceServerFactoryImpl(PORT)); - } - - public MediaServer(Context context, IResourceServerFactory factory) { - String address = Utils.getWiFiIPAddress(context); - mInetAddress = String.format("%s:%s", address, factory.getPort()); - mBaseUrl = String.format("http://%s:%s", address, factory.getPort()); - ContentFactory.initInstance(context, mBaseUrl); - try { - mDevice = createLocalDevice(context, address); - mResourceServer = factory.getInstance(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void start() { - if (mResourceServer != null) { - mResourceServer.startServer(); - } - } - - public void stop() { - if (mResourceServer != null) { - mResourceServer.stopServer(); - } - } - - public String getInetAddress() { - return mInetAddress; - } - - public String getBaseUrl() { - return mBaseUrl; - } - - @Nullable - public LocalDevice getDevice() { - return mDevice; - } - - private static final String DMS_DESC = "MSI MediaServer"; - private static final String ID_SALT = "GNaP-MediaServer"; - public final static String TYPE_MEDIA_SERVER = "MediaServer"; - private final static int VERSION = 1; - private final static int PORT = 8192; - - @SuppressWarnings({"unchecked", "rawtypes"}) - protected LocalDevice createLocalDevice(Context context, String ipAddress) throws ValidationException { - DeviceIdentity identity = new DeviceIdentity(createUniqueSystemIdentifier(ID_SALT, ipAddress)); - DeviceType type = new UDADeviceType(TYPE_MEDIA_SERVER, VERSION); - DeviceDetails details = new DeviceDetails(String.format("DMS (%s)", android.os.Build.MODEL), - new ManufacturerDetails(android.os.Build.MANUFACTURER), - new ModelDetails(android.os.Build.MODEL, DMS_DESC, "v1", mBaseUrl)); - final LocalService service = new AnnotationLocalServiceBinder().read(ContentDirectoryService.class); - service.setManager(new DefaultServiceManager(service, ContentDirectoryService.class)); - Icon icon = null; - try { - icon = new Icon("image/png", 48, 48, 32, "msi.png", - context.getResources().getAssets().open("ic_launcher.png")); - } catch (IOException ignored) { - } - return new LocalDevice(identity, type, details, icon, service); - } - - private static UDN createUniqueSystemIdentifier(@SuppressWarnings("SameParameterValue") String salt, String ipAddress) { - StringBuilder builder = new StringBuilder(); - builder.append(ipAddress); - builder.append(android.os.Build.MODEL); - builder.append(android.os.Build.MANUFACTURER); - try { - byte[] hash = MessageDigest.getInstance("MD5").digest(builder.toString().getBytes()); - return new UDN(new UUID(new BigInteger(-1, hash).longValue(), salt.hashCode())); - } catch (Exception ex) { - return new UDN(ex.getMessage() != null ? ex.getMessage() : "UNKNOWN"); - } - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaServer.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaServer.kt new file mode 100644 index 00000000..d1af14b5 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/MediaServer.kt @@ -0,0 +1,89 @@ +package com.skyd.imomoe.util.dlna.dms + +import android.content.Context +import android.os.Build +import com.skyd.imomoe.util.dlna.Utils.getWiFiIPAddress +import com.skyd.imomoe.util.dlna.dms.IResourceServerFactory.DefaultResourceServerFactoryImpl +import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder +import org.fourthline.cling.model.DefaultServiceManager +import org.fourthline.cling.model.ValidationException +import org.fourthline.cling.model.meta.* +import org.fourthline.cling.model.types.DeviceType +import org.fourthline.cling.model.types.UDADeviceType +import org.fourthline.cling.model.types.UDN +import java.io.IOException +import java.math.BigInteger +import java.security.MessageDigest +import java.util.* + +open class MediaServer @JvmOverloads constructor( + context: Context, factory: IResourceServerFactory = DefaultResourceServerFactoryImpl(PORT) +) { + private val address by lazy { getWiFiIPAddress(context) } + + //TODO:remove local device field? + val device: LocalDevice by lazy { createLocalDevice(context, address) } + private var resourceServer: IResourceServer = factory.instance + val inetAddress: String = String.format("%s:%s", address, factory.port) + val baseUrl: String = String.format("http://%s:%s", address, factory.port) + + fun start() { + resourceServer.startServer() + } + + fun stop() { + resourceServer.stopServer() + } + + @Throws(ValidationException::class) + @Suppress() + protected open fun createLocalDevice(context: Context, ipAddress: String): LocalDevice { + val identity = DeviceIdentity(createUniqueSystemIdentifier(ID_SALT, ipAddress)) + val type: DeviceType = UDADeviceType(TYPE_MEDIA_SERVER, VERSION) + val details = DeviceDetails( + String.format("DMS (%s)", Build.MODEL), + ManufacturerDetails(Build.MANUFACTURER), + ModelDetails(Build.MODEL, DMS_DESC, "v1", baseUrl) + ) + val service = AnnotationLocalServiceBinder().read(ContentDirectoryService::class.java) + //TODO: ContentDirectoryService::class.java??? + service.manager = DefaultServiceManager(service, ContentDirectoryService().javaClass) + var icon: Icon? = null + try { + icon = Icon( + "image/png", 48, 48, 32, "msi.png", + context.resources.assets.open("ic_launcher.png") + ) + } catch (ignored: IOException) { + } + return LocalDevice(identity, type, details, icon, service) + } + + companion object { + private const val DMS_DESC = "MSI MediaServer" + private const val ID_SALT = "GNaP-MediaServer" + const val TYPE_MEDIA_SERVER = "MediaServer" + private const val VERSION = 1 + private const val PORT = 8192 + private fun createUniqueSystemIdentifier( + @Suppress("SameParameterValue") salt: String, + ipAddress: String + ): UDN { + val builder = StringBuilder() + builder.append(ipAddress) + builder.append(Build.MODEL) + builder.append(Build.MANUFACTURER) + return try { + val hash = MessageDigest.getInstance("MD5").digest(builder.toString().toByteArray()) + UDN(UUID(BigInteger(-1, hash).toLong(), salt.hashCode().toLong())) + } catch (ex: Exception) { + UDN(if (ex.message != null) ex.message else "UNKNOWN") + } + } + } + + init { + ContentFactory.initInstance(context, baseUrl) +// resourceServer = factory.instance + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/NanoHttpServer.java b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/NanoHttpServer.java deleted file mode 100644 index 82e97bc4..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/NanoHttpServer.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.skyd.imomoe.util.dlna.dms; - -import android.text.TextUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import fi.iki.elonen.NanoHTTPD; - -class NanoHttpServer extends NanoHTTPD implements IResourceServer { - - private static final Map MIME_TYPE = new HashMap<>(); - private static final String MIME_PLAINTEXT = "text/plain"; - - static { - MIME_TYPE.put("jpg", "image/*"); - MIME_TYPE.put("jpeg", "image/*"); - MIME_TYPE.put("png", "image/*"); - MIME_TYPE.put("mp3", "audio/*"); - MIME_TYPE.put("mp4", "video/*"); - MIME_TYPE.put("wav", "video/*"); - } - - public NanoHttpServer(int port) { - super(port); - } - - @Override - public Response serve(IHTTPSession session) { - System.out.println("uri: " + session.getUri()); - System.out.println("header: " + session.getHeaders().toString()); - System.out.println("params: " + session.getParms().toString()); - String uri = session.getUri(); - if (TextUtils.isEmpty(uri) || !uri.startsWith("/")) { - return NanoHTTPD.newChunkedResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, null); - } - File file = new File(uri); - if (!file.exists() || file.isDirectory()) { - return NanoHTTPD.newChunkedResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, null); - } - String type = uri.substring(Math.min(uri.length(), uri.lastIndexOf(".") + 1)).toLowerCase(Locale.US); - String mimeType = MIME_TYPE.get(type); - if (TextUtils.isEmpty(mimeType)) { - mimeType = MIME_PLAINTEXT; - } - try { - return NanoHTTPD.newChunkedResponse(Response.Status.OK, mimeType, new FileInputStream(file)); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return NanoHTTPD.newChunkedResponse(Response.Status.SERVICE_UNAVAILABLE, mimeType, null); - } - } - - @Override - public void startServer() { - try { - start(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void stopServer() { - stop(); - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/dlna/dms/NanoHttpServer.kt b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/NanoHttpServer.kt new file mode 100644 index 00000000..f10bd64b --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/dlna/dms/NanoHttpServer.kt @@ -0,0 +1,63 @@ +package com.skyd.imomoe.util.dlna.dms + +import fi.iki.elonen.NanoHTTPD +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.util.* +import kotlin.math.min + +internal class NanoHttpServer(port: Int) : NanoHTTPD(port), IResourceServer { + companion object { + private val MIME_TYPE: MutableMap = HashMap() + private const val MIME_PLAINTEXT = "text/plain" + + init { + MIME_TYPE["jpg"] = "image/*" + MIME_TYPE["jpeg"] = "image/*" + MIME_TYPE["png"] = "image/*" + MIME_TYPE["mp3"] = "audio/*" + MIME_TYPE["mp4"] = "video/*" + MIME_TYPE["wav"] = "video/*" + } + } + + override fun serve(session: IHTTPSession): Response { + println("uri: " + session.uri) + println("header: " + session.headers.toString()) + println("params: " + session.parms.toString()) + val uri = session.uri + if (uri.isNullOrEmpty() || !uri.startsWith("/")) { + return newChunkedResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, null) + } + val file = File(uri) + if (!file.exists() || file.isDirectory) { + return newChunkedResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, null) + } + val type = uri.substring(min(uri.length, uri.lastIndexOf(".") + 1)) + .lowercase(Locale.US) + var mimeType = MIME_TYPE[type] + if (mimeType.isNullOrEmpty()) { + mimeType = MIME_PLAINTEXT + } + return try { + newChunkedResponse(Response.Status.OK, mimeType, FileInputStream(file)) + } catch (e: FileNotFoundException) { + e.printStackTrace() + newChunkedResponse(Response.Status.SERVICE_UNAVAILABLE, mimeType, null) + } + } + + override fun startServer() { + try { + start() + } catch (e: IOException) { + e.printStackTrace() + } + } + + override fun stopServer() { + stop() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/download/DownloadStatus.kt b/app/src/main/java/com/skyd/imomoe/util/download/DownloadStatus.kt new file mode 100644 index 00000000..d6083f64 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/download/DownloadStatus.kt @@ -0,0 +1,8 @@ +package com.skyd.imomoe.util.download + +enum class DownloadStatus { + DOWNLOADING, // 正在下载更新 + COMPLETE, // 下载完成 + CANCEL, // 取消下载 + ERROR // 下载失败 +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadHelper.kt b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadHelper.kt new file mode 100644 index 00000000..e5687c2b --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadHelper.kt @@ -0,0 +1,268 @@ +package com.skyd.imomoe.util.download.downloadanime + +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.config.Const.DownloadAnime.animeFilePath +import com.skyd.imomoe.database.entity.AnimeDownloadEntity +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadService.Companion.ANIME_EPISODE +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadService.Companion.ANIME_TITLE +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadService.Companion.DOWNLOAD_URL_KEY +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadService.Companion.STORE_DIRECTORY_PATH_KEY +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.listener.dsl.requestPermissions +import org.w3c.dom.Element +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import javax.xml.parsers.DocumentBuilder +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + + +object AnimeDownloadHelper { + + fun downloadAnime( + activity: AppCompatActivity, + url: String, + animeTitle: String, + animeEpisode: String + ) { + if (activity.isFinishing) { + appContext.getString(R.string.do_not_finish_the_page_when_parse_download_data) + .showToast() + return + } + XXPermissions.with(activity) + .permission( + Permission.MANAGE_EXTERNAL_STORAGE, + Permission.NOTIFICATION_SERVICE + ) + .requestPermissions { + onGranted { _, all -> + if (!all) return@onGranted + activity.startService( + Intent(activity, AnimeDownloadService::class.java) + .putExtra(DOWNLOAD_URL_KEY, url) + .putExtra(ANIME_TITLE, animeTitle) + .putExtra(ANIME_EPISODE, animeEpisode) + .putExtra(STORE_DIRECTORY_PATH_KEY, animeFilePath + animeTitle) + ) + } + onDenied { permissions, _ -> + if (permissions?.contains(Permission.MANAGE_EXTERNAL_STORAGE) == false) { + activity.getString(R.string.no_storage_can_not_download).showToast() + } + if (permissions?.contains(Permission.NOTIFICATION_SERVICE) == false) { + activity.getString(R.string.no_notification_service).showToast() + } + } + } + } + + private fun createXml(folderName: String) { + val builderFactory = DocumentBuilderFactory.newInstance() + // 从DOM工厂里获取DOM解析器 + val documentBuilder: DocumentBuilder + try { + documentBuilder = builderFactory.newDocumentBuilder() + val document = documentBuilder.newDocument() + //创建节点 + val data: Element = document.createElement("data") + //将节点添加到document中 + document.appendChild(data) + //保存 + val transformerFactory = TransformerFactory.newInstance() + val transformer = transformerFactory.newTransformer() + val domSource = DOMSource(document) + //设置编码类型 + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + val f = File(animeFilePath + folderName, "data.xml") + val result = StreamResult(FileOutputStream(f)) + //把DOM树转换为xml文件 + transformer.transform(domSource, result) + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + } + + fun save2Xml( + folderName: String, entity: AnimeDownloadEntity, + animeFilePath: String = Const.DownloadAnime.animeFilePath + ) { + try { + val file = File(animeFilePath + folderName, "data.xml") + if (!file.exists()) { + createXml(folderName) + } + // 1.得到DOM解析器的工厂实例 + val builderFactory = DocumentBuilderFactory.newInstance() + // 2.从DOM工厂里获取DOM解析器 + val documentBuilder: DocumentBuilder = builderFactory.newDocumentBuilder() + // 3.解析XML文档,得到DOM树 + val docs = documentBuilder.parse(file) + val animeList = docs.getElementsByTagName("anime") + var replaced = false + for (i in 0 until animeList.length) { + val anime: Node = animeList.item(i) + // 遍历anime的所有属性 + val nodeList: NodeList = anime.childNodes + var fileName: Node? = null + var title: Node? = null + var md5: Node? = null + for (j in 0 until nodeList.length) { + val node: Node = nodeList.item(j) + if (node.nodeType == Node.ELEMENT_NODE) { + when (node.nodeName) { + "fileName" -> fileName = node.firstChild + "md5" -> md5 = node.firstChild + "title" -> title = node.firstChild + } + } + } + if (entity.md5 == md5?.nodeValue) { + val newFileName = fileName?.cloneNode(true) + newFileName?.textContent = entity.fileName + val newTitle = title?.cloneNode(true) + newTitle?.textContent = entity.title + anime.replaceChild(newFileName, fileName) + anime.replaceChild(newTitle, title) + replaced = true + break + } + } + if (!replaced) { + //创建节点 + val animeElement = docs.createElement("anime") + val fileNameElement = docs.createElement("fileName") + fileNameElement.textContent = entity.fileName + val md5Element = docs.createElement("md5") + md5Element.textContent = entity.md5 + val titleElement = docs.createElement("title") + titleElement.textContent = entity.title + + //添加父子关系 + animeElement.appendChild(fileNameElement) + animeElement.appendChild(md5Element) + animeElement.appendChild(titleElement) + docs.documentElement.appendChild(animeElement) + } + + //保存xml文件 + val transformerFactory = TransformerFactory.newInstance() + val transformer = transformerFactory.newTransformer() + val domSource = DOMSource(docs) + //设置编码类型 + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + val result = StreamResult(FileOutputStream(file)) + //把DOM树转换为xml文件 + transformer.transform(domSource, result) + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + } + + fun getAnimeFromXml( + folderName: String, + animeFilePath: String = Const.DownloadAnime.animeFilePath + ): MutableList { + val list: MutableList = ArrayList() + try { + // 1. 创建DocumentBuilderFactory对象 + val dFactory = DocumentBuilderFactory.newInstance() + // 2. 创建DocumentBuilder对象 + val dBuilder = dFactory.newDocumentBuilder() + // 3. 通过DocumentBuilder的parse方法解析xml + val file = File("$animeFilePath$folderName/data.xml") + if (!file.exists()) { + createXml(folderName) + } + val doc = dBuilder.parse(FileInputStream(file)) + // 4. 根据根节点名称获取所有的people节点 + val animeList = doc.getElementsByTagName("anime") + // 5. 遍历所有的people节点 + for (i in 0 until animeList.length) { + val anime: Node = animeList.item(i) + // 遍历anime的所有属性 + val entity = AnimeDownloadEntity("", "", "") + val nodeList: NodeList = anime.childNodes + for (j in 0 until nodeList.length) { + val node: Node = nodeList.item(j) + if (node.nodeType == Node.ELEMENT_NODE) { + when (node.nodeName) { + "fileName" -> entity.fileName = node.firstChild.nodeValue + "md5" -> entity.md5 = node.firstChild.nodeValue + "title" -> entity.title = node.firstChild.nodeValue + } + } + } + list.add(entity) + } + } catch (e: Exception) { + e.printStackTrace() + } + return list + } + + fun deleteAnimeFromXml( + folderName: String, entity: AnimeDownloadEntity, + animeFilePath: String = Const.DownloadAnime.animeFilePath + ) { + try { + // 1. 创建DocumentBuilderFactory对象 + val builderFactory = DocumentBuilderFactory.newInstance() + // 2. 创建DocumentBuilder对象 + val documentBuilder = builderFactory.newDocumentBuilder() + val file = File("$animeFilePath$folderName/data.xml") + if (!file.exists()) return + // 3. 通过DocumentBuilder的parse方法解析xml + val document = documentBuilder.parse(FileInputStream(file)) + // 4. 根据根节点名称获取所有的people节点 + val animeList = document.getElementsByTagName("anime") + // 5. 遍历所有的people节点 + for (i in 0 until animeList.length) { + val anime: Node = animeList.item(i) + if (!anime.hasChildNodes()) continue + // 遍历anime的所有属性 + val nodeList: NodeList = anime.childNodes + var pickTimes = 0 // 匹配次数 + for (j in 0 until nodeList.length) { + val node: Node = nodeList.item(j) + if (node.nodeType == Node.ELEMENT_NODE) { + if ((node.nodeName == "fileName" && node.firstChild.nodeValue == entity.fileName) || + (node.nodeName == "md5" && node.firstChild.nodeValue == entity.md5) || + (node.nodeName == "title" && node.firstChild.nodeValue == entity.title) + ) + pickTimes++ + } + } + if (pickTimes == 3) anime.parentNode.removeChild(anime) + } + // 关闭 + //保存xml文件 + val transformerFactory = TransformerFactory.newInstance() + val transformer = transformerFactory.newTransformer() + val domSource = DOMSource(document) + //设置编码类型 + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + val result = StreamResult(FileOutputStream(file)) + //把DOM树转换为xml文件 + transformer.transform(domSource, result) + } catch (e: IOException) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadNotification.kt b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadNotification.kt new file mode 100644 index 00000000..03a4e7b5 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadNotification.kt @@ -0,0 +1,86 @@ +package com.skyd.imomoe.util.download.downloadanime + +import android.annotation.TargetApi +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import com.skyd.imomoe.R + + +class AnimeDownloadNotification( + val context: Context, + val taskId: Long, + val url: String, + var title: String +) { + companion object { + var MAX_NOTIFY_ID = 1000 + + const val CHANNEL_ID = "download_anime" + const val CHANNEL_NAME = "download_message" + } + + private val manager: NotificationManager by lazy { + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + private val builder: NotificationCompat.Builder by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel() + NotificationCompat.Builder(context, CHANNEL_ID) + } else { + NotificationCompat.Builder(context) + } + } + private val notifyId = ++MAX_NOTIFY_ID + + init { + val intent = Intent(context, AnimeDownloadReceiver::class.java).apply { + action = AnimeDownloadReceiver.CANCEL_ACTION + putExtra(AnimeDownloadReceiver.NOTIFY_ID, notifyId) + putExtra(AnimeDownloadReceiver.TASK_ID, taskId) + putExtra(AnimeDownloadReceiver.TASK_URL, url) + } + val pendingIntent: PendingIntent = PendingIntent.getBroadcast( + context, + notifyId, + intent, + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) PendingIntent.FLAG_CANCEL_CURRENT + else PendingIntent.FLAG_MUTABLE + ) + builder + .setContentTitle(context.getString(R.string.download_notification_title, title)) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentText("0%") + .setProgress(100, 0, false) + .setAutoCancel(false) + .addAction( + R.drawable.ic_close_24, + context.getString(R.string.cancel), + pendingIntent + ) + manager.notify(notifyId, builder.build()) + } + + fun upload(progress: Int) { + builder.setProgress(100, progress, false) + .setContentText("$progress%") + manager.notify(notifyId, builder.build()) + } + + fun cancel() { + manager.cancel(notifyId) + } + + @TargetApi(Build.VERSION_CODES.O) + private fun createNotificationChannel() { + val channel = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW) + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadReceiver.kt b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadReceiver.kt new file mode 100644 index 00000000..15ab56b3 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadReceiver.kt @@ -0,0 +1,32 @@ +package com.skyd.imomoe.util.download.downloadanime + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class AnimeDownloadReceiver : BroadcastReceiver() { + companion object { + const val NOTIFY_ID = "notifyID" + const val CANCEL_ACTION = "cancelAction" + const val TASK_ID = "taskId" + const val TASK_URL = "taskUrl" + } + + override fun onReceive(context: Context, intent: Intent?) { + val action = intent?.action.orEmpty() + + val notificationId = intent?.getIntExtra(NOTIFY_ID, -1) ?: return + if (notificationId == -1) return + + when (action) { + CANCEL_ACTION -> { + // 在Service里面统一cancel + val taskId = intent.getLongExtra(TASK_ID, -1) + val taskUrl = intent.getStringExtra(TASK_URL).orEmpty() + if (taskId != -1L) { + AnimeDownloadService.cancelTaskEvent.tryEmit(taskId to taskUrl) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadService.kt b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadService.kt new file mode 100644 index 00000000..b3b7528f --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/AnimeDownloadService.kt @@ -0,0 +1,311 @@ +package com.skyd.imomoe.util.download.downloadanime + +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import androidx.lifecycle.LifecycleService +import com.arialyy.annotations.Download +import com.arialyy.aria.core.Aria +import com.arialyy.aria.core.common.HttpOption +import com.arialyy.aria.core.download.DownloadEntity +import com.arialyy.aria.core.download.m3u8.M3U8VodOption +import com.arialyy.aria.core.task.DownloadTask +import com.skyd.imomoe.R +import com.skyd.imomoe.database.entity.AnimeDownloadEntity +import com.skyd.imomoe.database.getAppDataBase +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.ext.toMD5 +import com.skyd.imomoe.net.RetrofitManager +import com.skyd.imomoe.net.service.HtmlService +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadHelper.save2Xml +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + + +class AnimeDownloadService : LifecycleService() { + companion object { + val stopTaskEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + val cancelTaskEvent: MutableSharedFlow> = + MutableSharedFlow(extraBufferCapacity = 1) + val resumeTaskEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + + const val DOWNLOAD_URL_KEY = "downloadUrl" + const val STORE_DIRECTORY_PATH_KEY = "storeFilePath" + const val ANIME_TITLE = "animeTitle" + const val ANIME_EPISODE = "animeEpisode" + + const val M3U8_CONTENT_TYPE = "application/vnd.apple.mpegurl" + } + + private val coroutineScope by lazy(LazyThreadSafetyMode.NONE) { + CoroutineScope(Dispatchers.IO) + } + + private val onTaskPreEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskStartEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskCompleteEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskRunningEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskStopEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskCancelEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskFailEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskResumeEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + + private val notifyMap = hashMapOf() + private val animeTitleEpisodeMap = hashMapOf>() + + inner class AnimeDownloadBinder : Binder() { + val service: AnimeDownloadService + get() = this@AnimeDownloadService + val animeTitleEpisodeMap: HashMap> + get() = this@AnimeDownloadService.animeTitleEpisodeMap + val notCompleteList: List + get() = Aria.download(this).allNotCompleteTask.orEmpty() + + val onTaskPreEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskPreEvent + val onTaskStartEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskStartEvent + val onTaskCompleteEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskCompleteEvent + val onTaskRunningEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskRunningEvent + val onTaskStopEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskStopEvent + val onTaskResumeEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskResumeEvent + val onTaskCancelEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskCancelEvent + val onTaskFailEvent: MutableSharedFlow + get() = this@AnimeDownloadService.onTaskFailEvent + } + + private val animeDownloadBinder: Binder = AnimeDownloadBinder() + + fun stopTask(id: Long) { + if (id == -1L) return + Aria.download(this).load(id).stop() + } + + fun resumeTask(id: Long) { + if (id == -1L) return + Aria.download(this).load(id).resume() + } + + fun cancelTask(id: Long, url: String?) { + if (id == -1L) return + Aria.download(this).load(id).cancel() + if (url.isNullOrEmpty()) notifyMap.remove(url) + } + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return animeDownloadBinder + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + intent ?: return START_NOT_STICKY + + val downloadUrl = intent.getStringExtra(DOWNLOAD_URL_KEY).orEmpty() + val storeDirectoryPath = intent.getStringExtra(STORE_DIRECTORY_PATH_KEY).orEmpty() + val animeTitle = intent.getStringExtra(ANIME_TITLE).orEmpty() + val animeEpisode = intent.getStringExtra(ANIME_EPISODE).orEmpty() + + coroutineScope.launch { + runCatching { + val fileName = downloadUrl + .substringAfterLast("/", animeEpisode) + .ifBlank { animeEpisode } + .toMD5() + val contentType = RetrofitManager + .get() + .create(HtmlService::class.java) + .getResponseHeader(downloadUrl) + .headers()["Content-Type"] + withContext(Dispatchers.Main) { + addTask( + downloadUrl = downloadUrl, + filePath = "$storeDirectoryPath/$fileName", + animeTitle = animeTitle, + animeEpisode = animeEpisode, + isM3u8 = contentType.equals(M3U8_CONTENT_TYPE, ignoreCase = true) + ) + } + }.onFailure { + it.printStackTrace() + it.message?.showToast() + } + } + + return START_NOT_STICKY + } + + private fun addTask( + downloadUrl: String, + filePath: String, + animeTitle: String, + animeEpisode: String, + isM3u8: Boolean = false + ) { + val id = Aria.download(this) + .load(downloadUrl) + .ignoreCheckPermissions() + .option(HttpOption().apply { + useServerFileName(true) + }) + .setFilePath( + if (isM3u8 && filePath.endsWith(".m3u8", ignoreCase = true)) { + filePath.substringBeforeLast(".m3u8") + } else { + filePath + } + ) + .apply { + if (isM3u8) { + val option = M3U8VodOption() + option.setVodTsUrlConvert(MyVodTsUrlConverter()) + option.setBandWidthUrlConverter(MyBandWidthUrlConverter()) + option.setUseDefConvert(false) + m3u8VodOption(option) + } + } + .create() + animeTitleEpisodeMap[downloadUrl] = animeTitle to animeEpisode + + notifyMap[downloadUrl]?.cancel() + notifyMap[downloadUrl] = AnimeDownloadNotification( + applicationContext, + taskId = id, + url = downloadUrl, + title = "$animeTitle - $animeEpisode" + ) + } + + override fun onCreate() { + super.onCreate() + Aria.download(this).register() + + stopTaskEvent.collectWithLifecycle(this) { stopTask(it) } + cancelTaskEvent.collectWithLifecycle(this) { cancelTask(it.first, it.second) } + resumeTaskEvent.collectWithLifecycle(this) { resumeTask(it) } + } + + override fun onDestroy() { + super.onDestroy() + Aria.download(this).unRegister() + } + + @Download.onPre + fun onPre(task: DownloadTask?) { + task ?: return + onTaskPreEvent.tryEmit(task) + } + + @Download.onTaskPre + fun onTaskPre(task: DownloadTask?) { + task ?: return + onTaskPreEvent.tryEmit(task) + } + + @Download.onTaskStart + fun onTaskStart(task: DownloadTask?) { + task ?: return + animeTitleEpisodeMap[task.downloadEntity.url]?.run { + getString( + R.string.start_download, + "$first - $second" + ).showToast() + } + onTaskStartEvent.tryEmit(task) + } + + @Download.onTaskStop + fun onTaskStop(task: DownloadTask?) { + task ?: return + onTaskStopEvent.tryEmit(task) + } + + @Download.onTaskCancel + fun onTaskCancel(task: DownloadTask?) { + task ?: return + onTaskCancelEvent.tryEmit(task) + notifyMap[task.downloadEntity?.url]?.cancel() + } + + @Download.onTaskFail + fun onTaskFail(task: DownloadTask?) { + task ?: return + notifyMap[task.downloadEntity?.url]?.cancel() + animeTitleEpisodeMap[task.downloadEntity?.url]?.run { + getString( + R.string.download_failed, + "$first - $second" + ).showToast() + } + onTaskFailEvent.tryEmit(task) + } + + @Download.onTaskComplete + fun onTaskComplete(task: DownloadTask?) { + task ?: return + notifyMap[task.downloadEntity?.url]?.cancel() + val p = animeTitleEpisodeMap[task.downloadEntity?.url] + if (p != null) { + runCatching { + coroutineScope.launch { + val file = File( + task.downloadEntity.m3U8Entity?.filePath ?: task.downloadEntity.filePath + ) + file.toMD5()?.let { + val entity = AnimeDownloadEntity(it, p.second, file.name) + getAppDataBase().animeDownloadDao().insertAnimeDownload(entity) + save2Xml((file.parent ?: p.first).substringAfterLast("/"), entity) + } + } + }.onFailure { + it.printStackTrace() + it.message?.showToast() + } + } else { + getString(R.string.anime_download_service_get_title_failed).showToast() + } + onTaskCompleteEvent.tryEmit(task) + } + + @Download.onTaskRunning + fun onTaskRunning(task: DownloadTask?) { + task ?: return + val m3U8Entity = task.downloadEntity?.m3U8Entity + if (m3U8Entity == null) { + val len: Long = task.fileSize + val p = (task.currentProgress * 100.0 / len).toInt() + notifyMap[task.downloadEntity.url]?.upload(p) + } else { + val p = ((m3U8Entity.peerIndex + 1) * 100.0 / m3U8Entity.peerNum).toInt() + notifyMap[task.downloadEntity.url]?.upload(p) + } + onTaskRunningEvent.tryEmit(task) + } + + @Download.onTaskResume + fun onTaskResume(task: DownloadTask?) { + task ?: return + onTaskResumeEvent.tryEmit(task) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/MyBandWidthUrlConverter.kt b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/MyBandWidthUrlConverter.kt new file mode 100644 index 00000000..1c85cc09 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/MyBandWidthUrlConverter.kt @@ -0,0 +1,39 @@ +package com.skyd.imomoe.util.download.downloadanime + +import com.arialyy.aria.core.processor.IBandWidthUrlConverter +import java.net.MalformedURLException +import java.net.URL + + +class MyBandWidthUrlConverter : IBandWidthUrlConverter { + override fun convert(m3u8Url: String, bandWidthUrl: String): String { + val index = m3u8Url.lastIndexOf("/") + val parentUrl = m3u8Url.substring(0, index + 1) + return if (bandWidthUrl.startsWith("http")) { + bandWidthUrl + } else { + val temp = parentUrl + bandWidthUrl + try { + val url = URL(temp) + val host = url.host + val indexHost = temp.indexOf(host, 0) + val hosts = temp.substring(0, indexHost + host.length) + val newTemp = temp.substring(hosts.length) + val strings = newTemp.split("/").toTypedArray() + val stringBuilder = StringBuilder(hosts) + for (str in strings) { + if (str.isNotEmpty()) { + if (!stringBuilder.toString().contains(str)) { + stringBuilder.append("/") + stringBuilder.append(str) + } + } + } + return stringBuilder.toString() + } catch (e: MalformedURLException) { + e.printStackTrace() + } + temp + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/MyVodTsUrlConverter.kt b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/MyVodTsUrlConverter.kt new file mode 100644 index 00000000..f4009acc --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/download/downloadanime/MyVodTsUrlConverter.kt @@ -0,0 +1,44 @@ +package com.skyd.imomoe.util.download.downloadanime + +import com.arialyy.aria.core.processor.IVodTsUrlConverter +import java.net.MalformedURLException +import java.net.URL + +/** + * m3u8获取ts地址 + */ +class MyVodTsUrlConverter : IVodTsUrlConverter { + override fun convert(m3u8Url: String, tsUrls: List): List { + val index = m3u8Url.lastIndexOf("/") + val parentUrl = m3u8Url.substring(0, index + 1) + val newUrls: MutableList = ArrayList() + for (urls in tsUrls) { + if (urls.startsWith("http")) { + newUrls.add(urls) + } else { + val temp = parentUrl + urls + try { + val url = URL(temp) + val host: String = url.host + val indexHost = temp.indexOf(host, 0) + val hosts = temp.substring(0, indexHost + host.length) + val newTemp = temp.substring(hosts.length) + val strings = newTemp.split("/").toTypedArray() + val stringBuilder = StringBuilder(hosts) + for (str in strings) { + if (str.isNotEmpty()) { + if (!stringBuilder.toString().contains(str)) { + stringBuilder.append("/") + stringBuilder.append(str) + } + } + } + newUrls.add(stringBuilder.toString()) + } catch (e: MalformedURLException) { + e.printStackTrace() + } + } + } + return newUrls + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadHelper.kt b/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadHelper.kt deleted file mode 100644 index 2295a29e..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadHelper.kt +++ /dev/null @@ -1,275 +0,0 @@ -package com.skyd.imomoe.util.downloadanime - -import android.content.Intent -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.hjq.permissions.OnPermissionCallback -import com.hjq.permissions.Permission -import com.hjq.permissions.XXPermissions -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.config.Const.DownloadAnime.Companion.animeFilePath -import com.skyd.imomoe.database.entity.AnimeDownloadEntity -import com.skyd.imomoe.util.Util.showToast -import org.w3c.dom.Element -import org.w3c.dom.Node -import org.w3c.dom.NodeList -import java.io.* -import javax.xml.parsers.DocumentBuilder -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.OutputKeys -import javax.xml.transform.TransformerFactory -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult - - -class AnimeDownloadHelper private constructor() { - - companion object { - val downloadHashMap: HashMap> = HashMap() - val instance: AnimeDownloadHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { - AnimeDownloadHelper() - } - - fun createXml(folderName: String) { - val builderFactory = DocumentBuilderFactory.newInstance() - // 从DOM工厂里获取DOM解析器 - val documentBuilder: DocumentBuilder - try { - documentBuilder = builderFactory.newDocumentBuilder() - val document = documentBuilder.newDocument() - //创建节点 - val data: Element = document.createElement("data") - //将节点添加到document中 - document.appendChild(data) - //保存 - val transformerFactory = TransformerFactory.newInstance() - val transformer = transformerFactory.newTransformer() - val domSource = DOMSource(document) - //设置编码类型 - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") - transformer.setOutputProperty(OutputKeys.INDENT, "yes") - val f = File(animeFilePath + folderName, "data.xml") - val result = StreamResult(FileOutputStream(f)) - //把DOM树转换为xml文件 - transformer.transform(domSource, result) - } catch (e: java.lang.Exception) { - e.printStackTrace() - } - } - - fun save2Xml( - folderName: String, entity: AnimeDownloadEntity, - animeFilePath: String = Const.DownloadAnime.animeFilePath - ) { - try { - val file = File(animeFilePath + folderName, "data.xml") - if (!file.exists()) { - createXml(folderName) - } - // 1.得到DOM解析器的工厂实例 - val builderFactory = DocumentBuilderFactory.newInstance() - // 2.从DOM工厂里获取DOM解析器 - val documentBuilder: DocumentBuilder - documentBuilder = builderFactory.newDocumentBuilder() - // 3.解析XML文档,得到DOM树 - val docs = documentBuilder.parse(file) - val animeList = docs.getElementsByTagName("anime") - var replaced = false - for (i in 0 until animeList.length) { - val anime: Node = animeList.item(i) - // 遍历anime的所有属性 - val nodeList: NodeList = anime.childNodes - var fileName: Node? = null - var title: Node? = null - var md5: Node? = null - for (j in 0 until nodeList.length) { - val node: Node = nodeList.item(j) - if (node.nodeType == Node.ELEMENT_NODE) { - when (node.nodeName) { - "fileName" -> fileName = node.firstChild - "md5" -> md5 = node.firstChild - "title" -> title = node.firstChild - } - } - } - if (entity.md5 == md5?.nodeValue) { - val newFileName = fileName?.cloneNode(true) - newFileName?.textContent = entity.fileName - val newTitle = title?.cloneNode(true) - newTitle?.textContent = entity.title - anime.replaceChild(newFileName, fileName) - anime.replaceChild(newTitle, title) - replaced = true - break - } - } - if (!replaced) { - //创建节点 - val animeElement = docs.createElement("anime") - val fileNameElement = docs.createElement("fileName") - fileNameElement.textContent = entity.fileName - val md5Element = docs.createElement("md5") - md5Element.textContent = entity.md5 - val titleElement = docs.createElement("title") - titleElement.textContent = entity.title - - //添加父子关系 - animeElement.appendChild(fileNameElement) - animeElement.appendChild(md5Element) - animeElement.appendChild(titleElement) - docs.documentElement.appendChild(animeElement) - } - - //保存xml文件 - val transformerFactory = TransformerFactory.newInstance() - val transformer = transformerFactory.newTransformer() - val domSource = DOMSource(docs) - //设置编码类型 - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") - transformer.setOutputProperty(OutputKeys.INDENT, "yes") - val result = StreamResult(FileOutputStream(file)) - //把DOM树转换为xml文件 - transformer.transform(domSource, result) - } catch (e: java.lang.Exception) { - e.printStackTrace() - } - } - - fun getAnimeFromXml( - folderName: String, - animeFilePath: String = Const.DownloadAnime.animeFilePath - ): MutableList { - val list: MutableList = ArrayList() - try { - // 1. 创建DocumentBuilderFactory对象 - val dFactory = DocumentBuilderFactory.newInstance() - // 2. 创建DocumentBuilder对象 - val dBuilder = dFactory.newDocumentBuilder() - // 3. 通过DocumentBuilder的parse方法解析xml - val file = File("$animeFilePath$folderName/data.xml") - if (!file.exists()) { - createXml(folderName) - } - val doc = dBuilder.parse(FileInputStream(file)) - // 4. 根据根节点名称获取所有的people节点 - val animeList = doc.getElementsByTagName("anime") - // 5. 遍历所有的people节点 - for (i in 0 until animeList.length) { - val anime: Node = animeList.item(i) - // 遍历anime的所有属性 - val entity = AnimeDownloadEntity("", "", "") - val nodeList: NodeList = anime.childNodes - for (j in 0 until nodeList.length) { - val node: Node = nodeList.item(j) - if (node.nodeType == Node.ELEMENT_NODE) { - when (node.nodeName) { - "fileName" -> entity.fileName = node.firstChild.nodeValue - "md5" -> entity.md5 = node.firstChild.nodeValue - "title" -> entity.title = node.firstChild.nodeValue - } - } - } - list.add(entity) - } - } catch (e: Exception) { - e.printStackTrace() - } - return list - } - - fun deleteAnimeFromXml( - folderName: String, entity: AnimeDownloadEntity, - animeFilePath: String = Const.DownloadAnime.animeFilePath - ) { - try { - // 1. 创建DocumentBuilderFactory对象 - val builderFactory = DocumentBuilderFactory.newInstance() - // 2. 创建DocumentBuilder对象 - val documentBuilder = builderFactory.newDocumentBuilder() - val file = File("$animeFilePath$folderName/data.xml") - if (!file.exists()) return - // 3. 通过DocumentBuilder的parse方法解析xml - val document = documentBuilder.parse(FileInputStream(file)) - // 4. 根据根节点名称获取所有的people节点 - val animeList = document.getElementsByTagName("anime") - // 5. 遍历所有的people节点 - for (i in 0 until animeList.length) { - val anime: Node = animeList.item(i) - if (!anime.hasChildNodes()) continue - // 遍历anime的所有属性 - val nodeList: NodeList = anime.childNodes - var pickTimes = 0 // 匹配次数 - for (j in 0 until nodeList.length) { - val node: Node = nodeList.item(j) - if (node.nodeType == Node.ELEMENT_NODE) { - if ((node.nodeName == "fileName" && node.firstChild.nodeValue == entity.fileName) || - (node.nodeName == "md5" && node.firstChild.nodeValue == entity.md5) || - (node.nodeName == "title" && node.firstChild.nodeValue == entity.title) - ) - pickTimes++ - } - } - if (pickTimes == 3) anime.parentNode.removeChild(anime) - } - // 关闭 - //保存xml文件 - val transformerFactory = TransformerFactory.newInstance() - val transformer = transformerFactory.newTransformer() - val domSource = DOMSource(document) - //设置编码类型 - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") - transformer.setOutputProperty(OutputKeys.INDENT, "yes") - val result = StreamResult(FileOutputStream(file)) - //把DOM树转换为xml文件 - transformer.transform(domSource, result) - } catch (e: IOException) { - e.printStackTrace() - } - } - } - - fun getDownloadStatus(key: String): LiveData? = downloadHashMap[key] - - fun downloadAnime( - activity: AppCompatActivity, - url: String, - key: String, - folderAndFileName: String - ) { - if (activity.isFinishing) { - App.context.getString(R.string.do_not_finish_the_page_when_parse_download_data) - .showToast() - return - } - XXPermissions.with(activity).permission(Permission.MANAGE_EXTERNAL_STORAGE).request( - object : OnPermissionCallback { - override fun onGranted(permissions: MutableList?, all: Boolean) { - if (downloadHashMap[key]?.value == AnimeDownloadStatus.DOWNLOADING) { - "已经在下载啦...".showToast() - return - } /*else if (downloadHashMap[key]?.value == AnimeDownloadStatus.COMPLETE) { - "已经下载好啦...".showToast() - return - }*/ - val status = MutableLiveData() - status.value = AnimeDownloadStatus.DOWNLOADING - downloadHashMap[key] = status - activity.startService( - Intent(activity, AnimeDownloadService::class.java) - .putExtra("url", url) - .putExtra("key", key) - .putExtra("folderAndFileName", folderAndFileName) - ) - } - - override fun onDenied(permissions: MutableList?, never: Boolean) { - super.onDenied(permissions, never) - "未获取存储权限,无法下载".showToast() - } - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadNotificationReceiver.kt b/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadNotificationReceiver.kt deleted file mode 100644 index 321fdfec..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadNotificationReceiver.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.skyd.imomoe.util.downloadanime - -import android.app.NotificationManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import com.skyd.imomoe.App -import com.skyd.imomoe.model.AppUpdateModel -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.downloadanime.AnimeDownloadHelper.Companion.downloadHashMap - - -class AnimeDownloadNotificationReceiver : BroadcastReceiver() { - companion object { - const val DOWNLOAD_ANIME_NOTIFICATION_ID = "DownloadAnimeNotificationID" - } - - override fun onReceive(context: Context?, intent: Intent?) { - val action = intent?.action ?: "" - - val notificationId = intent?.getIntExtra(DOWNLOAD_ANIME_NOTIFICATION_ID, -1) ?: -1 - val key = intent?.getStringExtra("key") ?: "" - - when (action) { - "notification_canceled" -> { - if (notificationId != -1) { - val notificationManager = - App.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(notificationId) - downloadHashMap[key]?.postValue(AnimeDownloadStatus.CANCEL) - "取消下载".showToast() - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadService.kt b/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadService.kt deleted file mode 100644 index 516244a8..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadService.kt +++ /dev/null @@ -1,251 +0,0 @@ -package com.skyd.imomoe.util.downloadanime - -import android.annotation.SuppressLint -import android.annotation.TargetApi -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.net.ConnectivityManager -import android.os.Build -import android.os.IBinder -import android.widget.Toast -import androidx.core.app.NotificationCompat -import com.liulishuo.filedownloader.BaseDownloadTask -import com.liulishuo.filedownloader.FileDownloadListener -import com.liulishuo.filedownloader.FileDownloader -import com.skyd.imomoe.R -import com.skyd.imomoe.config.Const.DownloadAnime.Companion.animeFilePath -import com.skyd.imomoe.database.entity.AnimeDownloadEntity -import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.util.MD5.getMD5 -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.downloadanime.AnimeDownloadHelper.Companion.downloadHashMap -import com.skyd.imomoe.util.downloadanime.AnimeDownloadHelper.Companion.save2Xml -import com.skyd.imomoe.util.downloadanime.AnimeDownloadNotificationReceiver.Companion.DOWNLOAD_ANIME_NOTIFICATION_ID -import com.skyd.imomoe.view.activity.MainActivity -import kotlinx.coroutines.* -import java.io.File -import java.io.Serializable - - -class AnimeDownloadService : Service() { - private val downloadServiceHashMap: HashMap = HashMap() - private val folderAndFileNameHashMap: HashMap = HashMap() - - private var notificationManager: NotificationManager? = null - private var totalNotificationId = 1002 - private val coroutineScope = CoroutineScope(Dispatchers.IO) - - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - if (intent.extras == null) { - coroutineScope.cancel() - "取消下载".showToast() - return START_NOT_STICKY - } - val url = intent.getStringExtra("url") ?: "" - val key = intent.getStringExtra("key") ?: "" - val folderAndFileName = intent.getStringExtra("folderAndFileName") ?: "" - folderAndFileNameHashMap[key] = folderAndFileName - downloadServiceHashMap[key] = AnimeDownloadServiceDataBean(url, totalNotificationId++) - if (isNetWorkAvailable()) { - createNotification(key) - downloadAnime(key, url, object : DownloadListener { - override fun complete(fileName: String) { - deleteNotification(key) - val animeDir = folderAndFileName.split("/").first() - val title = folderAndFileName.split("/").last() - val file = File("$animeFilePath$animeDir/$fileName") - if (file.exists()) { - downloadHashMap[key]?.postValue(AnimeDownloadStatus.COMPLETE) - GlobalScope.launch(Dispatchers.IO) { - getMD5(file)?.let { - val entity = AnimeDownloadEntity(it, title, fileName) - getAppDataBase().animeDownloadDao().insertAnimeDownload(entity) - save2Xml(animeDir, entity) - } - } - "${folderAndFileName}下载完成".showToast() - } else { - if (downloadHashMap[key]?.value != AnimeDownloadStatus.CANCEL) { - downloadHashMap[key]?.postValue( - AnimeDownloadStatus.ERROR - ) - } - "文件未找到,下载失败".showToast() - } - } - - override fun error() { - super.error() - deleteNotification(key) - downloadHashMap[key]?.postValue(AnimeDownloadStatus.ERROR) - } - }) - "开始下载${folderAndFileName}...".showToast() - } - return START_NOT_STICKY - } - - private fun deleteNotification(key: String) { - downloadServiceHashMap[key]?.let { - (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) - .cancel(it.notificationId) - } - } - - @SuppressLint("MissingPermission") - private fun isNetWorkAvailable(): Boolean { - val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetInfo = connectivityManager.activeNetworkInfo - return activeNetInfo != null && activeNetInfo.isConnected - } - - override fun onDestroy() { - super.onDestroy() - coroutineScope.cancel() - for (key in downloadServiceHashMap.keys) { - downloadHashMap[key]?.postValue(AnimeDownloadStatus.CANCEL) - val file = File(animeFilePath + key) - if (file.exists()) { - file.delete() - } - } - downloadServiceHashMap.clear() - } - - private fun createNotification(key: String) { - val folderAndFileName = folderAndFileNameHashMap[key] - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val importance = NotificationManager.IMPORTANCE_LOW - createNotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance) - downloadServiceHashMap[key]?.builder = NotificationCompat.Builder(this, CHANNEL_ID) - } else { - downloadServiceHashMap[key]?.builder = NotificationCompat.Builder(this) - } - val notificationId = downloadServiceHashMap[key]?.notificationId ?: -1 - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val stopIntent = Intent(this, AnimeDownloadNotificationReceiver::class.java) - stopIntent.action = "notification_canceled" - stopIntent.putExtra(DOWNLOAD_ANIME_NOTIFICATION_ID, notificationId) - stopIntent.putExtra("key", key) - - val clickIntent = Intent(Intent.ACTION_MAIN) - clickIntent.addCategory(Intent.CATEGORY_LAUNCHER) - clickIntent.setClass(this, MainActivity::class.java) - clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - clickIntent.putExtra(DOWNLOAD_ANIME_NOTIFICATION_ID, notificationId) - downloadServiceHashMap[key]?.builder?.setSmallIcon(R.mipmap.ic_launcher) - ?.setContentTitle("正在下载$folderAndFileName") - ?.setContentText("0%") - ?.setProgress(100, 0, false) - ?.setDeleteIntent( - PendingIntent.getBroadcast( - this, - //requestCode需要不一样才能接收每次的消息 - notificationId, - stopIntent, - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) PendingIntent.FLAG_CANCEL_CURRENT - else PendingIntent.FLAG_MUTABLE - ) - )?.setContentIntent( - PendingIntent.getActivity( - this, - 0, - clickIntent, - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) PendingIntent.FLAG_CANCEL_CURRENT - else PendingIntent.FLAG_MUTABLE - ) - )?.setAutoCancel(false)?.setTicker(folderAndFileName) - val notification = downloadServiceHashMap[key]?.builder?.build() - notificationManager?.notify(notificationId, notification) - } - - @TargetApi(Build.VERSION_CODES.O) - private fun createNotificationChannel(channelId: String, channelName: String, importance: Int) { - val channel = NotificationChannel(channelId, channelName, importance) - val notificationManager = getSystemService( - Context.NOTIFICATION_SERVICE - ) as NotificationManager - notificationManager.createNotificationChannel(channel) - } - - private fun updateNotification(key: String, progress: Int) { - val n = downloadServiceHashMap[key]?.builder ?: return - - n.setProgress(100, progress, false) - n.setContentText("$progress%") - - val manager = notificationManager - ?: getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val notification = n.build() - manager.notify(downloadServiceHashMap[key]?.notificationId ?: -1, notification) - if (notificationManager == null) { - notificationManager = manager - } - } - - override fun onBind(intent: Intent): IBinder? { - return null - } - - private fun downloadAnime( - key: String, - param: String, - listener: DownloadListener - ) { - val animeDir = folderAndFileNameHashMap[key]?.split("/")?.first() - downloadHashMap[key]?.postValue(AnimeDownloadStatus.DOWNLOADING) - FileDownloader.getImpl().create(param) - .setPath(animeFilePath + animeDir, true) - .setListener(object : FileDownloadListener() { - override fun pending(task: BaseDownloadTask?, soFarBytes: Int, totalBytes: Int) { - } - - override fun progress(task: BaseDownloadTask?, soFarBytes: Int, totalBytes: Int) { - if (downloadHashMap[key]?.value == AnimeDownloadStatus.CANCEL) { - task?.let { - FileDownloader.getImpl().pause(it.id) - } - } - val progress = (soFarBytes.toFloat() / totalBytes * 100).toInt() - onProgressUpdate(key, progress) - } - - override fun completed(task: BaseDownloadTask?) { - listener.complete(task?.filename ?: "") - } - - override fun paused(task: BaseDownloadTask?, soFarBytes: Int, totalBytes: Int) { - listener.error() - } - - override fun error(task: BaseDownloadTask?, e: Throwable?) { - downloadHashMap[key]?.postValue(AnimeDownloadStatus.ERROR) - e?.printStackTrace() - e?.message?.showToast(Toast.LENGTH_LONG) - listener.error() - } - - override fun warn(task: BaseDownloadTask?) { - } - }).start() - } - - private fun onProgressUpdate(key: String, values: Int) { - updateNotification(key, values) - } - - inner class AnimeDownloadServiceDataBean( - var url: String, - var notificationId: Int, - var builder: NotificationCompat.Builder? = null - ) : Serializable - - companion object { - const val CHANNEL_ID = "download_anime" - const val CHANNEL_NAME = "下载消息" - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadStatus.kt b/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadStatus.kt deleted file mode 100644 index 190fc738..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/downloadanime/AnimeDownloadStatus.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.skyd.imomoe.util.downloadanime - -enum class AnimeDownloadStatus { - DOWNLOADING, // 正在下载更新 - COMPLETE, // 下载完成 - CANCEL, // 取消下载 - ERROR // 下载失败 -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/downloadanime/DownloadListener.kt b/app/src/main/java/com/skyd/imomoe/util/downloadanime/DownloadListener.kt deleted file mode 100644 index c8d17991..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/downloadanime/DownloadListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.skyd.imomoe.util.downloadanime - -interface DownloadListener { - fun complete(fileName: String) - - fun error() { - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/html/SnifferVideo.kt b/app/src/main/java/com/skyd/imomoe/util/html/SnifferVideo.kt deleted file mode 100644 index f25df632..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/html/SnifferVideo.kt +++ /dev/null @@ -1,288 +0,0 @@ -package com.skyd.imomoe.util.html - -import android.app.Activity -import android.util.Log -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.callbacks.onCancel -import com.skyd.imomoe.R -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.html.source.DefaultUICallback -import com.skyd.imomoe.util.html.source.GettingUICallback -import com.skyd.imomoe.util.html.source.web.GettingUtil -import org.jsoup.Jsoup -import kotlin.jvm.Throws - - -object SnifferVideo { - const val PARSE_URL_ERROR = -100 - const val KEY = "key" - const val AC = "ac" - const val VIDEO_ID = "id" - const val SERVER_API = "api" - const val DANMU_URL = "danmuUrl" - const val REFEREER_URL = "referer" - private val sniffingUrlList: MutableList by lazy { ArrayList() } - private var serverApi: String = "https://yuan.cuan.la/barrage" - private var serverKey: String = "mao" - private var videoId: String = "" - private var referer: String = "http://tup.yhdm.so/" - - @Throws(IndexOutOfBoundsException::class) - private fun getSrc(html: String, type: Int = 0): String { - return when (type) { - 0 -> { - Jsoup.parse(html).select("body")[0].select("[class=player]")[0] - .getElementById("playbox").select("iframe")[0] - .attr("src") - } - 1 -> { - Jsoup.parse(html).select("body")[0] - .select("iframe")[0] - .attr("src") - } - else -> { - val script = Jsoup.parse(html).select("body")[0].select("script")[0].toString() - val line = script.split("\n") - line.forEach { - val l = it.trim() - when { - l.contains("\"ServerApi\": ") -> { - serverApi = l.replace("\"ServerApi\": \"", "") - .replace(Regex("\",.*"), "") - } - l.contains("\"ServerKey\": ") -> { - serverKey = l.replace("\"ServerKey\": \"", "") - .replace(Regex("\",.*"), "") - } - l.contains("\"id\": ") -> { - videoId = l.replace("\"id\": \"", "") - .replace(Regex("\",.*"), "") - } - } - } - Jsoup.parse(html) - .select("body")[0].getElementById("player") - .select("[class=leleplayer-video-wrap]") - .select("video").attr("src") - } - } - } - - private fun getPlayerHtmlSource( - activity: Activity, - url: String, - referer: String = "", - listener: GettingUICallback, - type: Int = 0 - ) { - if (type > 2) return - if (type == 1) this.referer = url - activity.runOnUiThread { - GettingUtil.instance.activity(activity).referer(referer) - .url(url) - .start(object : DefaultUICallback() { - override fun onGettingSuccess(webView: View?, html: String) { - val src = try { - getSrc(html, type) - } catch (e: IndexOutOfBoundsException) { - // 解析地址出现错误 - e.printStackTrace() - Log.e("getSrc IOOBException", html) - onGettingError(webView, url, PARSE_URL_ERROR) - return - } - - Log.i("getPlayerHtmlSource $type", html) - if (type == 2) { - if (src.startsWith("blob:")) - "HTML5 blob格式资源".showToast() - else - listener.onGettingSuccess(webView, src) - return - } - getPlayerHtmlSource(activity, src, referer, listener, type + 1) - } - - override fun onGettingStart(webView: View?, url: String?) { - listener.onGettingStart(webView, url) - } - - override fun onGettingFinish(webView: View?, url: String?) { - listener.onGettingFinish(webView, url) - } - - override fun onGettingError(webView: View?, url: String?, errorCode: Int) { - listener.onGettingError(webView, url, errorCode) - } - }) - } - } - - fun getQzzVideoUrl( - activity: Activity, - partUrl: String, - referer: String = "", - callback: (url: String, paramMap: HashMap) -> Unit - ) { - if (sniffingUrlList.contains(partUrl)) { - activity.getString(R.string.getting_complex_video).showToast() - } else { - sniffingUrlList.add(partUrl) - } - getPlayerHtmlSource(activity, - Api.MAIN_URL + partUrl, - Api.MAIN_URL + referer, - object : GettingUICallback { - private lateinit var waitingDialog: MaterialDialog - private var error = false - override fun onGettingFinish(webView: View?, url: String?) { - waitingDialog.message( - text = activity.getString( - R.string.get_complex_video_finished, - "\n${url}" - ) - ) - } - - override fun onGettingError(webView: View?, url: String?, errorCode: Int) { - activity.runOnUiThread { GettingUtil.instance.releaseWebView() } - if (error) return - error = true - when (errorCode) { - PARSE_URL_ERROR -> { - activity.getString(R.string.fail_to_parse_page_url).showToast() - } - else -> { - activity.getString( - R.string.getting_complex_video_failed, errorCode.toString() - ).showToast() - } - } - sniffingUrlList.remove(partUrl) - waitingDialog.dismiss() - } - - override fun onGettingStart(webView: View?, url: String?) { - if (!this::waitingDialog.isInitialized) { - waitingDialog = showWaitingToSniffingDialog(activity, partUrl) - } - waitingDialog.message( - text = activity.getString( - R.string.start_get_complex_video, - "\n${url}" - ) - ) - } - - override fun onGettingSuccess(webView: View?, html: String) { - activity.runOnUiThread { - GettingUtil.instance.releaseWebView() - } - sniffingUrlList.remove(partUrl) - waitingDialog.dismiss() - Log.i("getQzzVideoUrl", html) - HashMap().apply { - put(KEY, serverKey) - put(AC, "dm") - put(VIDEO_ID, videoId) - put(SERVER_API, serverApi) - put(REFEREER_URL, this@SnifferVideo.referer) - put( - DANMU_URL, - "https:$serverApi/barrage/api?ac=dm&key=$serverKey&id=$videoId" - ) - callback(html, this) - } -// showChooseVideoUrlDialog(activity, html, callback) - } - } - ) - } - - fun askSnifferDialog( - activity: Activity, - title: String, - message: String, - partUrl: String, - referer: String = "", - callback: (url: String, paramMap: HashMap) -> Unit - ) { - MaterialDialog(activity).show { - title(text = title) - message(text = message) - positiveButton(text = "嗅探") { - if (sniffingUrlList.contains(partUrl)) { - dismiss() - } - getQzzVideoUrl(activity, partUrl, referer, callback) - } - negativeButton(text = "取消") { - dismiss() - } - } - } - - private fun showWaitingToSniffingDialog(activity: Activity, partUrl: String): MaterialDialog { - return MaterialDialog(activity).show { - title(res = R.string.get_complex_videos) - message(text = activity.getString(R.string.getting_complex_video)) - cancelOnTouchOutside(false) - onCancel { - GettingUtil.instance.releaseWebView() - sniffingUrlList.remove(partUrl) - activity.getString(R.string.cancel_to_get_video_url).showToast() - } - } - } - -// private fun showChooseVideoUrlDialog( -// activity: Activity, -// html: String, -// callback: ( -// dialog: MaterialDialog, index: Int, -// text: CharSequence, videos: MutableList -// ) -> Unit -// ): MaterialDialog { -// val removedDuplicateVideosList = removeDuplicateUrls(html) -// val list: MutableList = removedDuplicateVideosList.run { -// val l: MutableList = ArrayList() -// forEach { -// l.add(it.url.let { url -> -// if (url.length > 70) "${url.substring(0, 70)}…" -// else url -// }) -// } -// l -// } -// return MaterialDialog(activity).show { -// title( -// text = activity.getString( -// R.string.please_choose_a_video_link, list.size.toString() -// ) -// ) -// listItems(items = list) { dialog, index, text -> -// callback(dialog, index, text, removedDuplicateVideosList) -//// videoPlayer.startPlay(videos[index].url, viewModel.animeEpisodeDataBean.title) -// dialog.dismiss() -// } -// negativeButton { dismiss() } -// } -// } - - -// private fun removeDuplicateUrls(list: List): MutableList { -// val videos: MutableList = ArrayList() -// val urls: MutableList = ArrayList() -// list.forEach { sniffingVideo -> -// if (!urls.contains(sniffingVideo.url) && -// !sniffingVideo.url.startsWith("mp4") -// ) { -// urls.add(sniffingVideo.url) -// videos.add(sniffingVideo) -// } -// } -// return videos -// } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/DefaultUICallback.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/DefaultUICallback.kt index 7534394c..db579e83 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/DefaultUICallback.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/DefaultUICallback.kt @@ -2,6 +2,7 @@ package com.skyd.imomoe.util.html.source import android.view.View +@Deprecated("use WebSource instead!") open class DefaultUICallback : GettingUICallback { override fun onGettingStart(webView: View?, url: String?) {} diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/GettingCallback.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/GettingCallback.kt index 017f41a1..5ca64302 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/GettingCallback.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/GettingCallback.kt @@ -2,6 +2,7 @@ package com.skyd.imomoe.util.html.source import android.view.View +@Deprecated("use WebSource instead!") interface GettingCallback { /** * 获取源代码成功,只在{GettingWebView}调用 diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/GettingUICallback.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/GettingUICallback.kt index a2377f05..6058d0f4 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/GettingUICallback.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/GettingUICallback.kt @@ -2,6 +2,7 @@ package com.skyd.imomoe.util.html.source import android.view.View +@Deprecated("use WebSource instead!") interface GettingUICallback : GettingCallback { /** * 开始获取源代码 diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/Util.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/Util.kt index ad1321ce..42cdc045 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/Util.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/Util.kt @@ -1,16 +1,16 @@ package com.skyd.imomoe.util.html.source import android.text.TextUtils -import android.util.Log import android.webkit.WebView +import com.skyd.imomoe.util.logE import java.net.HttpURLConnection import java.net.URL import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.* -import kotlin.jvm.Throws +@Deprecated("use WebSource instead!") object Util { const val HTMLFLAG = "GettingVideo" fun getContent(url: String): Array { @@ -34,10 +34,10 @@ object Util { objects[0] = urlConnection.contentLength objects[1] = urlConnection.contentType } - Log.e("Util", "getContent code = $responseCode") + logE("Util", "getContent code = $responseCode") } catch (e: Exception) { e.printStackTrace() - Log.e("Util", "getContent error = $e") + logE("Util", "getContent error = $e") } finally { urlConnection?.disconnect() } diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/WebSource.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/WebSource.kt new file mode 100644 index 00000000..f9ae9427 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/WebSource.kt @@ -0,0 +1,165 @@ +package com.skyd.imomoe.util.html.source + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.net.http.SslError +import android.webkit.* +import com.skyd.imomoe.appContext +import com.skyd.imomoe.ext.containIn +import kotlinx.coroutines.* +import org.apache.commons.text.StringEscapeUtils +import java.io.ByteArrayInputStream +import java.net.SocketTimeoutException +import kotlin.coroutines.resume + +@SuppressLint("SetJavaScriptEnabled") +object WebSource { + private val webView by lazy { + WebView(appContext).apply { + settings.apply { + // Sets whether the WebView should not load image resources from the network. + // Note that this method has no effect unless getLoadsImagesAutomatically() returns true. + blockNetworkImage = true + // Sets whether the WebView should load image resources. + // Note that this method controls loading of all images, + // including those embedded using the data URI scheme. + loadsImagesAutomatically = false + javaScriptEnabled = true + // Tells JavaScript to op en windows automatically. + // This applies to the JavaScript function window.open(). + javaScriptCanOpenWindowsAutomatically = false + // Sets whether cross-origin requests in the context of a file scheme URL + // should be allowed to access content from other file scheme URLs. + allowFileAccessFromFileURLs = true + // Sets whether cross-origin requests in the context of a file scheme URL + // should be allowed to access content from any origin. + allowUniversalAccessFromFileURLs = true + // Sets whether the DOM storage API is enabled. The default value is false. + domStorageEnabled = true + // The default value is false. + databaseEnabled = true + // Configures the WebView's behavior when a secure origin attempts to + // load a resource from an insecure origin. + mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + // Overrides the way the cache is used. + // The way the cache is used is based on the navigation type. + cacheMode = WebSettings.LOAD_DEFAULT + setSupportZoom(true) + // Enables or disables content URL access within WebView. + // Content URL access allows WebView to load content from + // a content provider installed in the system. The default is enabled. + allowContentAccess = true + // Sets whether the WebView whether supports multiple windows. + // If set to true, WebChromeClient#onCreateWindow must be implemented + // by the host application. The default is false. + setSupportMultipleWindows(true) + } + CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) + } + } + + suspend fun getWebSource( + url: String, + encoding: String = "UTF-8", + userAgent: String? = null, + timeout: Long = 10000L + ): String = withContext(Dispatchers.Main) { + suspendCancellableCoroutine { con -> + con.invokeOnCancellation { + webView.stopLoading() + webView.pauseTimers() + webView.loadUrl("about:blank") + } + webView.settings.apply { + userAgent?.let { userAgentString = it } + defaultTextEncodingName = encoding + } + webView.webViewClient = WebSourceWebViewClientImpl(timeout) { con.resume(it) } + webView.resumeTimers() + webView.loadUrl(url) + } + } + + class WebSourceWebViewClientImpl( + private val timeout: Long, + private val onResult: WebView.(String) -> Unit + ) : WebSourceWebViewClient() { + private var isFinished = false + + private fun finished(web: WebView) { + isFinished = true + web.evaluateJavascript("(function() { return document.documentElement.outerHTML })()") { + web.apply { + stopLoading() + pauseTimers() + loadUrl("about:blank") + onResult(StringEscapeUtils.unescapeEcmaScript(it)) + } + } + } + + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + view.postDelayed({ + if (!isFinished) throw SocketTimeoutException("timeout") + }, timeout) + } + + override fun onPageFinished(view: WebView, url: String?) { + super.onPageFinished(view, url) + if (!isFinished) finished(view) + } + + override fun onLoadResource(view: WebView, url: String) { + super.onLoadResource(view, url) + } + } + + abstract class WebSourceWebViewClient( + // 若路径中有这些后缀,则不加载 + private val preventRequestSuffix: Array = arrayOf( + ".css", + ".png", + ".jpg", + ".jpeg", + ".webp", + ".ico", + ".bmp", + ".gif", + ".tiff", + ".mp4", + ".ts", + ".mp3", + ".m4a", + ".flv", + ) + ) : WebViewClient() { + @SuppressLint("WebViewClientOnReceivedSslError") + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + handler?.proceed() + } + + // 只加载指定的文件资源 + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest? + ): WebResourceResponse? { + request ?: return super.shouldInterceptRequest(view, request) + // 只加载指定类型的数据 + return if (request.url?.path?.containIn(preventRequestSuffix) == true) { + // 不需要加载任何资源,因此不需要任何数据 + WebResourceResponse( + "text/html", + "UTF-8", + ByteArrayInputStream(ByteArray(0)) + ) + } else { + super.shouldInterceptRequest(view, request) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingUtil.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingUtil.kt index 44c5bfa1..8006f45b 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingUtil.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingUtil.kt @@ -12,9 +12,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.SoftReference -import kotlin.collections.HashMap import kotlin.random.Random +@Deprecated("use WebSource instead!") class GettingUtil private constructor() { private var mWebView: GettingWebView? = null private var mUrl: String = "" diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebChromeClient.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebChromeClient.kt index 2bf5bf52..e964d5ab 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebChromeClient.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebChromeClient.kt @@ -5,6 +5,7 @@ import android.webkit.WebChromeClient import android.webkit.WebView import com.skyd.imomoe.util.html.source.Util +@Deprecated("use WebSource instead!") class GettingWebChromeClient(private val mClient: GettingWebViewClient) : WebChromeClient() { override fun onJsConfirm(webView: WebView, s: String, s1: String, jsResult: JsResult): Boolean { if (s1.contains(Util.HTMLFLAG)) { diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebView.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebView.kt index 12527ff1..8c6019f8 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebView.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebView.kt @@ -5,11 +5,10 @@ import android.content.Context import android.os.Build import android.util.AttributeSet import android.webkit.* -import com.skyd.imomoe.config.Const import com.skyd.imomoe.util.html.source.GettingCallback -import kotlin.random.Random @SuppressLint("SetJavaScriptEnabled", "ObsoleteSdkInt") +@Deprecated("use WebSource instead!") class GettingWebView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, diff --git a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebViewClient.kt b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebViewClient.kt index 604f5abf..37aa53fb 100644 --- a/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebViewClient.kt +++ b/app/src/main/java/com/skyd/imomoe/util/html/source/web/GettingWebViewClient.kt @@ -4,7 +4,6 @@ import android.graphics.Bitmap import android.net.http.SslError import android.os.Handler import android.os.Looper -import android.util.Log import android.view.View import android.webkit.SslErrorHandler import android.webkit.WebView @@ -12,7 +11,10 @@ import android.webkit.WebViewClient import com.skyd.imomoe.util.html.source.GettingCallback import com.skyd.imomoe.util.html.source.GettingUICallback import com.skyd.imomoe.util.html.source.Util +import com.skyd.imomoe.util.logE +import com.skyd.imomoe.util.logI +@Deprecated("use WebSource instead!") class GettingWebViewClient( private val mWebView: WebView?, private val mURL: String, @@ -50,7 +52,7 @@ class GettingWebViewClient( override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { if (mLastEndTime - mLastStartTime <= 500 || !isCompleteLoader) { // 基本上是302 重定向才会走这段逻辑 - Log.e("GettingUtil", "onStart( 302 ) --> $url") + logE("GettingUtil", "onStart( 302 ) --> $url") mFinished?.let { mH.removeCallbacks(it) } return } @@ -58,7 +60,7 @@ class GettingWebViewClient( mH.postDelayed( TimeOutRunnable(view, url, TYPE_CONN).also { mConnTimeout = it }, mConnTimeOut ) - Log.e("GettingWebViewClient", "onStart(onPageStarted) --> $url") + logE("GettingWebViewClient", "onStart(onPageStarted) --> $url") onGettingStart(view, url) } @@ -67,32 +69,13 @@ class GettingWebViewClient( mH.postDelayed(FinishedRunnable(view, url).also { mFinished = it }, mFinishedTimeOut) } - // @Override - // public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - // try { - //// LogUtil.e("GettingUtil", "shouldInterceptRequest(URL) --> " + url); - // if(url.lastIndexOf(".") < url.length() - 5){ - // Object[] content = Util.getContent(url); - // String s = content[1].toString(); - // if(s.toLowerCase().contains("video") || s.toLowerCase().contains("mpegurl")){ - // mVideos.add(new GettingVideo(url,"m3u8",(int) content[0],"m3u8")); - // } - // }else if (mFilter != null) { - // GettingVideo video = mFilter.onFilter(view, url); - // if (video != null) mVideos.add(video); - // } - // } catch (Throwable e) { - // e.printStackTrace(); - // } - // return null; - // } override fun onReceivedError( view: WebView, errorCode: Int, description: String, failingUrl: String ) { - Log.e("GettingWebViewClient", "onReceivedError(ReceivedError) --> $failingUrl") + logE("GettingWebViewClient", "onReceivedError(ReceivedError) --> $failingUrl") onGettingError(view, failingUrl, RECEIVED_ERROR) onGettingFinish(view, failingUrl) } @@ -154,7 +137,7 @@ class GettingWebViewClient( if (mConnTimeout == null) return mH.removeCallbacks(mConnTimeout!!) mConnTimeout = null - Log.i("GettingWebViewClient", "一次网页加载结束 --> $url") + logI("GettingWebViewClient", "一次网页加载结束 --> $url") onGettingFinish(view, url) Util.getHtmlSource(view) } @@ -170,7 +153,7 @@ class GettingWebViewClient( override fun run() { //加载网页超时了 if (type == TYPE_CONN) { - Log.e( + logE( "GettingWebViewClient", "ConnTimeOutRunnable( postDelayed 【alert ,confirm】 ) --> $url" ) @@ -182,7 +165,7 @@ class GettingWebViewClient( // mH.postDelayed(new ParserHtmlRunnable(view, "alert"), 5000); // mH.postDelayed(mJSRunnable = new ParserHtmlRunnable(view, "confirm"), 8000); } else if (type == TYPE_READ) { - Log.e("GettingWebViewClient", "ReadTimeOutRunnable(SUCCESS) --> $url") + logE("GettingWebViewClient", "ReadTimeOutRunnable(SUCCESS) --> $url") onGettingFinish(view, url) } } diff --git a/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadNotification.kt b/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadNotification.kt new file mode 100644 index 00000000..676a82dc --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadNotification.kt @@ -0,0 +1,91 @@ +package com.skyd.imomoe.util.market + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import com.skyd.imomoe.R + + +class DataSourceDownloadNotification( + val context: Context, + val taskId: Long, + val url: String, + var title: String +) { + companion object { + var MAX_NOTIFY_ID = 1000 + + const val CHANNEL_ID = "download_anime" + const val CHANNEL_NAME = "download_message" + } + + private val manager: NotificationManager by lazy { + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + private val builder: NotificationCompat.Builder by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel() + NotificationCompat.Builder(context, CHANNEL_ID) + } else { + NotificationCompat.Builder(context) + } + } + private val notifyId = ++MAX_NOTIFY_ID + + init { + val intent = Intent(context, DataSourceDownloadReceiver::class.java).apply { + action = DataSourceDownloadReceiver.CANCEL_ACTION + putExtra(DataSourceDownloadReceiver.NOTIFY_ID, notifyId) + putExtra(DataSourceDownloadReceiver.TASK_ID, taskId) + putExtra(DataSourceDownloadReceiver.TASK_URL, url) + } + val pendingIntent: PendingIntent = PendingIntent.getBroadcast( + context, + notifyId, + intent, + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) PendingIntent.FLAG_CANCEL_CURRENT + else PendingIntent.FLAG_MUTABLE + ) + builder + .setContentTitle(context.getString(R.string.download_notification_title, title)) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentText("0%") + .setProgress(100, 0, false) + .setAutoCancel(false) + .addAction( + R.drawable.ic_close_24, + context.getString(R.string.cancel), + pendingIntent + ) + manager.notify(notifyId, builder.build().apply { + flags = flags or Notification.FLAG_NO_CLEAR + }) + } + + fun upload(progress: Int) { + builder.setProgress(100, progress, false) + .setContentText("$progress%") + manager.notify(notifyId, builder.build().apply { + flags = flags or Notification.FLAG_NO_CLEAR + }) + } + + fun cancel() { + manager.cancel(notifyId) + } + + @TargetApi(Build.VERSION_CODES.O) + private fun createNotificationChannel() { + val channel = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW) + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadReceiver.kt b/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadReceiver.kt new file mode 100644 index 00000000..3d755a33 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadReceiver.kt @@ -0,0 +1,32 @@ +package com.skyd.imomoe.util.market + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class DataSourceDownloadReceiver : BroadcastReceiver() { + companion object { + const val NOTIFY_ID = "notifyID" + const val CANCEL_ACTION = "cancelAction" + const val TASK_ID = "taskId" + const val TASK_URL = "taskUrl" + } + + override fun onReceive(context: Context, intent: Intent?) { + val action = intent?.action.orEmpty() + + val notificationId = intent?.getIntExtra(NOTIFY_ID, -1) ?: return + if (notificationId == -1) return + + when (action) { + CANCEL_ACTION -> { + // 在Service里面统一cancel + val taskId = intent.getLongExtra(TASK_ID, -1) + val taskUrl = intent.getStringExtra(TASK_URL).orEmpty() + if (taskId != -1L) { + DataSourceDownloadService.cancelTaskEvent.tryEmit(taskId to taskUrl) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadService.kt b/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadService.kt new file mode 100644 index 00000000..c831ffcc --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/util/market/DataSourceDownloadService.kt @@ -0,0 +1,233 @@ +package com.skyd.imomoe.util.market + +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import androidx.lifecycle.LifecycleService +import com.arialyy.annotations.Download +import com.arialyy.aria.core.Aria +import com.arialyy.aria.core.common.HttpOption +import com.arialyy.aria.core.download.DownloadEntity +import com.arialyy.aria.core.task.DownloadTask +import com.skyd.imomoe.R +import com.skyd.imomoe.config.Api +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow + + +class DataSourceDownloadService : LifecycleService() { + companion object { + val stopTaskEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + val cancelTaskEvent: MutableSharedFlow> = + MutableSharedFlow(extraBufferCapacity = 1) + val resumeTaskEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + + const val DOWNLOAD_URL_KEY = "downloadUrl" + const val DATA_SOURCE_TITLE = "dataSourceTitle" + } + + private val coroutineScope by lazy(LazyThreadSafetyMode.NONE) { + CoroutineScope(Dispatchers.IO) + } + + private val onTaskPreEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskStartEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskCompleteEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskRunningEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskStopEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskCancelEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskFailEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private val onTaskResumeEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + + private val notifyMap = hashMapOf() + private val dataSourceTitleMap = hashMapOf() + + inner class DataSourceDownloadBinder : Binder() { + val service: DataSourceDownloadService + get() = this@DataSourceDownloadService + val dataSourceTitleMap: HashMap + get() = this@DataSourceDownloadService.dataSourceTitleMap + val notCompleteList: List + get() = Aria.download(this).allNotCompleteTask.orEmpty() + + val onTaskPreEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskPreEvent + val onTaskStartEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskStartEvent + val onTaskCompleteEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskCompleteEvent + val onTaskRunningEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskRunningEvent + val onTaskStopEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskStopEvent + val onTaskResumeEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskResumeEvent + val onTaskCancelEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskCancelEvent + val onTaskFailEvent: MutableSharedFlow + get() = this@DataSourceDownloadService.onTaskFailEvent + } + + private val dataSourceDownloadBinder: Binder = DataSourceDownloadBinder() + + fun stopTask(id: Long) { + if (id == -1L) return + Aria.download(this).load(id).stop() + } + + fun resumeTask(id: Long) { + if (id == -1L) return + Aria.download(this).load(id).resume() + } + + fun cancelTask(id: Long, url: String?) { + if (id == -1L) return + Aria.download(this).load(id).cancel() + if (url.isNullOrEmpty()) notifyMap.remove(url) + } + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return dataSourceDownloadBinder + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + intent ?: return START_NOT_STICKY + + val downloadUrl = intent.getStringExtra(DOWNLOAD_URL_KEY).orEmpty().let { + if (it.startsWith("/")) Api.DATA_SOURCE_PREFIX + it + else it + } + val dataSourceTitle = intent.getStringExtra(DATA_SOURCE_TITLE).orEmpty() + + addTask( + downloadUrl = downloadUrl, + filePath = "${DataSourceManager.getJarDirectory()}/$dataSourceTitle.ads", + dataSourceTitle = dataSourceTitle, + ) + + return START_NOT_STICKY + } + + private fun addTask( + downloadUrl: String, + filePath: String, + dataSourceTitle: String + ) { + val id = Aria.download(this) + .load(downloadUrl) + .ignoreCheckPermissions() + .option(HttpOption().apply { + useServerFileName(true) + }) + .setFilePath(filePath) + .ignoreFilePathOccupy() // 强制下载 + .create() + dataSourceTitleMap[downloadUrl] = dataSourceTitle + + notifyMap[downloadUrl]?.cancel() + notifyMap[downloadUrl] = DataSourceDownloadNotification( + applicationContext, + taskId = id, + url = downloadUrl, + title = dataSourceTitle + ) + } + + override fun onCreate() { + super.onCreate() + Aria.download(this).register() + + stopTaskEvent.collectWithLifecycle(this) { stopTask(it) } + cancelTaskEvent.collectWithLifecycle(this) { cancelTask(it.first, it.second) } + resumeTaskEvent.collectWithLifecycle(this) { resumeTask(it) } + } + + override fun onDestroy() { + super.onDestroy() + Aria.download(this).unRegister() + } + + @Download.onPre + fun onPre(task: DownloadTask?) { + task ?: return + onTaskPreEvent.tryEmit(task) + } + + @Download.onTaskPre + fun onTaskPre(task: DownloadTask?) { + task ?: return + onTaskPreEvent.tryEmit(task) + } + + @Download.onTaskStart + fun onTaskStart(task: DownloadTask?) { + task ?: return + dataSourceTitleMap[task.downloadEntity.url]?.also { + getString(R.string.start_download, it).showToast() + } + onTaskStartEvent.tryEmit(task) + } + + @Download.onTaskStop + fun onTaskStop(task: DownloadTask?) { + task ?: return + onTaskStopEvent.tryEmit(task) + } + + @Download.onTaskCancel + fun onTaskCancel(task: DownloadTask?) { + task ?: return + onTaskCancelEvent.tryEmit(task) + notifyMap[task.downloadEntity?.url]?.cancel() + } + + @Download.onTaskFail + fun onTaskFail(task: DownloadTask?) { + task ?: return + notifyMap[task.downloadEntity?.url]?.cancel() + dataSourceTitleMap[task.downloadEntity?.url]?.also { + getString(R.string.download_failed, it).showToast() + } + onTaskFailEvent.tryEmit(task) + } + + @Download.onTaskComplete + fun onTaskComplete(task: DownloadTask?) { + task ?: return + notifyMap[task.downloadEntity?.url]?.cancel() + onTaskCompleteEvent.tryEmit(task) + } + + @Download.onTaskRunning + fun onTaskRunning(task: DownloadTask?) { + task ?: return + val len: Long = task.fileSize + val p = (task.currentProgress * 100.0 / len).toInt() + notifyMap[task.downloadEntity.url]?.upload(p) + + onTaskRunningEvent.tryEmit(task) + } + + @Download.onTaskResume + fun onTaskResume(task: DownloadTask?) { + task ?: return + onTaskResumeEvent.tryEmit(task) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/util/skin/SkinUtil.kt b/app/src/main/java/com/skyd/imomoe/util/skin/SkinUtil.kt deleted file mode 100644 index 03cb6930..00000000 --- a/app/src/main/java/com/skyd/imomoe/util/skin/SkinUtil.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.skyd.imomoe.util.skin - - -object SkinUtil { - fun initCustomAttrIds() { -// SkinManager.addCustomAttrIds(R.attr.menu, object : SkinManager.CustomSetSkinTagListener { -// override fun setSkinTag(attrId: Int, resId: Int): Pair? { -// if (resId != -1) return Pair( -// MenuAttr.TAG, -// MenuAttr().apply { attrResourceRefId = resId }) -// return null -// } -// }) - } -} diff --git a/app/src/main/java/com/skyd/imomoe/util/update/AppUpdateHelper.kt b/app/src/main/java/com/skyd/imomoe/util/update/AppUpdateHelper.kt index 75e13416..82d026df 100644 --- a/app/src/main/java/com/skyd/imomoe/util/update/AppUpdateHelper.kt +++ b/app/src/main/java/com/skyd/imomoe/util/update/AppUpdateHelper.kt @@ -1,13 +1,13 @@ package com.skyd.imomoe.util.update -import android.text.Html -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.LiveData -import com.afollestad.materialdialogs.MaterialDialog +import android.app.Activity +import com.skyd.imomoe.R +import com.skyd.imomoe.ext.formatSize +import com.skyd.imomoe.ext.showMessageDialog +import com.skyd.imomoe.ext.toHtml import com.skyd.imomoe.model.AppUpdateModel -import com.skyd.imomoe.util.Util.getFormatSize import com.skyd.imomoe.util.Util.openBrowser -import java.lang.Exception +import kotlinx.coroutines.flow.StateFlow import java.text.SimpleDateFormat import java.util.* @@ -22,27 +22,21 @@ class AppUpdateHelper private constructor() { } } - fun getUpdateServer(): LiveData = AppUpdateModel.mldUpdateServer - - fun setUpdateServer(value: Int) { - AppUpdateModel.updateServer = value - } - - fun getUpdateStatus(): LiveData = AppUpdateModel.status + fun getUpdateStatus(): StateFlow = AppUpdateModel.status fun checkUpdate() { AppUpdateModel.checkUpdate() } - fun noticeUpdate(activity: AppCompatActivity) { + fun noticeUpdate(activity: Activity) { listOf> { checkUpdate() } val updateBean = AppUpdateModel.updateBean ?: return - MaterialDialog(activity).show { - title(text = "发现新版本:${updateBean.name}") - StringBuffer().apply { + activity.showMessageDialog( + title = "发现新版本\n版本名:${updateBean.name}\n版本代号:${updateBean.tagName}", + message = StringBuffer().run { val size = updateBean.assets[0].size if (size > 0) { - append("

大小:${getFormatSize(size.toDouble())}
") + append("

大小:${size.toDouble().formatSize()}
") } val updatedAt = updateBean.assets[0].updatedAt if (!updatedAt.isNullOrBlank()) { @@ -66,20 +60,18 @@ class AppUpdateHelper private constructor() { append("下载次数:${downloadCount}次

") } append(updateBean.body) - this@show.message( - text = Html.fromHtml(this.toString()) - ) - } - positiveButton(text = "下载更新") { - openBrowser( - AppUpdateModel.updateBean?.assets?.get(0)?.browserDownloadUrl - ?: return@positiveButton - ) - } - negativeButton(text = "取消") { - dismiss() + this.toString().toHtml() + }, + onNegative = { dialog, _ -> + dialog.dismiss() AppUpdateModel.status.value = AppUpdateStatus.LATER - } + }, + positiveText = activity.getString(R.string.download_update) + ) { _, _ -> + openBrowser( + AppUpdateModel.updateBean?.assets?.get(0)?.browserDownloadUrl + ?: return@showMessageDialog + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/AboutActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/AboutActivity.kt index e5afe8b2..a45c3d2b 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/AboutActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/AboutActivity.kt @@ -1,111 +1,392 @@ package com.skyd.imomoe.view.activity import android.annotation.SuppressLint -import android.content.Intent import android.os.Bundle -import android.text.Html -import com.afollestad.materialdialogs.MaterialDialog +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.material3.* +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.skyd.imomoe.BuildConfig import com.skyd.imomoe.R import com.skyd.imomoe.config.Api import com.skyd.imomoe.config.Const -import com.skyd.imomoe.databinding.ActivityAboutBinding +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.toHtml import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.route.Router.buildRouteUri +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.route.processor.StartActivityProcessor import com.skyd.imomoe.util.Util -import com.skyd.imomoe.util.Util.getAppVersionName import com.skyd.imomoe.util.Util.openBrowser -import com.skyd.imomoe.util.visible +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.AnimeTopBarStyle +import com.skyd.imomoe.view.component.compose.BackIcon +import com.skyd.imomoe.view.component.compose.MessageDialog +import java.net.URL import java.util.* -class AboutActivity : BaseActivity() { +class AboutActivity : BaseComposeActivity() { @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContentBase { + AboutScreen() + } + } +} - mBinding.run { - llAboutActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } - llAboutActivityToolbar.tvToolbar1Title.text = getString(R.string.about) - llAboutActivityToolbar.ivToolbar1Button1.visible() - llAboutActivityToolbar.ivToolbar1Button1.setImageResource(R.drawable.ic_info_white_24) - llAboutActivityToolbar.ivToolbar1Button1.setOnClickListener { - MaterialDialog(this@AboutActivity).show { - title(res = R.string.attention) - message(text = "本软件免费开源,严禁商用,支持Android 5.0+!仅在Github仓库长期发布!\n不介意的话可以给我的Github仓库点个Star") - positiveButton(text = "去点Star") { openBrowser(Const.Common.GITHUB_URL) } - negativeButton(res = R.string.cancel) { dismiss() } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +private fun AboutScreen() { + val context = LocalContext.current + val isLand = calculateWindowSizeClass(LocalContext.current.activity).run { + widthSizeClass != WindowWidthSizeClass.Compact + } + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + decayAnimationSpec = rememberSplineBasedDecay(), + state = rememberTopAppBarScrollState() + ) + var showAboutInfoDialog by remember { mutableStateOf(false) } + Scaffold( + topBar = { + AnimeTopBar( + style = if (isLand) AnimeTopBarStyle.Small else AnimeTopBarStyle.Large, + title = { + Text(text = stringResource(R.string.about)) + }, + scrollBehavior = scrollBehavior, + navigationIcon = { + BackIcon(onClick = { context.activity.finish() }) + }, + actions = { + IconButton(onClick = { + showAboutInfoDialog = true + }) { + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = stringResource(id = R.string.about_activity_menu_info) + ) + } + } + ) + } + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it) + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = WindowInsets.navigationBars.asPaddingValues() + ) { + if (isLand) { + item { + Row(modifier = Modifier.wrapContentSize()) { + AppIconArea(modifier = Modifier.weight(1f)) + AboutScreenList(modifier = Modifier.weight(1f)) + } + } + } else { + item { + AppIconArea() + } + item { + AboutScreenList() } } + } + if (showAboutInfoDialog) { + MessageDialog( + icon = R.drawable.ic_info_24, + title = stringResource(id = R.string.attention), + message = "本软件免费开源,严禁商用,支持Android 5.0+!仅在GitHub仓库长期发布!\n不介意的话可以给我的GitHub仓库点个Star", + positiveText = "去点Star", + onPositive = { + showAboutInfoDialog = false + openBrowser(Const.Common.GITHUB_URL) + }, + onNegative = { showAboutInfoDialog = false }, + onDismissRequest = { + showAboutInfoDialog = false + } + ) + } + } +} + +/** + * 显示图标、应用名和版本的上半部分 + */ +@Composable +private fun AppIconArea(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = 50.dp, bottom = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .width(100.dp) + .height(100.dp) + ) { + Image( + modifier = Modifier.size(100.dp), + painter = painterResource(id = R.drawable.ic_akarin), + contentDescription = null + ) val c: Calendar = Calendar.getInstance() val month = c.get(Calendar.MONTH) val day = c.get(Calendar.DAY_OF_MONTH) - if (month == Calendar.DECEMBER && day == 25) { // 圣诞节彩蛋 - ivAboutActivityIconEgg.visible() - ivAboutActivityIconEgg.setImageResource(R.drawable.ic_christmas_hat) + if (month == Calendar.DECEMBER && (day > 21 || day < 29)) { // 圣诞节彩蛋 + Image( + modifier = Modifier + .size(50.dp) + .align(Alignment.TopEnd), + painter = painterResource(id = R.drawable.ic_christmas_hat), + contentDescription = null + ) } + } + Text( + modifier = Modifier.padding(top = 15.dp), + text = stringResource(id = R.string.app_name), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium + ) + Text( + modifier = Modifier.padding( + top = 2.dp, start = 16.dp, end = 16.dp + ), + textAlign = TextAlign.Center, + text = stringResource(R.string.app_version_name, Util.getAppVersionName()) + "\n" + + stringResource(R.string.app_version_code, Util.getAppVersionCode()) + "\n" + + stringResource( + R.string.data_source_interface_version, + com.skyd.imomoe.model.interfaces.interfaceVersion + ) + "\n" + + stringResource(R.string.build_time, BuildConfig.BUILD_TIME), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge + ) + } +} - tvAboutActivityVersion.text = getAppVersionName() +/** + * 下方的列表部分 + */ +@Composable +private fun AboutScreenList(modifier: Modifier = Modifier) { + val context = LocalContext.current - rlAboutActivityImomoe.setOnClickListener { - openBrowser(Api.MAIN_URL) - } + var showDataSourceWebsiteDialog by remember { mutableStateOf(false) } + var showDataSourceInfoDialog by remember { mutableStateOf(false) } + var showUserNoticeDialog by remember { mutableStateOf(false) } + var showThanksDialog by remember { mutableStateOf(false) } + var showTestDeviceDialog by remember { mutableStateOf(false) } - ivAboutActivityCustomDataSourceAbout.setOnClickListener { - MaterialDialog(this@AboutActivity).show { - title(res = R.string.data_source_info) - message( - text = (DataSourceManager.getConst() - ?: com.skyd.imomoe.model.impls.Const()).run { - "${ - getString( - R.string.data_source_jar_version_name, - versionName() - ) - }\n${ - getString( - R.string.data_source_jar_version_code, - versionCode().toString() - ) - }\n${about()}" - } - ) - positiveButton(res = R.string.ok) { dismiss() } - } + Column(modifier = modifier) { + AboutScreenListItem( + title = stringResource(id = R.string.data_source_url), + showIcon = true, + onIconClick = { + showDataSourceInfoDialog = true + }, + onClick = { + showDataSourceWebsiteDialog = true } - - rlAboutActivityGithub.setOnClickListener { + ) + AboutScreenListItem( + title = stringResource(id = R.string.github), + onClick = { openBrowser(Const.Common.GITHUB_URL) } + ) + AboutScreenListItem( + title = stringResource(id = R.string.about_activity_data_source_repo), + onClick = { + openBrowser(Const.Common.GITHUB_DATA_SOURCE_URL) + } + ) + AboutScreenListItem( + title = stringResource(id = R.string.open_source_licenses), + onClick = { + StartActivityProcessor.route.buildRouteUri { + appendQueryParameter("cls", LicenseActivity::class.qualifiedName) + }.route(context.activity) + } + ) + AboutScreenListItem( + title = stringResource(id = R.string.user_notice), + onClick = { + showUserNoticeDialog = true + } + ) + AboutScreenListItem( + title = stringResource(id = R.string.test_device), + onClick = { + showTestDeviceDialog = true + } + ) + AboutScreenListItem( + title = stringResource(id = R.string.about_activity_thanks), + onClick = { + showThanksDialog = true + } + ) - rlAboutActivityLicense.setOnClickListener { - startActivity(Intent(this@AboutActivity, LicenseActivity::class.java)) + if (showDataSourceWebsiteDialog) { + var warningString: String = stringResource(R.string.jump_to_data_source_website_warning) + if (URL(Api.MAIN_URL).protocol == "http") { + warningString = stringResource(R.string.jump_to_browser_http_warning) + + "\n" + warningString } + MessageDialog( + icon = R.drawable.ic_warning_2_24, + message = warningString, + positiveText = stringResource(R.string.still_to_visit), + onPositive = { + openBrowser(Api.MAIN_URL) + showDataSourceWebsiteDialog = false + }, + onNegative = { showDataSourceWebsiteDialog = false }, + onDismissRequest = { + showDataSourceWebsiteDialog = false + } + ) + } - rlAboutActivityUserNotice.setOnClickListener { - MaterialDialog(this@AboutActivity).show { - title(res = R.string.user_notice) - message(text = Html.fromHtml(Util.getUserNoticeContent())) - cancelable(false) - positiveButton(res = R.string.ok) { - Util.setReadUserNoticeVersion(Const.Common.USER_NOTICE_VERSION) - } + if (showDataSourceInfoDialog) { + MessageDialog( + icon = R.drawable.ic_info_24, + title = stringResource(id = R.string.data_source_info), + message = (DataSourceManager.getConst() + ?: com.skyd.imomoe.model.impls.Const()).run { + "${ + stringResource( + R.string.data_source_jar_version_name, + versionName().toString() + ) + }\n${ + stringResource( + R.string.data_source_jar_version_code, + versionCode().toString() + ) + }\n${ + stringResource( + R.string.data_source_interface_version, + DataSourceManager.customDataSourceInfo?.get("interfaceVersion") + .orEmpty() + ) + }\n${about()}" + }, + onPositive = { showDataSourceInfoDialog = false }, + onDismissRequest = { + showDataSourceInfoDialog = false } - } + ) + } - rlAboutActivityTestDevice.setOnClickListener { - MaterialDialog(this@AboutActivity).show { - title(res = R.string.test_device) - message( - text = "Physical Device: \nAndroid 10\n\n" + - "Virtual Machine: \nPixel Android 5\n" + - "雷电模拟器4.0.63 Android 7.1.2\n" + - "Pixel 2 Android 8\n" + - "Pixel 3 Android 9\n" - ) - positiveButton(res = R.string.ok) { dismiss() } + if (showUserNoticeDialog) { + MessageDialog( + icon = R.drawable.ic_info_24, + title = stringResource(id = R.string.user_notice), + message = Util.getUserNoticeContent().toHtml().toString(), + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ), + onPositive = { + Util.setReadUserNoticeVersion(Const.Common.USER_NOTICE_VERSION) + showUserNoticeDialog = false + }, + onDismissRequest = { + showUserNoticeDialog = false } - } + ) + } + + if (showTestDeviceDialog) { + MessageDialog( + icon = R.drawable.ic_info_24, + title = stringResource(id = R.string.test_device), + message = "Physical Device: \nAndroid 10", + onPositive = { showTestDeviceDialog = false }, + onDismissRequest = { + showTestDeviceDialog = false + } + ) + } + + if (showThanksDialog) { + MessageDialog( + icon = R.drawable.ic_info_24, + title = stringResource(id = R.string.about_activity_thanks), + message = "MaybeQHL提供弹幕服务器:\nhttps://github.com/MaybeQHL/my_danmu_pub", + positiveText = stringResource(R.string.about_activity_open_danmaku_server_page), + onPositive = { + openBrowser("https://github.com/MaybeQHL/my_danmu_pub") + showThanksDialog = false + }, + onDismissRequest = { + showThanksDialog = false + } + ) } } +} - override fun getBinding(): ActivityAboutBinding = ActivityAboutBinding.inflate(layoutInflater) -} \ No newline at end of file +/** + * 下方列表部分的子项 + */ +@Composable +private fun AboutScreenListItem( + title: String, + showIcon: Boolean = false, + onIconClick: () -> Unit = {}, + onClick: () -> Unit = {} +) { + Row( + modifier = Modifier + .clickable { onClick() } + .padding(start = 25.dp, end = 16.dp) + .height(50.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = title, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium + ) + if (showIcon) { + IconButton( + modifier = Modifier.fillMaxHeight(), + onClick = { onIconClick() }) { + Icon(imageVector = Icons.Rounded.Info, contentDescription = null) + } + } + Icon( + modifier = Modifier.size(30.dp), + imageVector = Icons.Rounded.KeyboardArrowRight, contentDescription = null + ) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDetailActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDetailActivity.kt index 8e76ce93..93c85903 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDetailActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDetailActivity.kt @@ -1,134 +1,165 @@ package com.skyd.imomoe.view.activity -import android.content.res.Configuration -import android.graphics.Color import android.os.Bundle -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope +import androidx.activity.viewModels +import androidx.core.view.WindowInsetsControllerCompat import androidx.recyclerview.widget.GridLayoutManager import com.skyd.imomoe.R -import com.skyd.imomoe.bean.FavoriteAnimeBean import com.skyd.imomoe.config.Api import com.skyd.imomoe.config.Const import com.skyd.imomoe.database.getAppDataBase import com.skyd.imomoe.databinding.ActivityAnimeDetailBinding -import com.skyd.imomoe.util.Util.getSkinResourceId -import com.skyd.imomoe.util.Util.getStatusBarHeight -import com.skyd.imomoe.util.Util.setTransparentStatusBar -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.coil.DarkBlurTransformation +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.route.Router.buildRouteUri +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.route.processor.PlayActivityProcessor +import com.skyd.imomoe.state.DataState import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.util.smartNotifyDataSetChanged -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.adapter.AnimeDetailAdapter +import com.skyd.imomoe.util.coil.DarkBlurTransformation +import com.skyd.imomoe.util.compare.EpisodeTitleSort.sortEpisodeTitle import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration -import com.skyd.imomoe.view.adapter.spansize.AnimeDetailSpanSize -import com.skyd.imomoe.view.fragment.ShareDialogFragment +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.* +import com.skyd.imomoe.view.fragment.dialog.EpisodeDialogFragment +import com.skyd.imomoe.view.fragment.dialog.ShareDialogFragment import com.skyd.imomoe.viewmodel.AnimeDetailViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import dagger.hilt.android.AndroidEntryPoint import java.net.URL import kotlin.random.Random - +@AndroidEntryPoint class AnimeDetailActivity : BaseActivity() { - private var partUrl: String = "" - private var isFavorite: Boolean = false - private lateinit var viewModel: AnimeDetailViewModel - private lateinit var adapter: AnimeDetailAdapter - override var statusBarSkin: Boolean = false + private val viewModel: AnimeDetailViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter( + mutableListOf( + Header1Proxy(color = Header1Proxy.WHITE), + AnimeDescribe1Proxy(), + AnimeInfo1Proxy(onBindViewHolder = { holder, _, _ -> + // 查找番剧播放历史决定是否可续播 + holder.tvAnimeInfoContinuePlay.apply { + gone() + getAppDataBase().historyDao().getHistoryFlow(viewModel.partUrl).also { + setOnClickListener { v -> + val url = v.tag + if (url is String) { + val const = DataSourceManager.getConst() + if (const != null) { + PlayActivityProcessor.route.buildRouteUri { + appendQueryParameter("partUrl", url) + appendQueryParameter("detailPartUrl", viewModel.partUrl) + }.route(this@AnimeDetailActivity) + } + } + } + visible() + }.collectWithLifecycle(this@AnimeDetailActivity) { hb -> + //FIX_TODO 2022/1/22 14:53 0 这里没有在打开播放后更新,原因未知,所以暂时只能手动刷新 + if (hb != null) { + text = getString(R.string.play_last_time_episode, hb.lastEpisode) + tag = hb.lastEpisodeUrl + } else gone() // 小心复用,所以主要主动隐藏 + } + } + false + }), + AnimeCover1Proxy(color = AnimeCover1Proxy.WHITE), + HorizontalRecyclerView1Proxy( + color = HorizontalRecyclerView1Proxy.WHITE, + onMoreButtonClickListener = { _, data, _ -> + EpisodeDialogFragment { + title = getString(R.string.play_list) + dataList = data.episodeList.toMutableList().sortEpisodeTitle() + onEpisodeClick { _, data, _ -> + data.route.route(context) + dismiss() + } + }.show(supportFragmentManager, EpisodeDialogFragment.TAG) + }, + onAnimeEpisodeClickListener = { _, data, _ -> + data.route.route(this) + }) + ) + ) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowInsetsControllerCompat(window, mBinding.root).isAppearanceLightStatusBars = false - setTransparentStatusBar(window, isDark = true) - - val statusBarLinearParams = - mBinding.viewAnimeDetailActivityStatusBar.layoutParams //取控件当前的布局参数 - statusBarLinearParams.height = getStatusBarHeight() - mBinding.viewAnimeDetailActivityStatusBar.layoutParams = statusBarLinearParams + viewModel.partUrl = intent.getStringExtra("partUrl").orEmpty() - viewModel = ViewModelProvider(this).get(AnimeDetailViewModel::class.java) - adapter = AnimeDetailAdapter(this, viewModel.animeDetailList) + mBinding.tbAnimeDetailActivity.run { + addFitsSystemWindows(right = true, top = true) - partUrl = intent.getStringExtra("partUrl") ?: "" - - mBinding.llAnimeDetailActivityToolbar.run { - layoutToolbar1.setBackgroundColor(Color.TRANSPARENT) - tvToolbar1Title.isFocused = true - ivToolbar1Back.setOnClickListener { finish() } - // 分享 - ivToolbar1Button1.visible() - ivToolbar1Button1.setOnClickListener { - ShareDialogFragment().setShareContent(Api.MAIN_URL + partUrl) - .show(supportFragmentManager, "share_dialog") - } - // 收藏 - lifecycleScope.launch(Dispatchers.IO) { - val favoriteAnime = getAppDataBase().favoriteAnimeDao().getFavoriteAnime(partUrl) - isFavorite = if (favoriteAnime == null) { - ivToolbar1Button2.setImageResource(R.drawable.ic_star_border_white_24) - false - } else { - ivToolbar1Button2.setImageResource(R.drawable.ic_star_white_24_skin) - true - } - withContext(Dispatchers.Main) { - ivToolbar1Button2.visible() - } - } - ivToolbar1Button2.isEnabled = false - ivToolbar1Button2.setOnClickListener { - lifecycleScope.launch(Dispatchers.IO) { - if (isFavorite) { - getAppDataBase().favoriteAnimeDao().deleteFavoriteAnime(partUrl) - withContext(Dispatchers.Main) { - isFavorite = false - ivToolbar1Button2.setImageResource(R.drawable.ic_star_border_white_24) - getString(R.string.remove_favorite_succeed).showToast() - } - } else { - getAppDataBase().favoriteAnimeDao().insertFavoriteAnime( - FavoriteAnimeBean( - Const.ViewHolderTypeString.ANIME_COVER_8, "", - partUrl, - viewModel.title, - System.currentTimeMillis(), - viewModel.cover - ) - ) - withContext(Dispatchers.Main) { - isFavorite = true - ivToolbar1Button2.setImageResource(R.drawable.ic_star_white_24_skin) - getString(R.string.favorite_succeed).showToast() + setNavigationOnClickListener { finish() } + menu.getItem(1).isVisible = false + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.menu_item_anime_detail_activity_share -> { + ShareDialogFragment().setShareContent(Api.MAIN_URL + viewModel.partUrl) + .show(supportFragmentManager, "share_dialog") + true + } + R.id.menu_item_anime_detail_activity_favorite -> { + when (item.isChecked) { + true -> { + item.setIcon(R.drawable.ic_star_border_24) + viewModel.deleteFavorite() + } + false -> { + item.setIcon(R.drawable.ic_star_24) + viewModel.insertFavorite() + } } + true } + else -> false + } + } + // 收藏 + viewModel.favorite.collectWithLifecycle(this@AnimeDetailActivity) { + menu.findItem(R.id.menu_item_anime_detail_activity_favorite).apply { + isVisible = true + isChecked = it + setIcon(if (it) R.drawable.ic_star_24 else R.drawable.ic_star_border_24) } } } mBinding.run { - rvAnimeDetailActivityInfo.layoutManager = GridLayoutManager(this@AnimeDetailActivity, 4) - .apply { spanSizeLookup = AnimeDetailSpanSize(adapter) } + rvAnimeDetailActivityInfo.addFitsSystemWindows(right = true, bottom = true) + + rvAnimeDetailActivityInfo.layoutManager = GridLayoutManager( + this@AnimeDetailActivity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { spanSizeLookup = AnimeShowSpanSize(adapter) } // 复用AnimeShow的ItemDecoration rvAnimeDetailActivityInfo.addItemDecoration(AnimeShowItemDecoration()) rvAnimeDetailActivityInfo.adapter = adapter - srlAnimeDetailActivity.setOnRefreshListener { viewModel.getAnimeDetailData(partUrl) } - srlAnimeDetailActivity.setColorSchemeResources(getSkinResourceId(R.color.main_color_skin)) + srlAnimeDetailActivity.setOnRefreshListener { viewModel.getAnimeDetailData() } } - viewModel.mldAnimeDetailList.observe(this, Observer { + viewModel.animeDetailList.collectWithLifecycle(this) { data -> mBinding.srlAnimeDetailActivity.isRefreshing = false - adapter.smartNotifyDataSetChanged(it.first, it.second, viewModel.animeDetailList) - mBinding.llAnimeDetailActivityToolbar.ivToolbar1Button2.isEnabled = true + when (data) { + is DataState.Success -> { + adapter.dataList = data.data + } + else -> { + adapter.dataList = emptyList() + } + } - if (viewModel.cover.url.isBlank()) return@Observer + if (viewModel.cover.url.isNullOrBlank()) return@collectWithLifecycle mBinding.ivAnimeDetailActivityBackground.loadImage(viewModel.cover.url) { transformations(DarkBlurTransformation(this@AnimeDetailActivity)) - addHeader("Referer", viewModel.cover.referer) + viewModel.cover.referer?.let { referer -> + addHeader("Referer", referer) + } addHeader("Host", URL(viewModel.cover.url).host) addHeader("Accept", "*/*") addHeader("Accept-Encoding", "gzip, deflate") @@ -138,27 +169,13 @@ class AnimeDetailActivity : BaseActivity() { Const.Request.USER_AGENT_ARRAY[Random.nextInt(Const.Request.USER_AGENT_ARRAY.size)] ) } - mBinding.llAnimeDetailActivityToolbar.tvToolbar1Title.text = viewModel.title - }) + mBinding.tbAnimeDetailActivity.title = viewModel.title + } mBinding.srlAnimeDetailActivity.isRefreshing = true - viewModel.getAnimeDetailData(partUrl) + if (viewModel.animeDetailList.value is DataState.Empty) viewModel.getAnimeDetailData() } override fun getBinding(): ActivityAnimeDetailBinding = ActivityAnimeDetailBinding.inflate(layoutInflater) - - fun getPartUrl(): String = partUrl - - override fun onChangeSkin() { - super.onChangeSkin() - mBinding.llAnimeDetailActivityToolbar.layoutToolbar1.setBackgroundColor(Color.TRANSPARENT) - adapter.notifyDataSetChanged() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - mBinding.llAnimeDetailActivityToolbar.layoutToolbar1.setBackgroundColor(Color.TRANSPARENT) - adapter.notifyDataSetChanged() - } } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDownloadActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDownloadActivity.kt index 2126e9cf..8c0143d1 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDownloadActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/AnimeDownloadActivity.kt @@ -1,100 +1,217 @@ package com.skyd.imomoe.view.activity +import android.app.Activity import android.os.Bundle -import android.view.ViewStub import android.widget.Toast -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import com.afollestad.materialdialogs.MaterialDialog -import com.hjq.permissions.OnPermissionCallback -import com.hjq.permissions.Permission -import com.hjq.permissions.XXPermissions +import androidx.activity.viewModels +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel import com.skyd.imomoe.R -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.databinding.ActivityAnimeDownloadBinding -import com.skyd.imomoe.util.Util -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.adapter.AnimeDownloadAdapter +import com.skyd.imomoe.bean.AnimeCover7Bean +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import com.skyd.imomoe.view.adapter.compose.proxy.AnimeCover7Proxy +import com.skyd.imomoe.view.component.compose.* +import com.skyd.imomoe.viewmodel.AnimeDownloadUiState import com.skyd.imomoe.viewmodel.AnimeDownloadViewModel +import com.skyd.imomoe.viewmodel.DeleteUiState -class AnimeDownloadActivity : BaseActivity() { - private var mode = 0 //0是默认的,是番剧;1是番剧每一集 - private var actionBarTitle = "" - private var directoryName = "" - private var path = 0 - private lateinit var viewModel: AnimeDownloadViewModel - private lateinit var adapter: AnimeDownloadAdapter +class AnimeDownloadActivity : BaseComposeActivity() { + private val viewModel: AnimeDownloadViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mode = intent.getIntExtra("mode", 0) - actionBarTitle = - intent.getStringExtra("actionBarTitle") ?: getString(R.string.download_anime) - directoryName = intent.getStringExtra("directoryName") ?: "" - path = intent.getIntExtra("path", 0) + setContentBase { + AnimeDownloadScreen() + } - viewModel = ViewModelProvider(this).get(AnimeDownloadViewModel::class.java) - adapter = AnimeDownloadAdapter(this, viewModel.animeCoverList) + viewModel.mode = intent.getIntExtra("mode", 0) + viewModel.actionBarTitle = + intent.getStringExtra("actionBarTitle") ?: getString(R.string.download_anime) + viewModel.directoryName = intent.getStringExtra("directoryName").orEmpty() + viewModel.path = intent.getIntExtra("path", 0) + } +} - mBinding.run { - llAnimeDownloadActivityToolbar.tvToolbar1Title.text = actionBarTitle - llAnimeDownloadActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } - llAnimeDownloadActivityToolbar.ivToolbar1Button1.visible() - llAnimeDownloadActivityToolbar.ivToolbar1Button1.setImageResource(R.drawable.ic_info_white_24) - llAnimeDownloadActivityToolbar.ivToolbar1Button1.setOnClickListener { - MaterialDialog(this@AnimeDownloadActivity).show { - title(res = R.string.attention) - message( - text = "由于新版Android存储机制变更,因此新缓存的动漫将存储在App的私有路径," + - "以前缓存的动漫依旧能够观看,其后面将有“旧”字样。新缓存的动漫与以前缓存的互不影响。" + - "\n\n注意:新缓存的动漫将在App被卸载或数据被清除后丢失。" +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AnimeDownloadScreen(viewModel: AnimeDownloadViewModel = hiltViewModel()) { + val context = LocalContext.current + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + initData(activity = context.activity, viewModel = viewModel) + } + Scaffold( + topBar = { + AnimeTopBar( + title = { + Text(text = viewModel.actionBarTitle) + }, + navigationIcon = { + BackIcon( + onClick = { context.activity.finish() } ) - positiveButton { dismiss() } + }, + ) + }, + snackbarHost = { + SnackbarHost(snackbarHostState) { + Snackbar( + modifier = Modifier + .padding(12.dp) + .padding(WindowInsets.navigationBars.asPaddingValues()) + ) { + Text(it.visuals.message) } } - - rvAnimeDownloadActivity.layoutManager = LinearLayoutManager(this@AnimeDownloadActivity) - rvAnimeDownloadActivity.adapter = adapter - - layoutAnimeDownloadLoading.tvCircleProgressTextTip1.text = - getString(R.string.read_download_data_file) } - - viewModel.mldAnimeCoverList.observe(this, Observer { - if (it) { - mBinding.layoutAnimeDownloadLoading.layoutCircleProgressTextTip1.gone() - if (viewModel.animeCoverList.size == 0) { - showLoadFailedTip(getString(R.string.no_download_video), null) + ) { padding -> + val uiState by viewModel.animeCoverList.collectAsState() + when (uiState) { + is AnimeDownloadUiState.WithData -> { + val dataList = uiState.readOrNull().orEmpty() + if (dataList.isEmpty()) { + ImageTextPlaceholder( + modifier = Modifier.padding(padding + WindowInsets.navigationBars.asPaddingValues()), + message = stringResource(id = R.string.no_download_video) + ) + } else { + AnimeDownloadList(dataList = dataList, modifier = Modifier.padding(padding)) } - adapter.notifyDataSetChanged() } - }) - - XXPermissions.with(this).permission(Permission.MANAGE_EXTERNAL_STORAGE) - .request(object : OnPermissionCallback { - override fun onGranted(permissions: MutableList?, all: Boolean) { - if (mode == 0) viewModel.getAnimeCover() - else if (mode == 1) { - mBinding.layoutAnimeDownloadLoading.layoutCircleProgressTextTip1.visible() - viewModel.getAnimeCoverEpisode(directoryName, path) - } - } + is AnimeDownloadUiState.Refreshing, is AnimeDownloadUiState.None -> { + ProgressTextPlaceholder( + modifier = Modifier.padding(padding + WindowInsets.navigationBars.asPaddingValues()), + message = stringResource(id = R.string.read_download_data_file) + ) + } + else -> {} + } - override fun onDenied(permissions: MutableList?, never: Boolean) { - super.onDenied(permissions, never) - "无存储权限,无法播放本地缓存视频".showToast(Toast.LENGTH_LONG) - finish() + LaunchedEffect(Unit) { + viewModel.delete.collect { deleteUiState -> + if (deleteUiState !is DeleteUiState.None) { + showWaitingDialog = false + initData(activity = context.activity, force = true, viewModel = viewModel) + snackbarHostState.showSnackbar( + message = context.getString( + if (deleteUiState is DeleteUiState.Success) { + R.string.anime_download_activity_delete_success + } else { + R.string.anime_download_activity_delete_failed + }, + deleteUiState.getMessageData() + ) + ) } } + } + } +} + +@Composable +private fun AnimeDownloadList( + dataList: List, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), +) { + val adapter = remember { + LazyGridAdapter( + mutableListOf( + AnimeCover7Proxy(onMenuItemClickListener = { data -> + showMessageDialog = true + messageDialogData = data + }) ) + ) + } + AnimeLazyVerticalGrid( + modifier = modifier.fillMaxSize(), + dataList = dataList, + adapter = adapter, + contentPadding = contentPadding + WindowInsets.navigationBars.asPaddingValues() + ) + if (showMessageDialog) { + DeleteDialog() } + if (showWaitingDialog) { + WaitingDeleteDialog() + } +} - override fun getBinding(): ActivityAnimeDownloadBinding = - ActivityAnimeDownloadBinding.inflate(layoutInflater) +private var showMessageDialog by mutableStateOf(false) +private var messageDialogData by mutableStateOf(null) +private var showWaitingDialog by mutableStateOf(false) + +@Composable +private fun DeleteDialog(viewModel: AnimeDownloadViewModel = hiltViewModel()) { + val data = messageDialogData ?: return + MessageDialog( + message = stringResource( + id = R.string.anime_download_activity_delete_message, + data.title + ), + icon = Icons.Rounded.Delete, + onNegative = { showMessageDialog = false }, + onPositive = { + showMessageDialog = false + showWaitingDialog = true + viewModel.delete(data.path) + }, + onDismissRequest = { + showMessageDialog = false + } + ) +} - override fun getLoadFailedTipView(): ViewStub? = mBinding.layoutAnimeDownloadNoDownload +@Composable +private fun WaitingDeleteDialog(viewModel: AnimeDownloadViewModel = hiltViewModel()) { + WaitingDialog( + onDismissRequest = { showWaitingDialog = false }, + message = stringResource(id = R.string.anime_download_activity_deleting), + onNegative = { + viewModel.cancelDelete() + showWaitingDialog = false + }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) } + +private fun initData( + activity: Activity, + force: Boolean = false, + viewModel: AnimeDownloadViewModel +) { + activity.requestManageExternalStorage { + onGranted { + if (viewModel.mode == 0) { + if (viewModel.animeCoverList.value is AnimeDownloadUiState.None || force) { + viewModel.getAnimeCover() + } + } else if (viewModel.mode == 1) { + if (viewModel.animeCoverList.value is AnimeDownloadUiState.None || force) { + viewModel.getAnimeCoverEpisode() + } + } + } + onDenied { + activity.getString(R.string.no_storage_permission_can_not_olay_local_video) + .showToast(Toast.LENGTH_LONG) + activity.finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/BackupRestoreActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/BackupRestoreActivity.kt new file mode 100644 index 00000000..44f4523e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/activity/BackupRestoreActivity.kt @@ -0,0 +1,66 @@ +package com.skyd.imomoe.view.activity + +import android.os.Bundle +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidViewBinding +import com.skyd.imomoe.R +import com.skyd.imomoe.databinding.ActivityBackupRestoreContainerBinding +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.AnimeTopBarStyle +import com.skyd.imomoe.view.component.compose.BackIcon + +class BackupRestoreActivity : BaseComposeActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentBase { + WebDavScreen() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun WebDavScreen() { + val context = LocalContext.current + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + decayAnimationSpec = rememberSplineBasedDecay(), + state = rememberTopAppBarScrollState() + ) + Scaffold( + topBar = { + AnimeTopBar( + style = AnimeTopBarStyle.Large, + title = { + Text(text = stringResource(R.string.backup_and_restore)) + }, + scrollBehavior = scrollBehavior, + navigationIcon = { + BackIcon(onClick = { context.activity.finish() }) + } + ) + } + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it) + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = WindowInsets.navigationBars.asPaddingValues() + ) { + item { + AndroidViewBinding( + factory = ActivityBackupRestoreContainerBinding::inflate + ) + } + } + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/BaseActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/BaseActivity.kt index cb2d2a5f..f2f5c722 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/BaseActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/BaseActivity.kt @@ -1,59 +1,65 @@ package com.skyd.imomoe.view.activity import android.os.Bundle -import android.util.Log import android.view.View import android.view.ViewStub import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding -import com.skyd.skin.core.SkinBaseActivity +import com.flurry.android.FlurryAgent import com.skyd.imomoe.R -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.Util.setColorStatusBar +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.ext.theme.* +import com.skyd.imomoe.util.Util import com.skyd.imomoe.util.eventbus.EventBusSubscriber -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible -import com.skyd.skin.core.SkinResourceProcessor +import com.skyd.imomoe.util.logE import org.greenrobot.eventbus.EventBus -abstract class BaseActivity : SkinBaseActivity() { +abstract class BaseActivity : AppCompatActivity() { protected lateinit var mBinding: VB private lateinit var loadFailedTipView: View private lateinit var tvImageTextTip1: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setTheme(SkinResourceProcessor.instance.getSkinThemeId(R.style.Theme_樱花动漫)) + beforeSetContentView() + mBinding = getBinding() setContentView(mBinding.root) - setColorStatusBar(window, getResColor(R.color.main_color_2_skin)) - } - override fun onChangeSkin() { - super.onChangeSkin() - setTheme(SkinResourceProcessor.instance.getSkinThemeId(R.style.Theme_樱花动漫)) - } + if (transparentSystemBar()) { + window.transparentSystemBar(mBinding.root) + } - override fun onChangeStatusBarSkin() { - setColorStatusBar(window, getResColor(R.color.main_color_2_skin)) + if (Util.lastReadUserNoticeVersion() >= Const.Common.USER_NOTICE_VERSION) { + initializeFlurry(application) + } } + protected open fun transparentSystemBar(): Boolean = true + protected abstract fun getBinding(): VB override fun onStart() { super.onStart() if (this is EventBusSubscriber) EventBus.getDefault().register(this) + FlurryAgent.onStartSession(this) } override fun onStop() { super.onStop() if (this is EventBusSubscriber && EventBus.getDefault().isRegistered(this)) EventBus.getDefault().unregister(this) + FlurryAgent.onEndSession(this) } protected open fun getLoadFailedTipView(): ViewStub? = null - protected open fun showLoadFailedTip(text: String, onClickListener: View.OnClickListener?) { + protected open fun showLoadFailedTip( + text: String = getString(R.string.load_data_failed_click_to_retry), + onClickListener: View.OnClickListener? = null + ) { val loadFailedTipViewStub = getLoadFailedTipView() ?: return if (loadFailedTipViewStub.parent != null) { loadFailedTipView = loadFailedTipViewStub.inflate() @@ -64,7 +70,7 @@ abstract class BaseActivity : SkinBaseActivity() { if (this::loadFailedTipView.isInitialized) { loadFailedTipView.visible() } else { - Log.e("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") + logE("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") } } } @@ -75,7 +81,7 @@ abstract class BaseActivity : SkinBaseActivity() { if (this::loadFailedTipView.isInitialized) { loadFailedTipView.gone() } else { - Log.e("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") + logE("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") } } } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/BaseComposeActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/BaseComposeActivity.kt new file mode 100644 index 00000000..f4c16270 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/activity/BaseComposeActivity.kt @@ -0,0 +1,51 @@ +package com.skyd.imomoe.view.activity + +import android.os.Bundle +import android.view.ViewGroup +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable +import com.flurry.android.FlurryAgent +import com.google.android.material.composethemeadapter3.Mdc3Theme +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.ext.beforeSetContentView +import com.skyd.imomoe.ext.initializeFlurry +import com.skyd.imomoe.ext.theme.transparentSystemBar +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.util.eventbus.EventBusSubscriber +import org.greenrobot.eventbus.EventBus + +abstract class BaseComposeActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + beforeSetContentView() + + // 透明状态栏 + window.transparentSystemBar(window.decorView.findViewById(android.R.id.content)) + + if (Util.lastReadUserNoticeVersion() >= Const.Common.USER_NOTICE_VERSION) { + initializeFlurry(application) + } + } + + fun setContentBase(content: @Composable () -> Unit) { + setContent { + Mdc3Theme { + content.invoke() + } + } + } + + override fun onStart() { + super.onStart() + if (this is EventBusSubscriber) EventBus.getDefault().register(this) + FlurryAgent.onStartSession(this) + } + + override fun onStop() { + super.onStop() + if (this is EventBusSubscriber && EventBus.getDefault().isRegistered(this)) + EventBus.getDefault().unregister(this) + FlurryAgent.onEndSession(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/ClassifyActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/ClassifyActivity.kt index 15971176..bcfa9f97 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/ClassifyActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/ClassifyActivity.kt @@ -2,179 +2,166 @@ package com.skyd.imomoe.view.activity import android.annotation.SuppressLint import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter -import android.widget.TextView -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider +import androidx.activity.viewModels import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App import com.skyd.imomoe.R import com.skyd.imomoe.bean.ClassifyBean -import com.skyd.imomoe.bean.ClassifyDataBean -import com.skyd.imomoe.bean.GetDataEnum +import com.skyd.imomoe.bean.ClassifyTab1Bean import com.skyd.imomoe.databinding.ActivityClassifyBinding -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.smartNotifyDataSetChanged -import com.skyd.imomoe.view.adapter.BaseRvAdapter -import com.skyd.imomoe.view.adapter.SearchAdapter +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.ext.hideToolbarWhenCollapsed +import com.skyd.imomoe.ext.screenIsLand +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeCover3Proxy +import com.skyd.imomoe.view.adapter.variety.proxy.ClassifyTab1Proxy +import com.skyd.imomoe.view.listener.dsl.setOnItemSelectedListener import com.skyd.imomoe.viewmodel.ClassifyViewModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class ClassifyActivity : BaseActivity() { - private lateinit var viewModel: ClassifyViewModel - private var lastRefreshTime: Long = System.currentTimeMillis() - 500 - private lateinit var spinnerAdapter: ArrayAdapter - private lateinit var classifyTabAdapter: ClassifyTabAdapter - private val classifyTabList: MutableList = ArrayList() - private lateinit var classifyAdapter: SearchAdapter - private var classifyTabTitle: String = "" //如 地区 - private var classifyTitle: String = "" //如 大陆 - private var currentPartUrl: String = "" + private val viewModel: ClassifyViewModel by viewModels() + private var lastRefreshTime: Long = 0L + private val spinnerAdapter: ArrayAdapter by lazy { + ArrayAdapter(this, R.layout.item_spinner_item_1) + } + private val classifyTabAdapter: VarietyAdapter by lazy { + VarietyAdapter(mutableListOf(ClassifyTab1Proxy( + onClickListener = { _, data, _ -> classifyTabClicked(data) } + ))) + } + private val classifyAdapter: VarietyAdapter by lazy { + VarietyAdapter(mutableListOf(AnimeCover3Proxy())) + } @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(ClassifyViewModel::class.java) - - currentPartUrl = intent.getStringExtra("partUrl") ?: "" - classifyTabTitle = intent.getStringExtra("classifyTabTitle") ?: "" - classifyTitle = intent.getStringExtra("classifyTitle") ?: "" - - mBinding.llClassifyActivityToolbar.run { - ivToolbar1Back.setOnClickListener { finish() } - tvToolbar1Title.text = getString(R.string.anime_classify) - tvToolbar1Title.isFocused = true - } - - spinnerAdapter = ArrayAdapter(this, R.layout.item_spinner_item_1) - classifyTabAdapter = ClassifyTabAdapter(this, classifyTabList) - classifyAdapter = SearchAdapter(this, viewModel.classifyList) + viewModel.currentPartUrl = intent.getStringExtra("partUrl").orEmpty() + viewModel.classifyTabTitle = intent.getStringExtra("classifyTabTitle").orEmpty() + viewModel.classifyTitle = intent.getStringExtra("classifyTitle").orEmpty() mBinding.run { + tbClassifyActivity.setNavigationOnClickListener { finish() } + ablClassifyActivity.addFitsSystemWindows(right = true, top = true) + rvClassifyActivity.addFitsSystemWindows(right = true, bottom = true) srlClassifyActivity.setOnRefreshListener { //避免刷新间隔太短 if (System.currentTimeMillis() - lastRefreshTime > 500) { lastRefreshTime = System.currentTimeMillis() - if (viewModel.mldClassifyTabList.value?.second == GetDataEnum.REFRESH) - viewModel.getClassifyData(currentPartUrl) + if (!viewModel.classifyTabList.value.readOrNull().isNullOrEmpty()) + viewModel.getClassifyData(viewModel.currentPartUrl) else viewModel.getClassifyTabData() } else { srlClassifyActivity.finishRefresh() } } - srlClassifyActivity.setOnLoadMoreListener { - viewModel.pageNumberBean?.let { - viewModel.getClassifyData(it.actionUrl, isRefresh = false) - return@setOnLoadMoreListener - } - mBinding.srlClassifyActivity.finishLoadMore() - getString(R.string.no_more_info).showToast() - } + srlClassifyActivity.setOnLoadMoreListener { viewModel.loadMoreClassifyData() } - rvClassifyActivityTab.layoutManager = - GridLayoutManager(this@ClassifyActivity, 2, GridLayoutManager.HORIZONTAL, false) - rvClassifyActivityTab.setHasFixedSize(true) + rvClassifyActivityTab.layoutManager = GridLayoutManager( + this@ClassifyActivity, 2, + if (screenIsLand) GridLayoutManager.VERTICAL + else GridLayoutManager.HORIZONTAL, false + ) rvClassifyActivityTab.adapter = classifyTabAdapter - rvClassifyActivity.layoutManager = LinearLayoutManager(this@ClassifyActivity) - rvClassifyActivity.setHasFixedSize(true) + rvClassifyActivity.layoutManager = GridLayoutManager( + this@ClassifyActivity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { spanSizeLookup = AnimeShowSpanSize(classifyAdapter) } + rvClassifyActivity.addItemDecoration(AnimeShowItemDecoration()) rvClassifyActivity.adapter = classifyAdapter spinnerClassifyActivity.adapter = spinnerAdapter - spinnerClassifyActivity.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>, view: View, - pos: Int, id: Long - ) { - if (view is TextView) view.setTextColor(getResColor(R.color.foreground_main_color_2_skin)) - classifyTabList.clear() - classifyTabList.addAll(viewModel.classifyTabList[pos].classifyDataList) - classifyTabAdapter.notifyDataSetChanged() - } - - override fun onNothingSelected(parent: AdapterView<*>) { + spinnerClassifyActivity.setOnItemSelectedListener { + onItemSelected { _, _, position, _ -> + classifyTabAdapter.dataList = + spinnerAdapter.getItem(position)?.classifyDataList.orEmpty() } } + + ablClassifyActivity.hideToolbarWhenCollapsed(tbClassifyActivity) } - viewModel.mldClassifyTabList.observe(this, { - when (it.second) { - GetDataEnum.REFRESH -> { - viewModel.classifyTabList.apply { - clear() - spinnerAdapter.clear() - spinnerAdapter.notifyDataSetChanged() - addAll(it.first) - spinnerAdapter.addAll(it.first) - spinnerAdapter.notifyDataSetChanged() - } + viewModel.classifyTabList.collectWithLifecycle(this) { data -> + mBinding.srlClassifyActivity.finishRefresh() + when (data) { + is DataState.Success -> { + spinnerAdapter.clear() + spinnerAdapter.addAll(data.data) + spinnerAdapter.notifyDataSetChanged() //自动选中第一个 - if (currentPartUrl.isEmpty() && viewModel.classifyTabList.size > 0 && - viewModel.classifyTabList[0].classifyDataList.size > 0 + if (viewModel.currentPartUrl.isEmpty() && + data.data.isNotEmpty() && + data.data[0].classifyDataList.size > 0 ) { - val firstItem = viewModel.classifyTabList[0].classifyDataList[0] - currentPartUrl = firstItem.actionUrl - classifyTabTitle = viewModel.classifyTabList[0].toString() - classifyTitle = firstItem.title - tabSelected(currentPartUrl) + val firstItem = data.data[0].classifyDataList[0] + viewModel.currentPartUrl = firstItem.route + viewModel.classifyTabTitle = data.data[0].toString() + viewModel.classifyTitle = firstItem.title + tabSelected(viewModel.currentPartUrl) } else { var found = false - viewModel.classifyTabList.forEachIndexed { index, classifyBean -> + data.data.forEachIndexed { index, classifyBean -> classifyBean.classifyDataList.forEach { item -> - if (item.actionUrl == currentPartUrl) { + if (item.route == viewModel.currentPartUrl) { mBinding.spinnerClassifyActivity.setSelection(index, true) - classifyTabTitle = classifyBean.name - classifyTitle = item.title - tabSelected(currentPartUrl) + viewModel.classifyTabTitle = classifyBean.name + viewModel.classifyTitle = item.title + tabSelected(viewModel.currentPartUrl) found = true return@forEachIndexed } } } - if (!found) tabSelected(currentPartUrl) + if (!found) tabSelected(viewModel.currentPartUrl) } } - GetDataEnum.FAILED -> { - viewModel.classifyTabList.apply { - clear() - spinnerAdapter.clear() - spinnerAdapter.notifyDataSetChanged() - } - mBinding.srlClassifyActivity.finishRefresh() + is DataState.Error -> { + spinnerAdapter.clear() + spinnerAdapter.notifyDataSetChanged() } - else -> mBinding.srlClassifyActivity.finishRefresh() + else -> {} } - }) + } - viewModel.mldClassifyList.observe(this, { - mBinding.srlClassifyActivity.closeHeaderOrFooter() - viewModel.isRequesting = false - classifyAdapter.smartNotifyDataSetChanged(it.first, it.second, viewModel.classifyList) - if (it.first == GetDataEnum.REFRESH) { - mBinding.llClassifyActivityToolbar.tvToolbar1Title.text = - if (classifyTabTitle.isEmpty()) "${getString(R.string.anime_classify)} $classifyTitle" - else "${getString(R.string.anime_classify)} ${ - if (classifyTabTitle.endsWith(":") || - classifyTabTitle.endsWith(":") - ) classifyTabTitle.substring(0, classifyTabTitle.length - 1) - else classifyTabTitle - }:$classifyTitle" + viewModel.classifyList.collectWithLifecycle(this) { data -> + when (data) { + is DataState.Success -> { + classifyAdapter.dataList = data.data + mBinding.tbClassifyActivity.title = + if (viewModel.classifyTabTitle.isEmpty()) "${getString(R.string.anime_classify)} ${viewModel.classifyTitle}" + else "${getString(R.string.anime_classify)} ${ + if (viewModel.classifyTabTitle.endsWith(":") || + viewModel.classifyTabTitle.endsWith(":") + ) { + viewModel.classifyTabTitle.substring( + 0, viewModel.classifyTabTitle.length - 1 + ) + } else { + viewModel.classifyTabTitle + } + }:${viewModel.classifyTitle}" + } + is DataState.Error -> { + classifyAdapter.dataList = emptyList() + } + else -> {} } - }) + mBinding.srlClassifyActivity.closeHeaderOrFooter() + } viewModel.setActivity(this) - viewModel.getClassifyTabData() + if (viewModel.classifyTabList.value is DataState.Empty) viewModel.getClassifyTabData() } override fun onDestroy() { @@ -186,54 +173,15 @@ class ClassifyActivity : BaseActivity() { ActivityClassifyBinding.inflate(layoutInflater) private fun tabSelected(partUrl: String) { - currentPartUrl = partUrl + viewModel.currentPartUrl = partUrl mBinding.srlClassifyActivity.autoRefresh() } - class ClassifyTabAdapter( - val activity: ClassifyActivity, - private val dataList: List - ) : BaseRvAdapter(dataList) { - - class ClassifyTab1ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val textView = view.findViewById(R.id.text_view_1) - } - - override fun getItemViewType(position: Int): Int = 0 - - fun getItem(position: Int): ClassifyDataBean { - return dataList[position] - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - return ClassifyTab1ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_text_view_1, parent, false) - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = dataList[position] - - when (holder) { - is ClassifyTab1ViewHolder -> { - holder.textView.text = item.title - holder.itemView.setOnClickListener { - activity.classifyTabTitle = activity.spinnerAdapter.getItem( - activity.mBinding.spinnerClassifyActivity.selectedItemPosition - ).toString() - activity.classifyTitle = item.title - activity.tabSelected(item.actionUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } + private fun classifyTabClicked(data: ClassifyTab1Bean) { + viewModel.classifyTabTitle = spinnerAdapter.getItem( + mBinding.spinnerClassifyActivity.selectedItemPosition + ).toString() + viewModel.classifyTitle = data.title + tabSelected(data.route) } } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/ConfigDataSourceActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/ConfigDataSourceActivity.kt new file mode 100644 index 00000000..ba831efa --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/activity/ConfigDataSourceActivity.kt @@ -0,0 +1,281 @@ +package com.skyd.imomoe.view.activity + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import androidx.activity.viewModels +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayoutMediator +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.DataSource1Bean +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.databinding.ActivityConfigDataSourceBinding +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.util.Util.openBrowser +import com.skyd.imomoe.view.fragment.DataSourceMarketFragment +import com.skyd.imomoe.view.fragment.LocalDataSourceFragment +import com.skyd.imomoe.viewmodel.ConfigDataSourceViewModel +import java.io.File + + +class ConfigDataSourceActivity : BaseActivity() { + private val viewModel: ConfigDataSourceViewModel by viewModels() + private val adapter: VpAdapter by lazy { VpAdapter(this) } + + private val tabLayoutTitle by lazy { + arrayOf(getString(R.string.local_data_source), getString(R.string.data_source_market)) + } + + companion object { + const val SELECT_PAGE_INDEX = "selectPageIndex" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mBinding.apply { + ablConfigDataSourceActivity.hideToolbarWhenCollapsed(tbConfigDataSourceActivity) + ablConfigDataSourceActivity.addFitsSystemWindows(right = true, top = true) + + tbConfigDataSourceActivity.apply { + setNavigationOnClickListener { finish() } + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.menu_item_config_data_source_activity_reset -> { + resetDataSource() + true + } + R.id.menu_item_config_data_source_activity_repo -> { + openBrowser(Const.Common.GITHUB_DATA_SOURCE_URL) + true + } + else -> false + } + } + } + + vp2ConfigDataSourceActivity.adapter = adapter + + val tabLayoutMediator = TabLayoutMediator( + tlConfigDataSourceActivity, vp2ConfigDataSourceActivity + ) { tab, position -> + tab.text = tabLayoutTitle[position % 2] + } + tabLayoutMediator.attach() + adapter.notifyDataSetChanged() + } + + viewModel.deleteSource.collectWithLifecycle(this) { + adapter.getFragment(supportFragmentManager, 0) + ?.getDataSourceList() + } + + val selectPageIndex = intent.getIntExtra(SELECT_PAGE_INDEX, 0) + if (selectPageIndex in 0 until adapter.itemCount) { + mBinding.vp2ConfigDataSourceActivity.setCurrentItem(selectPageIndex, false) + } + + intent?.let { callToImport(it) } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + intent?.let { callToImport(it) } + } + + private fun callToImport(intent: Intent) { + val uri = intent.data + if (Intent.ACTION_VIEW == intent.action && uri != null) { + requestManageExternalStorage { + onGranted { + importDataSource(uri, + onSuccess = { + showSnackbar(getString(R.string.import_data_source_success, uri.path)) + adapter.getFragment(supportFragmentManager, 0) + ?.getDataSourceList() + }, + onFailed = { + val msg = + "建议更换其他文件管理器后重试。" + (if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) + "Android 6及以下,请勿使用MT管理器打开ads文件,失败原因未知!若有解决方案,欢迎到GitHub仓库提PR" + else "") + "\n\n" + it.message + showMessageDialog( + title = getString(R.string.import_data_source_failed), + message = msg, + onPositive = { dialog, _ -> dialog.dismiss() } + ) + } + ) + } + onDenied { + showSnackbar("无存储权限,无法导入", Toast.LENGTH_LONG) + } + } + } + } + + private fun importDataSource( + uri: Uri, + onSuccess: ((File) -> Unit)? = null, + onFailed: ((Exception) -> Unit)? = null + ) { + + val dataSourceSuffix = uri.fileSuffixName(contentResolver) + if (!dataSourceSuffix.equals("ads", true)) { + showSnackbar( + text = getString(R.string.invalid_data_source_suffix, dataSourceSuffix), + duration = Toast.LENGTH_LONG + ) + return + } + showMessageDialog( + title = getString(R.string.warning), + icon = R.drawable.ic_insert_drive_file_24, + message = getString(R.string.import_data_source), + cancelable = false, + onPositive = { _, _ -> + try { + val sourceFileName = uri.fileName(contentResolver) + val directory = File(DataSourceManager.getJarDirectory()) + if (!directory.exists()) directory.mkdirs() + val target = File( + DataSourceManager.getJarDirectory() + "/" + sourceFileName + ) + if (target.exists()) { + val needRestartApp = DataSourceManager.dataSourceFileName == sourceFileName + askOverwriteFile(needRestartApp) { + if (!it) onFailed?.invoke( + FileAlreadyExistsException( + file = target, + reason = "file already exists" + ) + ) + else { + Thread { + try { + uri.copyTo(target) + } catch (e: Exception) { + runOnUiThread { onFailed?.invoke(e) } + return@Thread + } + if (needRestartApp) { + viewModel.clearDataSourceCache() + Util.restartApp() + } else { + runOnUiThread { onSuccess?.invoke(target) } + } + }.start() + } + } + } else { + target.createNewFile() + Thread { + try { + uri.copyTo(target) + } catch (e: Exception) { + runOnUiThread { onFailed?.invoke(e) } + return@Thread + } + runOnUiThread { onSuccess?.invoke(target) } + }.start() + } + } catch (e: Exception) { + onFailed?.invoke(e) + } + }, + onNegative = { dialog, _ -> dialog.dismiss() } + ) + } + + private fun resetDataSource(runBeforeReset: (() -> Unit)? = null) { + showMessageDialog( + title = getString(R.string.warning), + icon = R.drawable.ic_category_24, + message = getString(R.string.request_restart_app), + positiveText = getString(R.string.restart), + onPositive = { _, _ -> + runBeforeReset?.invoke() + viewModel.resetDataSource() + }, + onNegative = { dialog, _ -> dialog.dismiss() } + ) + } + + fun setDataSource(name: String, showDialog: Boolean = true) { + if (!showDialog) { + viewModel.setDataSource(name) + return + } + showMessageDialog( + title = getString(R.string.warning), + icon = R.drawable.ic_category_24, + message = getString(R.string.custom_data_source_tip), + cancelable = false, + positiveText = getString(R.string.restart), + onPositive = { _, _ -> viewModel.setDataSource(name) }, + onNegative = { dialog, _ -> dialog.dismiss() } + ) + } + + fun deleteDataSource(bean: DataSource1Bean) { + showMessageDialog( + title = getString(R.string.warning), + icon = R.drawable.ic_category_24, + message = getString(R.string.ask_delete_data_source), + onPositive = { _, _ -> + if (DataSourceManager.dataSourceFileName == bean.file.name) { + resetDataSource { viewModel.deleteDataSource(bean) } + } else { + viewModel.deleteDataSource(bean) + } + }, + onNegative = { dialog, _ -> dialog.dismiss() } + ) + } + + private fun askOverwriteFile(needRestartApp: Boolean = false, callback: (Boolean) -> Unit) { + showMessageDialog( + title = getString(R.string.warning), + icon = R.drawable.ic_insert_drive_file_24, + message = getString(R.string.ask_overwrite_file), + cancelable = false, + positiveText = getString( + if (needRestartApp) R.string.overwrite_file_and_restart + else R.string.overwrite_file + ), + onPositive = { _, _ -> callback.invoke(true) }, + negativeText = getString(R.string.do_not_overwrite_file), + onNegative = { _, _ -> callback.invoke(false) } + ) + } + + override fun getBinding(): ActivityConfigDataSourceBinding = + ActivityConfigDataSourceBinding.inflate(layoutInflater) + + class VpAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getItemCount() = 2 + + override fun createFragment(position: Int) = when (position) { + 0 -> LocalDataSourceFragment() + 1 -> DataSourceMarketFragment() + else -> LocalDataSourceFragment() + } + + @Suppress("UNCHECKED_CAST") + fun getFragment(fm: FragmentManager, id: Long): T? { + return fm.findFragmentByTag("f$id") as? T + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/CrashActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/CrashActivity.kt index 23e2b2f1..bca5f401 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/CrashActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/CrashActivity.kt @@ -1,11 +1,14 @@ package com.skyd.imomoe.view.activity -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent import android.os.Bundle import android.os.Process -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import com.skyd.imomoe.config.Const.Common.Companion.GITHUB_NEW_ISSUE_URL +import com.skyd.imomoe.config.Const.Common.GITHUB_NEW_ISSUE_URL +import com.skyd.imomoe.ext.showMessageDialog import com.skyd.imomoe.util.Util.openBrowser import kotlin.system.exitProcess @@ -31,25 +34,26 @@ class CrashActivity : AppCompatActivity() { val crashInfo = intent.getStringExtra(CRASH_INFO) val message = "CrashInfo:\n$crashInfo" - AlertDialog.Builder(this).apply { - setMessage(message) - setTitle("哦呼,樱花动漫崩溃了!快去Github提Issue吧") - setPositiveButton("复制信息打开Github") { _: DialogInterface, i: Int -> - val cm = - this@CrashActivity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + showMessageDialog( + title = "哦呼,樱花动漫崩溃了!快去GitHub提Issue吧", + message = message, + cancelable = false, + positiveText = "复制信息打开GitHub", + onPositive = { _, _ -> + val cm = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("exception trace stack", message)) openBrowser(GITHUB_NEW_ISSUE_URL) - this@CrashActivity.finish() + finish() Process.killProcess(Process.myPid()) exitProcess(1) - } - setNegativeButton("退出") { _: DialogInterface, i: Int -> - this@CrashActivity.finish() + }, + negativeText = "退出", + onNegative = { _, _ -> + finish() Process.killProcess(Process.myPid()) exitProcess(1) } - setCancelable(false) - setFinishOnTouchOutside(false) - }.show() + ) + setFinishOnTouchOutside(false) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/DlnaActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/DlnaActivity.kt index 43a43651..e524279f 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/DlnaActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/DlnaActivity.kt @@ -1,91 +1,164 @@ package com.skyd.imomoe.view.activity +import android.content.Intent import android.os.Bundle -import android.util.Log -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.activity.viewModels +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.skyd.imomoe.R -import com.skyd.imomoe.databinding.ActivityDlnaBinding -import com.skyd.imomoe.util.Util.getRedirectUrl +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.collectWithLifecycle import com.skyd.imomoe.util.dlna.dmc.DLNACastManager -import com.skyd.imomoe.util.dlna.dmc.OnDeviceRegistryListener -import com.skyd.imomoe.view.adapter.UpnpAdapter -import com.skyd.imomoe.viewmodel.UpnpViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.fourthline.cling.model.meta.Device - -class DlnaActivity : BaseActivity() { - private lateinit var viewModel: UpnpViewModel - private lateinit var adapter: UpnpAdapter - lateinit var title: String - lateinit var url: String - private val deviceRegistryListener = object : OnDeviceRegistryListener { - override fun onDeviceRemoved(device: Device<*, *, *>?) { - if (viewModel.deviceList.contains(device)) { - viewModel.deviceList.remove(device) - adapter.notifyDataSetChanged() - } - } - - override fun onDeviceAdded(device: Device<*, *, *>?) { - if (!viewModel.deviceList.contains(device)) { - viewModel.deviceList.add(device) - adapter.notifyDataSetChanged() - } +import com.skyd.imomoe.util.dlna.dmc.OnDeviceRegistryListenerDsl +import com.skyd.imomoe.util.dlna.dmc.registerDeviceListener +import com.skyd.imomoe.util.dlna.dmc.unregisterListener +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import com.skyd.imomoe.view.adapter.compose.proxy.UpnpDevice1Proxy +import com.skyd.imomoe.view.component.compose.AnimeLazyVerticalGrid +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.AnimeTopBarStyle +import com.skyd.imomoe.view.component.compose.BackIcon +import com.skyd.imomoe.viewmodel.DlnaUiState +import com.skyd.imomoe.viewmodel.DlnaViewModel + +class DlnaActivity : BaseComposeActivity() { + private val viewModel: DlnaViewModel by viewModels() + private val deviceRegistryListener: OnDeviceRegistryListenerDsl.() -> Unit = { + onDeviceRemoved { device -> + viewModel.removeDevice(device) } - override fun onDeviceUpdated(device: Device<*, *, *>?) { + onDeviceAdded { device -> + viewModel.addDevice(device) } - - } - - companion object { - const val TAG = "DlnaActivity" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - url = intent.getStringExtra("url") ?: "" - title = intent.getStringExtra("title") ?: "" - - Log.i(TAG, url) - - viewModel = ViewModelProvider(this).get(UpnpViewModel::class.java) - adapter = UpnpAdapter(this, viewModel.deviceList) - - mBinding.run { - tbDlnaActivity.tvToolbar1Title.text = getString(R.string.play_on_tv) - tbDlnaActivity.ivToolbar1Back.setOnClickListener { finish() } - - rvDlnaActivityDevice.layoutManager = LinearLayoutManager(this@DlnaActivity) - rvDlnaActivityDevice.adapter = adapter + setContentBase { + DlnaScreen() } - lifecycleScope.launch(Dispatchers.IO) { - url = getRedirectUrl(this@DlnaActivity.url) + viewModel.initData( + url = intent.getStringExtra("url").orEmpty(), + title = intent.getStringExtra("title").orEmpty() + ) - DLNACastManager.getInstance().registerDeviceListener(deviceRegistryListener) - DLNACastManager.getInstance().search(DLNACastManager.DEVICE_TYPE_DMR) + viewModel.uiState.collectWithLifecycle(this) { + if (it is DlnaUiState.Initialized) { + DLNACastManager.instance.registerDeviceListener(deviceRegistryListener) + DLNACastManager.instance.search(DLNACastManager.DEVICE_TYPE_DMR) + } } } override fun onStart() { super.onStart() - DLNACastManager.getInstance().bindCastService(this) + DLNACastManager.instance.bindCastService(this) } override fun onStop() { - DLNACastManager.getInstance().bindCastService(this) + DLNACastManager.instance.bindCastService(this) super.onStop() } override fun onDestroy() { - DLNACastManager.getInstance().unregisterListener(deviceRegistryListener) + DLNACastManager.instance.unregisterListener(deviceRegistryListener) super.onDestroy() } - - override fun getBinding(): ActivityDlnaBinding = ActivityDlnaBinding.inflate(layoutInflater) -} \ No newline at end of file +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DlnaScreen() { + val context = LocalContext.current + Scaffold(topBar = { + AnimeTopBar( + style = AnimeTopBarStyle.Small, + title = { + Text(text = stringResource(R.string.play_on_tv)) + }, + navigationIcon = { + BackIcon( + onClick = { context.activity.finish() } + ) + } + ) + }) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + text = stringResource(id = R.string.device_list), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, + ) + CircularProgressIndicator(modifier = Modifier.size(21.dp)) + } + DeviceList() + Text( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 16.dp, vertical = 12.dp), + text = stringResource(id = R.string.dlna_step), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + .padding(WindowInsets.navigationBars.asPaddingValues()), + text = "1. 确保电视和手机在同一WiFi下,打开支持投屏的电视。\n2. 点击上方搜索到的设备进行投屏。\n3. 注意,部分未知格式的视频可能无法投屏。", + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} + +@Composable +private fun DeviceList(viewModel: DlnaViewModel = hiltViewModel()) { + val activity = LocalContext.current.activity + val uiState by viewModel.uiState.collectAsState() + val adapter: LazyGridAdapter = remember { + LazyGridAdapter( + mutableListOf( + UpnpDevice1Proxy(onClickListener = { _, data -> + val key = System.currentTimeMillis().toString() + DlnaControlActivity.deviceHashMap[key] = data + activity.startActivity( + Intent(activity, DlnaControlActivity::class.java) + .putExtra("url", uiState.url) + .putExtra("title", uiState.title) + .putExtra("deviceKey", key) + ) + }) + ) + ) + } + AnimeLazyVerticalGrid( + modifier = Modifier.fillMaxWidth(), + dataList = uiState.readOrNull().orEmpty(), + adapter = adapter + ) +} diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/DlnaControlActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/DlnaControlActivity.kt index b7c5dda5..4720ae29 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/DlnaControlActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/DlnaControlActivity.kt @@ -1,28 +1,35 @@ package com.skyd.imomoe.view.activity +import android.content.res.ColorStateList import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.Log -import android.view.View +import android.os.Message import android.widget.RelativeLayout -import android.widget.SeekBar +import androidx.core.content.ContextCompat import com.skyd.imomoe.R import com.skyd.imomoe.databinding.ActivityDlnaControlBinding -import com.skyd.imomoe.util.Util.getResColor +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible import com.skyd.imomoe.util.Util.setColorStatusBar -import com.skyd.imomoe.util.Util.showToast import com.skyd.imomoe.util.dlna.CastObject import com.skyd.imomoe.util.dlna.Utils +import com.skyd.imomoe.util.dlna.Utils.isLocalMediaAddress +import com.skyd.imomoe.util.dlna.Utils.toLocalHttpServerAddress import com.skyd.imomoe.util.dlna.dmc.DLNACastManager import com.skyd.imomoe.util.dlna.dmc.control.ICastInterface -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible +import com.skyd.imomoe.util.dlna.dmc.control.newGetInfoListener +import com.skyd.imomoe.util.dlna.dms.MediaServer +import com.skyd.imomoe.util.logE +import com.skyd.imomoe.util.logI +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.listener.dsl.setOnSeekBarChangeListener import org.fourthline.cling.model.meta.Device -import kotlin.collections.HashMap class DlnaControlActivity : BaseActivity() { private lateinit var layoutDlnaControlActivityLoading: RelativeLayout + private var mediaServer: MediaServer? = null private lateinit var deviceKey: String private lateinit var url: String private lateinit var title: String @@ -30,13 +37,13 @@ class DlnaControlActivity : BaseActivity() { companion object { const val TAG = "DlnaControlActivity" - val deviceHashMap = HashMap?>() + val deviceHashMap = HashMap>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setColorStatusBar(window, getResColor(R.color.gray_5)) + setColorStatusBar(window, ContextCompat.getColor(this, R.color.gray_5)) layoutDlnaControlActivityLoading = mBinding.layoutDlnaControlActivityLoading.layoutCircleProgressTextTip1 @@ -46,12 +53,13 @@ class DlnaControlActivity : BaseActivity() { deviceKey = intent.getStringExtra("deviceKey") ?: "" if (deviceKey.isEmpty()) { - "数据接收错误!".showToast() + getString(R.string.dlna_init_data_error).showToast() finish() } //禁止发送数据时点击 mBinding.run { + root.addFitsSystemWindows(right = true, top = true, bottom = true) layoutDlnaControlActivityLoading.layoutCircleProgressTextTip1.isClickable = true layoutDlnaControlActivityLoading.layoutCircleProgressTextTip1.isFocusable = true layoutDlnaControlActivityLoading.tvCircleProgressTextTip1.text = @@ -68,109 +76,135 @@ class DlnaControlActivity : BaseActivity() { ivDlnaControlActivityStop.setOnClickListener { finish() } } - - DLNACastManager.getInstance().registerActionCallbacks( + DLNACastManager.instance.registerActionCallbacks( object : ICastInterface.CastEventListener { override fun onSuccess(result: String) { - DLNACastManager.getInstance().getMediaInfo( - deviceHashMap[deviceKey] - ) { t, errMsg -> - Log.i(TAG, t?.currentURI.toString()) + deviceHashMap[deviceKey]?.let { + DLNACastManager.instance.getMediaInfo( + it, newGetInfoListener { t, _ -> logI(TAG, t?.currentURI.toString()) } + ) } if (!isPlaying) play() } - override fun onFailed(errMsg: String?) { - ("投屏失败了\n$errMsg").showToast() + override fun onFailed(errMsg: String) { + getString(R.string.dlna_cast_failed, errMsg).showToast() } }, object : ICastInterface.PlayEventListener { override fun onSuccess(result: Void?) { - "开始播放".showToast() + getString(R.string.dlna_play).showToast() isPlaying = true - mBinding.ivDlnaControlActivityPlay.setImageResource(R.drawable.ic_pause_circle_white_24) + mBinding.ivDlnaControlActivityPlay.setImageResource(R.drawable.ic_pause_circle_24) + mBinding.ivDlnaControlActivityPlay.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor(this@DlnaControlActivity, android.R.color.white) + ) - handler.postDelayed(positionRunnable, refreshPositionTime) + positionHandler.start(refreshPositionTime) + volumeHandler.start(refreshVolumeTime) layoutDlnaControlActivityLoading.gone() } - override fun onFailed(errMsg: String?) { - ("投屏失败了\n$errMsg").showToast() + override fun onFailed(errMsg: String) { + getString(R.string.dlna_play_failed, errMsg).showToast() + layoutDlnaControlActivityLoading.gone() } }, object : ICastInterface.PauseEventListener { override fun onSuccess(result: Void?) { - "暂停播放".showToast() + getString(R.string.dlna_pause).showToast() isPlaying = false - mBinding.ivDlnaControlActivityPlay.setImageResource(R.drawable.ic_play_circle_white_24) - - handler.post(positionRunnable) - handler.removeCallbacks(positionRunnable) + mBinding.ivDlnaControlActivityPlay.setImageResource(R.drawable.ic_play_circle_24) + mBinding.ivDlnaControlActivityPlay.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor(this@DlnaControlActivity, android.R.color.white) + ) layoutDlnaControlActivityLoading.gone() } - override fun onFailed(errMsg: String?) { - ("暂停失败了\n$errMsg").showToast() + override fun onFailed(errMsg: String) { + getString(R.string.dlna_pause_failed, errMsg).showToast() + layoutDlnaControlActivityLoading.gone() } }, object : ICastInterface.StopEventListener { override fun onSuccess(result: Void?) { - "停止投屏".showToast() + getString(R.string.dlna_stop).showToast() isPlaying = false - mBinding.ivDlnaControlActivityPlay.setImageResource(R.drawable.ic_play_circle_white_24) -// mPositionMsgHandler.stop() -// mVolumeMsgHandler.stop() - handler.post(positionRunnable) - handler.removeCallbacks(positionRunnable) + mBinding.ivDlnaControlActivityPlay.setImageResource(R.drawable.ic_play_circle_24) + mBinding.ivDlnaControlActivityPlay.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor(this@DlnaControlActivity, android.R.color.white) + ) + + positionHandler.stop() + volumeHandler.stop() layoutDlnaControlActivityLoading.gone() } - override fun onFailed(errMsg: String?) { - ("停止失败了\n$errMsg").showToast() + override fun onFailed(errMsg: String) { + getString(R.string.dlna_stop_failed, errMsg).showToast() + layoutDlnaControlActivityLoading.gone() } }, object : ICastInterface.SeekToEventListener { - override fun onSuccess(result: Long?) { - "快进到${Utils.getStringTime(result ?: 0)}".showToast() + override fun onSuccess(result: Long) { + getString(R.string.dlna_seek_to, Utils.getStringTime(result)).showToast() play() layoutDlnaControlActivityLoading.gone() } - override fun onFailed(errMsg: String?) { - ("快进失败了\n$errMsg").showToast() + override fun onFailed(errMsg: String) { + getString(R.string.dlna_seen_to_failed, errMsg).showToast() + layoutDlnaControlActivityLoading.gone() } } ) - mBinding.sbDlnaControlActivity.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar?) { + mBinding.sbDlnaControlActivity.setOnSeekBarChangeListener { + onStopTrackingTouch { seekBar -> if (durationMillSeconds > 0 && seekBar != null) { val position = (seekBar.progress * 1f / seekBar.max * durationMillSeconds).toInt() layoutDlnaControlActivityLoading.visible() - DLNACastManager.getInstance().seekTo(position.toLong()) + DLNACastManager.instance.seekTo(position.toLong()) } } - }) + } + + mBinding.sbDlnaControlActivityVolume.setOnSeekBarChangeListener { + onStopTrackingTouch { seekBar -> + seekBar ?: return@onStopTrackingTouch + val volume = (seekBar.progress * 100f / seekBar.max).toInt() + DLNACastManager.instance.setVolume(volume) + // 同时取消静音 + DLNACastManager.instance.setMute(false) + layoutDlnaControlActivityLoading.visible() + } + + onProgressChanged { seekBar, progress, _ -> + seekBar ?: return@onProgressChanged + val volume = (progress * 100f / seekBar.max).toInt() + mBinding.tvDlnaControlActivityVolume.text = getString( + R.string.dlna_volume, volume.toString() + ) + } + } + + // 若是本地视频,则转换为本地服务器地址 + if (url.isLocalMediaAddress()) { + mediaServer = MediaServer(this).apply { + start() + DLNACastManager.instance.addMediaServer(this) + } + url = url.toLocalHttpServerAddress() + } deviceHashMap[deviceKey]?.let { - DLNACastManager.getInstance().cast( + DLNACastManager.instance.cast( it, - CastObject.CastVideo.newInstance( - url, - System.currentTimeMillis().toString(), - title - ) + CastObject.CastVideo.newInstance(url, System.currentTimeMillis().toString(), title) ) } } @@ -180,54 +214,93 @@ class DlnaControlActivity : BaseActivity() { private fun play() { layoutDlnaControlActivityLoading.visible() - DLNACastManager.getInstance().play() + DLNACastManager.instance.play() } private fun pause() { layoutDlnaControlActivityLoading.visible() - DLNACastManager.getInstance().pause() + DLNACastManager.instance.pause() } private fun stop() { layoutDlnaControlActivityLoading.visible() - DLNACastManager.getInstance().stop() + DLNACastManager.instance.stop() } override fun onDestroy() { super.onDestroy() stop() - DLNACastManager.getInstance().unregisterActionCallbacks() + DLNACastManager.instance.unregisterActionCallbacks() deviceHashMap.remove(deviceKey) } private var durationMillSeconds: Long = 0 private val refreshPositionTime: Long = 500 + private val refreshVolumeTime: Long = 500 private val positionRunnable: Runnable = object : Runnable { override fun run() { - if (deviceHashMap[deviceKey] == null) return - DLNACastManager.getInstance() - .getPositionInfo(deviceHashMap[deviceKey]) { positionInfo, errMsg -> - if (positionInfo != null) { - if (layoutDlnaControlActivityLoading.visibility != View.GONE) - layoutDlnaControlActivityLoading.gone() - mBinding.tvDlnaControlActivityTime.text = java.lang.String.format( - "%s / %s", positionInfo.relTime, positionInfo.trackDuration - ) - if (positionInfo.trackDurationSeconds != 0L) { - durationMillSeconds = positionInfo.trackDurationSeconds * 1000 - mBinding.sbDlnaControlActivity.progress = - (positionInfo.trackElapsedSeconds * 100 / positionInfo.trackDurationSeconds).toInt() - } else { - mBinding.sbDlnaControlActivity.progress = 0 - } + val device = deviceHashMap[deviceKey] ?: return + DLNACastManager.instance.getPositionInfo(device, newGetInfoListener { t, errMsg -> + layoutDlnaControlActivityLoading.gone() + if (t != null) { + mBinding.tvDlnaControlActivityTime.text = + String.format("%s / %s", t.relTime, t.trackDuration) + if (t.trackDurationSeconds != 0L) { + durationMillSeconds = t.trackDurationSeconds * 1000 + mBinding.sbDlnaControlActivity.progress = + (t.trackElapsedSeconds * 100 / t.trackDurationSeconds).toInt() } else { - Log.e(TAG, errMsg.toString()) + mBinding.sbDlnaControlActivity.progress = 0 + } + } else { + logE(TAG, errMsg.toString()) + } + }) + } + } + + private val volumeRunnable: Runnable = object : Runnable { + override fun run() { + val device = deviceHashMap[deviceKey] ?: return + // update volume + DLNACastManager.instance.getVolumeInfo(device, newGetInfoListener { t, errMsg -> + layoutDlnaControlActivityLoading.gone() + if (t != null) { + if (t <= mBinding.sbDlnaControlActivityVolume.max) { + mBinding.sbDlnaControlActivityVolume.progress = t } - handler.postDelayed(this, refreshPositionTime) + mBinding.tvDlnaControlActivityVolume.text = getString( + R.string.dlna_volume, t.toString() + ) + } else { + logE(TAG, errMsg.toString()) } + }) } } - private val handler = Handler(Looper.getMainLooper()) + private val positionHandler = CircleMessageHandler(refreshPositionTime, positionRunnable) + private val volumeHandler = CircleMessageHandler(refreshVolumeTime, volumeRunnable) + + private class CircleMessageHandler(private val duration: Long, private val runnable: Runnable) : + Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + runnable.run() + sendEmptyMessageDelayed(MSG, duration) + } + + fun start(delay: Long) { + stop() + sendEmptyMessageDelayed(MSG, delay) + } + + fun stop() { + removeMessages(MSG) + } + + companion object { + private const val MSG = 101 + } + } } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/DownloadManagerActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/DownloadManagerActivity.kt new file mode 100644 index 00000000..64a9646d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/activity/DownloadManagerActivity.kt @@ -0,0 +1,120 @@ +package com.skyd.imomoe.view.activity + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import androidx.activity.viewModels +import androidx.recyclerview.widget.GridLayoutManager +import com.arialyy.aria.core.task.DownloadTask +import com.skyd.imomoe.databinding.ActivityDownloadManagerBinding +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadService +import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeDownload1Proxy +import com.skyd.imomoe.viewmodel.DownloadManagerViewModel +import kotlinx.coroutines.CoroutineScope + +class DownloadManagerActivity : BaseActivity() { + private val viewModel: DownloadManagerViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter( + mutableListOf( + AnimeDownload1Proxy( + onCancelClickListener = { _, data, _ -> + AnimeDownloadService.cancelTaskEvent.tryEmit(data.id to data.url) + }, onPauseClickListener = { _, data, _ -> + AnimeDownloadService.stopTaskEvent.tryEmit(data.id) + }, onResumeClickListener = { _, data, _ -> + AnimeDownloadService.resumeTaskEvent.tryEmit(data.id) + } + ) + ) + ) + } + private var binder: AnimeDownloadService.AnimeDownloadBinder? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mBinding.apply { + tbDownloadManagerActivity.setNavigationOnClickListener { finish() } + ablDownloadManagerActivity.addFitsSystemWindows(right = true, top = true) + + rvDownloadManagerActivity.addFitsSystemWindows(right = true, bottom = true) + rvDownloadManagerActivity.layoutManager = GridLayoutManager( + this@DownloadManagerActivity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { spanSizeLookup = AnimeShowSpanSize(adapter) } + rvDownloadManagerActivity.addItemDecoration(AnimeShowItemDecoration()) + rvDownloadManagerActivity.adapter = adapter + } + + bindService( + Intent(this, AnimeDownloadService::class.java), + serviceConnection, + Context.BIND_AUTO_CREATE + ) + + viewModel.downloadDataList.collectWithLifecycle(this) { + when (it) { + is DataState.Success -> { + adapter.dataList = it.data + } + else -> { + adapter.dataList = emptyList() + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + unbindService(serviceConnection) + } + + private val serviceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, service: IBinder?) { + binder = (service as? AnimeDownloadService.AnimeDownloadBinder)?.apply { + viewModel.initList(notCompleteList, animeTitleEpisodeMap) + + onTaskRunningEvent.collectWithLifecycle(this@DownloadManagerActivity) { task -> + viewModel.onTaskRunning(task.downloadEntity) + } + onTaskCompleteEvent.collectWithLifecycle(this@DownloadManagerActivity) { task -> + viewModel.onTaskComplete(task.downloadEntity) + } + onTaskCancelEvent.collectWithLifecycle(this@DownloadManagerActivity) { task -> + viewModel.onTaskCancel(task.downloadEntity) + } + onTaskStopEvent.collectWithLifecycle(this@DownloadManagerActivity) { task -> + viewModel.onTaskStateChanged(task.downloadEntity) + } + onTaskResumeEvent.collectWithLifecycle(this@DownloadManagerActivity) { task -> + viewModel.onTaskStateChanged(task.downloadEntity) + } + onTaskPreEvent.collectWithLifecycle(this@DownloadManagerActivity, onTaskPreStart) + onTaskStartEvent.collectWithLifecycle(this@DownloadManagerActivity, onTaskPreStart) + } + } + + override fun onServiceDisconnected(name: ComponentName) { + binder = null + } + } + + private val onTaskPreStart: suspend CoroutineScope.(data: DownloadTask) -> Unit = { task -> + val binder = this@DownloadManagerActivity.binder + if (binder != null) { + viewModel.onTaskPreStart(task.downloadEntity, binder.animeTitleEpisodeMap) + } + } + + override fun getBinding() = ActivityDownloadManagerBinding.inflate(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/FavoriteActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/FavoriteActivity.kt index 6dbd65bd..87419a2d 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/FavoriteActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/FavoriteActivity.kt @@ -1,62 +1,103 @@ package com.skyd.imomoe.view.activity import android.os.Bundle -import android.view.ViewStub -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.GridLayoutManager +import androidx.compose.foundation.layout.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.skyd.imomoe.R -import com.skyd.imomoe.databinding.ActivityFavoriteBinding -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.view.adapter.decoration.AnimeEpisodeItemDecoration -import com.skyd.imomoe.view.adapter.FavoriteAdapter +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.plus +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import com.skyd.imomoe.view.adapter.compose.proxy.AnimeCover8Proxy +import com.skyd.imomoe.view.component.compose.AnimeLazyVerticalGrid +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.BackIcon +import com.skyd.imomoe.view.component.compose.ImageTextPlaceholder import com.skyd.imomoe.viewmodel.FavoriteViewModel +import com.skyd.imomoe.viewmodel.FavoriteUiState -class FavoriteActivity : BaseActivity() { - private lateinit var viewModel: FavoriteViewModel - private lateinit var adapter: FavoriteAdapter - +class FavoriteActivity : BaseComposeActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - viewModel = ViewModelProvider(this).get(FavoriteViewModel::class.java) - adapter = FavoriteAdapter(this, viewModel.favoriteList) - - mBinding.run { - tbFavoriteActivity.ivToolbar1Back.setOnClickListener { finish() } - tbFavoriteActivity.tvToolbar1Title.text = getString(R.string.my_favorite) - - srlFavoriteActivity.setColorSchemeColors( - this@FavoriteActivity.getResColor(R.color.main_color_skin) - ) - srlFavoriteActivity.setOnRefreshListener { viewModel.getFavoriteData() } - rvFavoriteActivity.layoutManager = GridLayoutManager(this@FavoriteActivity, 3) - rvFavoriteActivity.adapter = adapter - rvFavoriteActivity.addItemDecoration(AnimeEpisodeItemDecoration()) + setContentBase { + FavoriteScreen() } + } +} - viewModel.mldFavoriteList.observe(this, Observer { - mBinding.srlFavoriteActivity.isRefreshing = false - if (it) { - if (viewModel.favoriteList.isEmpty()) showLoadFailedTip( - getString(R.string.no_favorite), - null +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun FavoriteScreen(viewModel: FavoriteViewModel = hiltViewModel()) { + val context = LocalContext.current + Scaffold(topBar = { + AnimeTopBar( + title = { + Text(text = stringResource(R.string.my_favorite)) + }, + navigationIcon = { + BackIcon( + onClick = { context.activity.finish() } ) - adapter.notifyDataSetChanged() } - }) - } - - override fun getBinding(): ActivityFavoriteBinding = - ActivityFavoriteBinding.inflate(layoutInflater) - - override fun onResume() { - super.onResume() - - mBinding.srlFavoriteActivity.isRefreshing = true - viewModel.getFavoriteData() + ) + }) { padding -> + val swipeRefreshState = rememberSwipeRefreshState( + isRefreshing = viewModel.uiState.value is FavoriteUiState.Refreshing + ) + val uiState = viewModel.uiState.collectAsState() + when (val uiStateValue = uiState.value) { + is FavoriteUiState.Error -> { + ImageTextPlaceholder( + modifier = Modifier.padding(WindowInsets.navigationBars.asPaddingValues()), + message = uiStateValue.message.ifBlank { stringResource(id = R.string.get_data_failed) } + ) + } + is FavoriteUiState.WithData -> { + SwipeRefresh( + modifier = Modifier.padding(padding), + state = swipeRefreshState, + onRefresh = { + viewModel.getFavoriteData() + } + ) { + val dataList = uiStateValue.dataList ?: return@SwipeRefresh + if (dataList.isEmpty()) { + ImageTextPlaceholder( + modifier = Modifier.padding(WindowInsets.navigationBars.asPaddingValues()), + message = stringResource(id = R.string.no_favorite) + ) + } else { + FavoriteList(dataList = dataList) + } + } + } + } } - - override fun getLoadFailedTipView(): ViewStub? = mBinding.layoutFavoriteActivityNoFavorite } +@Composable +private fun FavoriteList(dataList: List) { + val adapter = remember { + LazyGridAdapter( + mutableListOf(AnimeCover8Proxy()) + ) + } + AnimeLazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + dataList = dataList, + adapter = adapter, + contentPadding = WindowInsets.navigationBars.asPaddingValues() + + PaddingValues(vertical = 6.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/HistoryActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/HistoryActivity.kt index 0f5a9a25..0b33457a 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/HistoryActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/HistoryActivity.kt @@ -1,100 +1,134 @@ package com.skyd.imomoe.view.activity import android.os.Bundle -import android.view.ViewStub -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import com.afollestad.materialdialogs.MaterialDialog +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.skyd.imomoe.R -import com.skyd.imomoe.bean.HistoryBean -import com.skyd.imomoe.databinding.ActivityHistoryBinding -import com.skyd.imomoe.util.Util.dp -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.adapter.HistoryAdapter +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import com.skyd.imomoe.view.adapter.compose.proxy.AnimeCover9Proxy +import com.skyd.imomoe.view.component.compose.* +import com.skyd.imomoe.viewmodel.HistoryUiState import com.skyd.imomoe.viewmodel.HistoryViewModel -class HistoryActivity : BaseActivity() { - private lateinit var viewModel: HistoryViewModel - private lateinit var adapter: HistoryAdapter - +class HistoryActivity : BaseComposeActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(HistoryViewModel::class.java) - adapter = HistoryAdapter(this, viewModel.historyList) - - mBinding.run { - tbHistoryActivity.ivToolbar1Back.setOnClickListener { finish() } - tbHistoryActivity.tvToolbar1Title.text = getString(R.string.watch_history) - - srlHistoryActivity.setColorSchemeColors( - this@HistoryActivity.getResColor(R.color.main_color_skin) - ) - srlHistoryActivity.setOnRefreshListener { viewModel.getHistoryList() } - - rvHistoryActivity.layoutManager = LinearLayoutManager(this@HistoryActivity) - rvHistoryActivity.adapter = adapter + setContentBase { + HistoryScreen() } + } +} +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HistoryScreen(viewModel: HistoryViewModel = hiltViewModel()) { + val context = LocalContext.current + var showDeleteAllWarningDialog by remember { + mutableStateOf(false) + } - viewModel.mldHistoryList.observe(this, Observer { - adapter.notifyDataSetChanged() - mBinding.srlHistoryActivity.isRefreshing = false - if (it) { - if (viewModel.historyList.isEmpty()) showLoadFailedTip( - getString(R.string.no_history), - null + Scaffold(topBar = { + AnimeTopBar( + title = { + Text(text = stringResource(R.string.watch_history)) + }, + navigationIcon = { + BackIcon( + onClick = { context.activity.finish() } + ) + }, + actions = { + TopBarIcon( + imageVector = Icons.Rounded.Delete, + contentDescription = stringResource(id = R.string.history_activity_menu_delete_all), + onClick = { + showDeleteAllWarningDialog = true + } ) } - }) - - viewModel.mldDeleteHistory.observe(this, Observer { - if (viewModel.historyList.isEmpty()) showLoadFailedTip( - getString(R.string.no_history), - null - ) - if (it >= 0) adapter.notifyItemRemoved(it) - }) - - viewModel.mldDeleteAllHistory.observe(this, Observer { - showLoadFailedTip(getString(R.string.no_history), null) - if (it > 0) adapter.notifyItemRangeRemoved(0, it) - }) - - mBinding.tbHistoryActivity.run { - 12.dp.let { padding -> - ivToolbar1Button1.setPadding(padding, padding, padding, padding) + ) + }) { padding -> + val swipeRefreshState = rememberSwipeRefreshState( + isRefreshing = viewModel.uiState.value is HistoryUiState.Refreshing + ) + val uiState = viewModel.uiState.collectAsState() + when (val uiStateValue = uiState.value) { + is HistoryUiState.Error -> { + ImageTextPlaceholder( + modifier = Modifier.padding(WindowInsets.navigationBars.asPaddingValues()), + message = uiStateValue.message.ifBlank { stringResource(id = R.string.get_data_failed) } + ) } - ivToolbar1Button1.visible() - ivToolbar1Button1.setImageResource(R.drawable.ic_delete_white_24) - ivToolbar1Button1.setOnClickListener { - if (viewModel.historyList.isEmpty()) return@setOnClickListener - MaterialDialog(this@HistoryActivity).show { - icon(R.drawable.ic_delete_main_color_2_24_skin) - title(res = R.string.warning) - message(text = "确定要删除所有观看历史记录?") - positiveButton(res = R.string.delete) { viewModel.deleteAllHistory() } - negativeButton(res = R.string.cancel) { dismiss() } + is HistoryUiState.WithData -> { + SwipeRefresh( + modifier = Modifier.padding(padding), + state = swipeRefreshState, + onRefresh = { + viewModel.getHistoryList() + } + ) { + val dataList = uiStateValue.dataList ?: return@SwipeRefresh + if (dataList.isEmpty()) { + ImageTextPlaceholder( + modifier = Modifier.padding(WindowInsets.navigationBars.asPaddingValues()), + message = stringResource(id = R.string.no_history) + ) + } else { + HistoryList(dataList) + } } } } - } - override fun getBinding(): ActivityHistoryBinding = - ActivityHistoryBinding.inflate(layoutInflater) - - override fun onResume() { - super.onResume() - - mBinding.srlHistoryActivity.isRefreshing = true - viewModel.getHistoryList() + if (showDeleteAllWarningDialog) { + MessageDialog( + icon = Icons.Rounded.Warning, + message = stringResource(id = R.string.confirm_delete_all_watch_history), + positiveText = stringResource(R.string.delete), + onPositive = { + showDeleteAllWarningDialog = false + viewModel.deleteAllHistory() + }, + onNegative = { + showDeleteAllWarningDialog = false + }, + onDismissRequest = { + showDeleteAllWarningDialog = false + } + ) + } } +} - fun deleteHistory(historyBean: HistoryBean) { - viewModel.deleteHistory(historyBean) +@Composable +private fun HistoryList(dataList: List, viewModel: HistoryViewModel = hiltViewModel()) { + val adapter = remember { + LazyGridAdapter( + mutableListOf( + AnimeCover9Proxy(onDeleteButtonClickListener = { _, data -> + viewModel.deleteHistory(data) + }) + ) + ) } - - override fun getLoadFailedTipView(): ViewStub? = mBinding.layoutHistoryActivityNoHistory -} + AnimeLazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + dataList = dataList, + adapter = adapter, + contentPadding = WindowInsets.navigationBars.asPaddingValues() + + PaddingValues(vertical = 7.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/LicenseActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/LicenseActivity.kt index ffc05003..c377a91d 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/LicenseActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/LicenseActivity.kt @@ -1,168 +1,250 @@ package com.skyd.imomoe.view.activity import android.os.Bundle -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.skyd.imomoe.R -import com.skyd.imomoe.bean.LicenseBean -import com.skyd.imomoe.config.Const.ActionUrl -import com.skyd.imomoe.config.Const.ViewHolderTypeString -import com.skyd.imomoe.databinding.ActivityLicenseBinding -import com.skyd.imomoe.view.adapter.LicenseAdapter +import com.skyd.imomoe.bean.License1Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.buildRouteUri +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.route.processor.OpenBrowserProcessor +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.BackIcon -class LicenseActivity : BaseActivity() { - private val list: MutableList = ArrayList() - private val adapter: LicenseAdapter = LicenseAdapter(this, list) - +class LicenseActivity : BaseComposeActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - mBinding.run { - llLicenseActivityToolbar.tvToolbar1Title.text = getString(R.string.open_source_licenses) - llLicenseActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } - rvLicenseActivity.layoutManager = LinearLayoutManager(this@LicenseActivity) - rvLicenseActivity.adapter = adapter + setContentBase { + LicenseScreen() } + } +} - list.add(LicenseBean(ViewHolderTypeString.LICENSE_HEADER_1, "", "", "", "")) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/jhy/jsoup", - "jsoup", - "MIT License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/coil-kt/coil", - "coil", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/CarGuo/GSYVideoPlayer", - "GSYVideoPlayer", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/square/okhttp", - "okhttp", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/square/retrofit", - "retrofit", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/getActivity/XXPermissions", - "XXPermissions", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/Kotlin/kotlinx.coroutines", - "kotlinx.coroutines", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/afollestad/material-dialogs", - "material-dialogs", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/lingochamp/FileDownloader", - "FileDownloader", - "Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/4thline/cling", - "cling", - "LGPL License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/eclipse/jetty.project", - "jetty.project", - "EPL-2.0, Apache-2.0 License" - ) - ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/NanoHttpd/nanohttpd", - "nanohttpd", - "BSD-3-Clause License" +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicenseScreen() { + val context = LocalContext.current + Scaffold( + topBar = { + AnimeTopBar( + title = { + Text(text = stringResource(R.string.open_source_licenses)) + }, + navigationIcon = { + BackIcon(onClick = { context.activity.finish() }) + } ) + } + ) { + val dataList = remember { initData() } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentPadding = WindowInsets.navigationBars.asPaddingValues() + ) { + item { + LicenseHeader() + } + items(items = dataList) { item -> + LicenseItem(item.title, item.license) { + item.route.route(context) + } + } + } + } +} + +@Composable +fun LicenseHeader() { + Row( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 10.dp) + ) { + Text( + modifier = Modifier.weight(1f), text = stringResource(id = R.string.license_name), + style = MaterialTheme.typography.titleMedium ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/greenrobot/EventBus", - "EventBus", - "Apache-2.0 License" - ) + Text( + modifier = Modifier.weight(1f), text = stringResource(id = R.string.license), + style = MaterialTheme.typography.titleMedium ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/scwang90/SmartRefreshLayout", - "SmartRefreshLayout", - "Apache-2.0 License" - ) + } +} + +@Composable +fun LicenseItem(name: String, license: String, onClick: (() -> Unit)? = null) { + Row(modifier = Modifier + .run { + if (onClick == null) this + else clickable(onClick = onClick) + } + .padding(horizontal = 16.dp, vertical = 10.dp)) { + Text( + modifier = Modifier.weight(1f), text = name, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium ) - list.add( - LicenseBean( - ViewHolderTypeString.LICENSE_1, - ActionUrl.ANIME_BROWSER, - "https://github.com/bilibili/DanmakuFlameMaster", - "DanmakuFlameMaster", - "Apache-2.0 License" - ) + Text( + modifier = Modifier.weight(1f), text = license, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium ) - adapter.notifyDataSetChanged() } +} - override fun getBinding(): ActivityLicenseBinding = - ActivityLicenseBinding.inflate(layoutInflater) +private fun initData(): List { + val list: MutableList = ArrayList() + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://source.android.com/") + }.toString(), + "Android Open Source Project", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/google/accompanist") + }.toString(), + "Accompanist", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/jhy/jsoup") + }.toString(), + "jsoup", + "MIT License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/coil-kt/coil") + }.toString(), + "Coil", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/CarGuo/GSYVideoPlayer") + }.toString(), + "GSYVideoPlayer", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/square/okhttp") + }.toString(), + "OkHttp", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/square/retrofit") + }.toString(), + "Retrofit", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/getActivity/XXPermissions") + }.toString(), + "XXPermissions", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/Kotlin/kotlinx.coroutines") + }.toString(), + "kotlinx.coroutines", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/AriaLyy/Aria") + }.toString(), + "Aria", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/4thline/cling") + }.toString(), + "Cling", + "LGPL License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/eclipse/jetty.project") + }.toString(), + "Eclipse Jetty", + "EPL-2.0, Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/NanoHttpd/nanohttpd") + }.toString(), + "NanoHTTPD", + "BSD-3-Clause License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/greenrobot/EventBus") + }.toString(), + "EventBus", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/scwang90/SmartRefreshLayout") + }.toString(), + "SmartRefreshLayout", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/KwaiAppTeam/AkDanmaku") + }.toString(), + "AkDanmaku", + "MIT License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/thegrizzlylabs/sardine-android") + }.toString(), + "sardine-android", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/apache/commons-text") + }.toString(), + "Apache Commons Text", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/vadiole/colorpicker") + }.toString(), + "Color Picker", + "Apache-2.0 License" + ) + list += License1Bean( + OpenBrowserProcessor.route.buildRouteUri { + appendQueryParameter("url", "https://github.com/re-ovo/iwara4a") + }.toString(), + "Iwara4A", + "Apache-2.0 License" + ) + return list } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/MainActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/MainActivity.kt index 70f4571e..aa833c7e 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/MainActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/MainActivity.kt @@ -1,267 +1,135 @@ package com.skyd.imomoe.view.activity import android.content.Intent -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager -import android.graphics.drawable.Icon -import android.os.Build +import android.net.Uri import android.os.Bundle -import android.text.Html -import android.widget.Toast -import androidx.fragment.app.FragmentTransaction -import com.afollestad.materialdialogs.MaterialDialog -import com.skyd.imomoe.App +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.navigation.NavigationBarView import com.skyd.imomoe.R import com.skyd.imomoe.config.Const -import com.skyd.imomoe.config.Const.ShortCuts.Companion.ACTION_EVERYDAY -import com.skyd.imomoe.config.Const.ShortCuts.Companion.ID_DOWNLOAD -import com.skyd.imomoe.config.Const.ShortCuts.Companion.ID_EVERYDAY -import com.skyd.imomoe.config.Const.ShortCuts.Companion.ID_FAVORITE import com.skyd.imomoe.databinding.ActivityMainBinding -import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.ext.* import com.skyd.imomoe.util.Util.getUserNoticeContent import com.skyd.imomoe.util.Util.lastReadUserNoticeVersion import com.skyd.imomoe.util.Util.setReadUserNoticeVersion -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.clickScale import com.skyd.imomoe.util.eventbus.EventBusSubscriber import com.skyd.imomoe.util.eventbus.MessageEvent -import com.skyd.imomoe.util.eventbus.RefreshEvent import com.skyd.imomoe.util.eventbus.SelectHomeTabEvent +import com.skyd.imomoe.util.logE +import com.skyd.imomoe.util.registerShortcuts +import com.skyd.imomoe.util.showToast import com.skyd.imomoe.util.update.AppUpdateHelper import com.skyd.imomoe.util.update.AppUpdateStatus import com.skyd.imomoe.view.fragment.EverydayAnimeFragment import com.skyd.imomoe.view.fragment.HomeFragment import com.skyd.imomoe.view.fragment.MoreFragment -import com.umeng.message.PushAgent -import org.greenrobot.eventbus.EventBus +import dagger.hilt.android.AndroidEntryPoint import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import javax.inject.Inject +@AndroidEntryPoint class MainActivity : BaseActivity(), EventBusSubscriber { - private var selectedTab = -1 + @Inject + lateinit var appUpdateHelper: AppUpdateHelper private var backPressTime = 0L - private var homeFragment: HomeFragment? = null - private var everydayAnimeFragment: EverydayAnimeFragment? = null - private var moreFragment: MoreFragment? = null + private val adapter: VpAdapter by lazy { VpAdapter(this) } private lateinit var action: String override fun onCreate(savedInstanceState: Bundle?) { + // Handle the splash screen transition. + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) - action = intent.action ?: "" - - PushAgent.getInstance(this).onAppStart() - - if (DataSourceManager.useCustomDataSource) - getString(R.string.using_custom_data_source).showToast(Toast.LENGTH_LONG) + if (screenIsLand) { + mBinding.nvMainActivity.addFitsSystemWindows(top = true, bottom = true, left = true) + } else { + mBinding.nvMainActivity.addFitsSystemWindows(bottom = true, left = true) + } if (lastReadUserNoticeVersion() < Const.Common.USER_NOTICE_VERSION) { - MaterialDialog(this).show { - title(res = R.string.user_notice) - message(text = Html.fromHtml(getUserNoticeContent())) - cancelable(false) - positiveButton(res = R.string.ok) { + showMessageDialog( + title = getString(R.string.user_notice_update), + message = getUserNoticeContent().toHtml(), + cancelable = false, + positiveText = getString(R.string.agree), + onPositive = { _, _ -> setReadUserNoticeVersion(Const.Common.USER_NOTICE_VERSION) - } - } - } + initData() + initializeFlurry(application) + }, + negativeText = getString(R.string.disagree_and_exit), + onNegative = { _, _ -> finish() } + ) + } else initData() + } - //检查更新 - val appUpdateHelper = AppUpdateHelper.instance - appUpdateHelper.getUpdateStatus().observe(this, { + private fun initData() { + doIntent(intent) + action = intent.action.orEmpty() + // 检查更新 + appUpdateHelper.getUpdateStatus().collectWithLifecycle(this) { when (it) { AppUpdateStatus.UNCHECK -> appUpdateHelper.checkUpdate() - AppUpdateStatus.DATED -> appUpdateHelper.noticeUpdate(this) + AppUpdateStatus.DATED -> appUpdateHelper.noticeUpdate(this@MainActivity) else -> Unit } - }) - - if (savedInstanceState != null) { - homeFragment = supportFragmentManager.getFragment( - savedInstanceState, - HOME_FRAGMENT_KEY - ) as HomeFragment? - everydayAnimeFragment = supportFragmentManager.getFragment( - savedInstanceState, - EVERYDAY_ANIME_FRAGMENT_KEY - ) as EverydayAnimeFragment? - moreFragment = supportFragmentManager.getFragment( - savedInstanceState, - MORE_FRAGMENT_KEY - ) as MoreFragment? - setTabSelection(savedInstanceState.getInt(SELECTED_TAB)) - } else { - if (action == ACTION_EVERYDAY) setTabSelection(1) - else setTabSelection(0) - } - - mBinding.run { - clHomeButton.setOnClickListener { - it.clickScale(0.8f) - setTabSelection(0) - } - - clEverydayAnimeButton.setOnClickListener { - it.clickScale(0.8f) - setTabSelection(1) - } - - clMoreButton.setOnClickListener { - it.clickScale(0.8f) - setTabSelection(2) - } } - registerShortcuts() - } - - /** - * - * 设置app图标快捷菜单 - */ - private fun registerShortcuts() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - val mShortcutManager = getSystemService(ShortcutManager::class.java) - val shortcutInfoList = listOf( - ShortcutInfo.Builder(this, ID_FAVORITE) - .setShortLabel(getString(R.string.shortcuts_favorite_short)) - .setLongLabel(getString(R.string.shortcuts_favorite_long)) - .setIcon( - Icon.createWithResource(this, R.drawable.layerlist_shortcuts_favorite_24) - ) - .setIntent( - Intent(this, FavoriteActivity::class.java).setAction(Intent.ACTION_VIEW) - ) - .build(), - ShortcutInfo.Builder(this, ID_EVERYDAY) - .setShortLabel(getString(R.string.shortcuts_everyday_short)) - .setLongLabel(getString(R.string.shortcuts_everyday_long)) - .setIcon( - Icon.createWithResource(this, R.drawable.layerlist_shortcuts_everyday_24) - ) - .setIntent( - Intent(this, MainActivity::class.java).setAction(ACTION_EVERYDAY) - ) - .build(), - ShortcutInfo.Builder(this, ID_DOWNLOAD) - .setShortLabel(getString(R.string.shortcuts_download_short)) - .setLongLabel(getString(R.string.shortcuts_download_long)) - .setIcon( - Icon.createWithResource(this, R.drawable.layerlist_shortcuts_download_24) - ) - .setIntent( - Intent(this, AnimeDownloadActivity::class.java) - .setAction(Intent.ACTION_VIEW) - ) - .build() - ) - mShortcutManager.dynamicShortcuts = shortcutInfoList + mBinding.vp2MainActivity.also { + it.adapter = adapter + it.offscreenPageLimit = adapter.itemCount + it.isUserInputEnabled = false } - } - - override fun getBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater) - private fun setTabSelection(index: Int) { - // 如果已经选中了,则刷新 - if (selectedTab == index) { - EventBus.getDefault().post(RefreshEvent()) - return - } - clearAllSelected() - supportFragmentManager.beginTransaction().apply { - hideFragments(this) - when (index) { - 0 -> { - mBinding.ivControlBarHome.isSelected = true - mBinding.tvControlBarHome.isSelected = true - if (homeFragment == null) { - val fragment = HomeFragment() - homeFragment = fragment - add(R.id.fl_main_activity_fragment_container, fragment) - } else { - homeFragment?.run { - show(this) - } - } + (mBinding.nvMainActivity as NavigationBarView).setOnItemSelectedListener { item -> + when (item.itemId) { + R.id.home_fragment -> { + mBinding.vp2MainActivity.setCurrentItem(0, false) + true } - 1 -> { - mBinding.ivControlBarEverydayAnime.isSelected = true - mBinding.tvControlBarEverydayAnime.isSelected = true - if (everydayAnimeFragment == null) { - val fragment = EverydayAnimeFragment() - everydayAnimeFragment = fragment - add(R.id.fl_main_activity_fragment_container, fragment) - } else { - everydayAnimeFragment?.run { - show(this) - } - } + R.id.everyday_anime_fragment -> { + mBinding.vp2MainActivity.setCurrentItem(1, false) + true } - 2 -> { - mBinding.ivControlBarMore.isSelected = true - mBinding.tvControlBarMore.isSelected = true - if (moreFragment == null) { - val fragment = MoreFragment() - moreFragment = fragment - add(R.id.fl_main_activity_fragment_container, fragment) - } else { - moreFragment?.run { - show(this) - } - } + R.id.more_fragment -> { + mBinding.vp2MainActivity.setCurrentItem(2, false) + true } else -> { - mBinding.ivControlBarHome.isSelected = true - mBinding.tvControlBarHome.isSelected = true - if (homeFragment == null) { - val fragment = HomeFragment() - homeFragment = fragment - add(R.id.fl_main_activity_fragment_container, fragment) - } else { - homeFragment?.run { - show(this) - } - } + false } } - selectedTab = index - }.commitAllowingStateLoss() + } + + registerShortcuts() } - private fun clearAllSelected() { - mBinding.run { - ivControlBarHome.isSelected = false - tvControlBarHome.isSelected = false - ivControlBarEverydayAnime.isSelected = false - tvControlBarEverydayAnime.isSelected = false - ivControlBarMore.isSelected = false - tvControlBarMore.isSelected = false - } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + doIntent(intent) } - private fun hideFragments(transaction: FragmentTransaction) { - transaction.run { - homeFragment?.run { - hide(this) - } - everydayAnimeFragment?.run { - hide(this) - } - moreFragment?.run { - hide(this) - } + // TODO + private fun doIntent(intent: Intent?) { + val uri: Uri = intent?.data ?: return + runCatching { + + }.onFailure { + logE(it.message.toString()) + it.message?.showToast() } } + override fun getBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater) + private fun processBackPressed() { val now = System.currentTimeMillis() if (now - backPressTime > 2000) { - String.format( - App.context.getString(R.string.press_again_to_exit), - App.context.getString(App.context.applicationInfo.labelRes) - ).showToast() + getString(R.string.press_again_to_exit).showToast() backPressTime = now } else { super.onBackPressed() @@ -276,33 +144,24 @@ class MainActivity : BaseActivity(), EventBusSubscriber { } } - override fun onSaveInstanceState(outState: Bundle) { - homeFragment?.let { - supportFragmentManager.putFragment(outState, HOME_FRAGMENT_KEY, it) - } - everydayAnimeFragment?.let { - supportFragmentManager.putFragment(outState, EVERYDAY_ANIME_FRAGMENT_KEY, it) - } - moreFragment?.let { - supportFragmentManager.putFragment(outState, MORE_FRAGMENT_KEY, it) - } - outState.putInt(SELECTED_TAB, selectedTab) - super.onSaveInstanceState(outState) - } - @Subscribe(threadMode = ThreadMode.MAIN) override fun onMessageEvent(event: MessageEvent) { when (event) { is SelectHomeTabEvent -> { - setTabSelection(0) + mBinding.vp2MainActivity.setCurrentItem(0, false) } } } - companion object { - private const val HOME_FRAGMENT_KEY = "homeFragment" - private const val EVERYDAY_ANIME_FRAGMENT_KEY = "everydayAnimeFragment" - private const val MORE_FRAGMENT_KEY = "moreFragment" - private const val SELECTED_TAB = "selectedTab" + class VpAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { + + override fun getItemCount() = 3 + + override fun createFragment(position: Int) = when (position) { + 0 -> HomeFragment() + 1 -> EverydayAnimeFragment() + 2 -> MoreFragment() + else -> HomeFragment() + } } } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/MonthAnimeActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/MonthAnimeActivity.kt index dba022ce..01f31a62 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/MonthAnimeActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/MonthAnimeActivity.kt @@ -1,81 +1,77 @@ package com.skyd.imomoe.view.activity import android.os.Bundle -import android.view.View import android.view.ViewStub -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.activity.viewModels +import androidx.recyclerview.widget.GridLayoutManager import com.skyd.imomoe.R import com.skyd.imomoe.databinding.ActivityMonthAnimeBinding -import com.skyd.imomoe.util.Util -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.view.adapter.SearchAdapter +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeCover3Proxy import com.skyd.imomoe.viewmodel.MonthAnimeViewModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MonthAnimeActivity : BaseActivity() { - private var partUrl: String = "" - private lateinit var viewModel: MonthAnimeViewModel - private lateinit var adapter: SearchAdapter + private val viewModel: MonthAnimeViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { VarietyAdapter(mutableListOf(AnimeCover3Proxy())) } private var lastRefreshTime: Long = System.currentTimeMillis() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - partUrl = intent.getStringExtra("partUrl") ?: "" - - viewModel = ViewModelProvider(this).get(MonthAnimeViewModel::class.java) - adapter = SearchAdapter(this, viewModel.monthAnimeList) + viewModel.partUrl = intent.getStringExtra("partUrl").orEmpty() mBinding.run { - llMonthAnimeActivityToolbar.tvToolbar1Title.text = getString( - R.string.year_month_anime, - partUrl - ) + tbMonthAnimeActivity.title = getString(R.string.year_month_anime, viewModel.partUrl) + tbMonthAnimeActivity.setNavigationOnClickListener { finish() } + ablMonthAnimeActivity.addFitsSystemWindows(right = true, top = true) + rvMonthAnimeActivity.addFitsSystemWindows(right = true, bottom = true) - rvMonthAnimeActivity.layoutManager = LinearLayoutManager(this@MonthAnimeActivity) - rvMonthAnimeActivity.setHasFixedSize(true) + rvMonthAnimeActivity.layoutManager = GridLayoutManager( + this@MonthAnimeActivity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { spanSizeLookup = AnimeShowSpanSize(adapter) } + rvMonthAnimeActivity.addItemDecoration(AnimeShowItemDecoration()) rvMonthAnimeActivity.adapter = adapter - llMonthAnimeActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } srlMonthAnimeActivity.setOnRefreshListener { //避免刷新间隔太短 if (System.currentTimeMillis() - lastRefreshTime > 500) { lastRefreshTime = System.currentTimeMillis() - viewModel.getMonthAnimeData(partUrl) + viewModel.getMonthAnimeData(viewModel.partUrl) } else { srlMonthAnimeActivity.closeHeaderOrFooter() } } - srlMonthAnimeActivity.setOnLoadMoreListener { - viewModel.pageNumberBean?.let { - viewModel.getMonthAnimeData(it.actionUrl, isRefresh = false) - return@setOnLoadMoreListener - } - mBinding.srlMonthAnimeActivity.finishLoadMore() - getString(R.string.no_more_info).showToast() - } + srlMonthAnimeActivity.setOnLoadMoreListener { viewModel.loadMoreMonthAnimeData() } } - viewModel.mldMonthAnimeList.observe(this, Observer { - mBinding.srlMonthAnimeActivity.closeHeaderOrFooter() - if (it) { - hideLoadFailedTip() - } else { - showLoadFailedTip( - getString(R.string.load_data_failed_click_to_retry), - View.OnClickListener { - viewModel.getMonthAnimeData(partUrl) - hideLoadFailedTip() - }) + viewModel.monthAnimeList.collectWithLifecycle(this) { data -> + when (data) { + is DataState.Empty -> mBinding.srlMonthAnimeActivity.autoRefresh() + is DataState.Success -> { + hideLoadFailedTip() + mBinding.srlMonthAnimeActivity.closeHeaderOrFooter() + adapter.dataList = data.data + } + is DataState.Error -> { + adapter.dataList = emptyList() + showLoadFailedTip { + viewModel.getMonthAnimeData(viewModel.partUrl) + } + mBinding.srlMonthAnimeActivity.closeHeaderOrFooter() + } + else -> {} } - adapter.notifyDataSetChanged() - }) - - mBinding.srlMonthAnimeActivity.autoRefresh() + } } - override fun getBinding(): ActivityMonthAnimeBinding = - ActivityMonthAnimeBinding.inflate(layoutInflater) + override fun getBinding() = ActivityMonthAnimeBinding.inflate(layoutInflater) - override fun getLoadFailedTipView(): ViewStub? = mBinding.layoutMonthAnimeActivityLoadFailed + override fun getLoadFailedTipView(): ViewStub = mBinding.layoutMonthAnimeActivityLoadFailed } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/NoticeActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/NoticeActivity.kt deleted file mode 100644 index 7f73d67e..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/activity/NoticeActivity.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.skyd.imomoe.view.activity - -import android.os.Bundle -import android.text.Html -import com.skyd.imomoe.databinding.ActivityNoticeBinding -import java.io.UnsupportedEncodingException -import java.net.URLDecoder - -class NoticeActivity : BaseActivity() { - companion object { - const val PARAM = "param" - const val TOOLBAR_TITLE = "toolbarTitle" - const val TITLE = "title" - const val CONTENT = "content" - } - - private val paramMap: HashMap = HashMap() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - (intent.getStringExtra(PARAM) ?: "").split("&").forEachIndexed { index, s -> - s.split("=").let { - if (it.size != 2) return@let - try { - // 此处URL解码,因此要求传入的参数需要经过URL编码!!! - paramMap[it[0]] = URLDecoder.decode(it[1], "UTF-8") - } catch (e: UnsupportedEncodingException) { - e.printStackTrace() - } - } - } - - mBinding.run { - llNoticeActivityToolbar.run { - ivToolbar1Back.setOnClickListener { finish() } - tvToolbar1Title.apply { - isFocused = true - text = paramMap[TOOLBAR_TITLE] ?: "通知" - } - } - tvNoticeActivityTitle.text = paramMap[TITLE] ?: "" - tvNoticeActivityContent.text = Html.fromHtml(paramMap[CONTENT] ?: "") - } - } - - override fun getBinding(): ActivityNoticeBinding = ActivityNoticeBinding.inflate(layoutInflater) -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/PlayActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/PlayActivity.kt index 4a3cf3f4..0a07dfd3 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/PlayActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/PlayActivity.kt @@ -1,406 +1,349 @@ package com.skyd.imomoe.view.activity import android.animation.ValueAnimator -import android.app.Dialog import android.content.Intent import android.content.res.Configuration import android.graphics.Color import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.appcompat.widget.Toolbar -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.ViewCompat -import androidx.core.widget.NestedScrollView -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider +import androidx.activity.viewModels +import androidx.core.content.ContextCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.appbar.CollapsingToolbarLayout -import com.google.android.material.bottomsheet.BottomSheetDialog import com.shuyu.gsyvideoplayer.GSYVideoManager import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder import com.shuyu.gsyvideoplayer.model.VideoOptionModel -import com.shuyu.gsyvideoplayer.player.PlayerFactory -import com.shuyu.gsyvideoplayer.utils.GSYVideoType import com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer -import com.shuyu.gsyvideoplayer.video.base.GSYVideoView.CURRENT_STATE_AUTO_COMPLETE -import com.skyd.imomoe.App +import com.shuyu.gsyvideoplayer.video.base.GSYVideoView import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeEpisodeDataBean -import com.skyd.imomoe.bean.FavoriteAnimeBean import com.skyd.imomoe.config.Api -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.util.AnimeEpisode2ViewHolder -import com.skyd.imomoe.util.MD5.getMD5 -import com.skyd.imomoe.util.Util.dp -import com.skyd.imomoe.util.Util.getDetailLinkByEpisodeLink -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.Util.getResDrawable -import com.skyd.imomoe.util.Util.getSkinResourceId +import com.skyd.imomoe.databinding.ActivityPlayBinding +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.ext.theme.getAttrColor +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.Util import com.skyd.imomoe.util.Util.openVideoByExternalPlayer -import com.skyd.imomoe.util.Util.setColorStatusBar -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.downloadanime.AnimeDownloadHelper -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.html.SnifferVideo -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.adapter.AnimeDetailAdapter -import com.skyd.imomoe.view.adapter.PlayAdapter -import com.skyd.imomoe.view.adapter.decoration.AnimeEpisodeItemDecoration +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadHelper +import com.skyd.imomoe.util.showToast import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration -import com.skyd.imomoe.view.adapter.spansize.PlaySpanSize +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.* import com.skyd.imomoe.view.component.player.AnimeVideoPlayer +import com.skyd.imomoe.view.component.player.AnimeVideoPositionMemoryStore import com.skyd.imomoe.view.component.player.DanmakuVideoPlayer import com.skyd.imomoe.view.component.player.DetailPlayerActivity -import com.skyd.imomoe.view.component.textview.TypefaceTextView -import com.skyd.imomoe.view.fragment.MoreDialogFragment -import com.skyd.imomoe.view.fragment.ShareDialogFragment +import com.skyd.imomoe.view.component.player.danmaku.DanmakuManager +import com.skyd.imomoe.view.fragment.dialog.EpisodeDialogFragment +import com.skyd.imomoe.view.fragment.dialog.MoreDialogFragment +import com.skyd.imomoe.view.fragment.dialog.ShareDialogFragment import com.skyd.imomoe.viewmodel.PlayViewModel -import kotlinx.coroutines.* -import tv.danmaku.ijk.media.exo2.Exo2PlayerManager +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import tv.danmaku.ijk.media.player.IjkMediaPlayer import kotlin.math.abs -class PlayActivity : DetailPlayerActivity() { - override var statusBarSkin: Boolean = false - private var isFavorite: Boolean = false - private var favoriteBeanDataReady: Int = 0 - set(value) { - field = value - if (value == 2) ivPlayActivityFavorite.isEnabled = true - } - private var partUrl: String = "" - private var detailPartUrl: String = "" - private lateinit var viewModel: PlayViewModel - private lateinit var adapter: PlayAdapter - private var isFirstTime = true - private var danmuUrl: String = "" - private var danmuParamMap: HashMap = HashMap() +@AndroidEntryPoint +class PlayActivity : DetailPlayerActivity() { + private val viewModel: PlayViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter( + mutableListOf( + Header1Proxy(), + AnimeCover1Proxy(), + AnimeCover2Proxy(), + HorizontalRecyclerView1Proxy(onMoreButtonClickListener = { _, _, _ -> + EpisodeDialogFragment( + backgroundDim = false, + offsetFromTop = if (screenIsLand) { + null + } else { + mBinding.avpPlayActivity.height + } + ) { + title = getString(R.string.play_list) + dataList = viewModel.episodesList.value.readOrNull().orEmpty() + onEpisodeClick { _, data, index -> + viewModel.playAnotherEpisode(data.route, index) + dismiss() + } + val job = viewModel.episodesList.collectWithLifecycle(this) { + dataList = it.readOrNull().orEmpty() + } + onDismissListener = { job.cancel() } + }.show(supportFragmentManager, EpisodeDialogFragment.TAG) + }, onAnimeEpisodeClickListener = { _, data, index -> + viewModel.playAnotherEpisode(data.route, index) + }) + ) + ) + } private var currentNightMode: Int = 0 private var lastCanCollapsed: Boolean? = null - private lateinit var ivPlayActivityFavorite: ImageView - private lateinit var avpPlayActivity: DanmakuVideoPlayer - private var ivPlayActivityToolbarMore: ImageView? = null - private lateinit var rvPlayActivity: RecyclerView - private lateinit var srlPlayActivity: SwipeRefreshLayout - private lateinit var tvPlayActivityTitle: TypefaceTextView - private var nsvPlayActivity: NestedScrollView? = null - private var tvPlayActivityToolbarTitle: TextView? = null - private var ablPlayActivity: AppBarLayout? = null + override fun transparentSystemBar(): Boolean = false private fun initView() { - ivPlayActivityFavorite = findViewById(R.id.iv_play_activity_favorite) - ivPlayActivityToolbarMore = findViewById(R.id.iv_play_activity_toolbar_more) - rvPlayActivity = findViewById(R.id.rv_play_activity) - srlPlayActivity = findViewById(R.id.srl_play_activity) - tvPlayActivityTitle = findViewById(R.id.tv_play_activity_title) - nsvPlayActivity = findViewById(R.id.nsv_play_activity) - tvPlayActivityToolbarTitle = findViewById(R.id.tv_play_activity_toolbar_title) - ablPlayActivity = findViewById(R.id.abl_play_activity) - avpPlayActivity = findViewById(R.id.avp_play_activity) - - val tbPlayActivity: Toolbar? = findViewById(R.id.tb_play_activity) - val ctlPlayActivity: CollapsingToolbarLayout? = findViewById(R.id.ctl_play_activity) - val clPlayActivityToolbarLayout: ConstraintLayout? = - findViewById(R.id.cl_play_activity_toolbar_layout) - val ivPlayActivityToolbarBack: ImageView? = findViewById(R.id.iv_play_activity_toolbar_back) - - if (tbPlayActivity != null && ctlPlayActivity != null && - clPlayActivityToolbarLayout != null && ivPlayActivityToolbarBack != null && - tvPlayActivityToolbarTitle != null - ) { - setSupportActionBar(tbPlayActivity) - supportActionBar?.setDisplayShowTitleEnabled(false) - - ablPlayActivity?.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> - when { - abs(verticalOffset) > ctlPlayActivity.scrimVisibleHeightTrigger -> { - if (tbPlayActivity.visibility != View.VISIBLE) { - clPlayActivityToolbarLayout.visible() - tbPlayActivity.visible() + mBinding.apply { + root.addFitsSystemWindows(right = true, top = true) + rvPlayActivity.addFitsSystemWindows(right = true, bottom = true) + + if (ctlPlayActivity != null && ablPlayActivity != null) { + ablPlayActivity.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> + when { + abs(verticalOffset) > ctlPlayActivity.scrimVisibleHeightTrigger -> { + tvPlayActivityToolbarTitle?.visible(animate = true, dur = 200L) + } + else -> { + tvPlayActivityToolbarTitle?.gone() } } - else -> { - if (tbPlayActivity.visibility != View.GONE) { - clPlayActivityToolbarLayout.gone() - tbPlayActivity.gone(true, 600) + }) + } + + tbPlayActivity.setNavigationOnClickListener { finish() } + tbPlayActivity.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.menu_item_play_activity_share -> { + ShareDialogFragment().setShareContent(Api.MAIN_URL + viewModel.partUrl) + .show(supportFragmentManager, "share_dialog") + true + } + R.id.menu_item_play_activity_download -> { + EpisodeDialogFragment( + backgroundDim = false, + offsetFromTop = if (screenIsLand) { + null + } else { + mBinding.avpPlayActivity.height + } + ) { + title = this@PlayActivity.getString(R.string.download_anime) + dataList = viewModel.episodesList.value.readOrNull().orEmpty() + onEpisodeClick { _, data, index -> + getString(R.string.parsing_video).showToast() + viewModel.getAnimeDownloadUrl(data.route, index) + } + val job = viewModel.episodesList.collectWithLifecycle(this) { + dataList = it.readOrNull().orEmpty() + } + onDismissListener = { job.cancel() } + }.show(supportFragmentManager, EpisodeDialogFragment.TAG) + true + } + R.id.menu_item_play_activity_more -> { + MoreDialogFragment().apply { + show(supportFragmentManager, MoreDialogFragment.TAG) + onCancelButtonClick { dismiss() } + onDlnaButtonClick { + val url = avpPlayActivity.getUrl() + if (url == null) { + getString(R.string.please_wait_video_loaded).showToast() + return@onDlnaButtonClick + } + startActivity( + Intent(this@PlayActivity, DlnaActivity::class.java) + .putExtra("url", url) + .putExtra("title", avpPlayActivity.getTitle()) + ) + dismiss() + } + onOpenInOtherPlayerButtonClick { + if (!openVideoByExternalPlayer( + this@PlayActivity, + viewModel.animeEpisodeDataBean.videoUrl + ) + ) getString(R.string.matched_app_not_found).showToast() + dismiss() + } } + true } + else -> false } - }) + } - ivPlayActivityToolbarBack.setOnClickListener { finish() } tvPlayActivityToolbarTitle?.setOnClickListener { (avpPlayActivity.currentPlayer as AnimeVideoPlayer).clickStartIcon() } + + avpPlayActivity.setTopContainer(tbPlayActivity as? ViewGroup) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_play) - - initView() currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + window.statusBarColor = Color.BLACK + WindowInsetsControllerCompat(window, mBinding.root).isAppearanceLightStatusBars = false - setColorStatusBar(window, Color.BLACK) + initView() - viewModel = ViewModelProvider(this).get(PlayViewModel::class.java) viewModel.setActivity(this) - adapter = PlayAdapter(this, viewModel.playBeanDataList) initVideoBuilderMode() - avpPlayActivity.run { - getDownloadButton()?.setOnClickListener { getSheetDialog("download").show() } - // 设置返回按键功能 - backButton?.setOnClickListener { onBackPressed() } - } + viewModel.partUrl = intent.getStringExtra("partUrl").orEmpty() - partUrl = intent.getStringExtra("partUrl") ?: "" - detailPartUrl = intent.getStringExtra("detailPartUrl") ?: "" + mBinding.apply { + rvPlayActivity.layoutManager = GridLayoutManager( + this@PlayActivity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { + spanSizeLookup = AnimeShowSpanSize(adapter = adapter, enableLandScape = false) + } + rvPlayActivity.addItemDecoration(AnimeShowItemDecoration()) + rvPlayActivity.adapter = adapter - // 如果没有传入详情页面的网址,则通过播放页面的网址计算出详情页面的网址 - val const = DataSourceManager.getConst() ?: com.skyd.imomoe.model.impls.Const() - if (detailPartUrl.isBlank() || detailPartUrl == const.actionUrl.ANIME_DETAIL()) - detailPartUrl = getDetailLinkByEpisodeLink(partUrl) + srlPlayActivity.setOnRefreshListener { viewModel.getPlayData() } + avpPlayActivity.playPositionMemoryStore = AnimeVideoPositionMemoryStore + } - // 分享按钮 - avpPlayActivity.getShareButton()?.setOnClickListener { - ShareDialogFragment().setShareContent(Api.MAIN_URL + viewModel.partUrl) - .show(supportFragmentManager, "share_dialog") + viewModel.favorite.collectWithLifecycle(this) { + mBinding.ivPlayActivityFavorite.setImageResource( + if (it) R.drawable.ic_star_24 + else R.drawable.ic_star_border_24 + ) } - // 更多按钮 - View.OnClickListener { - MoreDialogFragment().run { - setOnClickListener( - arrayOf(View.OnClickListener { dismiss() }, - View.OnClickListener { - startActivity( - Intent(this@PlayActivity, DlnaActivity::class.java) - .putExtra("url", avpPlayActivity.getUrl()) - .putExtra("title", avpPlayActivity.getTitle()) - ) - dismiss() - }, View.OnClickListener { - if (!openVideoByExternalPlayer( - this@PlayActivity, - viewModel.animeEpisodeDataBean.videoUrl - ) - ) getString(R.string.matched_app_not_found).showToast() - dismiss() - }) - ) - show(supportFragmentManager, "more_dialog") + + mBinding.ivPlayActivityFavorite.setOnClickListener { + when (viewModel.favorite.value) { + true -> viewModel.deleteFavorite() + false -> viewModel.insertFavorite() } - }.let { - avpPlayActivity.getMoreButton()?.setOnClickListener(it) - ivPlayActivityToolbarMore?.setOnClickListener(it) } - rvPlayActivity.layoutManager = GridLayoutManager(this@PlayActivity, 4) - .apply { spanSizeLookup = PlaySpanSize(adapter) } - // 复用AnimeShow的ItemDecoration - rvPlayActivity.addItemDecoration(AnimeShowItemDecoration()) - rvPlayActivity.setHasFixedSize(true) - rvPlayActivity.adapter = adapter - - srlPlayActivity.setOnRefreshListener { viewModel.getPlayData(partUrl) } - srlPlayActivity.setColorSchemeResources(getSkinResourceId(R.color.main_color_skin)) - - lifecycleScope.launch(Dispatchers.IO) { - val favoriteAnime = getAppDataBase().favoriteAnimeDao().getFavoriteAnime(detailPartUrl) - runOnUiThread { - isFavorite = if (favoriteAnime == null) { - ivPlayActivityFavorite.setImageDrawable(getResDrawable(R.drawable.ic_star_border_main_color_2_24_skin)) - false - } else { - ivPlayActivityFavorite.setImageDrawable(getResDrawable(R.drawable.ic_star_main_color_2_24_skin)) - true + viewModel.playDataList.collectWithLifecycle(this) { + when (it) { + is DataState.Refreshing -> { + mBinding.srlPlayActivity.isRefreshing = true } - ivPlayActivityFavorite.setOnClickListener { - if (isFavorite) { - Thread { - getAppDataBase().favoriteAnimeDao().deleteFavoriteAnime(detailPartUrl) - }.start() - isFavorite = false - ivPlayActivityFavorite.setImageDrawable(getResDrawable(R.drawable.ic_star_border_main_color_2_24_skin)) - getString(R.string.remove_favorite_succeed).showToast() - } else { - Thread { - getAppDataBase().favoriteAnimeDao().insertFavoriteAnime( - FavoriteAnimeBean( - Const.ViewHolderTypeString.ANIME_COVER_8, "", - detailPartUrl, - viewModel.playBean?.title?.title ?: "", - System.currentTimeMillis(), - viewModel.animeCover, - lastEpisodeUrl = viewModel.partUrl, - lastEpisode = viewModel.animeEpisodeDataBean.title - ) - ) - }.start() - isFavorite = true - ivPlayActivityFavorite.setImageDrawable(getResDrawable(R.drawable.ic_star_main_color_2_24_skin)) - getString(R.string.favorite_succeed).showToast() + is DataState.Success -> { + mBinding.srlPlayActivity.isRefreshing = false + mBinding.tvPlayActivityTitle.text = viewModel.playBean.title.title + adapter.dataList = it.data + if (viewModel.isFirstTimeToPlay) { + mBinding.avpPlayActivity.currentPlayer.startPlay() + viewModel.isFirstTimeToPlay = false } } - } - } - ivPlayActivityFavorite.isEnabled = false + else -> { + mBinding.srlPlayActivity.isRefreshing = false + adapter.dataList = emptyList() - viewModel.mldAnimeCover.observe(this, { - if (it) { - favoriteBeanDataReady++ + } } - }) + } - viewModel.mldPlayBean.observe(this, { - srlPlayActivity.isRefreshing = false + viewModel.animeDownloadUrl.collectWithLifecycle(this) { + AnimeDownloadHelper.downloadAnime( + this@PlayActivity, + url = it.videoUrl, + animeTitle = viewModel.playBean.title.title, + animeEpisode = it.title + ) + } - val title = viewModel.playBean?.title?.title - tvPlayActivityTitle.text = title + viewModel.playAnotherEpisodeEvent.collectWithLifecycle(this) { + if (it) mBinding.avpPlayActivity.currentPlayer.startPlay() + } - adapter.notifyDataSetChanged() + viewModel.episodesList.collectWithLifecycle(this) { + mBinding.avpPlayActivity.setEpisodeAdapter( + VarietyAdapter( + mutableListOf( + PlayerEpisode1Proxy(onBindViewHolder = { holder, data, index, _ -> + holder.tvTitle.text = data.title + if (data.route == viewModel.animeEpisodeDataBean.route) { + holder.tvTitle.setTextColor(getAttrColor(R.attr.colorPrimary)) + (mBinding.avpPlayActivity.currentPlayer as AnimeVideoPlayer) + .rvEpisode?.scrollToPosition(index) + } else { + holder.tvTitle.setTextColor( + ContextCompat.getColor( + this@PlayActivity, + android.R.color.white + ) + ) + } + holder.itemView.setOnClickListener { + mBinding.avpPlayActivity.currentPlayer.run { + if (this is AnimeVideoPlayer) { + getRightContainer()?.gone() + // 因为右侧界面显示时,不在xx秒后隐藏界面,所以要恢复xx秒后隐藏控制界面 + enableDismissControlViewTimer(true) + } + } + viewModel.playAnotherEpisode(data.route, index) + } + true + }) + ) + ).apply { dataList = viewModel.episodesList.value.readOrNull().orEmpty() } + ) + } - favoriteBeanDataReady++ + viewModel.loadingEpisodeData.collectWithLifecycle(this) { + mBinding.avpPlayActivity.currentPlayer.release() + } - if (isFirstTime) { - avpPlayActivity.startPlay() - isFirstTime = false - } - }) - - //缓存番剧调用getAnimeEpisodeData()来获取视频url - viewModel.mldGetAnimeEpisodeData.observe(this, Observer { - val url = viewModel.episodesList[it].videoUrl - if (url.endsWith("\$qzz", true)) { - SnifferVideo.getQzzVideoUrl( - this@PlayActivity, viewModel.episodesList[it].actionUrl, detailPartUrl - ) { videoUrl, paramMap -> -// danmuUrl = danMuUrl -// danmuParamMap.clear() -// danmuParamMap.putAll(paramMap) - runOnUiThread { - AnimeDownloadHelper.instance.downloadAnime( - this, videoUrl, getMD5(videoUrl), - viewModel.playBean?.title?.title + "/" + - viewModel.episodesList[it].title - ) - } - } - return@Observer - } else { - AnimeDownloadHelper.instance.downloadAnime( - this, url, getMD5(url), - viewModel.playBean?.title?.title + "/" + - viewModel.episodesList[it].title - ) + mBinding.avpPlayActivity.onPlayNextEpisode = { + if (!viewModel.playNextEpisode()) { + getString(R.string.have_no_next_episode).showToast() } - }) - - viewModel.mldAnimeEpisodeDataRefreshed.observe(this, { - if (it) avpPlayActivity.currentPlayer - .startPlay(partUrl = viewModel.animeEpisodeDataBean.actionUrl) - }) + } - srlPlayActivity.isRefreshing = true - viewModel.getPlayData(partUrl) - viewModel.getAnimeCoverImageBean(detailPartUrl) + if (viewModel.isFirstTimeToPlay) { + mBinding.srlPlayActivity.isRefreshing = true + viewModel.getPlayData() + } val videoOptionModel = VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1) GSYVideoManager.instance().optionModelList = listOf(videoOptionModel) } - fun startPlay(url: String, currentEpisodeIndex: Int, title: String) { - viewModel.refreshAnimeEpisodeData(url, currentEpisodeIndex, title) - } - - fun startPlay2(url: String, title: String, partUrl: String = this@PlayActivity.partUrl) { - avpPlayActivity.startPlay(url, title, partUrl) - } - - private fun GSYBaseVideoPlayer.startPlay( - url: String = "", - title: String = "", - partUrl: String = this@PlayActivity.partUrl - ) { - PlayerFactory.setPlayManager(Exo2PlayerManager().javaClass) - GSYVideoType.disableMediaCodec() // 关闭硬解码 - //设置播放URL - if (url.isBlank()) { - if (!isDestroyed) { - viewModel.updateFavoriteData( - detailPartUrl, - viewModel.partUrl, - viewModel.animeEpisodeDataBean.title, - System.currentTimeMillis() - ) - viewModel.insertHistoryData(detailPartUrl) + override fun getBinding() = ActivityPlayBinding.inflate(layoutInflater) + + private fun GSYBaseVideoPlayer.startPlay() { + if (isDestroyed) return + DanmakuManager.enableDanmaku = true + currentPlayer.apply { + val videoUrl = viewModel.animeEpisodeDataBean.videoUrl + val episodeTitle = viewModel.animeEpisodeDataBean.title + val animeTitle = viewModel.playBean.title.title + if (this is AnimeVideoPlayer) { + this.animeTitle = animeTitle } - if (!viewModel.animeEpisodeDataBean.videoUrl.endsWith("\$qzz", true)) { - danmuUrl = "" - setUp( - viewModel.animeEpisodeDataBean.videoUrl, - false, viewModel.animeEpisodeDataBean.title - ) - } else { - SnifferVideo.getQzzVideoUrl( - this@PlayActivity, - partUrl, - detailPartUrl - ) { videoUrl, paramMap -> - danmuParamMap.clear() - danmuParamMap.putAll(paramMap) - danmuUrl = paramMap[SnifferVideo.DANMU_URL] ?: "" - runOnUiThread { - setUp(videoUrl, false, viewModel.animeEpisodeDataBean.title) - //开始播放 + mBinding.tbPlayActivity.title = episodeTitle + // 设置播放URL + viewModel.updateFavoriteData() + viewModel.insertHistoryData() + setUp(videoUrl, false, episodeTitle) + lifecycleScope.launch { + val playPosition = AnimeVideoPositionMemoryStore.getPlayPosition(videoUrl) + // 若用户设置了自动跳转 且 没有播放完 + if (playPosition != null && playPosition != -1L && sharedPreferences() + .getBoolean("autoJumpToLastPosition", true) + ) seekOnStart = playPosition + withContext(Dispatchers.Main) { + if (!activityInBackground) { + // 开始播放 startPlayLogic() } } - return - } - } else { - if (!isDestroyed) { - viewModel.updateFavoriteData( - detailPartUrl, viewModel.partUrl, title, - System.currentTimeMillis() - ) - viewModel.insertHistoryData(detailPartUrl) - } - if (!url.endsWith("\$qzz", true)) { - danmuUrl = "" - setUp(url, false, title) - } else { - SnifferVideo.getQzzVideoUrl(this@PlayActivity, partUrl, detailPartUrl) - { videoUrl, paramMap -> - danmuParamMap.clear() - danmuParamMap.putAll(paramMap) - danmuUrl = paramMap[SnifferVideo.DANMU_URL] ?: "" - setUp(videoUrl, false, title) - // 开始播放 - startPlayLogic() - } - return } } - //开始播放 - startPlayLogic() } override fun onDestroy() { @@ -409,253 +352,136 @@ class PlayActivity : DetailPlayerActivity() { } override fun onVideoSizeChanged() { - val tag = avpPlayActivity.tag - if (tag is String && tag == "sw600dp-land") return - avpPlayActivity.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - ) - val videoHeight: Int = avpPlayActivity.currentVideoHeight - val videoWidth: Int = avpPlayActivity.currentVideoWidth - val ratio = videoWidth.toDouble() / videoHeight - val playerWidth: Int = avpPlayActivity.width - if (abs(playerWidth.toDouble() / avpPlayActivity.height - ratio) < 0.001) return - var playerHeight = playerWidth / ratio - val playerParent = window.decorView as ViewGroup - playerParent.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - ) - val parentHeight = playerParent.height - if (playerHeight > parentHeight * 0.75) playerHeight = parentHeight * 0.75 - val layoutParams: ViewGroup.LayoutParams = avpPlayActivity.layoutParams - ValueAnimator.ofInt(layoutParams.height, playerHeight.toInt()) - .setDuration(200) - .apply { - addUpdateListener { animation -> - layoutParams.height = animation.animatedValue as Int - avpPlayActivity.requestLayout() + resizePlayer() + } + + override fun onDanmakuStart() { + resizePlayer() + } + + /** + * 根据视频和是否显示弹幕调整播放器高度 + */ + private fun resizePlayer() { + mBinding.apply { + avpPlayActivity.currentPlayer.post { + val tag = avpPlayActivity.tag + val state = avpPlayActivity.currentPlayer.currentState + if (avpPlayActivity.isIfCurrentIsFullscreen || + state == GSYVideoView.CURRENT_STATE_ERROR || + state == GSYVideoView.CURRENT_STATE_AUTO_COMPLETE || + state == GSYVideoView.CURRENT_STATE_PREPAREING || + (tag is String && tag == "sw600dp-land") + ) { + return@post + } + val videoHeight: Int = avpPlayActivity.currentVideoHeight + val videoWidth: Int = avpPlayActivity.currentVideoWidth + if (videoHeight <= 10 || videoWidth <= 10) return@post + val ratio = videoWidth.toDouble() / videoHeight + if (ratio < 0.001) return@post + avpPlayActivity.measure( + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) + val playerWidth: Int = avpPlayActivity.width + var playerHeight = playerWidth / ratio + avpPlayActivity.currentPlayer.let { + if (it is DanmakuVideoPlayer) playerHeight += it.getDanmakuControllerHeight() } - start() + val parentHeight = Util.getScreenHeight(true) + if (playerHeight > parentHeight * 0.75) playerHeight = parentHeight * 0.75 + val layoutParams: ViewGroup.LayoutParams = avpPlayActivity.layoutParams + avpPlayActivity.requestLayout() + ValueAnimator.ofInt(layoutParams.height, playerHeight.toInt()) + .setDuration(200) + .apply { + addUpdateListener { animation -> + layoutParams.height = animation.animatedValue as Int + avpPlayActivity.requestLayout() + } + start() + } } + } } override fun onPlayError(url: String?, vararg objects: Any?) { super.onPlayError(url, *objects) "${objects[0].toString()}, ${getString(R.string.get_data_failed)}".showToast() -// SnifferVideo.askSnifferDialog( -// this, objects[0].toString() + ", " + getString(R.string.get_data_failed), -// getString(R.string.will_you_try_to_sniffer_video), -// partUrl, detailPartUrl -// ) { videoUrl, danMuUrl -> -// videoPlayer.startPlay(videoUrl, viewModel.animeEpisodeDataBean.title) -// } - } - - override fun onQuitFullscreen(url: String?, vararg objects: Any?) { - super.onQuitFullscreen(url, *objects) - adapter.notifyDataSetChanged() } override fun onPrepared(url: String?, vararg objects: Any?) { super.onPrepared(url, *objects) //调整触摸滑动快进的比例 //毫秒,刚好划一屏1分35秒 - avpPlayActivity.currentPlayer.apply { + mBinding.avpPlayActivity.currentPlayer.apply { seekRatio = duration / 90_000f - if (danmuUrl.isNotBlank() && this is DanmakuVideoPlayer && !this@PlayActivity.isDestroyed) { - this@PlayActivity.getString(R.string.the_video_has_danmu).showToast() - this.setDanmaKuUrl(danmuUrl, paramMap = danmuParamMap) - } } } override fun videoPlayStatusChanged(playing: Boolean) { super.videoPlayStatusChanged(playing) - canCollapsed(!playing) - tvPlayActivityToolbarTitle?.text = - if (avpPlayActivity.currentState == CURRENT_STATE_AUTO_COMPLETE) - getString(R.string.replay_video) - else getString(R.string.play_video_now) - } - - private fun canCollapsed(enable: Boolean) { - if (lastCanCollapsed == enable) return - lastCanCollapsed = enable - nsvPlayActivity?.let { - ViewCompat.setNestedScrollingEnabled(it, enable) + mBinding.apply { + canCollapsed(!playing) + tvPlayActivityToolbarTitle?.text = + if (avpPlayActivity.currentPlayer.currentState == + GSYVideoView.CURRENT_STATE_AUTO_COMPLETE + ) getString(R.string.replay_video) + else getString(R.string.play_video_now) } - ablPlayActivity?.let { - val params = it.layoutParams as CoordinatorLayout.LayoutParams - if (params.behavior == null) params.behavior = AppBarLayout.Behavior() - val behaviour = params.behavior as AppBarLayout.Behavior - behaviour.setDragCallback(object : AppBarLayout.Behavior.DragCallback() { - override fun canDrag(appBarLayout: AppBarLayout): Boolean { - return enable - } - }) - if (!enable) it.setExpanded(true) - } - } - - override fun getGSYVideoPlayer(): DanmakuVideoPlayer = avpPlayActivity - - override fun getGSYVideoOptionBuilder(): GSYVideoOptionBuilder { - return GSYVideoOptionBuilder() - .setReleaseWhenLossAudio(false) //音频焦点冲突时是否释放 - .setPlayTag(this.javaClass.simpleName) //防止错位设置 - .setIsTouchWiget(true) - .setRotateViewAuto(false) - .setLockLand(false) - .setShowFullAnimation(false) //打开动画 - .setNeedLockFull(true) - .setDismissControlTime(5000) } - override fun clickForFullScreen() {} - - override fun getDetailOrientationRotateAuto(): Boolean = true - - fun getSheetDialog(action: String): BottomSheetDialog { - val bottomSheetDialog = BottomSheetDialog(this, R.style.BottomSheetDialogTheme) - val contentView = View.inflate(this, R.layout.dialog_bottom_sheet_2, null) - bottomSheetDialog.setContentView(contentView) - val tvTitle = - contentView.findViewById(R.id.tv_dialog_bottom_sheet_2_title) - tvTitle.text = when (action) { - "play" -> getString(R.string.play_list) - "download" -> getString(R.string.download_anime) - else -> "" - } - val recyclerView = contentView.findViewById(R.id.rv_dialog_bottom_sheet_2) - recyclerView.layoutManager = GridLayoutManager(this, 3) - recyclerView.post { - recyclerView.setPadding(16.dp, 16.dp, 16.dp, 16.dp) - recyclerView.scrollToPosition(0) - } - if (recyclerView.itemDecorationCount == 0) { - recyclerView.addItemDecoration(AnimeEpisodeItemDecoration()) - } - val adapter = EpisodeRecyclerViewAdapter( - this, - viewModel.episodesList, - bottomSheetDialog, - 1, - action - ) - recyclerView.adapter = adapter - viewModel.mldEpisodesList.observe(this, Observer { - adapter.notifyDataSetChanged() - avpPlayActivity.setEpisodeAdapter( - PlayerEpisodeRecyclerViewAdapter( - this, - viewModel.episodesList - ) - ) - }) - return bottomSheetDialog - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK).let { - if (it != currentNightMode) { - currentNightMode = it - adapter.notifyDataSetChanged() + /** + * 是否需要必须显示工具栏 + * + * @param show false:不需要显示;true:需要显示 + */ + private fun needShowToolbar(show: Boolean) { + mBinding.apply { + if (show) { + avpPlayActivity.setTopContainer(null) + tbPlayActivity.visible() + } else { + avpPlayActivity.setTopContainer(tbPlayActivity as? ViewGroup) } } } - class EpisodeRecyclerViewAdapter( - private val activity: PlayActivity, - private val dataList: List, - private val dialog: Dialog? = null, - private val showType: Int = 0, //0是横向,1是三列 - private val action: String = "play" - ) : AnimeDetailAdapter.EpisodeRecyclerView1Adapter(activity, dataList, dialog, showType) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = dataList[position] - - when (holder) { - is AnimeEpisode2ViewHolder -> { - holder.tvAnimeEpisode2.text = item.title - holder.tvAnimeEpisode2.setTextColor( - activity.getResColor(R.color.foreground_main_color_2_skin) - ) - val layoutParams = holder.itemView.layoutParams - if (showType == 0) { - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - if (layoutParams is ViewGroup.MarginLayoutParams) { - layoutParams.setMargins(0, 5.dp, 10.dp, 5.dp) - } - holder.itemView.layoutParams = layoutParams - } else { - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - holder.itemView.setPadding(0, 10.dp, 0, 10.dp) - holder.itemView.layoutParams = layoutParams - } - if (action == "play") { - holder.itemView.setOnClickListener { - activity.startPlay(item.actionUrl, position, item.title) - dialog?.dismiss() - } - } else if (action == "download") { - holder.itemView.setOnClickListener { - activity.getString(R.string.parsing_video).showToast() - activity.viewModel.getAnimeEpisodeUrlData(item.actionUrl, position) - } - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } + private fun canCollapsed(enable: Boolean) { + if (lastCanCollapsed == enable) return + needShowToolbar(enable) + lastCanCollapsed = enable + mBinding.ablPlayActivity?.let { + val mAppBarChildAt: View = it.getChildAt(0) + val mAppBarParams = mAppBarChildAt.layoutParams as AppBarLayout.LayoutParams + mAppBarParams.scrollFlags = if (enable) { + AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or + AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED + } else { + AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL } + mAppBarChildAt.layoutParams = mAppBarParams + Handler(Looper.getMainLooper()).postDelayed({ + if (!enable) it.setExpanded(true) + }, 500) } } - class PlayerEpisodeRecyclerViewAdapter( - private val activity: PlayActivity, - private val dataList: List, - ) : AnimeVideoPlayer.EpisodeRecyclerViewAdapter(activity, dataList) { - - override val currentIndex: Int - get() = activity.viewModel.currentEpisodeIndex - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = dataList[position] - - when (holder) { - is AnimeVideoPlayer.RightRecyclerViewViewHolder -> { - holder.tvTitle.setTextColor( - activity.getResColor( - if (item.title == activity.viewModel.animeEpisodeDataBean.title) - R.color.unchanged_main_color_2_skin - else R.color.foreground_white_skin - ) - ) - holder.tvTitle.text = item.title - holder.itemView.setOnClickListener { - activity.avpPlayActivity.currentPlayer.run { - if (this is AnimeVideoPlayer) { - getRightContainer()?.gone() - // 因为右侧界面显示时,不在xx秒后隐藏界面,所以要恢复xx秒后隐藏控制界面 - enableDismissControlViewTimer(true) - } - } - activity.startPlay(item.actionUrl, position, item.title) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } + override fun getGSYVideoPlayer(): DanmakuVideoPlayer = mBinding.avpPlayActivity + + override val gsyVideoOptionBuilder = GSYVideoOptionBuilder().apply { + setReleaseWhenLossAudio(false) // 音频焦点冲突时是否释放 + setPlayTag(this.javaClass.simpleName) // 防止错位设置 + setIsTouchWiget(true) + setRotateViewAuto(false) + setLockLand(false) + setShowFullAnimation(false) // 打开动画 + setNeedLockFull(true) + setDismissControlTime(5000) } - companion object { - const val TAG = "PlayActivity" - } + override fun clickForFullScreen() {} + + override val detailOrientationRotateAuto = true } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/RankActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/RankActivity.kt index ff85a8ad..ddf49975 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/RankActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/RankActivity.kt @@ -1,121 +1,101 @@ package com.skyd.imomoe.view.activity import android.os.Bundle -import android.view.View import android.view.ViewStub +import androidx.activity.viewModels import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.skyd.imomoe.R import com.skyd.imomoe.databinding.ActivityRankBinding -import com.skyd.imomoe.view.fragment.AnimeShowFragment +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.ext.hideToolbarWhenCollapsed +import com.skyd.imomoe.state.DataState import com.skyd.imomoe.view.fragment.RankFragment +import com.skyd.imomoe.view.listener.dsl.addOnTabSelectedListener import com.skyd.imomoe.viewmodel.RankViewModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class RankActivity : BaseActivity() { - private lateinit var viewModel: RankViewModel - private lateinit var adapter: VpAdapter + private val viewModel: RankViewModel by viewModels() + private val adapter: VpAdapter by lazy { VpAdapter() } private var offscreenPageLimit = 1 private var selectedTabIndex = -1 - private var lastRefreshTime: Long = System.currentTimeMillis() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(RankViewModel::class.java) - - adapter = VpAdapter(this) mBinding.run { - llRankActivityToolbar.tvToolbar1Title.text = getString(R.string.rank_list) - llRankActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } - - vp2RankActivity.setOffscreenPageLimit(offscreenPageLimit) + ablRankActivity.hideToolbarWhenCollapsed(tbRankActivity) + ablRankActivity.addFitsSystemWindows(right = true, top = true) - tlRankActivity.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - selectedTabIndex = tab.position - } - - override fun onTabUnselected(tab: TabLayout.Tab) { - } + tbRankActivity.setNavigationOnClickListener { finish() } - override fun onTabReselected(tab: TabLayout.Tab) { - } + vp2RankActivity.offscreenPageLimit = offscreenPageLimit - }) + tlRankActivity.addOnTabSelectedListener { + onTabSelected { tab -> selectedTabIndex = tab?.position ?: return@onTabSelected } + } //添加rv - vp2RankActivity.setAdapter(adapter) + vp2RankActivity.adapter = adapter val tabLayoutMediator = TabLayoutMediator( tlRankActivity, vp2RankActivity.getViewPager() ) { tab, position -> - if (position < viewModel.tabList.size) - tab.text = viewModel.tabList[position].title + val list = viewModel.rankData.value.readOrNull().orEmpty() + if (position < list.size) { + tab.text = list[position].title + } } tabLayoutMediator.attach() } - viewModel.mldRankData.observe(this, Observer { - adapter.clearAllFragment() - if (it) { - hideLoadFailedTip() - viewModel.tabList.size.let { size -> - if (size > 0) mBinding.vp2RankActivity.setOffscreenPageLimit(size) - } - for (i in viewModel.tabList.indices) { - val fragment = RankFragment() - val bundle = Bundle() - bundle.putString("partUrl", viewModel.tabList[i].actionUrl) - fragment.arguments = bundle - adapter.addFragment(fragment) + viewModel.rankData.collectWithLifecycle(this) { data -> + when (data) { + is DataState.Success -> { + hideLoadFailedTip() + val list = data.data + if (list.isNotEmpty()) { + mBinding.vp2RankActivity.offscreenPageLimit = list.size + } + adapter.notifyDataSetChanged() } - } else { - showLoadFailedTip(getString(R.string.load_data_failed_click_to_retry), - View.OnClickListener { + is DataState.Error -> { + showLoadFailedTip { viewModel.getRankTabData() - hideLoadFailedTip() } - ) + adapter.notifyDataSetChanged() + } + else -> {} } - adapter.notifyDataSetChanged() - viewModel.isRequesting = false - }) - - viewModel.getRankTabData() + } } - override fun getBinding(): ActivityRankBinding = ActivityRankBinding.inflate(layoutInflater) + override fun getBinding() = ActivityRankBinding.inflate(layoutInflater) override fun finish() { super.finish() overridePendingTransition(0, R.anim.anl_push_left_out) } - override fun getLoadFailedTipView(): ViewStub? = mBinding.layoutRankActivityLoadFailed - - class VpAdapter : FragmentStateAdapter { - - constructor(fragmentActivity: FragmentActivity) : super(fragmentActivity) + override fun getLoadFailedTipView(): ViewStub = mBinding.layoutRankActivityLoadFailed - constructor(fragment: Fragment) : super(fragment) + inner class VpAdapter : FragmentStateAdapter(this) { - private val fragments = mutableListOf() + override fun getItemCount() = viewModel.rankData.value.readOrNull().orEmpty().size - fun clearAllFragment() { - fragments.clear() + override fun createFragment(position: Int): Fragment { + val fragment = RankFragment() + val bundle = Bundle() + bundle.putString( + "partUrl", + viewModel.rankData.value.readOrNull().orEmpty()[position].route + ) + fragment.arguments = bundle + return fragment } - - fun addFragment(fragment: RankFragment) { - fragments.add(fragment) - } - - override fun getItemCount() = fragments.size - - override fun createFragment(position: Int) = fragments[position] } } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/SearchActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/SearchActivity.kt index 96389a29..26eb1e21 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/SearchActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/SearchActivity.kt @@ -1,168 +1,162 @@ package com.skyd.imomoe.view.activity import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.view.KeyEvent -import android.view.inputmethod.EditorInfo import android.widget.RelativeLayout import android.widget.TextView -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import com.skyd.imomoe.App +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import androidx.recyclerview.widget.GridLayoutManager import com.skyd.imomoe.R -import com.skyd.imomoe.bean.GetDataEnum import com.skyd.imomoe.bean.SearchHistoryBean -import com.skyd.imomoe.config.Const import com.skyd.imomoe.databinding.ActivitySearchBinding -import com.skyd.imomoe.util.Util.showKeyboard -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.smartNotifyDataSetChanged -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.adapter.SearchAdapter -import com.skyd.imomoe.view.adapter.SearchHistoryAdapter +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeCover3Proxy +import com.skyd.imomoe.view.adapter.variety.proxy.SearchHistory1Proxy +import com.skyd.imomoe.view.adapter.variety.proxy.SearchHistoryHeader1Proxy import com.skyd.imomoe.viewmodel.SearchViewModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class SearchActivity : BaseActivity() { private lateinit var mLayoutCircleProgressTextTip1: RelativeLayout private lateinit var tvCircleProgressTextTip1: TextView - private lateinit var viewModel: SearchViewModel - private lateinit var adapter: SearchAdapter - private lateinit var historyAdapter: SearchHistoryAdapter + private val viewModel: SearchViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter( + mutableListOf(AnimeCover3Proxy(), SearchHistoryHeader1Proxy(), SearchHistory1Proxy( + onClickListener = { _, data, _ -> search(data.title) }, + onDeleteButtonClickListener = { _, data, _ -> viewModel.deleteSearchHistory(data) } + )) + ) + } + private var searchHistoryListShow: Boolean = true private var lastRefreshTime: Long = System.currentTimeMillis() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(SearchViewModel::class.java) - adapter = SearchAdapter(this, viewModel.searchResultList) - historyAdapter = SearchHistoryAdapter(this, viewModel.searchHistoryList) - - val pageNumber = intent.getStringExtra("pageNumber") ?: "" - viewModel.keyWord = intent.getStringExtra("keyWord") ?: "" + val pageNumber = intent.getStringExtra("pageNumber").orEmpty() + viewModel.keyword = intent.getStringExtra("keyword").orEmpty() mBinding.run { srlSearchActivity.setEnableRefresh(false) - srlSearchActivity.setOnLoadMoreListener { - viewModel.pageNumberBean?.let { - viewModel.getSearchData(viewModel.keyWord, isRefresh = false, it.actionUrl) - return@setOnLoadMoreListener - } - mBinding.srlSearchActivity.finishLoadMore() - getString(R.string.no_more_info).showToast() + srlSearchActivity.setOnLoadMoreListener { viewModel.loadMoreSearchData() } + ablSearchActivity.addFitsSystemWindows(right = true, top = true) + + rvSearchActivity.addFitsSystemWindows(right = true, bottom = true) + rvSearchActivity.layoutManager = GridLayoutManager( + this@SearchActivity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { + spanSizeLookup = AnimeShowSpanSize(adapter) } - - rvSearchActivity.layoutManager = LinearLayoutManager(this@SearchActivity) - rvSearchActivity.setHasFixedSize(true) - setSearchAdapter() - - etSearchActivitySearch.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - } - - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { + rvSearchActivity.addItemDecoration(AnimeShowItemDecoration()) + rvSearchActivity.adapter = adapter + showSearchHistory() + + svSearchActivity.isSubmitButtonEnabled = true + svSearchActivity.requestFocus() + svSearchActivity.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return if (query.isNullOrBlank()) { + getString(R.string.search_input_keywords_tips).showToast() + false + } else { + // 避免刷新间隔太短 + if (System.currentTimeMillis() - lastRefreshTime > 500) { + lastRefreshTime = System.currentTimeMillis() + search(query) + true + } else { + false + } + } } - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + override fun onQueryTextChange(newText: String?): Boolean { if (this@SearchActivity::mLayoutCircleProgressTextTip1.isInitialized) mLayoutCircleProgressTextTip1.gone() - if (s == null || s.isEmpty()) { + if (newText.isNullOrEmpty()) { tvSearchActivityTip.text = getString(R.string.search_history) - ivSearchActivityClearKeyWords.gone() - viewModel.searchResultList.clear() - setHistoryAdapter() - historyAdapter.notifyDataSetChanged() - } else ivSearchActivityClearKeyWords.visible() + showSearchHistory() + } + return true } }) - - ivSearchActivityClearKeyWords.setOnClickListener { - etSearchActivitySearch.setText("") - } } - viewModel.mldSearchResultList.observe(this, { - mBinding.srlSearchActivity.closeHeaderOrFooter() - if (this::mLayoutCircleProgressTextTip1.isInitialized) mLayoutCircleProgressTextTip1.gone() - // 仅在搜索框不为“”时展示搜索结果 - if (mBinding.etSearchActivitySearch.text.toString().isNotEmpty()) { - if (mBinding.rvSearchActivity.adapter != adapter) setSearchAdapter() - adapter.smartNotifyDataSetChanged(it.first, it.second, viewModel.searchResultList) - when (it.first) { - GetDataEnum.REFRESH, GetDataEnum.LOAD_MORE -> { + viewModel.searchResultList.collectWithLifecycle(this) { data -> + when (data) { + is DataState.Success -> { + if (!searchHistoryListShow) { + adapter.dataList = data.data + if (this@SearchActivity::mLayoutCircleProgressTextTip1.isInitialized) { + mLayoutCircleProgressTextTip1.gone() + } mBinding.tvSearchActivityTip.text = getString( - R.string.search_activity_tip, viewModel.keyWord, - viewModel.searchResultList.size + R.string.search_activity_tip, viewModel.keyword, data.data.size ) - } - GetDataEnum.FAILED -> { - mBinding.tvSearchActivityTip.text = - getString(R.string.search_activity_failed) + mBinding.srlSearchActivity.closeHeaderOrFooter() } } + is DataState.Loading -> { + mBinding.srlSearchActivity.autoLoadMoreAnimationOnly() + } + else -> {} } - }) + } - viewModel.mldSearchHistoryList.observe(this, { - if (viewModel.searchResultList.size == 0) { - mBinding.tvSearchActivityTip.text = getString(R.string.search_history) - setHistoryAdapter() - historyAdapter.notifyDataSetChanged() + viewModel.searchHistoryList.collectWithLifecycle(this) { data -> + when (data) { + is DataState.Success -> { + if (searchHistoryListShow) { + mBinding.tvSearchActivityTip.text = getString(R.string.search_history) + adapter.dataList = data.data + } + } + else -> {} } - }) + } - viewModel.mldDeleteCompleted.observe(this, { - if (viewModel.searchResultList.size == 0) { - setHistoryAdapter() - historyAdapter.notifyItemRemoved(it) + viewModel.deleteCompleted.collectWithLifecycle(this) { data -> + if (searchHistoryListShow && data != null && adapter.dataList.contains(data)) { + adapter.dataList -= data } - }) - - mBinding.tvSearchActivityCancel.setOnClickListener { finish() } - - mBinding.etSearchActivitySearch.showKeyboard() - - mBinding.etSearchActivitySearch.setOnEditorActionListener(object : - TextView.OnEditorActionListener { - override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean { - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - if (v.text.toString().isBlank()) { - App.context.resources.getString(R.string.search_input_keywords_tips) - .showToast() - return false - } + } - //避免刷新间隔太短 - return if (System.currentTimeMillis() - lastRefreshTime > 500) { - lastRefreshTime = System.currentTimeMillis() - search(v.text.toString()) - true - } else { - false - } + mBinding.tbSearchActivity.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.menu_item_search_activity_close -> { + finish() + true } - return true + else -> false } - }) + } - if (viewModel.keyWord.isBlank()) viewModel.getSearchHistoryData() - else search(viewModel.keyWord, pageNumber) + if (viewModel.keyword.isBlank()) { + if (viewModel.searchHistoryList.value is DataState.Empty) { + viewModel.getSearchHistoryData() + } + } else { + if (viewModel.searchResultList.value is DataState.Empty) { + search(viewModel.keyword, pageNumber) + } + } } - override fun getBinding(): ActivitySearchBinding = ActivitySearchBinding.inflate(layoutInflater) + override fun getBinding() = ActivitySearchBinding.inflate(layoutInflater) fun search(key: String, partUrl: String = "") { //setText一定要在加载布局之前,否则progressbar会被gone掉 mBinding.run { - etSearchActivitySearch.setText(key) - etSearchActivitySearch.setSelection(key.length) + svSearchActivity.hideKeyboard() + svSearchActivity.setQuery(key, false) if (this@SearchActivity::tvCircleProgressTextTip1.isInitialized) { mLayoutCircleProgressTextTip1.visible() } else { @@ -171,30 +165,25 @@ class SearchActivity : BaseActivity() { tvCircleProgressTextTip1 = mLayoutCircleProgressTextTip1.findViewById(R.id.tv_circle_progress_text_tip_1) } - viewModel.searchResultList.clear() if (this@SearchActivity::tvCircleProgressTextTip1.isInitialized) tvCircleProgressTextTip1.gone() - setSearchAdapter() + showSearchResult() + adapter.dataList = emptyList() } viewModel.insertSearchHistory( - SearchHistoryBean( - Const.ViewHolderTypeString.SEARCH_HISTORY_1, - "", System.currentTimeMillis(), key - ) + SearchHistoryBean("", System.currentTimeMillis(), key) ) - viewModel.getSearchData(key, isRefresh = true, partUrl = partUrl) - } - - fun deleteSearchHistory(position: Int) { - viewModel.deleteSearchHistory(position) + viewModel.getSearchData(key, partUrl = partUrl) } - private fun setSearchAdapter() { - mBinding.rvSearchActivity.adapter = adapter + private fun showSearchResult() { + searchHistoryListShow = false + adapter.dataList = emptyList() mBinding.srlSearchActivity.setEnableLoadMore(true) } - private fun setHistoryAdapter() { - mBinding.rvSearchActivity.adapter = historyAdapter + private fun showSearchHistory() { + searchHistoryListShow = true + adapter.dataList = viewModel.searchHistoryList.value.readOrNull().orEmpty().toList() mBinding.srlSearchActivity.setEnableLoadMore(false) } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/SettingActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/SettingActivity.kt index 4715e1a2..001800e4 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/SettingActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/SettingActivity.kt @@ -1,223 +1,69 @@ package com.skyd.imomoe.view.activity -import android.annotation.SuppressLint -import android.os.Build import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import com.afollestad.materialdialogs.MaterialDialog +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidViewBinding import com.skyd.imomoe.R -import com.skyd.imomoe.config.Api -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.databinding.ActivitySettingBinding -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.util.Util.getAppVersionName -import com.skyd.imomoe.util.Util.restartApp -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.Util.showToastOnIOThread -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.update.AppUpdateHelper -import com.skyd.imomoe.util.update.AppUpdateStatus -import com.skyd.imomoe.viewmodel.SettingViewModel -import com.skyd.skin.SkinManager -import kotlinx.coroutines.* +import com.skyd.imomoe.databinding.ActivitySettingContainerBinding +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.AnimeTopBarStyle +import com.skyd.imomoe.view.component.compose.BackIcon +import dagger.hilt.android.AndroidEntryPoint -class SettingActivity : BaseActivity() { - private val viewModel: SettingViewModel by lazy { ViewModelProvider(this).get(SettingViewModel::class.java) } - private var selfUpdateCheck = false - - @SuppressLint("SetTextI18n") +@AndroidEntryPoint +class SettingActivity : BaseComposeActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - val appUpdateHelper = AppUpdateHelper.instance - - mBinding.run { - llSettingActivityToolbar.tvToolbar1Title.text = getString(R.string.setting) - llSettingActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } - - tvSettingActivityDownloadPathInfo.isFocused = true - tvSettingActivityDownloadPathInfo.text = Const.DownloadAnime.animeFilePath - } - - // 清理历史记录 - viewModel.mldDeleteAllHistory.observe(this, Observer { - if (it == null) return@Observer - if (it) getString(R.string.delete_all_history_succeed).showToast() - else getString(R.string.delete_all_history_failed).showToast() - viewModel.mldDeleteAllHistory.postValue(null) - }) - mBinding.tvSettingActivityDeleteAllHistoryInfo.isFocused = true - mBinding.rlSettingActivityDeleteAllHistory.setOnClickListener { - MaterialDialog(this).show { - icon(R.drawable.ic_delete_main_color_2_24_skin) - title(res = R.string.warning) - message(text = "确定要删除所有历史记录?包括搜索历史和观看历史") - positiveButton(res = R.string.delete) { viewModel.deleteAllHistory() } - negativeButton(res = R.string.cancel) { dismiss() } - } - } - - // 清理缓存文件 - viewModel.mldCacheSize.observe(this, { - mBinding.tvSettingActivityClearCacheSize.text = it - }) - viewModel.mldClearAllCache.observe(this, Observer { - if (it == null) return@Observer - lifecycleScope.launch(Dispatchers.IO) { - delay(1000) - viewModel.getCacheSize() - if (it) getString(R.string.clear_cache_succeed).showToastOnIOThread() - else getString(R.string.clear_cache_failed).showToastOnIOThread() - } - viewModel.mldClearAllCache.postValue(null) - }) - viewModel.getCacheSize() - mBinding.tvSettingActivityClearCache.isFocused = true - mBinding.rlSettingActivityClearCache.setOnClickListener { - MaterialDialog(this).show { - icon(R.drawable.ic_sd_storage_main_color_2_24_skin) - title(res = R.string.warning) - message(text = "确定清理所有缓存?不包括缓存视频") - positiveButton(res = R.string.clean) { viewModel.clearAllCache() } - negativeButton(res = R.string.cancel) { dismiss() } - } - } - - mBinding.run { - ivSettingActivityDownloadInfo.setOnClickListener { - MaterialDialog(this@SettingActivity).show { - title(res = R.string.attention) - message( - text = "由于新版Android存储机制变更,因此新缓存的动漫将存储在App的私有路径," + - "以前缓存的动漫依旧能够观看,其后面将有“旧”字样。新缓存的动漫与以前缓存的互不影响。" + - "\n\n注意:新缓存的动漫将在App被卸载或数据被清除后丢失。" - ) - positiveButton { dismiss() } - } - } - - tvSettingActivityUpdateInfo.text = - getString(R.string.current_version, getAppVersionName()) - - appUpdateHelper.getUpdateStatus().observe(this@SettingActivity, Observer { - when (it) { - AppUpdateStatus.UNCHECK -> { - tvSettingActivityUpdateInfo.text = "未检查" -// appUpdateHelper.checkUpdate() - } - AppUpdateStatus.CHECKING -> { - tvSettingActivityUpdateTip.text = getString(R.string.checking_update) - } - AppUpdateStatus.DATED -> { - tvSettingActivityUpdateTip.text = getString(R.string.find_new_version) - if (selfUpdateCheck) appUpdateHelper.noticeUpdate(this@SettingActivity) - } - AppUpdateStatus.VALID -> { - tvSettingActivityUpdateTip.text = getString(R.string.is_latest_version) - if (selfUpdateCheck) getString(R.string.is_latest_version).showToast() - } - AppUpdateStatus.LATER -> { - tvSettingActivityUpdateTip.text = "暂不更新" - } - AppUpdateStatus.ERROR -> { - tvSettingActivityUpdateTip.text = "更新失败" - if (selfUpdateCheck) "获取更新失败!".showToast() - } - else -> return@Observer - } - }) + setContentBase { + SettingScreen() } - - mBinding.rlSettingActivityUpdate.setOnClickListener { - selfUpdateCheck = true - when (appUpdateHelper.getUpdateStatus().value) { - AppUpdateStatus.CHECKING -> { - "已在检查,请稍等...".showToast() - } - else -> appUpdateHelper.checkUpdate() - } - } - - mBinding.tvSettingActivityInfoDomain.text = Api.MAIN_URL - - mBinding.switchSettingActivityCustomDataSource.isChecked = - DataSourceManager.useCustomDataSource - mBinding.switchSettingActivityCustomDataSource.setOnCheckedChangeListener { buttonView, isChecked -> - if (DataSourceManager.useCustomDataSource == isChecked) return@setOnCheckedChangeListener - MaterialDialog(this).show { - icon(R.drawable.ic_category_main_color_2_24_skin) - title(res = R.string.warning) - message(res = if (isChecked) R.string.custom_data_source_tip else R.string.request_restart_app) - cancelable(false) - positiveButton(res = R.string.restart) { - DataSourceManager.useCustomDataSource = isChecked - DataSourceManager.clearCache() - restartApp() - } - negativeButton(res = R.string.cancel) { - buttonView.isChecked = !isChecked - dismiss() - } - } - } - - initNightMode() } +} - private fun initNightMode() { - mBinding.run { - when (SkinManager.getDarkMode()) { - SkinManager.DARK_MODE_YES -> { - switchSettingActivityNightMode.isChecked = true - cbSettingActivityNightModeFollowSystem.isChecked = false - tvSettingActivityNightModeInfo.text = getString(R.string.dark) - } - SkinManager.DARK_MODE_NO -> { - switchSettingActivityNightMode.isChecked = false - cbSettingActivityNightModeFollowSystem.isChecked = false - tvSettingActivityNightModeInfo.text = getString(R.string.light) - } - SkinManager.DARK_FOLLOW_SYSTEM -> { - switchSettingActivityNightMode.isEnabled = false - cbSettingActivityNightModeFollowSystem.isChecked = true - tvSettingActivityNightModeInfo.text = getString(R.string.follow_system) - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - cbSettingActivityNightModeFollowSystem.isEnabled = true - cbSettingActivityNightModeFollowSystem.setOnCheckedChangeListener { buttonView, isChecked -> - switchSettingActivityNightMode.isEnabled = !isChecked - if (isChecked) { - switchSettingActivityNightMode.isChecked = false - SkinManager.setDarkMode(SkinManager.DARK_FOLLOW_SYSTEM) - tvSettingActivityNightModeInfo.text = getString(R.string.follow_system) - } else { - SkinManager.setDarkMode(SkinManager.DARK_MODE_NO) - tvSettingActivityNightModeInfo.text = getString(R.string.light) - } - } - } else { - cbSettingActivityNightModeFollowSystem.gone() - cbSettingActivityNightModeFollowSystem.isEnabled = false - } - - switchSettingActivityNightMode.setOnCheckedChangeListener { buttonView, isChecked -> - if (isChecked) { - SkinManager.setDarkMode(SkinManager.DARK_MODE_YES) - tvSettingActivityNightModeInfo.text = getString(R.string.dark) - } else { - SkinManager.setDarkMode(SkinManager.DARK_MODE_NO) - tvSettingActivityNightModeInfo.text = getString(R.string.light) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SettingScreen() { + val context = LocalContext.current + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + decayAnimationSpec = rememberSplineBasedDecay(), + state = rememberTopAppBarScrollState() + ) + Scaffold( + topBar = { + AnimeTopBar( + style = AnimeTopBarStyle.Large, + title = { + Text(text = stringResource(R.string.setting)) + }, + scrollBehavior = scrollBehavior, + navigationIcon = { + BackIcon(onClick = { context.activity.finish() }) } + ) + } + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it) + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = WindowInsets.navigationBars.asPaddingValues() + ) { + item { + AndroidViewBinding( + factory = ActivitySettingContainerBinding::inflate + ) } } } - - override fun getBinding(): ActivitySettingBinding = - ActivitySettingBinding.inflate(layoutInflater) } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/SimplePlayActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/SimplePlayActivity.kt index 531b3246..6c3bbede 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/SimplePlayActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/SimplePlayActivity.kt @@ -1,56 +1,80 @@ package com.skyd.imomoe.view.activity +import android.net.Uri import android.os.Bundle import android.view.View -import androidx.lifecycle.lifecycleScope import com.shuyu.gsyvideoplayer.GSYVideoManager import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack import com.shuyu.gsyvideoplayer.model.VideoOptionModel import com.shuyu.gsyvideoplayer.utils.OrientationUtils -import com.skyd.imomoe.database.getAppDataBase +import com.shuyu.gsyvideoplayer.video.base.GSYVideoView.CURRENT_STATE_NORMAL +import com.shuyu.gsyvideoplayer.video.base.GSYVideoView.CURRENT_STATE_PAUSE import com.skyd.imomoe.databinding.ActivitySimplePlayBinding -import com.skyd.imomoe.util.MD5.getMD5 +import com.skyd.imomoe.ext.fileName +import com.skyd.imomoe.ext.getMediaTitle +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.sharedPreferences import com.skyd.imomoe.util.Util.setFullScreen -import com.skyd.imomoe.util.gone -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.view.component.player.danmaku.DanmakuManager import tv.danmaku.ijk.media.player.IjkMediaPlayer -import java.io.File class SimplePlayActivity : BaseActivity() { + companion object { + const val URL = "url" + const val ANIME_TITLE = "animeTitle" + const val EPISODE_TITLE = "episodeTitle" + } + private var url = "" - private var title = "" + private var animeTitle = "" + private var episodeTitle = "" private lateinit var orientationUtils: OrientationUtils + // 是否是在onPause方法里自动暂停的 + private var isPause = false + + private var onPausePosition: Long = 0 + private var onPauseState: Int = 0 + + // 是否播放过视频 + private var startedPlayVideo: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setFullScreen(window) - url = intent.getStringExtra("url") ?: "" - title = intent.getStringExtra("title") ?: "" + val uri: Uri? = intent.data + animeTitle = intent.getStringExtra(ANIME_TITLE) + .orEmpty() + .ifBlank { uri?.fileName(contentResolver).orEmpty() } + + url = intent.getStringExtra(URL) + .orEmpty() + .ifBlank { + if (uri != null) { + getMediaTitle(uri)?.let { animeTitle = it } + uri.toString() + } else "" + } + + episodeTitle = intent.getStringExtra(EPISODE_TITLE) + .orEmpty() + .ifBlank { animeTitle } init() - mBinding.run { - avpSimplePlayActivity.startPlayLogic() - orientationUtils.resolveByClick() - avpSimplePlayActivity.startWindowFullscreen( + mBinding.avpSimplePlayActivity.run { + val title = episodeTitle + titleTextView?.text = title + fullWindowPlayer?.titleTextView?.text = title + startPlayLogic() + startWindowFullscreen( this@SimplePlayActivity, actionBar = true, statusBar = true ) - - lifecycleScope.launch(Dispatchers.IO) { - val title = getAppDataBase().animeDownloadDao() - .getAnimeDownloadTitleByMd5(getMD5(File(url.replaceFirst("file://", ""))) ?: "") - ?: this@SimplePlayActivity.title - runOnUiThread { - avpSimplePlayActivity.titleTextView?.text = title - avpSimplePlayActivity.fullWindowPlayer?.titleTextView?.text = title - } - } } val videoOptionModel = @@ -58,54 +82,95 @@ class SimplePlayActivity : BaseActivity() { GSYVideoManager.instance().optionModelList = listOf(videoOptionModel) } - override fun getBinding(): ActivitySimplePlayBinding = - ActivitySimplePlayBinding.inflate(layoutInflater) + override fun getBinding() = ActivitySimplePlayBinding.inflate(layoutInflater) private fun init() { mBinding.avpSimplePlayActivity.run { - //设置旋转 + // 设置是否启用自带弹幕功能 + DanmakuManager.enableDanmaku = + sharedPreferences().getBoolean("enableDanmakuInLocalVideo", false) + // 设置旋转 orientationUtils = OrientationUtils(this@SimplePlayActivity, this) - getDownloadButton()?.gone() + // 进横屏旋转,不会竖屏 +// orientationUtils.isOnlyRotateLand = true + // 锁定后不随屏幕旋转而旋转视频 + setLockClickListener { _, lock -> + orientationUtils.isEnable = !lock + currentPlayer.isRotateViewAuto = !lock + } setEpisodeButtonVisibility(View.GONE) fullscreenButton.gone() - //是否开启自动旋转 + dismissControlTime = 5000 + // 是否开启自动旋转 isRotateViewAuto = false - //是否需要全屏锁定屏幕功能 + // 是否需要全屏锁定屏幕功能 isIfCurrentIsFullscreen = true isNeedLockFull = true - //设置触摸显示控制ui的消失时间 - dismissControlTime = 5000 - //设置退出全屏的监听器 + // 不要全屏时从左上角放大的动画 + isShowFullAnimation = false + // 设置退出全屏的监听器 setBackFromFullScreenListener { finish() } - //是否可以滑动调整 + // 是否可以滑动调整 setIsTouchWiget(true) setVideoAllCallBack(object : GSYSampleCallBack() { override fun onPrepared(url: String?, vararg objects: Any?) { super.onPrepared(url, *objects) + startedPlayVideo = true + isPause = false this@run.currentPlayer.seekRatio = this@run.currentPlayer.duration / 90_000f } }) - setUp(url, false, title) + this.animeTitle = this@SimplePlayActivity.animeTitle + setUp(url, false, episodeTitle) } } override fun onPause() { super.onPause() - orientationUtils.setIsPause(true) - mBinding.avpSimplePlayActivity.currentPlayer.onVideoPause() + + mBinding.avpSimplePlayActivity.currentPlayer.apply { + onPauseState = currentState + onPausePosition = currentPositionWhenPlaying + + if (currentState != CURRENT_STATE_PAUSE) { + onVideoPause() + orientationUtils.setIsPause(true) + isPause = true + } + } } override fun onResume() { super.onResume() + orientationUtils.setIsPause(false) - mBinding.avpSimplePlayActivity.currentPlayer.onVideoResume() + mBinding.avpSimplePlayActivity.currentPlayer.apply { + if (currentState == CURRENT_STATE_NORMAL && + onPausePosition != -1L && + onPauseState != -1 && + startedPlayVideo + ) { + seekOnStart = onPausePosition + startPlayLogic() + isPause = false + if (onPauseState == CURRENT_STATE_PAUSE) { + onVideoPause() + isPause = true + } + } else { + if (isPause) { + onVideoResume() + orientationUtils.setIsPause(false) + isPause = false + } + } + } } override fun onDestroy() { super.onDestroy() mBinding.avpSimplePlayActivity.currentPlayer.release() mBinding.avpSimplePlayActivity.setVideoAllCallBack(null) - GSYVideoManager.releaseAllVideos() orientationUtils.releaseListener() } } diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/SkinActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/SkinActivity.kt index 71849165..9fbcec58 100644 --- a/app/src/main/java/com/skyd/imomoe/view/activity/SkinActivity.kt +++ b/app/src/main/java/com/skyd/imomoe/view/activity/SkinActivity.kt @@ -1,113 +1,123 @@ package com.skyd.imomoe.view.activity -import android.content.res.Configuration -import android.graphics.Color +import android.content.Context +import android.os.Build import android.os.Bundle -import androidx.recyclerview.widget.GridLayoutManager +import androidx.compose.foundation.layout.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import com.skyd.imomoe.R -import com.skyd.imomoe.bean.SkinBean -import com.skyd.imomoe.config.Const.ViewHolderTypeString -import com.skyd.imomoe.databinding.ActivitySkinBinding -import com.skyd.imomoe.util.Util.getDefaultResColor -import com.skyd.imomoe.view.adapter.SkinAdapter -import com.skyd.imomoe.view.adapter.decoration.SkinItemDecoration -import com.skyd.imomoe.view.adapter.spansize.SkinSpanSize -import com.skyd.skin.core.SkinResourceProcessor - -class SkinActivity : BaseActivity() { - private val list: MutableList = ArrayList() - private val adapter: SkinAdapter = SkinAdapter(this, list) +import com.skyd.imomoe.bean.SkinCover1Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.plus +import com.skyd.imomoe.ext.theme.appThemeRes +import com.skyd.imomoe.ext.theme.getAttrColor +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import com.skyd.imomoe.view.adapter.compose.proxy.SkinCover1Proxy +import com.skyd.imomoe.view.component.compose.AnimeLazyVerticalGrid +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.BackIcon +class SkinActivity : BaseComposeActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - initSkinData() - mBinding.run { - llSkinActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } - llSkinActivityToolbar.tvToolbar1Title.text = getString(R.string.skin_center) - - rvSkinActivity.layoutManager = GridLayoutManager(this@SkinActivity, 3) - .apply { spanSizeLookup = SkinSpanSize(adapter) } - rvSkinActivity.addItemDecoration(SkinItemDecoration()) - rvSkinActivity.adapter = adapter + setContentBase { + SkinScreen() } } +} - override fun getBinding(): ActivitySkinBinding = ActivitySkinBinding.inflate(layoutInflater) - - private fun usingSkin(skinPath: String, skinSuffix: String): Boolean { - return SkinResourceProcessor.instance.skinPath == skinPath && - SkinResourceProcessor.instance.skinSuffix == skinSuffix - } - - override fun onChangeSkin() { - super.onChangeSkin() - initSkinData() - adapter.notifyDataSetChanged() +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SkinScreen() { + val context = LocalContext.current + Scaffold(topBar = { + AnimeTopBar( + title = { + Text(text = stringResource(R.string.skin_center)) + }, + navigationIcon = { + BackIcon( + onClick = { context.activity.finish() } + ) + } + ) + }) { padding -> + SkinList(modifier = Modifier.padding(padding)) } +} - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - initSkinData() - adapter.notifyDataSetChanged() +@Composable +private fun SkinList(modifier: Modifier) { + val context = LocalContext.current + val adapter = remember { + LazyGridAdapter( + mutableListOf(SkinCover1Proxy()) + ) } + val dataList = remember { initSkinData(context = context) } + AnimeLazyVerticalGrid( + modifier = modifier.fillMaxSize(), + dataList = dataList, + adapter = adapter, + contentPadding = WindowInsets.navigationBars.asPaddingValues() + + PaddingValues(vertical = 7.dp) + ) +} - private fun initSkinData() { - list.clear() - list.add( - SkinBean( - ViewHolderTypeString.SKIN_COVER_1, - "", - getDefaultResColor(R.color.main_color_2_skin), - "粉色少女🎀", - usingSkin("", ""), - "", - "" - ) - ) - list.add( - SkinBean( - ViewHolderTypeString.SKIN_COVER_1, - "", - getDefaultResColor(R.color.black), - "deep♂️dark♂️fantasy", - usingSkin("", "_dark"), - "", - "_dark" - ) - ) - list.add( - SkinBean( - ViewHolderTypeString.SKIN_COVER_1, - "", - getDefaultResColor(R.color.main_color_2_skin_blue), - "♂️深蓝幻想", - usingSkin("", "_blue"), - "", - "_blue" - ) - ) - list.add( - SkinBean( - ViewHolderTypeString.SKIN_COVER_1, - "", - getDefaultResColor(R.color.main_color_2_skin_lemon), - "柠檬酸🍋", - usingSkin("", "_lemon"), - "", - "_lemon" - ) - ) - list.add( - SkinBean( - ViewHolderTypeString.SKIN_COVER_1, - "", - getDefaultResColor(R.color.main_color_2_skin_sweat_soybean), - "流汗黄豆😅", - usingSkin("", "_sweat_soybean"), - "", - "_sweat_soybean" - ) +private fun initSkinData(context: Context): List { + val list = mutableListOf() + list += SkinCover1Bean( + "", + ContextCompat.getColor(context, R.color.primary_pink), + context.getString(R.string.theme_pink_title), + appThemeRes == R.style.Theme_Anime_Pink, + R.style.Theme_Anime_Pink + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + list += SkinCover1Bean( + "", + context.getAttrColor(R.attr.colorPrimary), + context.getString(R.string.theme_dynamic_title), + appThemeRes == R.style.Theme_Anime_Dynamic, + R.style.Theme_Anime_Dynamic ) } + list += SkinCover1Bean( + "", + ContextCompat.getColor(context, R.color.primary_blue), + context.getString(R.string.theme_blue_title), + appThemeRes == R.style.Theme_Anime_Blue, + R.style.Theme_Anime_Blue + ) + list += SkinCover1Bean( + "", + ContextCompat.getColor(context, R.color.primary_lemon), + context.getString(R.string.theme_lemon_title), + appThemeRes == R.style.Theme_Anime_Lemon, + R.style.Theme_Anime_Lemon + ) + list += SkinCover1Bean( + "", + ContextCompat.getColor(context, R.color.primary_purple), + context.getString(R.string.theme_purple_title), + appThemeRes == R.style.Theme_Anime_Purple, + R.style.Theme_Anime_Purple + ) + list += SkinCover1Bean( + "", + ContextCompat.getColor(context, R.color.primary_green), + context.getString(R.string.theme_green_title), + appThemeRes == R.style.Theme_Anime_Green, + R.style.Theme_Anime_Green + ) + return list } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/UrlMapActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/UrlMapActivity.kt new file mode 100644 index 00000000..33606d9a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/activity/UrlMapActivity.kt @@ -0,0 +1,517 @@ +package com.skyd.imomoe.view.activity + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.skyd.imomoe.R +import com.skyd.imomoe.database.entity.UrlMapEntity +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.ext.plus +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.AnimeTopBarStyle +import com.skyd.imomoe.view.component.compose.BackIcon +import com.skyd.imomoe.view.component.compose.TopBarIcon +import com.skyd.imomoe.view.fragment.DataSourceMarketFragment +import com.skyd.imomoe.viewmodel.UrlMapViewModel + +class UrlMapActivity : BaseComposeActivity() { + private val viewModel: UrlMapViewModel by viewModels() + + companion object { + const val ENABLED = "enabled" + const val JSON_DATA = "jsonData" + const val AUTO_ADD = "autoAdd" + const val AUTO_ADD_AND_FINISH = "autoAddAndFinish" + const val COMPLETED = 0 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val jsonData = intent.getStringExtra(JSON_DATA) + viewModel.autoAdd = intent.getBooleanExtra(AUTO_ADD, false) + viewModel.autoAddAndFinish = intent.getBooleanExtra(AUTO_ADD_AND_FINISH, false) + if (intent.getBooleanExtra(ENABLED, false)) { + com.skyd.imomoe.net.urlMapEnabled = true + urlMapEnabled = true + } + if (!jsonData.isNullOrBlank()) { + if (viewModel.autoAdd || viewModel.autoAddAndFinish) { + viewModel.setUrlMap(jsonData) + } else { + jsonDialogData.value = jsonData + showJsonDialog.value = true + } + } + viewModel.requestFinish.collectWithLifecycle(this) { + if (it) { + DataSourceMarketFragment.needRefresh.tryEmit(true) + finish() + } + } + setContentBase { + UrlMapScreen() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun UrlMapScreen(viewModel: UrlMapViewModel = hiltViewModel()) { + val context = LocalContext.current + Scaffold(topBar = { + AnimeTopBar( + style = AnimeTopBarStyle.Small, + title = { + Text(text = stringResource(R.string.url_map_activity_title)) + }, + navigationIcon = { + BackIcon( + onClick = { context.activity.finish() } + ) + }, + actions = { + TopBarIcon( + painter = painterResource(id = R.drawable.ic_playlist_add_24), + contentDescription = stringResource(id = R.string.url_map_activity_add_by_script), + onClick = { showJsonDialog.value = true } + ) + } + ) + }, floatingActionButton = { + ExtendedFloatingActionButton( + modifier = Modifier.navigationBarsPadding(), + text = { + Text(text = stringResource(id = R.string.add)) + }, + icon = { + Icon(Icons.Rounded.Add, null) + }, + onClick = { + showEditDialog.value = true + }, + ) + }) { + UrlMapList(modifier = Modifier.padding(it)) + if (showEditDialog.value) { + EditDialog( + title = stringResource(id = R.string.add), + onConfirm = { oldUrl, newUrl -> + if (oldUrl.isNotBlank() && newUrl.isNotBlank()) { + viewModel.setUrlMap(oldUrl, newUrl) + } + } + ) + } + if (showJsonDialog.value) { + JsonDialog( + title = stringResource(id = R.string.url_map_activity_add_by_script), + onConfirm = { jsonData -> + if (jsonData.isNotBlank()) { + viewModel.setUrlMap(jsonData) + } + } + ) + } + } +} + +/** + * URL替换总开关 + */ +private var urlMapEnabled by mutableStateOf(com.skyd.imomoe.net.urlMapEnabled) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun UrlMapEnabledCard() { + Card( + modifier = Modifier + .padding(vertical = 7.dp) + .fillMaxWidth() + ) { + Row( + modifier = Modifier + .clickable { + com.skyd.imomoe.net.urlMapEnabled = !urlMapEnabled + urlMapEnabled = !urlMapEnabled + } + .padding(15.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 15.dp) + ) { + Text( + text = stringResource(id = R.string.url_map_activity_enable), + style = MaterialTheme.typography.titleLarge + ) + Text( + modifier = Modifier.padding(top = 7.dp), + text = stringResource(id = R.string.url_map_activity_enable_disadvantage), + style = MaterialTheme.typography.bodyMedium + ) + } + Switch( + checked = urlMapEnabled, + onCheckedChange = { + com.skyd.imomoe.net.urlMapEnabled = it + urlMapEnabled = it + } + ) + } + } +} + +/** + * 展示列表 + */ +@Composable +private fun UrlMapList(modifier: Modifier = Modifier) { + val viewModel: UrlMapViewModel = hiltViewModel() + val urlMapListState by viewModel.urlMapList.collectAsState() + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = WindowInsets.navigationBars.asPaddingValues() + + PaddingValues(horizontal = 16.dp, vertical = 6.dp), + ) { + item { + UrlMapEnabledCard() + } + when (urlMapListState) { + is DataState.Success -> { + val list = urlMapListState.read() + items(list.size) { index -> + UrlMapItem(list[index]) + } + } + else -> {} + } + } +} + +/** + * 列表的每一项 + */ +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +private fun UrlMapItem(urlMapEntity: UrlMapEntity) { + val viewModel: UrlMapViewModel = hiltViewModel() + var menuExpanded by remember { mutableStateOf(false) } + val enabledData = urlMapEntity.enabled + var enabled by remember { mutableStateOf(enabledData) } + Card( + modifier = Modifier + .padding(vertical = 7.dp) + .fillMaxWidth() + ) { + Row( + modifier = Modifier + .combinedClickable( + onLongClick = { menuExpanded = true }, + onClick = { + if (urlMapEnabled) { + enabled = !enabled + viewModel.enabledUrlMap(urlMapEntity.oldUrl, enabled) + } + } + ) + .padding(15.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 15.dp) + ) { + Text( + text = urlMapEntity.oldUrl, + style = MaterialTheme.typography.titleLarge + ) + Text( + modifier = Modifier.padding(top = 17.dp), + text = stringResource(R.string.url_map_activity_new, urlMapEntity.newUrl), + style = MaterialTheme.typography.bodyMedium + ) + } + Switch( + checked = enabled, + onCheckedChange = { + enabled = it + viewModel.enabledUrlMap(urlMapEntity.oldUrl, it) + }, + enabled = urlMapEnabled + ) + } + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false } + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.edit)) }, + onClick = { + showEditDialog.value = true + editDialogData.value = urlMapEntity.oldUrl to urlMapEntity.newUrl + menuExpanded = false + }, + leadingIcon = { + Icon( + Icons.Rounded.Edit, + contentDescription = null + ) + }) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.delete)) }, + onClick = { + showDeleteDialog.value = true + deleteDialogOldUrl.value = urlMapEntity.oldUrl + menuExpanded = false + }, + leadingIcon = { + Icon( + Icons.Rounded.Delete, + contentDescription = null + ) + }) + } + } + if (showDeleteDialog.value && deleteDialogOldUrl.value == urlMapEntity.oldUrl) { + DeleteDialog() + } + if (showEditDialog.value && + editDialogData.value?.first == urlMapEntity.oldUrl && + editDialogData.value?.second == urlMapEntity.newUrl + ) { + EditDialog( + title = stringResource(id = R.string.edit), + onConfirm = { oldUrl, newUrl -> + if (oldUrl.isNotBlank() && newUrl.isNotBlank()) { + viewModel.editUrlMap( + old = urlMapEntity.oldUrl to urlMapEntity.newUrl, + new = oldUrl to newUrl, + enabled = urlMapEntity.enabled + ) + } + } + ) + } +} + +private val showEditDialog = mutableStateOf(false) +private val editDialogData = mutableStateOf?>(null) + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun EditDialog( + title: String, + onConfirm: (oldUrl: String, newUrl: String) -> Unit +) { + var oldUrl by rememberSaveable { mutableStateOf(editDialogData.value?.first.orEmpty()) } + var newUrl by rememberSaveable { mutableStateOf(editDialogData.value?.second.orEmpty()) } + + AlertDialog( + onDismissRequest = { + showEditDialog.value = false + editDialogData.value = null + }, + icon = { + Icon(Icons.Rounded.Edit, null) + }, + title = { + Text(text = title) + }, + text = { + Column { + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + TextField( + value = oldUrl, + singleLine = true, + onValueChange = { oldUrl = it }, + label = { + Text(text = stringResource(id = R.string.url_map_activity_input_old)) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + focusManager.moveFocus(FocusDirection.Next) + }) + ) + TextField( + modifier = Modifier.padding(top = 12.dp), + value = newUrl, + singleLine = true, + onValueChange = { newUrl = it }, + label = { + Text(text = stringResource(id = R.string.url_map_activity_input_new)) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + keyboardController?.hide() + focusManager.clearFocus() + }) + ) + } + }, + confirmButton = { + TextButton( + enabled = oldUrl.isNotBlank() && newUrl.isNotBlank(), + onClick = { + onConfirm.invoke(oldUrl, newUrl) + showEditDialog.value = false + editDialogData.value = null + } + ) { + Text(stringResource(id = R.string.ok)) + } + }, + dismissButton = { + TextButton( + onClick = { + showEditDialog.value = false + editDialogData.value = null + } + ) { + Text(stringResource(id = R.string.cancel)) + } + } + ) +} + +private val showDeleteDialog = mutableStateOf(false) +private val deleteDialogOldUrl = mutableStateOf(null) + +@Composable +private fun DeleteDialog(viewModel: UrlMapViewModel = hiltViewModel()) { + AlertDialog( + onDismissRequest = { + showDeleteDialog.value = false + deleteDialogOldUrl.value = null + }, + icon = { + Icon(Icons.Rounded.Warning, null) + }, + title = { + Text(text = stringResource(id = R.string.warning)) + }, + text = { + Text(text = stringResource(id = R.string.url_map_activity_delete)) + }, + confirmButton = { + TextButton( + onClick = { + deleteDialogOldUrl.value?.let { + viewModel.deleteUrlMap(it) + } + showDeleteDialog.value = false + deleteDialogOldUrl.value = null + } + ) { + Text(stringResource(id = R.string.ok)) + } + }, + dismissButton = { + TextButton( + onClick = { + showDeleteDialog.value = false + deleteDialogOldUrl.value = null + } + ) { + Text(stringResource(id = R.string.cancel)) + } + } + ) +} + +private val showJsonDialog = mutableStateOf(false) +private val jsonDialogData = mutableStateOf(null) + +@Composable +private fun JsonDialog( + title: String, + onConfirm: (jsonData: String) -> Unit +) { + var jsonData by rememberSaveable { mutableStateOf(jsonDialogData.value.orEmpty()) } + val focusRequester = remember { FocusRequester() } + AlertDialog( + onDismissRequest = { + showJsonDialog.value = false + jsonDialogData.value = null + }, + icon = { + Icon(painter = painterResource(id = R.drawable.ic_playlist_add_24), null) + }, + title = { + Text(text = title) + }, + text = { + Column { + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + Text(text = stringResource(id = R.string.url_map_activity_json_warning)) + TextField( + modifier = Modifier + .focusRequester(focusRequester) + .wrapContentHeight(), + value = jsonData, + onValueChange = { jsonData = it }, + label = { + Text(text = stringResource(id = R.string.url_map_activity_input_json)) + } + ) + } + }, + confirmButton = { + TextButton( + enabled = jsonData.isNotBlank() && jsonData.isNotBlank(), + onClick = { + onConfirm.invoke(jsonData) + showJsonDialog.value = false + jsonDialogData.value = null + } + ) { + Text(stringResource(id = R.string.ok)) + } + }, + dismissButton = { + TextButton( + onClick = { + showJsonDialog.value = false + jsonDialogData.value = null + } + ) { + Text(stringResource(id = R.string.cancel)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/activity/WebViewActivity.kt b/app/src/main/java/com/skyd/imomoe/view/activity/WebViewActivity.kt deleted file mode 100644 index 270b0512..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/activity/WebViewActivity.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.skyd.imomoe.view.activity - -import android.annotation.SuppressLint -import android.content.ActivityNotFoundException -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.webkit.* -import com.skyd.imomoe.databinding.ActivityWebViewBinding - - -class WebViewActivity : BaseActivity() { - private lateinit var url: String - private lateinit var headers: HashMap - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - url = intent.getStringExtra("url") ?: "" - intent.getSerializableExtra("headers").let { - headers = if (it == null) { - HashMap() - } else { - it as HashMap - } - } - mBinding.llWebViewActivityToolbar.ivToolbar1Back.setOnClickListener { finish() } - mBinding.wvWebViewActivity.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - } - - override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { - if (url.startsWith("https") || url.startsWith("http")) { - view.loadUrl(url) - } else { - try { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) - } catch (e: ActivityNotFoundException) { - e.printStackTrace() - } - } - return true - } - } - initSettings() - - mBinding.wvWebViewActivity.loadUrl(url, headers) - } - - @SuppressLint("SetJavaScriptEnabled") - private fun initSettings() { - mBinding.wvWebViewActivity.settings.run { - setAllowFileAccess(true) - setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS) - setSupportZoom(true) - setBuiltInZoomControls(true) - setUseWideViewPort(true) - setSupportMultipleWindows(false) - setAppCacheEnabled(true) - setDomStorageEnabled(true) - setJavaScriptEnabled(true) - setGeolocationEnabled(true) - setAppCacheMaxSize(Long.MAX_VALUE) - setAppCachePath(getDir("appcache", 0).path) - setDatabasePath(getDir("databases", 0).path) - setGeolocationDatabasePath( - getDir("geolocation", 0) - .path - ) - setPluginState(WebSettings.PluginState.ON_DEMAND) - setLoadWithOverviewMode(true) - setCacheMode(WebSettings.LOAD_NO_CACHE) - val mUserAgent: String = getUserAgentString() - setUserAgentString("$mUserAgent App/AppName") - syncCookie() - setUseWideViewPort(true) - setLoadWithOverviewMode(true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK) - } else { - setCacheMode(WebSettings.LOAD_DEFAULT) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - setDisplayZoomControls(false) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - setLoadsImagesAutomatically(true) - } else { - setLoadsImagesAutomatically(false) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) CookieManager.getInstance() - .setAcceptThirdPartyCookies(mBinding.wvWebViewActivity, true) - - mBinding.wvWebViewActivity.setScrollBarStyle(WebView.SCROLLBARS_INSIDE_OVERLAY) - mBinding.wvWebViewActivity.setHorizontalScrollBarEnabled(false) - mBinding.wvWebViewActivity.setHorizontalFadingEdgeEnabled(false) - mBinding.wvWebViewActivity.setVerticalFadingEdgeEnabled(false) - - mBinding.wvWebViewActivity.requestFocus() -// defaultTextEncodingName = "utf-8" -// cacheMode = WebSettings.LOAD_DEFAULT -// useWideViewPort = true -// allowFileAccess = true -// setSupportZoom(true) -// allowContentAccess = true -// javaScriptEnabled = true -// domStorageEnabled = true -// pluginState = WebSettings.PluginState.ON// 可以使用插件 -// setSupportMultipleWindows(true) -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { -// mixedContentMode = 0 -// } -// mediaPlaybackRequiresUserGesture = true -// allowFileAccessFromFileURLs = true -// allowUniversalAccessFromFileURLs = true -// javaScriptCanOpenWindowsAutomatically = true -// loadsImagesAutomatically = true -// setAppCacheEnabled(true) -// setAppCachePath(cacheDir.absolutePath) -// databaseEnabled = true -// setAppCachePath(getDir("appCache", 0).path) -// setGeolocationDatabasePath(getDir("database", 0).path) -// setGeolocationDatabasePath(getDir("geolocation", 0).path) -// setGeolocationEnabled(true) -// val instance = CookieManager.getInstance() -// instance.setAcceptCookie(true) -// if (Build.VERSION.SDK_INT >= 21) { -// instance.setAcceptThirdPartyCookies(mBinding.wvWebViewActivity, true) -// } - - - } - } - - private fun syncCookie() { - CookieSyncManager.createInstance(this) - val cookieManager = CookieManager.getInstance() - cookieManager.setAcceptCookie(true) - CookieSyncManager.getInstance().sync() - } - - override fun getBinding(): ActivityWebViewBinding = - ActivityWebViewBinding.inflate(layoutInflater) -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeDetailAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeDetailAdapter.kt deleted file mode 100644 index fa57577d..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeDetailAdapter.kt +++ /dev/null @@ -1,260 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.annotation.SuppressLint -import android.app.Activity -import android.app.Dialog -import android.content.res.ColorStateList -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.AnimeEpisodeDataBean -import com.skyd.imomoe.bean.IAnimeDetailBean -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.util.* -import com.skyd.imomoe.util.Util.dp -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.Util.getResDrawable -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.view.activity.AnimeDetailActivity -import com.skyd.imomoe.view.adapter.decoration.AnimeCoverItemDecoration -import com.skyd.imomoe.view.adapter.decoration.AnimeEpisodeItemDecoration -import com.skyd.imomoe.view.component.BottomSheetRecyclerView - -class AnimeDetailAdapter( - val activity: AnimeDetailActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - private val gridItemDecoration = AnimeCoverItemDecoration() - - private val animeEpisodeItemDecoration = AnimeEpisodeItemDecoration() - - @SuppressLint("ClickableViewAccessibility", "SetTextI18n") - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when { - holder is Header1ViewHolder -> { - holder.tvHeader1Title.textSize = 15f - holder.tvHeader1Title.text = item.title - holder.tvHeader1Title.setTextColor( - activity.getResColor(R.color.foreground_white_skin) - ) - } - holder is GridRecyclerView1ViewHolder -> { - item.animeCoverList?.let { - val layoutManager = GridLayoutManager(activity, 4) - holder.rvGridRecyclerView1.post { - holder.rvGridRecyclerView1.setPadding(16.dp, 0, 16.dp, 0) - } - if (holder.rvGridRecyclerView1.itemDecorationCount == 0) { - holder.rvGridRecyclerView1.addItemDecoration(gridItemDecoration) - } - holder.rvGridRecyclerView1.layoutManager = layoutManager - holder.rvGridRecyclerView1.adapter = - AnimeShowAdapter.GridRecyclerView1Adapter( - activity, it, - activity.getResColor(R.color.foreground_white_skin) - ) - } - } - holder is HorizontalRecyclerView1ViewHolder -> { - item.episodeList?.let { - holder.rvHorizontalRecyclerView1.adapter.let { adapter -> - if (adapter == null) { - holder.rvHorizontalRecyclerView1.adapter = - EpisodeRecyclerView1Adapter( - activity, it, detailPartUrl = activity.getPartUrl() - ) - } else adapter.notifyDataSetChanged() - } - holder.ivHorizontalRecyclerView1More.setImageDrawable(getResDrawable(R.drawable.ic_keyboard_arrow_down_main_color_2_24_skin)) - holder.ivHorizontalRecyclerView1More.imageTintList = - ColorStateList.valueOf(activity.getResColor(R.color.foreground_white_skin)) - holder.ivHorizontalRecyclerView1More.setOnClickListener { it1 -> - showEpisodeSheetDialog(it).show() - } - } - } - holder is AnimeDescribe1ViewHolder -> { - holder.tvAnimeDescribe1.text = item.describe - holder.tvAnimeDescribe1.setOnClickListener { } - holder.tvAnimeDescribe1.setTextColor( - activity.getResColor(R.color.foreground_white_skin) - ) - } - holder is AnimeInfo1ViewHolder -> { - item.headerInfo?.let { - holder.ivAnimeInfo1Cover.setTag(R.id.image_view_tag, it.cover.url) - if (holder.ivAnimeInfo1Cover.getTag(R.id.image_view_tag) == it.cover.url) { - holder.ivAnimeInfo1Cover.loadImage( - it.cover.url, - referer = it.cover.referer, - placeholder = 0, - error = 0 - ) - } - holder.tvAnimeInfo1Title.text = it.title - holder.tvAnimeInfo1Alias.text = it.alias - holder.tvAnimeInfo1Area.text = it.area - holder.tvAnimeInfo1Year.text = it.year - holder.tvAnimeInfo1Index.text = - App.context.getString(R.string.anime_detail_index) + it.index - holder.tvAnimeInfo1Info.text = it.info - holder.flAnimeInfo1Type.removeAllViews() - for (i in it.animeType.indices) { - val tvFlowLayout: TextView = activity.layoutInflater - .inflate( - R.layout.item_anime_type_1, - holder.flAnimeInfo1Type, - false - ) as TextView - tvFlowLayout.text = it.animeType[i].title - tvFlowLayout.setOnClickListener { it1 -> - if (it.animeType[i].actionUrl.isBlank()) return@setOnClickListener - //此处是”类型“,若要修改,需要注意Tab大分类是否还是”类型“ - val actionUrl = it.animeType[i].actionUrl.run { - if (endsWith("/")) "${this}${it.animeType[i].title}" - else "${this}/${it.animeType[i].title}" - } - process( - activity, - Const.ActionUrl.ANIME_CLASSIFY + actionUrl - ) - } - holder.flAnimeInfo1Type.addView(tvFlowLayout) - } - - holder.flAnimeInfo1Tag.removeAllViews() - for (i in it.tag.indices) { - val tvFlowLayout: TextView = activity.layoutInflater - .inflate( - R.layout.item_anime_type_1, - holder.flAnimeInfo1Tag, - false - ) as TextView - tvFlowLayout.text = it.tag[i].title - tvFlowLayout.setOnClickListener { _ -> - //此处是”标签“,由于分类没有这一大项,因此传入”“串 - val actionUrl = it.tag[i].actionUrl.run { - if (endsWith("/")) "${this}${it.tag[i].title}" - else "${this}/${it.tag[i].title}" - } - process( - activity, - Const.ActionUrl.ANIME_CLASSIFY + actionUrl - ) - } - holder.flAnimeInfo1Tag.addView(tvFlowLayout) - } - } - } - holder is AnimeCover1ViewHolder && item is AnimeCoverBean -> { - holder.ivAnimeCover1Cover.setTag(R.id.image_view_tag, item.cover?.url) - holder.tvAnimeCover1Title.setTextColor(activity.getResColor(R.color.foreground_white_skin)) - if (holder.ivAnimeCover1Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover1Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - holder.tvAnimeCover1Title.text = item.title - if (item.episode.isBlank()) { - holder.tvAnimeCover1Episode.gone() - } else { - holder.tvAnimeCover1Episode.visible() - holder.tvAnimeCover1Episode.text = item.episode - } - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } - - @SuppressLint("ClickableViewAccessibility") - private fun showEpisodeSheetDialog(dataList: List): BottomSheetDialog { - val bottomSheetDialog = BottomSheetDialog(activity, R.style.BottomSheetDialogTheme) - val contentView = View.inflate(activity, R.layout.dialog_bottom_sheet_2, null) - bottomSheetDialog.setContentView(contentView) - val recyclerView = - contentView.findViewById(R.id.rv_dialog_bottom_sheet_2) - recyclerView.layoutManager = GridLayoutManager(activity, 3) - recyclerView.post { - recyclerView.setPadding(16.dp, 16.dp, 16.dp, 16.dp) - recyclerView.scrollToPosition(0) - } - if (recyclerView.itemDecorationCount == 0) { - recyclerView.addItemDecoration(animeEpisodeItemDecoration) - } - recyclerView.adapter = EpisodeRecyclerView1Adapter( - activity, - dataList, - bottomSheetDialog, - showType = 1, - detailPartUrl = activity.getPartUrl() - ) - return bottomSheetDialog - } - - open class EpisodeRecyclerView1Adapter( - private val activity: Activity, - private val dataList: List, - private val dialog: Dialog? = null, - private val showType: Int = 0, //0是横向,1是三列 - private val detailPartUrl: String = "" - ) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is AnimeEpisode2ViewHolder -> { - holder.tvAnimeEpisode2.text = item.title - val layoutParams = holder.itemView.layoutParams - holder.itemView.background = if (showType == 0) { - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - if (layoutParams is ViewGroup.MarginLayoutParams) { - layoutParams.setMargins(0, 5.dp, 10.dp, 5.dp) - } - holder.itemView.layoutParams = layoutParams - holder.tvAnimeEpisode2.setTextColor(activity.getResColor(R.color.foreground_white_skin)) - getResDrawable(R.drawable.shape_circle_corner_edge_white_ripper_5_skin) - } else { - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - holder.itemView.setPadding(0, 10.dp, 0, 10.dp) - holder.itemView.layoutParams = layoutParams - holder.tvAnimeEpisode2.setTextColor(activity.getResColor(R.color.foreground_main_color_2_skin)) - getResDrawable(R.drawable.shape_circle_corner_edge_main_color_2_ripper_5_skin) - } - holder.itemView.setOnClickListener { - val const = DataSourceManager.getConst() - if (const != null && item.actionUrl.startsWith(const.actionUrl.ANIME_PLAY())) - process(activity, item.actionUrl + detailPartUrl, item.actionUrl) - else process(activity, item.actionUrl, item.actionUrl) - dialog?.dismiss() - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeDownloadAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeDownloadAdapter.kt deleted file mode 100644 index 75de661a..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeDownloadAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.util.AnimeCover7ViewHolder -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.invisible -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.activity.AnimeDownloadActivity - -class AnimeDownloadAdapter( - val activity: AnimeDownloadActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is AnimeCover7ViewHolder -> { - holder.tvAnimeCover7Title.isFocused = true - holder.tvAnimeCover7Title.text = item.title - holder.tvAnimeCover7Size.isFocused = true - holder.tvAnimeCover7Size.text = item.size - if (item.path == 1) { - holder.tvAnimeCover7OldPath.text = activity.getString(R.string.old_path) - holder.tvAnimeCover7OldPath.visible() - } else { - holder.tvAnimeCover7OldPath.gone() - } - if (item.actionUrl.startsWith(Const.ActionUrl.ANIME_ANIME_DOWNLOAD_EPISODE)) { - holder.tvAnimeCover7Episodes.text = item.episodeCount - holder.tvAnimeCover7Episodes.visible() - } else { - holder.tvAnimeCover7Episodes.invisible() - } - holder.itemView.setOnClickListener { - process(activity, item.actionUrl + "/" + item.path) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeShowAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeShowAdapter.kt deleted file mode 100644 index d36fb2cf..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/AnimeShowAdapter.kt +++ /dev/null @@ -1,467 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.app.Activity -import android.graphics.Color -import android.graphics.Rect -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.IAnimeShowBean -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.util.* -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.view.fragment.AnimeShowFragment -import com.skyd.imomoe.view.component.bannerview.adapter.MyCycleBannerAdapter -import com.skyd.imomoe.view.component.bannerview.indicator.DotIndicator -import com.skyd.imomoe.config.Const.ViewHolderTypeString -import com.skyd.imomoe.util.Util.dp -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.Util.getResDrawable -import com.skyd.imomoe.view.adapter.decoration.AnimeCoverItemDecoration - -class AnimeShowAdapter( - val fragment: AnimeShowFragment, - private val dataList: List, - private val childViewPool: RecyclerView.RecycledViewPool = RecyclerView.RecycledViewPool() -) : BaseRvAdapter(dataList) { - - private val gridItemDecoration = AnimeCoverItemDecoration() - - override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { - super.onViewDetachedFromWindow(holder) - when (holder) { - is Banner1ViewHolder -> { - holder.banner1.stopPlay() - } - } - } - - override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { - super.onViewAttachedToWindow(holder) - when (holder) { - is Banner1ViewHolder -> { - holder.banner1.startPlay(5000) - } - } - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - val holder = super.onCreateViewHolder(parent, viewType) - when (holder) { - is GridRecyclerView1ViewHolder -> { - holder.rvGridRecyclerView1.setRecycledViewPool(childViewPool) - holder.rvGridRecyclerView1.setHasFixedSize(true) - } - } - return holder - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - when { - holder is GridRecyclerView1ViewHolder -> { - item.animeCoverList?.let { - if (it.isNotEmpty()) { - val itemDecorationCount = holder.rvGridRecyclerView1.itemDecorationCount - when (it[0].type) { - ViewHolderTypeString.ANIME_COVER_3, ViewHolderTypeString.ANIME_COVER_5 -> { - holder.rvGridRecyclerView1.layoutManager = - LinearLayoutManager(fragment.activity) - holder.rvGridRecyclerView1.post { - holder.rvGridRecyclerView1.setPadding(0, 0, 0, 0) - } - for (i in 0 until itemDecorationCount) { - holder.rvGridRecyclerView1.removeItemDecorationAt(i) - } - } - ViewHolderTypeString.ANIME_COVER_1 -> { - holder.rvGridRecyclerView1.layoutManager = - GridLayoutManager(fragment.activity, 4) - if (itemDecorationCount == 0) { - holder.rvGridRecyclerView1.post { - holder.rvGridRecyclerView1.setPadding(16.dp, 0, 16.dp, 0) - } - holder.rvGridRecyclerView1.addItemDecoration(gridItemDecoration) - } - } - ViewHolderTypeString.ANIME_COVER_4 -> { - holder.rvGridRecyclerView1.layoutManager = - GridLayoutManager(fragment.activity, 4) - if (itemDecorationCount == 0) { - holder.rvGridRecyclerView1.post { - holder.rvGridRecyclerView1.setPadding(16.dp, 0, 16.dp, 0) - } - holder.rvGridRecyclerView1.addItemDecoration(gridItemDecoration) - } - } - else -> { - return@let - } - } - } - - holder.rvGridRecyclerView1.adapter = - fragment.activity?.let { it1 -> GridRecyclerView1Adapter(it1, it) } - } - } - holder is Header1ViewHolder -> { - fragment.activity?.let { - holder.tvHeader1Title.setTextColor(it.getResColor(R.color.foreground_main_color_2_skin)) - } - holder.tvHeader1Title.text = item.title - } - holder is Banner1ViewHolder -> { - fragment.activity?.let { - item.animeCoverList?.let { it1 -> - holder.banner1.setAdapter(MyCycleBannerAdapter(it, it1)) - holder.banner1.setIndicator(DotIndicator(it)) - holder.banner1.startPlay(5000) - } - } - } - holder is AnimeCover1ViewHolder && item is AnimeCoverBean -> { - holder.ivAnimeCover1Cover.setTag(R.id.image_view_tag, item.cover?.url) - fragment.activity?.let { activity -> - if (holder.ivAnimeCover1Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover1Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - } - holder.tvAnimeCover1Title.text = item.title - if (item.episode.isBlank()) { - holder.tvAnimeCover1Episode.gone() - } else { - holder.tvAnimeCover1Episode.visible() - holder.tvAnimeCover1Episode.text = item.episode - } - holder.itemView.setOnClickListener { - process(fragment, item.actionUrl) - } - } - holder is AnimeCover3ViewHolder && item is AnimeCoverBean -> { - holder.ivAnimeCover3Cover.setTag(R.id.image_view_tag, item.cover?.url) - fragment.activity?.let { activity -> - if (holder.ivAnimeCover3Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover3Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - } - holder.tvAnimeCover3Title.text = item.title - if (item.episode.isBlank()) { - holder.tvAnimeCover3Episode.gone() - } else { - holder.tvAnimeCover3Episode.visible() - holder.tvAnimeCover3Episode.text = item.episode - } - item.animeType?.let { - holder.flAnimeCover3Type.removeAllViews() - for (i in it.indices) { - val tvFlowLayout: TextView = fragment.activity?.layoutInflater - ?.inflate( - R.layout.item_anime_type_1, - holder.flAnimeCover3Type, - false - ) as TextView - tvFlowLayout.text = it[i].title - tvFlowLayout.setOnClickListener { _ -> - if (it[i].actionUrl.isBlank()) return@setOnClickListener - //此处是”类型“,若要修改,需要注意Tab大分类是否还是”类型“ - val actionUrl = it[i].actionUrl.run { - if (endsWith("/")) "${this}${it[i].title}" - else "${this}/${it[i].title}" - } - process( - fragment, - Const.ActionUrl.ANIME_CLASSIFY + actionUrl - ) - } - holder.flAnimeCover3Type.addView(tvFlowLayout) - } - } - holder.tvAnimeCover3Describe.text = item.describe - holder.itemView.setOnClickListener { - process(fragment, item.actionUrl) - } - } - holder is AnimeCover4ViewHolder && item is AnimeCoverBean -> { - holder.ivAnimeCover4Cover.setTag(R.id.image_view_tag, item.cover?.url) - fragment.activity?.let { activity -> - if (holder.ivAnimeCover4Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover4Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - } - holder.tvAnimeCover4Title.text = item.title - holder.itemView.setOnClickListener { - process(fragment, item.actionUrl) - } - } - holder is AnimeCover5ViewHolder && item is AnimeCoverBean -> { - holder.tvAnimeCover5Rank.gone() - if (item.area == null || item.area?.title == "") { - holder.tvAnimeCover5Area.gone() - holder.tvAnimeCover5Date.post { - holder.tvAnimeCover5Date.setPadding(0, 0, 0, 0) - } - } else { - holder.tvAnimeCover5Area.background = - getResDrawable(R.drawable.shape_fill_circle_corner_main_color_2_50_skin) - holder.tvAnimeCover5Area.visible() - holder.tvAnimeCover5Date.post { - holder.tvAnimeCover5Date.setPadding(12.dp, 0, 0, 0) - } - } - if (item.date == null || item.date == "") { - holder.tvAnimeCover5Date.gone() - } else { - holder.tvAnimeCover5Date.visible() - } - holder.tvAnimeCover5Title.text = item.title - holder.tvAnimeCover5Area.text = item.area?.title - holder.tvAnimeCover5Date.text = item.date - holder.tvAnimeCover5Episode.text = item.episodeClickable?.title - if (holder.tvAnimeCover5Area.visibility == View.GONE && - holder.tvAnimeCover5Date.visibility == View.GONE - ) { - holder.tvAnimeCover5Title.post { - holder.tvAnimeCover5Title.setPadding( - holder.tvAnimeCover5Title.paddingStart, 12.dp, - holder.tvAnimeCover5Title.paddingEnd, 12.dp - ) - } - } - holder.itemView.setOnClickListener { - if (item.episodeClickable?.actionUrl.equals(item.actionUrl)) - process(fragment, item.episodeClickable?.actionUrl) - else process(fragment, item.episodeClickable?.actionUrl + item.actionUrl) - } - holder.tvAnimeCover5Area.setOnClickListener { - val actionUrl = item.area?.actionUrl.toString().run { - if (endsWith("/")) "${this}${item.area?.title}" - else "${this}/${item.area?.title}" - } - process( - fragment, - Const.ActionUrl.ANIME_CLASSIFY + actionUrl - ) - } - holder.tvAnimeCover5Title.setOnClickListener { - process(fragment, item.actionUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } - - class GridRecyclerView1Adapter( - private val activity: Activity, - private val dataList: List, - private var titleColor: Int = activity.getResColor(R.color.foreground_black_skin) - ) : BaseRvAdapter(dataList) { - //必须四个参数都不是-1才生效 - var padding = Rect(-1, -1, -1, -1) - - //是否显示排行榜排行,目前仅支持animeCover5 - var showRankNumber = false - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = dataList[position] - - if (padding.left != -1 && padding.top != -1 && padding.right != -1 && padding.bottom != -1) { - holder.itemView.post { - holder.itemView.setPadding( - padding.left, - padding.top, - padding.right, - padding.bottom - ) - } - } - - when (holder) { - is AnimeCover1ViewHolder -> { - holder.viewAnimeCover1Night.setBackgroundColor(activity.getResColor(R.color.transparent_skin)) - holder.tvAnimeCover1Title.setTextColor(titleColor) - holder.tvAnimeCover1Episode.setTextColor(activity.getResColor(R.color.main_color_skin)) - holder.ivAnimeCover1Cover.setTag(R.id.image_view_tag, item.cover?.url) - if (holder.ivAnimeCover1Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover1Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - holder.tvAnimeCover1Title.text = item.title - if (item.episode.isBlank()) { - holder.tvAnimeCover1Episode.gone() - } else { - holder.tvAnimeCover1Episode.visible() - holder.tvAnimeCover1Episode.text = item.episode - } - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - is AnimeCover3ViewHolder -> { - holder.viewAnimeCover3Night.setBackgroundColor(activity.getResColor(R.color.transparent_skin)) - holder.tvAnimeCover3Title.setTextColor(titleColor) - holder.tvAnimeCover3Episode.setTextColor(activity.getResColor(R.color.main_color_skin)) - holder.ivAnimeCover3Cover.setTag(R.id.image_view_tag, item.cover?.url) - if (holder.ivAnimeCover3Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover3Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - holder.tvAnimeCover3Title.text = item.title - if (item.episode.isBlank()) { - holder.tvAnimeCover3Episode.gone() - } else { - holder.tvAnimeCover3Episode.visible() - holder.tvAnimeCover3Episode.text = item.episode - } - item.animeType?.let { - holder.flAnimeCover3Type.removeAllViews() - for (i in it.indices) { - val tvFlowLayout: TextView = activity.layoutInflater - .inflate( - R.layout.item_anime_type_1, - holder.flAnimeCover3Type, - false - ) as TextView - tvFlowLayout.text = it[i].title - tvFlowLayout.setOnClickListener { _ -> - if (it[i].actionUrl.isBlank()) return@setOnClickListener - //此处是”类型“,若要修改,需要注意Tab大分类是否还是”类型“ - val actionUrl = it[i].actionUrl.run { - if (endsWith("/")) "${this}${it[i].title}" - else "${this}/${it[i].title}" - } - process( - activity, - Const.ActionUrl.ANIME_CLASSIFY + actionUrl - ) - } - holder.flAnimeCover3Type.addView(tvFlowLayout) - } - } - holder.tvAnimeCover3Describe.text = item.describe - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - is AnimeCover4ViewHolder -> { - holder.viewAnimeCover4Night.setBackgroundColor(activity.getResColor(R.color.transparent_skin)) - holder.tvAnimeCover4Title.setTextColor(titleColor) - holder.ivAnimeCover4Cover.setTag(R.id.image_view_tag, item.cover?.url) - if (holder.ivAnimeCover4Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover4Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - holder.tvAnimeCover4Title.text = item.title - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - is AnimeCover5ViewHolder -> { - if (showRankNumber) { - holder.tvAnimeCover5Rank.setTextColor(Color.WHITE) - holder.tvAnimeCover5Rank.text = (position + 1).toString() - holder.tvAnimeCover5Rank.background = if (position in 0..2) { - val backgrounds = intArrayOf( - R.drawable.shape_fill_circle_corner_golden_50, - R.drawable.shape_fill_circle_corner_silvery_50, - R.drawable.shape_fill_circle_corner_coppery_50 - ) - getResDrawable(backgrounds[position]) - } else { - getResDrawable(R.drawable.shape_fill_circle_corner_main_color_2_50_skin) - } - holder.tvAnimeCover5Rank.visible() - } else { - holder.tvAnimeCover5Rank.gone() - } - holder.tvAnimeCover5Title.setTextColor(titleColor) - if (item.area == null || item.area?.title == "") { - holder.tvAnimeCover5Area.gone() - holder.tvAnimeCover5Date.post { - holder.tvAnimeCover5Date.setPadding(0, 0, 0, 0) - } - } else { - holder.tvAnimeCover5Area.background = - getResDrawable(R.drawable.shape_fill_circle_corner_main_color_2_50_skin) - holder.tvAnimeCover5Area.visible() - holder.tvAnimeCover5Date.post { - holder.tvAnimeCover5Date.setPadding(12.dp, 0, 0, 0) - } - } - if (item.date == null || item.date == "") { - holder.tvAnimeCover5Date.gone() - } else { - holder.tvAnimeCover5Date.setTextColor(activity.getResColor(R.color.main_color_skin)) - holder.tvAnimeCover5Date.visible() - } - holder.tvAnimeCover5Title.text = item.title - holder.tvAnimeCover5Area.text = item.area?.title - holder.tvAnimeCover5Date.text = item.date - holder.tvAnimeCover5Episode.setTextColor(activity.getResColor(R.color.foreground_main_color_2_skin)) - holder.tvAnimeCover5Episode.text = item.episodeClickable?.title - if (holder.tvAnimeCover5Area.visibility == View.GONE && - holder.tvAnimeCover5Date.visibility == View.GONE - ) { - holder.tvAnimeCover5Title.post { - holder.tvAnimeCover5Title.setPadding( - holder.tvAnimeCover5Title.paddingStart, 12.dp, - holder.tvAnimeCover5Title.paddingEnd, 12.dp - ) - } - } - holder.itemView.setOnClickListener { - if (item.episodeClickable?.actionUrl.equals(item.actionUrl)) - process(activity, item.episodeClickable?.actionUrl) - else process(activity, item.episodeClickable?.actionUrl + item.actionUrl) - } - holder.tvAnimeCover5Area.setOnClickListener { - val actionUrl = item.area?.actionUrl.toString().run { - if (endsWith("/")) "${this}${item.area?.title}" - else "${this}/${item.area?.title}" - } - process( - activity, - Const.ActionUrl.ANIME_CLASSIFY + actionUrl - ) - } - holder.tvAnimeCover5Title.setOnClickListener { - process(activity, item.actionUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/BaseRvAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/BaseRvAdapter.kt deleted file mode 100644 index 33ee8d07..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/BaseRvAdapter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import androidx.recyclerview.widget.RecyclerView -import com.skyd.skin.SkinManager -import com.skyd.imomoe.bean.BaseBean -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.util.ViewHolderUtil.Companion.getItemViewType - -abstract class BaseRvAdapter( - private val dataList: List -) : SkinRvAdapter() { - - override fun getItemViewType(position: Int): Int { - return if (position < dataList.size) getItemViewType(dataList[position]) - else Const.ViewHolderTypeInt.UNKNOWN - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - SkinManager.applyViews(holder.itemView) - } - - override fun getItemCount(): Int = dataList.size -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/FavoriteAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/FavoriteAdapter.kt deleted file mode 100644 index 38d79e00..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/FavoriteAdapter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.FavoriteAnimeBean -import com.skyd.imomoe.util.AnimeCover8ViewHolder -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.activity.FavoriteActivity - -class FavoriteAdapter( - val activity: FavoriteActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is AnimeCover8ViewHolder -> { - holder.ivAnimeCover8Cover.loadImage( - url = item.cover.url, - referer = item.cover.referer - ) - holder.tvAnimeCover8Title.text = item.animeTitle - if (item.lastEpisode == null) { - holder.tvAnimeCover8Episodes.gone() - } else { - holder.tvAnimeCover8Episodes.visible() - holder.tvAnimeCover8Episodes.text = item.lastEpisode - } - holder.itemView.setOnClickListener { - if (item.lastEpisodeUrl != null) - process( - activity, - item.lastEpisodeUrl + item.animeUrl, - item.lastEpisodeUrl ?: "" - ) - else - process(activity, item.animeUrl, item.animeUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/HistoryAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/HistoryAdapter.kt deleted file mode 100644 index 795c1269..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/HistoryAdapter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.HistoryBean -import com.skyd.imomoe.util.AnimeCover9ViewHolder -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.Util.time2Now -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.view.activity.HistoryActivity - -class HistoryAdapter( - val activity: HistoryActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is AnimeCover9ViewHolder -> { - holder.ivAnimeCover9Cover.loadImage( - url = item.cover.url, - referer = item.cover.referer - ) - holder.tvAnimeCover9Title.text = item.animeTitle - holder.tvAnimeCover9Episodes.text = item.lastEpisode - holder.tvAnimeCover9Time.text = time2Now(item.time) - holder.tvAnimeCover9DetailPage.setOnClickListener { - process(activity, item.animeUrl, item.animeUrl) - } - holder.ivAnimeCover9Delete.setOnClickListener { - activity.deleteHistory(item) - } - holder.itemView.setOnClickListener { - if (item.lastEpisodeUrl != null) - process( - activity, - item.lastEpisodeUrl + item.animeUrl, - item.lastEpisodeUrl ?: "" - ) - else - process(activity, item.animeUrl, item.animeUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/LicenseAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/LicenseAdapter.kt deleted file mode 100644 index 08209680..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/LicenseAdapter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.LicenseBean -import com.skyd.imomoe.util.License1ViewHolder -import com.skyd.imomoe.util.LicenseHeader1ViewHolder -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.view.activity.LicenseActivity - -class LicenseAdapter( - val activity: LicenseActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is LicenseHeader1ViewHolder -> { - } - is License1ViewHolder -> { - holder.tvLicense1Name.text = item.title - holder.tvLicense1License.text = item.license - holder.itemView.setOnClickListener { - process(activity, item.actionUrl + item.url) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/MoreAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/MoreAdapter.kt deleted file mode 100644 index af3cd959..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/MoreAdapter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.MoreBean -import com.skyd.imomoe.util.More1ViewHolder -import com.skyd.imomoe.util.Util.getResDrawable -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.clickScale -import com.skyd.imomoe.view.fragment.MoreFragment - -class MoreAdapter( - val fragment: MoreFragment, - private val dataList: List -) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is More1ViewHolder -> { - holder.ivMore1.setImageDrawable(getResDrawable(item.image)) - holder.tvMore1.text = item.title - holder.itemView.setOnClickListener { - it.clickScale() - process(fragment, item.actionUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/PlayAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/PlayAdapter.kt deleted file mode 100644 index 3eeae1e1..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/PlayAdapter.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.content.res.ColorStateList -import android.view.View -import android.widget.TextView -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.IAnimeDetailBean -import com.skyd.imomoe.util.* -import com.skyd.imomoe.util.Util.dp -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.Util.getResDrawable -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.Util.sp -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.view.activity.PlayActivity -import com.skyd.imomoe.view.adapter.decoration.AnimeCoverItemDecoration - -class PlayAdapter( - val activity: PlayActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - private val gridItemDecoration = AnimeCoverItemDecoration() - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when { - holder is GridRecyclerView1ViewHolder -> { - item.animeCoverList?.let { - val layoutManager = GridLayoutManager(activity, 4) - holder.rvGridRecyclerView1.post { - holder.rvGridRecyclerView1.setPadding(16.dp, 0, 16.dp, 0) - } - holder.rvGridRecyclerView1.removeItemDecoration(gridItemDecoration) - holder.rvGridRecyclerView1.addItemDecoration(gridItemDecoration) - holder.rvGridRecyclerView1.layoutManager = layoutManager - holder.rvGridRecyclerView1.adapter = - AnimeShowAdapter.GridRecyclerView1Adapter(activity, it) - } - } - holder is Header1ViewHolder -> { - holder.tvHeader1Title.text = item.title - } - holder is AnimeCover2ViewHolder -> { - holder.tvAnimeCover1Title.text = item.title - holder.tvAnimeCover1Episode.gone() - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - holder is AnimeEpisodeFlowLayout1ViewHolder -> { - item.episodeList?.let { - holder.flAnimeEpisodeFlowLayout1.removeAllViews() - for (i in it.indices) { - val tvFlowLayout: TextView = activity.layoutInflater - .inflate( - R.layout.item_anime_episode_1, - holder.flAnimeEpisodeFlowLayout1, - false - ) as TextView - tvFlowLayout.text = it[i].title - tvFlowLayout.setOnClickListener { _ -> - activity.startPlay(it[i].actionUrl, position, it[i].title) - } - holder.flAnimeEpisodeFlowLayout1.addView(tvFlowLayout) - } - } - } - holder is HorizontalRecyclerView1ViewHolder -> { - item.episodeList?.let { - val dialog = activity.getSheetDialog("play") - if (holder.rvHorizontalRecyclerView1.adapter == null) { - holder.rvHorizontalRecyclerView1.adapter = - PlayActivity.EpisodeRecyclerViewAdapter( - activity, it, dialog, 0, "play" - ) - } else holder.rvHorizontalRecyclerView1.adapter?.notifyDataSetChanged() - holder.ivHorizontalRecyclerView1More.setImageDrawable(getResDrawable(R.drawable.ic_keyboard_arrow_down_main_color_2_24_skin)) - holder.ivHorizontalRecyclerView1More.imageTintList = - ColorStateList.valueOf(activity.getResColor(R.color.foreground_main_color_2_skin)) - holder.ivHorizontalRecyclerView1More.setOnClickListener { dialog.show() } - } - } - holder is AnimeCover1ViewHolder && item is AnimeCoverBean -> { - holder.ivAnimeCover1Cover.setTag(R.id.image_view_tag, item.cover?.url) - if (holder.ivAnimeCover1Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover1Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - holder.tvAnimeCover1Title.text = item.title - if (item.episode.isBlank()) { - holder.tvAnimeCover1Episode.gone() - } else { - holder.tvAnimeCover1Episode.visible() - holder.tvAnimeCover1Episode.text = item.episode - } - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/SearchAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/SearchAdapter.kt deleted file mode 100644 index 305af77b..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/SearchAdapter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.app.Activity -import android.view.View -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.config.Const -import com.skyd.imomoe.util.AnimeCover3ViewHolder -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible - -class SearchAdapter( - val activity: Activity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is AnimeCover3ViewHolder -> { - holder.ivAnimeCover3Cover.setTag(R.id.image_view_tag, item.cover?.url) - if (holder.ivAnimeCover3Cover.getTag(R.id.image_view_tag) == item.cover?.url) { - holder.ivAnimeCover3Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - } - holder.tvAnimeCover3Title.text = item.title - if (item.episode == "") { - holder.tvAnimeCover3Episode.gone() - } else { - holder.tvAnimeCover3Episode.visible() - holder.tvAnimeCover3Episode.text = item.episode - } - item.animeType?.let { - holder.flAnimeCover3Type.removeAllViews() - for (i in it.indices) { - val tvFlowLayout: TextView = activity.layoutInflater - .inflate( - R.layout.item_anime_type_1, - holder.flAnimeCover3Type, - false - ) as TextView - tvFlowLayout.text = it[i].title - tvFlowLayout.setOnClickListener { _ -> - if (it[i].actionUrl.isBlank()) return@setOnClickListener - //此处是”类型“,若要修改,需要注意Tab大分类是否还是”类型“ - val actionUrl = it[i].actionUrl.run { - if (endsWith("/")) "${this}${it[i].title}" - else "${this}/${it[i].title}" - } - process( - activity, - Const.ActionUrl.ANIME_CLASSIFY + actionUrl - ) - } - holder.flAnimeCover3Type.addView(tvFlowLayout) - } - } - holder.tvAnimeCover3Describe.text = item.describe - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/SearchHistoryAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/SearchHistoryAdapter.kt deleted file mode 100644 index 80533450..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/SearchHistoryAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.SearchHistoryBean -import com.skyd.imomoe.util.SearchHistory1ViewHolder -import com.skyd.imomoe.util.SearchHistoryHeader1ViewHolder -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.view.activity.SearchActivity - -class SearchHistoryAdapter( - val activity: SearchActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is SearchHistoryHeader1ViewHolder -> { - holder.tvSearchHistoryHeader1Title.text = item.title - } - is SearchHistory1ViewHolder -> { - holder.tvSearchHistory1Title.text = item.title - holder.ivSearchHistory1Delete.setOnClickListener { - activity.deleteSearchHistory(position) - } - holder.itemView.setOnClickListener { - activity.search(item.title) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/SkinAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/SkinAdapter.kt deleted file mode 100644 index d8a283b5..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/SkinAdapter.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.graphics.drawable.ColorDrawable -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.SkinBean -import com.skyd.imomoe.util.SkinCover1ViewHolder -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.activity.SkinActivity -import com.skyd.skin.SkinManager - -class SkinAdapter( - val activity: SkinActivity, - private val dataList: List -) : BaseRvAdapter(dataList) { - private var selectedItem: SkinCover1ViewHolder? = null - private var selectedItemPosition: Int = -1 - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is SkinCover1ViewHolder -> { - if (item.using) { - holder.ivSkinCover1Selected.visible() - selectedItem = holder - selectedItemPosition = position - } else holder.ivSkinCover1Selected.gone() - holder.tvSkinCover1Title.text = item.title - item.cover.let { cover -> - if (cover is Int) { - holder.ivSkinCover1Cover.setImageDrawable(ColorDrawable(cover)) - } else if (cover is String) { - holder.ivSkinCover1Cover.loadImage(cover) - } - } - holder.itemView.setOnClickListener { - if (item.using) return@setOnClickListener - if (item.skinSuffix == SkinManager.KEY_SKIN_DARK_SUFFIX && item.skinPath == "") - SkinManager.setDarkMode(SkinManager.DARK_MODE_YES) - else SkinManager.notifyListener(item.skinPath, item.skinSuffix) - holder.ivSkinCover1Selected.visible() - dataList[selectedItemPosition].using = false - selectedItem?.ivSkinCover1Selected?.gone() - selectedItem = holder - selectedItemPosition = position - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/SkinRvAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/SkinRvAdapter.kt deleted file mode 100644 index 9144c5bf..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/SkinRvAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.skyd.skin.SkinManager -import com.skyd.imomoe.util.ViewHolderUtil.Companion.getViewHolder - -abstract class SkinRvAdapter : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return getViewHolder(parent, viewType).apply { - SkinManager.setSkin(itemView) - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - SkinManager.applyViews(holder.itemView) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/UpnpAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/UpnpAdapter.kt deleted file mode 100644 index 28e470d7..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/UpnpAdapter.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.skyd.imomoe.view.adapter - -import android.content.Intent -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.config.Const.ViewHolderTypeInt -import com.skyd.imomoe.util.UpnpDevice1ViewHolder -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.view.activity.DlnaActivity -import com.skyd.imomoe.view.activity.DlnaControlActivity -import org.fourthline.cling.model.meta.Device - -class UpnpAdapter( - val activity: DlnaActivity, - private val dataList: List?> -) : SkinRvAdapter() { - - override fun getItemViewType(position: Int): Int = ViewHolderTypeInt.UPNP_DEVICE_1 - - override fun getItemCount(): Int = dataList.size - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = dataList[position] - - when (holder) { - is UpnpDevice1ViewHolder -> { - holder.tvUpnpDevice1Title.text = item?.details?.friendlyName - holder.itemView.setOnClickListener { - val key = System.currentTimeMillis().toString() - DlnaControlActivity.deviceHashMap[key] = item - activity.startActivity( - Intent(activity, DlnaControlActivity::class.java) - .putExtra("url", activity.url) - .putExtra("title", activity.title) - .putExtra("deviceKey", key) - ) - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/AnimeItemSpace.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/AnimeItemSpace.kt new file mode 100644 index 00000000..153f6a29 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/AnimeItemSpace.kt @@ -0,0 +1,163 @@ +package com.skyd.imomoe.view.adapter.compose + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.bean.AnimeCover7Bean +import com.skyd.imomoe.bean.AnimeEpisode1Bean +import com.skyd.imomoe.bean.HorizontalRecyclerView1Bean +import com.skyd.imomoe.bean.SearchHistory1Bean +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize + +object AnimeItemSpace { + val ITEM_SPACING = 12.dp + val HORIZONTAL_PADDING = 16.dp + + fun Modifier.animeItemSpace(item: Any, spanSize: Int, spanIndex: Int) = + this.padding(getItemSpace(item, spanSize, spanIndex)) + + fun getItemSpace(item: Any, spanSize: Int, spanIndex: Int): PaddingValues { + var top = 0.dp + var bottom = 0.dp + var start = 0.dp + var end = 0.dp + if (needVerticalMargin(item.javaClass)) { + top = 10.dp + bottom = 2.dp + } + if (spanSize == AnimeShowSpanSize.MAX_SPAN_SIZE) { + /** + * 只有一列 + */ + if (noHorizontalMargin(item.javaClass)) { + return PaddingValues(top = top, bottom = bottom, start = start, end = end) + } + start = HORIZONTAL_PADDING + end = HORIZONTAL_PADDING + } else if (spanSize == AnimeShowSpanSize.MAX_SPAN_SIZE / 2) { + /** + * 只有两列,没有在中间的item + * 2x = ITEM_SPACING + */ + val x = ITEM_SPACING / 2f + if (spanIndex == 0) { + start = HORIZONTAL_PADDING + end = x + } else { + start = x + end = HORIZONTAL_PADDING + } + } else if (spanSize == AnimeShowSpanSize.MAX_SPAN_SIZE / 3) { + /** + * 只有三列,一个在中间的item + * HORIZONTAL_PADDING + x = 2y + * x + y = ITEM_SPACING + */ + val y = (HORIZONTAL_PADDING + ITEM_SPACING) / 3f + val x = ITEM_SPACING - y + if (spanIndex == 0) { + start = HORIZONTAL_PADDING + end = x + } else if (spanIndex + spanSize == AnimeShowSpanSize.MAX_SPAN_SIZE) { + // 最右侧最后一个 + start = x + end = HORIZONTAL_PADDING + } else { + start = y + end = y + } + } else if (spanSize == AnimeShowSpanSize.MAX_SPAN_SIZE / 5) { + /** + * 只有五列 + * HORIZONTAL_PADDING + x = y + z + * x + y = ITEM_SPACING + * z + (HORIZONTAL_PADDING + x) / 2 = ITEM_SPACING + */ + val x = (ITEM_SPACING * 4 - HORIZONTAL_PADDING * 3) / 5f + val y = ITEM_SPACING - x + val z = HORIZONTAL_PADDING + x - y + if (spanIndex == 0) { + // 最左侧第一个 + start = HORIZONTAL_PADDING + end = x + } else if (spanIndex + spanSize == AnimeShowSpanSize.MAX_SPAN_SIZE) { + // 最右侧最后一个 + start = x + end = HORIZONTAL_PADDING + } else if (spanIndex == spanSize) { + // 第二个 + start = y + end = z + } else if (spanIndex == AnimeShowSpanSize.MAX_SPAN_SIZE - 2 * spanSize) { + // 倒数第二个 + start = z + end = y + } else { + // 最中间的 + start = (HORIZONTAL_PADDING + x) / 2f + end = (HORIZONTAL_PADDING + x) / 2f + } + } else { + /** + * 多于三列(不包括五列),有在中间的item + */ + if ((AnimeShowSpanSize.MAX_SPAN_SIZE / spanSize) % 2 == 0) { + /** + * 偶数个item + * HORIZONTAL_PADDING + x = y + ITEM_SPACING / 2 + * x + y = ITEM_SPACING + */ + val y = (HORIZONTAL_PADDING + ITEM_SPACING / 2f) / 2f + val x = ITEM_SPACING - y + if (spanIndex == 0) { + // 最左侧第一个 + start = HORIZONTAL_PADDING + end = x + } else if (spanIndex + spanSize == AnimeShowSpanSize.MAX_SPAN_SIZE) { + // 最右侧最后一个 + start = x + end = HORIZONTAL_PADDING + } else { + // 中间的项目 + if (spanIndex < AnimeShowSpanSize.MAX_SPAN_SIZE / 2) { + // 左侧部分 + start = y + end = ITEM_SPACING / 2 + } else { + // 右侧部分 + start = ITEM_SPACING / 2 + end = y + } + } + } else { + /** + * 奇数个item,严格大于5的奇数(暂无需求,未实现) + */ + } + } + return PaddingValues(top = top, bottom = bottom, start = start, end = end) + } + + private val noHorizontalMarginType: Set> = setOf( + HorizontalRecyclerView1Bean::class.java, + SearchHistory1Bean::class.java, + AnimeCover7Bean::class.java, + ) + + fun noHorizontalMargin(clz: Class<*>?): Boolean { + clz ?: return true + if (clz in noHorizontalMarginType) return true + return false + } + + private val needVerticalMarginType: Set> = setOf( + AnimeEpisode1Bean::class.java, + ) + + fun needVerticalMargin(clz: Class<*>?): Boolean { + clz ?: return false + if (clz in needVerticalMarginType) return true + return false + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/AnimeShowSpan.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/AnimeShowSpan.kt new file mode 100644 index 00000000..793c0227 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/AnimeShowSpan.kt @@ -0,0 +1,32 @@ +package com.skyd.imomoe.view.adapter.compose + +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.* +import com.skyd.imomoe.ext.screenIsLand +import org.fourthline.cling.model.meta.Device + +const val MAX_SPAN_SIZE = 60 +fun animeShowSpan( + data: Any, + enableLandScape: Boolean = true +): Int = if (enableLandScape && appContext.screenIsLand) { + when (data) { + is SkinCover1Bean, + is More1Bean -> MAX_SPAN_SIZE / 3 + is AnimeCover7Bean, + is AnimeCover9Bean, + is Device<*, *, *> -> MAX_SPAN_SIZE + is AnimeCover8Bean -> MAX_SPAN_SIZE / 5 + else -> MAX_SPAN_SIZE / 3 + } +} else { + when (data) { + is SkinCover1Bean, + is More1Bean -> MAX_SPAN_SIZE / 2 + is AnimeCover7Bean, + is AnimeCover9Bean, + is Device<*, *, *> -> MAX_SPAN_SIZE + is AnimeCover8Bean -> MAX_SPAN_SIZE / 3 + else -> MAX_SPAN_SIZE / 3 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/LazyGridAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/LazyGridAdapter.kt new file mode 100644 index 00000000..fd1a7e92 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/LazyGridAdapter.kt @@ -0,0 +1,41 @@ +package com.skyd.imomoe.view.adapter.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import java.lang.reflect.ParameterizedType + +class LazyGridAdapter( + private var proxyList: MutableList> = mutableListOf(), +) { + @Suppress("UNCHECKED_CAST") + @Composable + fun draw(modifier: Modifier, index: Int, data: Any) { + val type: Int = getProxyIndex(data) + if (type != -1) (proxyList[type] as Proxy).draw(modifier, index, data) + } + + // 获取策略在列表中的索引,可能返回-1 + private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst { + // 如果Proxy中的第一个类型参数T和数据的类型相同,则返回对应策略的索引 + (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].let { argument -> + if (argument.toString() == data.javaClass.toString()) + true // 正常情况 + else if (((argument as? ParameterizedType)?.rawType as? Class<*>) + ?.isAssignableFrom(data.javaClass) == true + ) { + true // data是T的子类的情况 + } else { + // Proxy第一个泛型是类似List,又嵌套了个泛型 + if (argument is ParameterizedType) + argument.rawType.toString() == data.javaClass.toString() + else false + } + } + } + + // 抽象策略类 + abstract class Proxy { + @Composable + abstract fun draw(modifier: Modifier, index: Int, data: T) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover7Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover7Proxy.kt new file mode 100644 index 00000000..34df9cab --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover7Proxy.kt @@ -0,0 +1,109 @@ +package com.skyd.imomoe.view.adapter.compose.proxy + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover7Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.route.processor.EpisodeDownloadProcessor +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter + +class AnimeCover7Proxy( + private val onMenuItemClickListener: (( + data: AnimeCover7Bean, + ) -> Unit)? = null +) : LazyGridAdapter.Proxy() { + @Composable + override fun draw(modifier: Modifier, index: Int, data: AnimeCover7Bean) { + AnimeCover7Item(data = data, onMenuItemClickListener = onMenuItemClickListener) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AnimeCover7Item( + data: AnimeCover7Bean, + onMenuItemClickListener: (( + data: AnimeCover7Bean, + ) -> Unit)? = null +) { + val activity = LocalContext.current.activity + var menuExpanded by remember { mutableStateOf(false) } + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { + data.route.route(activity) + }, + onLongClick = { + menuExpanded = true + } + ) + .padding(horizontal = 16.dp, vertical = 9.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = data.title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + modifier = Modifier + .padding(start = 10.dp) + .widthIn(min = 45.dp), + text = data.size.orEmpty(), + textAlign = TextAlign.End, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + if (data.route.startsWith(EpisodeDownloadProcessor.route, ignoreCase = true)) { + Text( + modifier = Modifier.padding(start = 10.dp), + text = data.episodeCount.orEmpty(), + textAlign = TextAlign.End, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.secondary + ) + } + if (data.pathType == 1) { + Text( + modifier = Modifier + .padding(start = 10.dp), + text = stringResource(id = R.string.old_path), + textAlign = TextAlign.End, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + } + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false } + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.anime_download_activity_item_menu_delete)) }, + onClick = { + menuExpanded = false + onMenuItemClickListener?.invoke(data) + }, + leadingIcon = { + Icon( + Icons.Rounded.Delete, + contentDescription = null + ) + } + ) + } + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover8Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover8Proxy.kt new file mode 100644 index 00000000..515eefdd --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover8Proxy.kt @@ -0,0 +1,94 @@ +package com.skyd.imomoe.view.adapter.compose.proxy + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover8Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.coil.AnimeAsyncImage +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter + +class AnimeCover8Proxy : LazyGridAdapter.Proxy() { + @Composable + override fun draw(modifier: Modifier, index: Int, data: AnimeCover8Bean) { + AnimeCover8Item(modifier = modifier, data = data) + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun AnimeCover8Item( + modifier: Modifier = Modifier, + data: AnimeCover8Bean, +) { + Card( + modifier = modifier + .padding(top = 7.dp, bottom = 5.dp) + .aspectRatio(7 / 10f) + ) { + val activity = LocalContext.current.activity + Box( + modifier = Modifier.combinedClickable( + onClick = { + val lastEpisodeUrl = data.lastEpisodeUrl + if (lastEpisodeUrl != null) { + lastEpisodeUrl.route(activity) + } else { + data.animeUrl.route(activity) + } + }, + onLongClick = { + data.animeUrl.route(activity) + } + ) + ) { + AnimeAsyncImage( + modifier = Modifier.fillMaxSize(), + url = data.cover.url, + referer = data.cover.referer, + contentScale = ContentScale.Crop + ) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + 0f to Color.Transparent, + 1f to Color(0x54000000) + ) + ) + .padding(horizontal = 7.dp) + .padding(top = 20.dp, bottom = 7.dp) + ) { + Text( + modifier = Modifier.padding(top = 5.dp), + text = data.animeTitle, + maxLines = 1, + style = MaterialTheme.typography.titleSmall, + color = Color.White + ) + Text( + modifier = Modifier.padding(top = 2.dp), + text = data.lastEpisode?.let { + stringResource(id = R.string.already_seen_episode_x, it) + } ?: stringResource(id = R.string.have_not_watched_this_anime), + maxLines = 1, + style = MaterialTheme.typography.titleSmall, + color = Color.White + ) + } + } + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover9Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover9Proxy.kt new file mode 100644 index 00000000..aa528573 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/AnimeCover9Proxy.kt @@ -0,0 +1,146 @@ +package com.skyd.imomoe.view.adapter.compose.proxy + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover9Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.util.coil.AnimeAsyncImage +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter + +class AnimeCover9Proxy( + private val onDeleteButtonClickListener: (( + index: Int, + data: AnimeCover9Bean, + ) -> Unit)? = null +) : LazyGridAdapter.Proxy() { + @Composable + override fun draw(modifier: Modifier, index: Int, data: AnimeCover9Bean) { + AnimeCover9Item( + modifier = modifier, + index = index, + data = data, + onDeleteButtonClickListener = onDeleteButtonClickListener + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AnimeCover9Item( + modifier: Modifier = Modifier, + index: Int, + data: AnimeCover9Bean, + onDeleteButtonClickListener: (( + index: Int, + data: AnimeCover9Bean, + ) -> Unit)? = null +) { + val activity = LocalContext.current.activity + Card( + modifier = modifier + .padding(vertical = 7.dp) + .fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .clickable { + val lastEpisodeUrl = data.lastEpisodeUrl + if (lastEpisodeUrl != null) { + lastEpisodeUrl.route(activity) + } else { + data.animeUrl.route(activity) + } + } + ) { + AnimeAsyncImage( + modifier = Modifier + .width(120.dp) + .fillMaxHeight(), + url = data.cover.url, + referer = data.cover.referer, + contentScale = ContentScale.Crop, + ) + Column(modifier = Modifier.padding(start = 10.dp)) { + Row { + Text( + modifier = Modifier + .padding(top = 10.dp, end = 16.dp) + .weight(1f), + text = data.animeTitle, + maxLines = 3, + style = MaterialTheme.typography.titleMedium + ) + Row( + modifier = Modifier + .padding(top = 13.dp, end = 12.dp) + .clickable { + data.animeUrl.route(activity) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.detail_page), + maxLines = 3, + style = MaterialTheme.typography.labelMedium + ) + Icon( + modifier = Modifier.size(12.dp), + painter = painterResource(id = R.drawable.ic_arrow_forward_ios_12), + contentDescription = null + ) + } + } + Row { + Column(modifier = Modifier.weight(1f)) { + OutlinedCard( + modifier = Modifier.padding(top = 7.dp, end = 16.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), + text = data.lastEpisode.orEmpty(), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + Text( + modifier = Modifier + .weight(1f) + .padding(top = 7.dp, bottom = 10.dp), + text = Util.time2Now(data.time), + maxLines = 3, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary + ) + } + IconButton( + modifier = Modifier.align(Alignment.Bottom), + onClick = { + onDeleteButtonClickListener?.invoke(index, data) + } + ) { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = stringResource(id = R.string.delete) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/More1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/More1Proxy.kt new file mode 100644 index 00000000..4956ce22 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/More1Proxy.kt @@ -0,0 +1,68 @@ +package com.skyd.imomoe.view.adapter.compose.proxy + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.bean.More1Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter + +class More1Proxy : LazyGridAdapter.Proxy() { + @Composable + override fun draw(modifier: Modifier, index: Int, data: More1Bean) { + More1Item(modifier = modifier, data = data) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun More1Item( + modifier: Modifier = Modifier, + data: More1Bean, +) { + ElevatedCard(modifier = modifier.padding(vertical = 6.dp)) { + CardContent(data) + } +} + +@Composable +private fun CardContent(data: More1Bean) { + val activity = LocalContext.current.activity + var padding by remember { mutableStateOf(0.dp) } + Column( + modifier = Modifier + .fillMaxSize() + .clickable { data.route.route(activity) } + .padding(horizontal = 20.dp, vertical = 17.dp + padding / 2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.size(40.dp), + painter = painterResource(id = data.image), + contentDescription = null + ) + val density = LocalDensity.current.density + Text( + modifier = Modifier.padding(top = 10.dp), + text = data.title, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + onTextLayout = { + val lineCount = it.lineCount + val height = (it.size.height / density).dp + padding = if (lineCount > 1) 0.dp else height + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/SkinCover1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/SkinCover1Proxy.kt new file mode 100644 index 00000000..8cf4fc16 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/SkinCover1Proxy.kt @@ -0,0 +1,83 @@ +package com.skyd.imomoe.view.adapter.compose.proxy + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.SkinCover1Bean +import com.skyd.imomoe.ext.theme.appThemeRes +import com.skyd.imomoe.util.coil.AnimeAsyncImage +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter + +class SkinCover1Proxy : LazyGridAdapter.Proxy() { + @Composable + override fun draw(modifier: Modifier, index: Int, data: SkinCover1Bean) { + SkinCover1Item(modifier = modifier, data = data) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SkinCover1Item( + modifier: Modifier = Modifier, + data: SkinCover1Bean, +) { + ElevatedCard( + modifier = modifier + .padding(vertical = 6.dp) + .fillMaxWidth() + .wrapContentHeight() + .aspectRatio(ratio = 2 / 1.4f) + ) { + Box( + modifier = Modifier.clickable { + if (data.using) return@clickable + // 设置appThemeRes后,所有Activity都会重启 + appThemeRes = data.themeRes + } + ) { + val cover = data.cover + if (cover is Int) { + Image( + modifier = Modifier.fillMaxSize(), + painter = ColorPainter(Color(cover)), + contentDescription = null + ) + } else if (cover is String) { + AnimeAsyncImage( + modifier = Modifier.fillMaxSize(), + url = cover, + contentDescription = null + ) + } + if (data.using) { + Image( + modifier = Modifier + .padding(top = 6.dp, end = 6.dp) + .size(16.dp) + .align(Alignment.TopEnd), + painter = painterResource(id = R.drawable.ic_right_32), + colorFilter = ColorFilter.tint(color = Color.White), + contentDescription = null + ) + } + Text( + modifier = Modifier + .padding(9.dp) + .align(Alignment.BottomEnd), + text = data.title, + style = MaterialTheme.typography.titleMedium, + color = Color.White + ) + } + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/UpnpDevice1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/UpnpDevice1Proxy.kt new file mode 100644 index 00000000..7579767b --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/compose/proxy/UpnpDevice1Proxy.kt @@ -0,0 +1,50 @@ +package com.skyd.imomoe.view.adapter.compose.proxy + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import org.fourthline.cling.model.meta.Device + +class UpnpDevice1Proxy( + private val onClickListener: (( + index: Int, + data: Device<*, *, *>, + ) -> Unit)? = null +) : LazyGridAdapter.Proxy>() { + @Composable + override fun draw(modifier: Modifier, index: Int, data: Device<*, *, *>) { + UpnpDevice1Item( + index = index, + data = data, + onClickListener = onClickListener + ) + } +} + +@Composable +fun UpnpDevice1Item( + index: Int, + data: Device<*, *, *>, + onClickListener: (( + index: Int, + data: Device<*, *, *>, + ) -> Unit)? = null +) { + Text( + modifier = Modifier + .fillMaxWidth() + .run { + if (onClickListener != null) clickable { onClickListener(index, data) } + else this + } + .padding(vertical = 10.dp, horizontal = 16.dp), + text = data.details?.friendlyName.orEmpty(), + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + color = MaterialTheme.colorScheme.primary + ) +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeCoverItemDecoration.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeCoverItemDecoration.kt deleted file mode 100644 index dc967b41..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeCoverItemDecoration.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.skyd.imomoe.view.adapter.decoration - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -class AnimeCoverItemDecoration : RecyclerView.ItemDecoration() { - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - super.getItemOffsets(outRect, view, parent, state) - val childPosition = parent.getChildAdapterPosition(view) - when (childPosition % 4) { - // 一共15*3px的间距,四个item,三个空白间距 - // 每个item空白区域总宽度要一样才能让imageView图片宽度一样 - 0 -> { - outRect.left = 0 - outRect.right = 10 - } - 1 -> { - outRect.left = 5 - outRect.right = (15 / 2.0).toInt() - } - 2 -> { - outRect.left = (15 / 2.0).toInt() - outRect.right = 5 - } - 3 -> { - outRect.left = 10 - outRect.right = 0 - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeEpisodeItemDecoration.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeEpisodeItemDecoration.kt deleted file mode 100644 index a30e2865..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeEpisodeItemDecoration.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.skyd.imomoe.view.adapter.decoration - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import kotlin.math.ceil - -class AnimeEpisodeItemDecoration : RecyclerView.ItemDecoration() { - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - super.getItemOffsets(outRect, view, parent, state) - val childPosition = parent.getChildAdapterPosition(view) - val childCount = parent.adapter?.itemCount ?: 0 - when (childPosition % 3) { - // 一共15*2px的间距,3个item,2个空白间距 - // 每个item空白区域总宽度要一样才能让imageView图片宽度一样 - 0 -> { - outRect.left = 0 - outRect.right = 18 - } - 1 -> { - outRect.left = 9 - outRect.right = 9 - } - 2 -> { - outRect.left = 18 - outRect.right = 0 - } - } - when (ceil((childPosition + 1) / 3.0).toInt()) { - ceil(childCount / 3.0).toInt() -> { - outRect.top = 0 - outRect.bottom = 0 - } - else -> { - outRect.top = 0 - outRect.bottom = 18 - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeShowItemDecoration.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeShowItemDecoration.kt index 69bac7d0..d3e64443 100644 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeShowItemDecoration.kt +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/AnimeShowItemDecoration.kt @@ -4,7 +4,13 @@ import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.bean.AnimeEpisode1Bean +import com.skyd.imomoe.bean.HorizontalRecyclerView1Bean +import com.skyd.imomoe.bean.SearchHistory1Bean import com.skyd.imomoe.util.Util.dp +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize.Companion.MAX_SPAN_SIZE +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import kotlin.math.roundToInt class AnimeShowItemDecoration : RecyclerView.ItemDecoration() { @@ -18,32 +24,149 @@ class AnimeShowItemDecoration : RecyclerView.ItemDecoration() { val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams val spanSize = layoutParams.spanSize val spanIndex = layoutParams.spanIndex - /** - * 不相等时说明是Grid形式显示的 - * 然后判断是左边还有右边显示,分别设置间距为15 - */ - if (spanSize == 1) { - when (spanIndex) { - // 16+x=y+5/2 - // x+y=5 - // x=-4.25 y=9.25 - 0 -> { - outRect.left = 16.dp - outRect.right = -(4.25f.dp).toInt() // -4.25 - } - 1 -> { - outRect.left = 9.25f.dp.toInt() // 9.25 - outRect.right = 2.5f.dp.toInt() // 测试机5dp==15px - } - 2 -> { - outRect.left = 2.5f.dp.toInt() - outRect.right = 9.25f.dp.toInt() // 9.25 - } - 3 -> { - outRect.left = -(4.25f.dp).toInt() // -4.25 - outRect.right = 16.dp + + val item = (parent.adapter as? VarietyAdapter) + ?.dataList + // 注意这里使用getChildLayoutPosition的目的 + // 如果使用getChildAdapterPosition,刷新的时候可能会(边框)闪动一下,(返回-1) + ?.getOrNull(parent.getChildLayoutPosition(view)) + if (needVerticalMargin(item?.javaClass)) { + outRect.top = 10.dp + outRect.bottom = 2.dp + } + if (spanSize == MAX_SPAN_SIZE) { + /** + * 只有一列 + */ + if (noHorizontalMargin(item?.javaClass)) return + outRect.left = HORIZONTAL_PADDING + outRect.right = HORIZONTAL_PADDING + } else if (spanSize == MAX_SPAN_SIZE / 2) { + /** + * 只有两列,没有在中间的item + * 2x = ITEM_SPACING + */ + val x: Int = (ITEM_SPACING / 2f).roundToInt() + if (spanIndex == 0) { + outRect.left = HORIZONTAL_PADDING + outRect.right = x + } else { + outRect.left = x + outRect.right = HORIZONTAL_PADDING + } + } else if (spanSize == MAX_SPAN_SIZE / 3) { + /** + * 只有三列,一个在中间的item + * HORIZONTAL_PADDING + x = 2y + * x + y = ITEM_SPACING + */ + val y: Int = ((HORIZONTAL_PADDING + ITEM_SPACING) / 3f).roundToInt() + val x: Int = ITEM_SPACING - y + if (spanIndex == 0) { + outRect.left = HORIZONTAL_PADDING + outRect.right = x + } else if (spanIndex + spanSize == MAX_SPAN_SIZE) { + // 最右侧最后一个 + outRect.left = x + outRect.right = HORIZONTAL_PADDING + } else { + outRect.left = y + outRect.right = y + } + } else if (spanSize == MAX_SPAN_SIZE / 5) { + /** + * 只有五列 + * HORIZONTAL_PADDING + x = y + z + * x + y = ITEM_SPACING + * z + (HORIZONTAL_PADDING + x) / 2 = ITEM_SPACING + */ + val x: Int = ((4 * ITEM_SPACING - 3 * HORIZONTAL_PADDING) / 5f).roundToInt() + val y: Int = ITEM_SPACING - x + val z: Int = HORIZONTAL_PADDING + x - y + if (spanIndex == 0) { + // 最左侧第一个 + outRect.left = HORIZONTAL_PADDING + outRect.right = x + } else if (spanIndex + spanSize == MAX_SPAN_SIZE) { + // 最右侧最后一个 + outRect.left = x + outRect.right = HORIZONTAL_PADDING + } else if (spanIndex == spanSize) { + // 第二个 + outRect.left = y + outRect.right = z + } else if (spanIndex == MAX_SPAN_SIZE - 2 * spanSize) { + // 倒数第二个 + outRect.left = z + outRect.right = y + } else { + // 最中间的 + outRect.left = ((HORIZONTAL_PADDING + x) / 2f).roundToInt() + outRect.right = ((HORIZONTAL_PADDING + x) / 2f).roundToInt() + } + } else { + /** + * 多于三列(不包括五列),有在中间的item + */ + if ((MAX_SPAN_SIZE / spanSize) % 2 == 0) { + /** + * 偶数个item + * HORIZONTAL_PADDING + x = y + ITEM_SPACING / 2 + * x + y = ITEM_SPACING + */ + val y: Int = ((HORIZONTAL_PADDING + ITEM_SPACING / 2f) / 2f).roundToInt() + val x: Int = ITEM_SPACING - y + if (spanIndex == 0) { + // 最左侧第一个 + outRect.left = HORIZONTAL_PADDING + outRect.right = x + } else if (spanIndex + spanSize == MAX_SPAN_SIZE) { + // 最右侧最后一个 + outRect.left = x + outRect.right = HORIZONTAL_PADDING + } else { + // 中间的项目 + if (spanIndex < MAX_SPAN_SIZE / 2) { + // 左侧部分 + outRect.left = y + outRect.right = ITEM_SPACING / 2 + } else { + // 右侧部分 + outRect.left = ITEM_SPACING / 2 + outRect.right = y + } } + } else { + /** + * 奇数个item,严格大于5的奇数(暂无需求,未实现) + */ } } } + + companion object { + val ITEM_SPACING: Int = 12.dp + val HORIZONTAL_PADDING: Int = 16.dp + + private val noHorizontalMarginType: Set> = setOf( + HorizontalRecyclerView1Bean::class.java, + SearchHistory1Bean::class.java, + ) + + fun noHorizontalMargin(clz: Class<*>?): Boolean { + clz ?: return true + if (clz in noHorizontalMarginType) return true + return false + } + + private val needVerticalMarginType: Set> = setOf( + AnimeEpisode1Bean::class.java, + ) + + fun needVerticalMargin(clz: Class<*>?): Boolean { + clz ?: return false + if (clz in needVerticalMarginType) return true + return false + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/HorizontalRecyclerViewDecoration.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/HorizontalRecyclerViewDecoration.kt new file mode 100644 index 00000000..1bb2ced1 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/HorizontalRecyclerViewDecoration.kt @@ -0,0 +1,18 @@ +package com.skyd.imomoe.view.adapter.decoration + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.util.Util.dp + +class HorizontalRecyclerViewDecoration : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + outRect.right = 7.dp + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/SkinItemDecoration.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/SkinItemDecoration.kt deleted file mode 100644 index 7c22ccb1..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/decoration/SkinItemDecoration.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.skyd.imomoe.view.adapter.decoration - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.util.Util.dp - - -class SkinItemDecoration : RecyclerView.ItemDecoration() { - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - super.getItemOffsets(outRect, view, parent, state) - val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams - val spanSize = layoutParams.spanSize - val spanIndex = layoutParams.spanIndex - /** - * 不相等时说明是Grid形式显示的 - * 然后判断是左边还有右边显示 - */ - if (spanSize == 1) { - when (spanIndex) { - // 16+x=2y - // x+y=14 - // x=4 y=10 - 0 -> { - outRect.left = 16.dp - outRect.right = 4.dp // 4 - } - 1 -> { - outRect.left = 10.dp // 10 - outRect.right = 10.dp // 10 - } - 2 -> { - outRect.left = 4.dp // 4 - outRect.right = 16.dp - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/AnimeDetailSpanSize.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/AnimeDetailSpanSize.kt deleted file mode 100644 index 6c09538c..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/AnimeDetailSpanSize.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.skyd.imomoe.view.adapter.spansize - -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.config.Const.ViewHolderTypeInt - -class AnimeDetailSpanSize(val adapter: RecyclerView.Adapter) : - GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when(adapter.getItemViewType(position)) { - ViewHolderTypeInt.HEADER_1 -> 4 - ViewHolderTypeInt.GRID_RECYCLER_VIEW_1 -> 4 - ViewHolderTypeInt.HORIZONTAL_RECYCLER_VIEW_1 -> 4 - ViewHolderTypeInt.ANIME_DESCRIBE_1 -> 4 - ViewHolderTypeInt.ANIME_INFO_1 -> 4 - else -> 1 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/AnimeShowSpanSize.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/AnimeShowSpanSize.kt index 15594975..d6edebc4 100644 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/AnimeShowSpanSize.kt +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/AnimeShowSpanSize.kt @@ -1,19 +1,53 @@ package com.skyd.imomoe.view.adapter.spansize import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.config.Const.ViewHolderTypeInt +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.* +import com.skyd.imomoe.ext.screenIsLand +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeShowSpanSize( + val adapter: VarietyAdapter, + val enableLandScape: Boolean = true +) : GridLayoutManager.SpanSizeLookup() { + companion object { + const val MAX_SPAN_SIZE = 60 + } -class AnimeShowSpanSize(val adapter: RecyclerView.Adapter) : - GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { - return when (adapter.getItemViewType(position)) { - ViewHolderTypeInt.GRID_RECYCLER_VIEW_1 -> 4 - ViewHolderTypeInt.HEADER_1 -> 4 - ViewHolderTypeInt.BANNER_1 -> 4 - ViewHolderTypeInt.ANIME_COVER_3 -> 4 - ViewHolderTypeInt.ANIME_COVER_5 -> 4 - else -> 1 + return if (enableLandScape && appContext.screenIsLand) { + when (adapter.dataList[position]) { + is Header1Bean, + is Banner1Bean, + is AnimeDescribe1Bean, + is AnimeInfo1Bean, + is SearchHistory1Bean, + is AnimeDownload1Bean, + is HorizontalRecyclerView1Bean -> MAX_SPAN_SIZE + is AnimeEpisode1Bean, + is AnimeCover1Bean -> MAX_SPAN_SIZE / 5 + is AnimeCover3Bean, + is AnimeCover5Bean, + is AnimeCover11Bean -> MAX_SPAN_SIZE / 2 + else -> MAX_SPAN_SIZE / 3 + } + } else { + when (adapter.dataList[position]) { + is Header1Bean, + is Banner1Bean, + is AnimeDescribe1Bean, + is AnimeInfo1Bean, + is AnimeCover3Bean, + is AnimeCover5Bean, + is AnimeCover11Bean, + is SearchHistory1Bean, + is AnimeDownload1Bean, + is HorizontalRecyclerView1Bean -> MAX_SPAN_SIZE + is AnimeEpisode1Bean, + is AnimeCover1Bean -> MAX_SPAN_SIZE / 3 + is AnimeCover4Bean -> MAX_SPAN_SIZE / 2 + else -> MAX_SPAN_SIZE / 3 + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/PlaySpanSize.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/PlaySpanSize.kt deleted file mode 100644 index 8983af79..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/PlaySpanSize.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.skyd.imomoe.view.adapter.spansize - -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.config.Const.ViewHolderTypeInt - -class PlaySpanSize(val adapter: RecyclerView.Adapter) : - GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when(adapter.getItemViewType(position)) { - ViewHolderTypeInt.HEADER_1 -> 4 - ViewHolderTypeInt.GRID_RECYCLER_VIEW_1 -> 4 - ViewHolderTypeInt.ANIME_EPISODE_FLOW_LAYOUT_1 -> 4 - ViewHolderTypeInt.HORIZONTAL_RECYCLER_VIEW_1 -> 4 - else -> 1 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/SkinSpanSize.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/SkinSpanSize.kt deleted file mode 100644 index 3d939d72..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/adapter/spansize/SkinSpanSize.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.skyd.imomoe.view.adapter.spansize - -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.config.Const.ViewHolderTypeInt - -class SkinSpanSize(val adapter: RecyclerView.Adapter) : - GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when (adapter.getItemViewType(position)) { - ViewHolderTypeInt.HEADER_1 -> 4 - else -> 1 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/AsyncListDiffer.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/AsyncListDiffer.kt new file mode 100644 index 00000000..ec8b2c9d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/AsyncListDiffer.kt @@ -0,0 +1,112 @@ +package com.skyd.imomoe.view.adapter.variety + +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListUpdateCallback +import kotlinx.coroutines.* + +/** + * Helper for computing difference between two list via [DiffUtil] in a background thread + */ +class AsyncListDiffer( + /** + * [ListUpdateCallback] which diff result dispatched to + */ + var listUpdateCallback: ListUpdateCallback, + /** + * a [CoroutineDispatcher] defined by yourself + * common usage is to turn existing [Executor] into [CoroutineDispatcher] by [asCoroutineDispatcher] + */ + dispatcher: CoroutineDispatcher +) : CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher) { + /** + * the result list after last diffing + */ + var oldList = listOf() + + /** + * an Int to auto-increase by the times of [submitList] invocation + */ + private var maxSubmitGeneration: Int = 0 + + /** + * submit a new list and begin diffing with the old list in a background thread, + * when the diff is completed, the result will be dispatched to [ListUpdateCallback] + */ + fun submitList(newList: List) { + val newListCopy = newList.toList() + val submitGeneration = ++maxSubmitGeneration + + // fast return: old list is empty, just add all new list + if (this.oldList.isEmpty()) { + oldList = newListCopy + listUpdateCallback.onInserted(0, newList.size) + return + } + + // begin diffing in a new coroutine + launch { + val diffResult = DiffUtil.calculateDiff(DiffCallback(oldList, newListCopy)) + // dispatch the diff result to main thread + withContext(Dispatchers.Main) { + // just apply the last diffResult, discard the others + if (submitGeneration == maxSubmitGeneration) { + oldList = newListCopy + diffResult.dispatchUpdatesTo(listUpdateCallback) + } + } + } + } + + inner class DiffCallback(var oldList: List, var newList: List) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + val oldDiff = oldItem as? Diff + val newDiff = newItem as? Diff + return if (oldDiff == null || newDiff == null) oldItem.hashCode() == newItem.hashCode() + else oldDiff sameAs newItem + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + val oldDiff = oldItem as? Diff + val newDiff = newItem as? Diff + return if (oldDiff == null || newDiff == null) oldItem == newItem + else oldDiff contentSameAs newDiff + } + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val oldItem = oldList[oldItemPosition] as? Diff + val newItem = newList[newItemPosition] as? Diff + if (oldItem == null || newItem == null) return null + // if new and old items are the same object but have different content, call diff() to find the precise difference + return oldItem diff newItem + } + } +} + +/** + * an interface should be implemented by object wanna be differentiated by [AsyncListDiffer] + */ +interface Diff { + /** + * diff one object to [o] object + * @return the detail of difference defined by yourself + */ + infix fun diff(o: Any?): Any? = null + + /** + * whether this object and [o] is the same object + */ + infix fun sameAs(o: Any?): Boolean = this == o + + /** + * whether this object has the same content with [o] + */ + infix fun contentSameAs(o: Any?): Boolean +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/VarietyAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/VarietyAdapter.kt new file mode 100644 index 00000000..c95517e8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/VarietyAdapter.kt @@ -0,0 +1,131 @@ +package com.skyd.imomoe.view.adapter.variety + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.AdapterListUpdateCallback +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.skyd.imomoe.BuildConfig +import com.skyd.imomoe.util.EmptyViewHolder +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import java.lang.reflect.ParameterizedType + +class VarietyAdapter( + private var proxyList: MutableList> = mutableListOf(), + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : RecyclerView.Adapter() { + private val dataDiffer = AsyncListDiffer(AdapterListUpdateCallback(this), dispatcher) + + // 一定要保证set时传入的List是新的List,与旧的引用不能相同,里面的Item引用最好也不要相同 + var dataList: List + set(value) { + dataDiffer.submitList(value) + } + get() = dataDiffer.oldList + + var action: ((Any?) -> Unit)? = null + var onAttachedToRecyclerView: ((recyclerView: RecyclerView) -> Unit)? = null + var onDetachedFromRecyclerView: ((recyclerView: RecyclerView) -> Unit)? = null + var onFailedToRecycleView: ((holder: ViewHolder) -> Boolean)? = null + var onViewAttachedToWindow: ((holder: ViewHolder) -> Unit)? = null + var onViewDetachedFromWindow: ((holder: ViewHolder) -> Unit)? = null + var onViewRecycled: ((holder: ViewHolder) -> Unit)? = null + + fun addProxy(proxy: Proxy) { + proxyList.add(proxy) + } + + fun removeProxy(proxy: Proxy) { + proxyList.remove(proxy) + } + + override fun onViewAttachedToWindow(holder: ViewHolder) { + onViewAttachedToWindow?.invoke(holder) + } + + override fun onViewDetachedFromWindow(holder: ViewHolder) { + onViewDetachedFromWindow?.invoke(holder) + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + onAttachedToRecyclerView?.invoke(recyclerView) + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + dataDiffer.cancel() + onDetachedFromRecyclerView?.invoke(recyclerView) + } + + override fun onViewRecycled(holder: ViewHolder) { + onViewRecycled?.invoke(holder) + } + + override fun onFailedToRecycleView(holder: ViewHolder): Boolean { + return onFailedToRecycleView?.invoke(holder) ?: super.onFailedToRecycleView(holder) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + // debug模式下让他崩溃,以便检查错误出处 + if (viewType == -1 && !BuildConfig.DEBUG) return EmptyViewHolder(View(parent.context)) + return proxyList[viewType].onCreateViewHolder(parent, viewType) + } + + @Suppress("UNCHECKED_CAST") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val type = getItemViewType(position) + if (type != -1) (proxyList[type] as Proxy) + .onBindViewHolder(holder, dataList[position], position, action) + } + + // 布局刷新 + @Suppress("UNCHECKED_CAST") + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + val type = getItemViewType(position) + if (type != -1) (proxyList[type] as Proxy) + .onBindViewHolder(holder, dataList[position], position, action, payloads) + } + + override fun getItemCount(): Int = dataList.size + + override fun getItemViewType(position: Int): Int { + return getProxyIndex(dataList[position]) + } + + // 获取策略在列表中的索引,可能返回-1 + private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst { + // 如果Proxy中的第一个类型参数T和数据的类型相同,则返回对应策略的索引 + (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].let { argument -> + if (argument.toString() == data.javaClass.toString()) + true // 正常情况 + else { + // Proxy第一个泛型是类似List,又嵌套了个泛型 + if (argument is ParameterizedType) + argument.rawType.toString() == data.javaClass.toString() + else false + } + } + } + + // 抽象策略类 + abstract class Proxy { + abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder + abstract fun onBindViewHolder( + holder: VH, + data: T, + index: Int, + action: ((Any?) -> Unit)? = null + ) + + open fun onBindViewHolder( + holder: VH, + data: T, + index: Int, + action: ((Any?) -> Unit)? = null, + payloads: MutableList + ) { + onBindViewHolder(holder, data, index, action) + } + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover11Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover11Proxy.kt new file mode 100644 index 00000000..5a9ab0c1 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover11Proxy.kt @@ -0,0 +1,45 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover11Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover11ViewHolder +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover11Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover11ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_11, parent, false) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder( + holder: AnimeCover11ViewHolder, + data: AnimeCover11Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.tvAnimeCover11Rank.text = "${index + 1}" + holder.tvAnimeCover11Rank.background = if (index in 0..2) { + val backgrounds = intArrayOf( + R.drawable.shape_fill_circle_corner_golden_50, + R.drawable.shape_fill_circle_corner_silvery_50, + R.drawable.shape_fill_circle_corner_coppery_50 + ) + Util.getResDrawable(backgrounds[index]) + } else { + Util.getResDrawable(R.drawable.shape_fill_circle_corner_50) + } + holder.tvAnimeCover11Title.text = data.title + holder.itemView.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover12Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover12Proxy.kt new file mode 100644 index 00000000..770533c2 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover12Proxy.kt @@ -0,0 +1,37 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover12Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover12ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover12Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover12ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_12, parent, false) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder( + holder: AnimeCover12ViewHolder, + data: AnimeCover12Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.tvAnimeCover12Title.text = data.title + holder.tvAnimeCover12Episode.text = data.episodeClickable.title + holder.itemView.setOnClickListener { + data.episodeClickable.route.ifBlank { data.route }.route(activity) + } + holder.tvAnimeCover12Title.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover1Proxy.kt new file mode 100644 index 00000000..1416f44d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover1Proxy.kt @@ -0,0 +1,61 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.IntRange +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover1Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover1ViewHolder +import com.skyd.imomoe.util.coil.CoilUtil.loadImage +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover1Proxy( + @IntRange(from = 0, to = 1) private val color: Int = BLACK, +) : VarietyAdapter.Proxy() { + companion object { + const val BLACK = 0 + const val WHITE = 1 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_1, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeCover1ViewHolder, + data: AnimeCover1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + when (color) { + WHITE -> holder.tvAnimeCover1Title.setTextColor( + ContextCompat.getColor( + holder.itemView.context, + android.R.color.white + ) + ) + } + holder.ivAnimeCover1Cover.setTag(R.id.image_view_tag, data.cover.url) + if (holder.ivAnimeCover1Cover.getTag(R.id.image_view_tag) == data.cover.url) { + holder.ivAnimeCover1Cover.loadImage(data.cover.url, referer = data.cover.referer) + } + holder.tvAnimeCover1Title.text = data.title + if (data.episode.isBlank()) { + holder.tvAnimeCover1Episode.gone() + } else { + holder.tvAnimeCover1Episode.visible() + holder.tvAnimeCover1Episode.text = data.episode + } + holder.itemView.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover2Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover2Proxy.kt new file mode 100644 index 00000000..72744769 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover2Proxy.kt @@ -0,0 +1,33 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover2Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover2ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover2Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover2ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_2, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeCover2ViewHolder, + data: AnimeCover2Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.tvAnimeCover1Title.text = data.title + holder.tvAnimeCover1Episode.gone() + holder.itemView.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover3Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover3Proxy.kt new file mode 100644 index 00000000..366b79ef --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover3Proxy.kt @@ -0,0 +1,64 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover3Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover3ViewHolder +import com.skyd.imomoe.util.coil.CoilUtil.loadImage +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover3Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover3ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_3, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeCover3ViewHolder, + data: AnimeCover3Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.ivAnimeCover3Cover.setTag(R.id.image_view_tag, data.cover?.url) + holder.tvAnimeCover3Title.text = data.title + holder.tvAnimeCover3Describe.text = data.describe + if (data.episode.isNullOrBlank()) { + holder.tvAnimeCover3Episode.gone() + } else { + holder.tvAnimeCover3Episode.visible() + holder.tvAnimeCover3Episode.text = data.episode + } + holder.flAnimeCover3Type.removeAllViews() + if (activity != null) { + if (holder.ivAnimeCover3Cover.getTag(R.id.image_view_tag) == data.cover?.url) { + holder.ivAnimeCover3Cover.loadImage(data.cover?.url, referer = data.cover?.referer) + } + + data.animeType.orEmpty().forEach { type -> + val cardView = activity.layoutInflater.inflate( + R.layout.item_anime_type_1, + holder.flAnimeCover3Type, + false + ) as CardView + cardView.findViewById(R.id.tv_anime_type_1).text = type.title + cardView.setOnClickListener { + if (type.route.isBlank()) return@setOnClickListener + type.route.route(activity) + } + holder.flAnimeCover3Type.addView(cardView) + } + } + holder.itemView.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover4Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover4Proxy.kt new file mode 100644 index 00000000..307cd388 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover4Proxy.kt @@ -0,0 +1,38 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover4Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover4ViewHolder +import com.skyd.imomoe.util.coil.CoilUtil.loadImage +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover4Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover4ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_4, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeCover4ViewHolder, + data: AnimeCover4Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.ivAnimeCover4Cover.setTag(R.id.image_view_tag, data.cover.url) + if (activity != null) { + if (holder.ivAnimeCover4Cover.getTag(R.id.image_view_tag) == data.cover.url) { + holder.ivAnimeCover4Cover.loadImage(data.cover.url, referer = data.cover.referer) + } + } + holder.tvAnimeCover4Title.text = data.title + holder.itemView.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover5Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover5Proxy.kt new file mode 100644 index 00000000..b232ac41 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover5Proxy.kt @@ -0,0 +1,68 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isGone +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover5Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover5ViewHolder +import com.skyd.imomoe.util.Util.dp +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover5Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover5ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_5, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeCover5ViewHolder, + data: AnimeCover5Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + if (data.area.title.isNullOrBlank()) { + holder.tvAnimeCover5Area.gone() + holder.tvAnimeCover5Date.post { + holder.tvAnimeCover5Date.setPadding(0, 0, 0, 0) + } + } else { + holder.tvAnimeCover5Area.visible() + holder.tvAnimeCover5Date.post { + holder.tvAnimeCover5Date.setPadding(12.dp, 0, 0, 0) + } + } + if (data.date.isBlank()) { + holder.tvAnimeCover5Date.gone() + } else { + holder.tvAnimeCover5Date.visible() + } + holder.tvAnimeCover5Title.text = data.title + holder.tvAnimeCover5Area.text = data.area.title + holder.tvAnimeCover5Date.text = data.date + holder.tvAnimeCover5Episode.text = data.episodeClickable.title + if (holder.tvAnimeCover5Area.isGone && holder.tvAnimeCover5Date.isGone) { + holder.tvAnimeCover5Title.post { + holder.tvAnimeCover5Title.setPadding( + holder.tvAnimeCover5Title.paddingStart, 12.dp, + holder.tvAnimeCover5Title.paddingEnd, 12.dp + ) + } + } + holder.itemView.setOnClickListener { + data.episodeClickable.route.route(activity) + } + holder.tvAnimeCover5Area.setOnClickListener { + data.area.route.route(activity) + } + holder.tvAnimeCover5Title.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover6Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover6Proxy.kt new file mode 100644 index 00000000..4ca303dd --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeCover6Proxy.kt @@ -0,0 +1,51 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeCover6Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeCover6ViewHolder +import com.skyd.imomoe.util.coil.CoilUtil.loadImage +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeCover6Proxy( + private val height: Int? = null, + private val width: Int? = null +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeCover6ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_anime_cover_6, parent, false) + ).apply { + itemView.layoutParams.let { layoutParams -> + height?.let { layoutParams.height = it } + width?.let { layoutParams.width = it } + itemView.layoutParams = layoutParams + } + } + + override fun onBindViewHolder( + holder: AnimeCover6ViewHolder, + data: AnimeCover6Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.ivAnimeCover6Cover.loadImage(data.cover?.url, referer = data.cover?.referer) + holder.tvAnimeCover6Title.text = data.title + holder.tvAnimeCover6Episode.text = data.episodeClickable?.title + if (data.describe.isNullOrEmpty()) { + holder.tvAnimeCover6Describe.gone() + } else { + holder.tvAnimeCover6Describe.visible() + holder.tvAnimeCover6Describe.text = data.describe + } + holder.itemView.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeDescribe1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeDescribe1Proxy.kt new file mode 100644 index 00000000..fa49e406 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeDescribe1Proxy.kt @@ -0,0 +1,27 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeDescribe1Bean +import com.skyd.imomoe.util.AnimeDescribe1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeDescribe1Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeDescribe1ViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_anime_describe_1, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeDescribe1ViewHolder, + data: AnimeDescribe1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + holder.tvAnimeDescribe1.text = data.describe + holder.tvAnimeDescribe1.setOnClickListener { } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeDownload1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeDownload1Proxy.kt new file mode 100644 index 00000000..4bd5474f --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeDownload1Proxy.kt @@ -0,0 +1,180 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.arialyy.aria.core.inf.IEntity +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeDownload1Bean +import com.skyd.imomoe.ext.formatSize +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.percentage +import com.skyd.imomoe.ext.visible +import com.skyd.imomoe.util.AnimeDownload1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeDownload1Proxy( + private val onCancelClickListener: (( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean, + index: Int + ) -> Unit)? = null, + private val onPauseClickListener: (( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean, + index: Int + ) -> Unit)? = null, + private val onResumeClickListener: (( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean, + index: Int + ) -> Unit)? = null, + private val onFailedRetryClickListener: (( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean, + index: Int + ) -> Unit)? = null +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeDownload1ViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_anime_download_1, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + updateState(holder, data, index) + updateSpeed(holder, data) + holder.tvAnimeDownload1Title.text = data.title + holder.tvAnimeDownload1Episode.text = data.episode + holder.tvAnimeDownload1Size.text = data.run { + if (isM3U8) { + holder.itemView.context.getString( + R.string.m3u8_peer_count, + peerNum.toString() + ) + } else { + fileSize.formatSize() + } + } + holder.tvAnimeDownload1Percent.text = percent(data) + holder.ivAnimeDownload1Cancel.setOnClickListener { + onCancelClickListener?.invoke(holder, data, index) + } + holder.pbAnimeDownload1.setProgressCompat(progress(data), true) + } + + override fun onBindViewHolder( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean, + index: Int, + action: ((Any?) -> Unit)?, + payloads: MutableList + ) { + payloads.forEach { + if (it is List<*>) { + it.forEach { item -> + when (item) { + AnimeDownload1Bean.PEER_INDEX, AnimeDownload1Bean.PERCENT -> { + holder.pbAnimeDownload1.setProgressCompat(progress(data), true) + holder.tvAnimeDownload1Percent.text = percent(data) + return + } + AnimeDownload1Bean.STATE -> { + updateState(holder, data, index) + } + AnimeDownload1Bean.SPEED -> { + updateSpeed(holder, data) + } + } + } + } + } + onBindViewHolder(holder, data, index, action) + } + + private fun progress(data: AnimeDownload1Bean): Int { + return if (data.isM3U8) { + if (data.peerNum > 0) { + ((data.peerIndex + 1) * 100.0 / data.peerNum).toInt() + } else { + 0 + } + } else { + data.percent + } + } + + private fun percent(data: AnimeDownload1Bean): String { + return if (data.isM3U8) { + if (data.peerNum > 0) { + ((data.peerIndex + 1) * 100.0 / data.peerNum).percentage() + } else { + 0.0.percentage() + } + } else { + data.percent.percentage + } + } + + private fun updateState( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean, + index: Int + ) { + when (data.state) { + IEntity.STATE_RUNNING, IEntity.STATE_PRE, IEntity.STATE_POST_PRE -> { + holder.ivAnimeDownload1State.apply { + setImageResource(R.drawable.ic_pause_24) + setOnClickListener { + onPauseClickListener?.invoke(holder, data, index) + setImageResource(R.drawable.ic_play_24) + } + } + holder.tvAnimeDownload1Speed.visible() + } + IEntity.STATE_WAIT, IEntity.STATE_STOP -> { + holder.ivAnimeDownload1State.apply { + setImageResource(R.drawable.ic_play_24) + setOnClickListener { + onResumeClickListener?.invoke(holder, data, index) + setImageResource(R.drawable.ic_pause_24) + } + } + holder.tvAnimeDownload1Speed.gone() + } + IEntity.STATE_FAIL -> { + holder.ivAnimeDownload1State.apply { + setImageResource(R.drawable.ic_replay_24) + setOnClickListener { + onFailedRetryClickListener?.invoke(holder, data, index) + } + } + holder.tvAnimeDownload1Speed.gone() + } + else -> { + holder.ivAnimeDownload1State.apply { + setImageResource(R.drawable.ic_replay_24) + setOnClickListener { } + } + holder.tvAnimeDownload1Speed.gone() + } + } + } + + @SuppressLint("SetTextI18n") + private fun updateSpeed( + holder: AnimeDownload1ViewHolder, + data: AnimeDownload1Bean + ) { + if (holder.tvAnimeDownload1Speed.isVisible) { + holder.tvAnimeDownload1Speed.text = "${data.speed.formatSize()}/s" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeEpisode1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeEpisode1Proxy.kt new file mode 100644 index 00000000..479ed4d8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeEpisode1Proxy.kt @@ -0,0 +1,45 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeEpisode1Bean +import com.skyd.imomoe.util.AnimeEpisode1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + + +class AnimeEpisode1Proxy( + private val onClickListener: (( + holder: AnimeEpisode1ViewHolder, + data: AnimeEpisode1Bean, + index: Int + ) -> Unit)? = null, + private val height: Int? = null, + private val width: Int? = null +) : VarietyAdapter.Proxy() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeEpisode1ViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_anime_episode_1, parent, false) + ).apply { + itemView.layoutParams.let { layoutParams -> + height?.let { layoutParams.height = it } + width?.let { layoutParams.width = it } + itemView.layoutParams = layoutParams + } + } + + override fun onBindViewHolder( + holder: AnimeEpisode1ViewHolder, + data: AnimeEpisode1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + holder.tvAnimeEpisode1.apply { + text = data.title + setOnClickListener { onClickListener?.invoke(holder, data, index) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeInfo1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeInfo1Proxy.kt new file mode 100644 index 00000000..8e54cc16 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/AnimeInfo1Proxy.kt @@ -0,0 +1,86 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.AnimeInfo1Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.AnimeInfo1ViewHolder +import com.skyd.imomoe.util.coil.CoilUtil.loadImage +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class AnimeInfo1Proxy( + private val onBindViewHolder: (( + holder: AnimeInfo1ViewHolder, + data: AnimeInfo1Bean, + index: Int + ) -> Boolean)? = null +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeInfo1ViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_anime_info_1, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeInfo1ViewHolder, + data: AnimeInfo1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + if (onBindViewHolder?.invoke(holder, data, index) == true) return + val activity = holder.itemView.activity + holder.ivAnimeInfo1Cover.setTag(R.id.image_view_tag, data.cover?.url) + if (holder.ivAnimeInfo1Cover.getTag(R.id.image_view_tag) == data.cover?.url) { + holder.ivAnimeInfo1Cover.loadImage( + data.cover?.url, referer = data.cover?.referer, placeholder = 0, error = 0 + ) + } + holder.tvAnimeInfo1Title.text = data.title + holder.tvAnimeInfo1Alias.text = data.alias + holder.tvAnimeInfo1Area.text = data.area + holder.tvAnimeInfo1Year.text = data.year + if (data.index.isNullOrBlank()) { + holder.tvAnimeInfo1Index.gone() + } else { + holder.tvAnimeInfo1Index.visible() + holder.tvAnimeInfo1Index.text = + appContext.getString(R.string.anime_detail_index, data.index) + } + holder.tvAnimeInfo1Info.text = data.info + holder.flAnimeInfo1Type.removeAllViews() + data.animeType?.forEach { type -> + val cardView = activity.layoutInflater.inflate( + R.layout.item_anime_type_1, + holder.flAnimeInfo1Type, + false + ) as CardView + cardView.findViewById(R.id.tv_anime_type_1).text = type.title + cardView.setOnClickListener { + if (type.route.isBlank()) return@setOnClickListener + type.route.route(activity) + } + holder.flAnimeInfo1Type.addView(cardView) + } + holder.flAnimeInfo1Tag.removeAllViews() + data.tag?.forEach { tag -> + val cardView = activity.layoutInflater.inflate( + R.layout.item_anime_type_1, + holder.flAnimeInfo1Type, + false + ) as CardView + cardView.findViewById(R.id.tv_anime_type_1).text = tag.title + cardView.setOnClickListener { + tag.route.route(activity) + } + holder.flAnimeInfo1Tag.addView(cardView) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/Banner1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/Banner1Proxy.kt new file mode 100644 index 00000000..56f195f8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/Banner1Proxy.kt @@ -0,0 +1,33 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.Banner1Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.util.Banner1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.component.bannerview.adapter.MyCycleBannerAdapter +import com.skyd.imomoe.view.component.bannerview.indicator.DotIndicator + +class Banner1Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + Banner1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_banner_1, parent, false) + ) + + override fun onBindViewHolder( + holder: Banner1ViewHolder, + data: Banner1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.banner1.apply { + setAdapter(MyCycleBannerAdapter(data.animeCoverList)) + setIndicator(DotIndicator(activity)) + startPlay(5000) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/ClassifyTab1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/ClassifyTab1Proxy.kt new file mode 100644 index 00000000..12304cd0 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/ClassifyTab1Proxy.kt @@ -0,0 +1,32 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.ClassifyTab1Bean +import com.skyd.imomoe.util.ClassifyTab1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class ClassifyTab1Proxy( + private val onClickListener: (( + holder: ClassifyTab1ViewHolder, + data: ClassifyTab1Bean, + index: Int + ) -> Unit)? = null +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + ClassifyTab1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_text_view_1, parent, false) + ) + + override fun onBindViewHolder( + holder: ClassifyTab1ViewHolder, + data: ClassifyTab1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + holder.textView.text = data.title + holder.itemView.setOnClickListener { onClickListener?.invoke(holder, data, index) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/DataSource1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/DataSource1Proxy.kt new file mode 100644 index 00000000..3b9b4fe9 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/DataSource1Proxy.kt @@ -0,0 +1,48 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.DataSource1Bean +import com.skyd.imomoe.ext.formatSize +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible +import com.skyd.imomoe.util.DataSource1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class DataSource1Proxy( + private val onClickListener: (( + holder: DataSource1ViewHolder, + data: DataSource1Bean, + index: Int + ) -> Unit)? = null, + private val onLongClickListener: (( + holder: DataSource1ViewHolder, + data: DataSource1Bean, + index: Int + ) -> Boolean)? = null +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + DataSource1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_data_source_1, parent, false) + ) + + override fun onBindViewHolder( + holder: DataSource1ViewHolder, + data: DataSource1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + holder.tvDataSource1Name.text = data.name + holder.tvDataSource1Size.text = data.file.formatSize() + if (data.selected) holder.ivDataSource1Selected.visible() + else holder.ivDataSource1Selected.gone() + holder.itemView.setOnClickListener { + onClickListener?.invoke(holder, data, index) + } + holder.itemView.setOnLongClickListener { + onLongClickListener?.invoke(holder, data, index) ?: false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/DataSource2Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/DataSource2Proxy.kt new file mode 100644 index 00000000..349ef348 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/DataSource2Proxy.kt @@ -0,0 +1,121 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.DataSource2Bean +import com.skyd.imomoe.bean.DataSourceRepositoryBean +import com.skyd.imomoe.config.Api +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.ext.theme.getAttrColor +import com.skyd.imomoe.ext.toTimeString +import com.skyd.imomoe.util.DataSource2ViewHolder +import com.skyd.imomoe.util.coil.CoilUtil.loadImage +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class DataSource2Proxy( + private val onActionClickListener: (( + holder: DataSource2ViewHolder, + data: DataSource2Bean, + index: Int + ) -> Unit)? = null +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + DataSource2ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_data_source_2, parent, false) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder( + holder: DataSource2ViewHolder, + data: DataSource2Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + holder.tvDataSource2Name.text = data.name + holder.tvDataSource2Version.text = "${data.versionName}(${data.versionCode})" + holder.tvDataSource2PublishAt.text = data.publicAt.toTimeString("yyyy-MM-dd HH:mm") + holder.tvDataSource2Describe.text = data.describe + holder.tvDataSource2Author.text = + appContext.getString(R.string.data_source_author, data.author) + updateStatus(holder, data) + data.icon.let { + if (it.isNullOrBlank()) { + holder.ivDataSource2Icon.loadImage(R.drawable.ic_insert_drive_file_24) + holder.ivDataSource2Icon.imageTintList = ColorStateList.valueOf( + appContext.getAttrColor(R.attr.colorPrimary) + ) + } else { + holder.ivDataSource2Icon.loadImage( + if (it.startsWith("/")) Api.DATA_SOURCE_PREFIX + it + else it + ) + holder.ivDataSource2Icon.imageTintList = null + } + } + holder.btnDataSource2Action.setOnClickListener { + onActionClickListener?.invoke(holder, data, index) + } + } + + override fun onBindViewHolder( + holder: DataSource2ViewHolder, + data: DataSource2Bean, + index: Int, + action: ((Any?) -> Unit)?, + payloads: MutableList + ) { + payloads.forEach { + if (it is List<*>) { + it.forEach { item -> + when (item) { + DataSource2Bean.STATUS -> { + updateStatus(holder, data) + return + } + } + } + } + } + onBindViewHolder(holder, data, index, action) + } + + private fun updateStatus( + holder: DataSource2ViewHolder, + data: DataSource2Bean, + ) { + val activity = holder.itemView.activity + holder.btnDataSource2Action.apply { + text = when (data.status) { + DataSourceRepositoryBean.Status.NONE -> { + isEnabled = true + activity.getString(R.string.download) + } + DataSourceRepositoryBean.Status.NEWEST -> { + isEnabled = false + activity.getString(R.string.installed) + } + DataSourceRepositoryBean.Status.DOWNLOADING -> { + isEnabled = false + activity.getString(R.string.downloading) + } + DataSourceRepositoryBean.Status.INSTALLING -> { + isEnabled = false + activity.getString(R.string.installing) + } + DataSourceRepositoryBean.Status.OUTDATED -> { + isEnabled = true + activity.getString(R.string.update) + } + else -> { + isEnabled = true + activity.getString(R.string.download) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/GridRecyclerView1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/GridRecyclerView1Proxy.kt new file mode 100644 index 00000000..6090e317 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/GridRecyclerView1Proxy.kt @@ -0,0 +1,41 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.GridRecyclerView1 +import com.skyd.imomoe.util.GridRecyclerView1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class GridRecyclerView1Proxy( + private val onBindViewHolder: (( + holder: GridRecyclerView1ViewHolder, + data: GridRecyclerView1, + index: Int + ) -> Unit)? = null, + private val height: Int? = null, + private val width: Int? = null +) : + VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + GridRecyclerView1ViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_grid_recycler_view_1, parent, false) + ).apply { + itemView.layoutParams.let { layoutParams -> + height?.let { layoutParams.height = it } + width?.let { layoutParams.width = it } + itemView.layoutParams = layoutParams + } + } + + override fun onBindViewHolder( + holder: GridRecyclerView1ViewHolder, + data: GridRecyclerView1, + index: Int, + action: ((Any?) -> Unit)? + ) { + onBindViewHolder?.invoke(holder, data, index) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/Header1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/Header1Proxy.kt new file mode 100644 index 00000000..535e6c63 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/Header1Proxy.kt @@ -0,0 +1,51 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.IntRange +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.Header1Bean +import com.skyd.imomoe.ext.theme.getAttrColor +import com.skyd.imomoe.util.Header1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + + +class Header1Proxy( + @IntRange(from = 0, to = 1) private val color: Int = THEME_COLOR +) : VarietyAdapter.Proxy() { + companion object { + const val THEME_COLOR = 0 + const val WHITE = 1 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + Header1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_header_1, parent, false) + ) + + override fun onBindViewHolder( + holder: Header1ViewHolder, + data: Header1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + when (color) { + THEME_COLOR -> { + holder.tvHeader1Title.setTextColor( + holder.itemView.context.getAttrColor(R.attr.colorPrimary) + ) + } + WHITE -> { + holder.tvHeader1Title.setTextColor( + ContextCompat.getColor( + holder.itemView.context, + android.R.color.white + ) + ) + } + } + holder.tvHeader1Title.text = data.title + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/HorizontalRecyclerView1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/HorizontalRecyclerView1Proxy.kt new file mode 100644 index 00000000..5013379e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/HorizontalRecyclerView1Proxy.kt @@ -0,0 +1,87 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.IntRange +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeEpisode1Bean +import com.skyd.imomoe.bean.HorizontalRecyclerView1Bean +import com.skyd.imomoe.ext.theme.getAttrColor +import com.skyd.imomoe.util.AnimeEpisode1ViewHolder +import com.skyd.imomoe.util.HorizontalRecyclerView1ViewHolder +import com.skyd.imomoe.util.compare.EpisodeTitleSort.sortEpisodeTitle +import com.skyd.imomoe.view.adapter.decoration.HorizontalRecyclerViewDecoration +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class HorizontalRecyclerView1Proxy( + @IntRange(from = 0, to = 1) private val color: Int = THEME, + private val onMoreButtonClickListener: (( + holder: HorizontalRecyclerView1ViewHolder, + data: HorizontalRecyclerView1Bean, + index: Int + ) -> Unit)? = null, + private val onAnimeEpisodeClickListener: (( + holder: AnimeEpisode1ViewHolder, + data: AnimeEpisode1Bean, + index: Int + ) -> Unit)? = null, + private val animeEpisodeHeight: Int? = null, + private val animeEpisodeWidth: Int? = null +) : VarietyAdapter.Proxy() { + companion object { + const val THEME = 0 + const val WHITE = 1 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + HorizontalRecyclerView1ViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_horizontal_recycler_view_1, parent, false) + ) + + @SuppressLint("NotifyDataSetChanged") + override fun onBindViewHolder( + holder: HorizontalRecyclerView1ViewHolder, + data: HorizontalRecyclerView1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + when (color) { + THEME -> { + holder.ivHorizontalRecyclerView1More + .setImageResource(R.drawable.ic_keyboard_arrow_down_24) + holder.ivHorizontalRecyclerView1More.imageTintList = ColorStateList.valueOf( + holder.itemView.context.getAttrColor(R.attr.colorPrimary) + ) + } + WHITE -> { + holder.ivHorizontalRecyclerView1More + .setImageResource(R.drawable.ic_keyboard_arrow_down_24) + holder.ivHorizontalRecyclerView1More.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor(holder.itemView.context, android.R.color.white) + ) + } + } + holder.rvHorizontalRecyclerView1.apply { + if (itemDecorationCount == 0) addItemDecoration(HorizontalRecyclerViewDecoration()) + if (adapter == null) { + adapter = VarietyAdapter( + mutableListOf( + AnimeEpisode1Proxy( + onClickListener = onAnimeEpisodeClickListener, + height = animeEpisodeHeight, + width = animeEpisodeWidth + ) + ) + ).apply { dataList = data.episodeList.toMutableList().sortEpisodeTitle() } + } else adapter?.notifyDataSetChanged() + } + holder.ivHorizontalRecyclerView1More.setOnClickListener { + onMoreButtonClickListener?.invoke(holder, data, index) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/More1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/More1Proxy.kt new file mode 100644 index 00000000..90b41cab --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/More1Proxy.kt @@ -0,0 +1,33 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.More1Bean +import com.skyd.imomoe.ext.activity +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.util.More1ViewHolder +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class More1Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + More1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_more_1, parent, false) + ) + + override fun onBindViewHolder( + holder: More1ViewHolder, + data: More1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + val activity = holder.itemView.activity + holder.ivMore1.setImageDrawable(Util.getResDrawable(data.image)) + holder.tvMore1.text = data.title + holder.itemView.setOnClickListener { + data.route.route(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/PlayerEpisode1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/PlayerEpisode1Proxy.kt new file mode 100644 index 00000000..963d8b89 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/PlayerEpisode1Proxy.kt @@ -0,0 +1,35 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeEpisodeDataBean +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.component.player.AnimeVideoPlayer + +class PlayerEpisode1Proxy( + private val onBindViewHolder: (( + holder: AnimeVideoPlayer.RightRecyclerViewViewHolder, + data: AnimeEpisodeDataBean, + index: Int, + action: ((Any?) -> Unit)? + ) -> Boolean)? = null // 返回值指是否消费了onBindViewHolder +) : VarietyAdapter.Proxy() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeVideoPlayer.RightRecyclerViewViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_player_list_item_1, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeVideoPlayer.RightRecyclerViewViewHolder, + data: AnimeEpisodeDataBean, + index: Int, + action: ((Any?) -> Unit)? + ) { + if (onBindViewHolder?.invoke(holder, data, index, action) == true) return + holder.tvTitle.text = data.title + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/RestoreFile1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/RestoreFile1Proxy.kt new file mode 100644 index 00000000..5ad02a84 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/RestoreFile1Proxy.kt @@ -0,0 +1,53 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.ext.formatSize +import com.skyd.imomoe.util.RestoreFile1ViewHolder +import com.skyd.imomoe.util.Util.getResDrawable +import com.skyd.imomoe.util.Util.time2Now +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.thegrizzlylabs.sardineandroid.DavResource + +class RestoreFile1Proxy( + private val onClickListener: (( + holder: RestoreFile1ViewHolder, + data: DavResource, + index: Int + ) -> Unit)? = null, + private val onLongClickListener: (( + holder: RestoreFile1ViewHolder, + data: DavResource, + index: Int + ) -> Boolean)? = null +) : VarietyAdapter.Proxy() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + RestoreFile1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_restore_file_1, parent, false) + ) + + override fun onBindViewHolder( + holder: RestoreFile1ViewHolder, + data: DavResource, + index: Int, + action: ((Any?) -> Unit)? + ) { + if (data.name.contains(".db")) { + holder.ivRestoreFile1Icon.setImageDrawable(getResDrawable(R.drawable.ic_database_24)) + } else { + holder.ivRestoreFile1Icon.setImageDrawable(getResDrawable(R.drawable.ic_insert_drive_file_24)) + } + holder.tvRestoreFile1Title.text = data.displayName + holder.tvRestoreFile1Size.text = data.contentLength.formatSize() + holder.tvRestoreFile1LastModified.text = time2Now(data.modified.time) + holder.itemView.setOnClickListener { + onClickListener?.invoke(holder, data, index) + } + holder.itemView.setOnLongClickListener { + onLongClickListener?.invoke(holder, data, index) ?: false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/SearchHistory1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/SearchHistory1Proxy.kt new file mode 100644 index 00000000..b3d00336 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/SearchHistory1Proxy.kt @@ -0,0 +1,43 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.SearchHistory1Bean +import com.skyd.imomoe.util.SearchHistory1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class SearchHistory1Proxy( + private val onClickListener: (( + holder: SearchHistory1ViewHolder, + data: SearchHistory1Bean, + index: Int + ) -> Unit)? = null, + private val onDeleteButtonClickListener: (( + holder: SearchHistory1ViewHolder, + data: SearchHistory1Bean, + index: Int + ) -> Unit)? = null +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + SearchHistory1ViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_search_history_1, parent, false) + ) + + override fun onBindViewHolder( + holder: SearchHistory1ViewHolder, + data: SearchHistory1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + holder.tvSearchHistory1Title.text = data.title + holder.ivSearchHistory1Delete.setOnClickListener { + onDeleteButtonClickListener?.invoke(holder, data, index) + } + holder.itemView.setOnClickListener { + onClickListener?.invoke(holder, data, index) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/SearchHistoryHeader1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/SearchHistoryHeader1Proxy.kt new file mode 100644 index 00000000..f97b4e5c --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/SearchHistoryHeader1Proxy.kt @@ -0,0 +1,25 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.SearchHistoryHeader1Bean +import com.skyd.imomoe.util.SearchHistoryHeader1ViewHolder +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter + +class SearchHistoryHeader1Proxy : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + SearchHistoryHeader1ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_search_history_header_1, parent, false) + ) + + override fun onBindViewHolder( + holder: SearchHistoryHeader1ViewHolder, + data: SearchHistoryHeader1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + holder.tvSearchHistoryHeader1Title.text = data.title + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/VideoSpeed1Proxy.kt b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/VideoSpeed1Proxy.kt new file mode 100644 index 00000000..5ca535cb --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/adapter/variety/proxy/VideoSpeed1Proxy.kt @@ -0,0 +1,33 @@ +package com.skyd.imomoe.view.adapter.variety.proxy + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.skyd.imomoe.R +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.component.player.AnimeVideoPlayer + +class VideoSpeed1Proxy( + private val onBindViewHolder: (( + holder: AnimeVideoPlayer.RightRecyclerViewViewHolder, + data: AnimeVideoPlayer.Speed1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) -> Boolean)? = null // 返回值指是否消费了onBindViewHolder +) : VarietyAdapter.Proxy() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + AnimeVideoPlayer.RightRecyclerViewViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.item_player_list_item_1, parent, false) + ) + + override fun onBindViewHolder( + holder: AnimeVideoPlayer.RightRecyclerViewViewHolder, + data: AnimeVideoPlayer.Speed1Bean, + index: Int, + action: ((Any?) -> Unit)? + ) { + if (onBindViewHolder?.invoke(holder, data, index, action) == true) return + holder.tvTitle.text = data.title + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/AnimeToast.kt b/app/src/main/java/com/skyd/imomoe/view/component/AnimeToast.kt deleted file mode 100644 index 7001af34..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/AnimeToast.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.skyd.imomoe.view.component - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.widget.TextView -import android.widget.Toast -import com.skyd.imomoe.R -import com.skyd.imomoe.util.Util.getResDrawable -import com.skyd.skin.SkinManager -import com.skyd.skin.core.SkinResourceProcessor - -object AnimeToast { - fun makeText( - context: Context, - text: CharSequence, duration: Int = Toast.LENGTH_SHORT - ): Toast { - val toast = Toast(context) - val view: View = - (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate( - R.layout.toast_1, - null - ) - view.findViewById(R.id.tv_toast_1).apply { - if (SkinResourceProcessor.isInitialized()) { - background = - getResDrawable(R.drawable.shape_fill_circle_corner_main_color_2_50_skin) - } - this.text = text - } - toast.view = view - toast.duration = duration - return toast - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/SmartScrollingFooterBehavior.kt b/app/src/main/java/com/skyd/imomoe/view/component/SmartScrollingFooterBehavior.kt deleted file mode 100644 index 973e484c..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/SmartScrollingFooterBehavior.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.skyd.imomoe.view.component - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import androidx.coordinatorlayout.widget.CoordinatorLayout -import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior - -class SmartScrollingFooterBehavior : ScrollingViewBehavior { - private lateinit var appBarLayout: AppBarLayout - - constructor() : super() - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) - - override fun onDependentViewChanged( - parent: CoordinatorLayout, - child: View, - dependency: View - ): Boolean { - if (!this::appBarLayout.isInitialized) { - appBarLayout = dependency as AppBarLayout - } - val result = super.onDependentViewChanged(parent, child, dependency) - val bottomPadding = calculateBottomPadding(appBarLayout) - val paddingChanged = bottomPadding != child.paddingBottom - if (paddingChanged) { - child.setPadding(child.paddingLeft, child.paddingTop, child.paddingRight, bottomPadding) - child.requestLayout() - } - return paddingChanged || result - } - - private fun calculateBottomPadding(dependency: AppBarLayout): Int { - val totalScrollRange = dependency.totalScrollRange - return totalScrollRange + dependency.top - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/ViewPager2View.kt b/app/src/main/java/com/skyd/imomoe/view/component/ViewPager2View.kt index 6ab98641..323655c3 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/ViewPager2View.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/ViewPager2View.kt @@ -6,15 +6,39 @@ import android.view.MotionEvent import android.view.ViewConfiguration import android.widget.FrameLayout import androidx.annotation.Nullable -import androidx.core.view.size import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING import kotlin.math.abs -class ViewPager2View(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { +class ViewPager2View(context: Context, attrs: AttributeSet?) : + FrameLayout(context, attrs) { - private val mViewPager2: ViewPager2 = ViewPager2(context) + private val mViewPager2: ViewPager2 = ViewPager2(context, attrs) + + var adapter: RecyclerView.Adapter<*>? + get() = mViewPager2.adapter + set(value) { + mViewPager2.adapter = value + } + + var offscreenPageLimit: Int + get() = mViewPager2.offscreenPageLimit + set(value) { + mViewPager2.offscreenPageLimit = value + } + + var currentItem: Int + get() = mViewPager2.currentItem + set(value) { + mViewPager2.currentItem = value + } + + var orientation: Int + get() = mViewPager2.orientation + set(value) { + mViewPager2.orientation = value + } private var mStartX = 0f private var mStartY = 0f @@ -26,30 +50,16 @@ class ViewPager2View(context: Context, attrs: AttributeSet?) : FrameLayout(conte attachViewToParent(mViewPager2, 0, mViewPager2.layoutParams) } - fun setAdapter(adapter: RecyclerView.Adapter) { - mViewPager2.adapter = adapter - } - - fun setOffscreenPageLimit(limit: Int) { - mViewPager2.offscreenPageLimit = limit - } - fun getViewPager() = mViewPager2 fun setPageTransformer(@Nullable transformer: ViewPager2.PageTransformer) { mViewPager2.setPageTransformer(transformer) } - fun setCurrentItem(item: Int) { - mViewPager2.currentItem = item - } - fun setCurrentItem(item: Int, smoothScroll: Boolean) { mViewPager2.setCurrentItem(item, smoothScroll) } - fun getOrientation() = mViewPager2.orientation - override fun dispatchTouchEvent(ev: MotionEvent): Boolean { when (ev.action) { MotionEvent.ACTION_DOWN -> { diff --git a/app/src/main/java/com/skyd/imomoe/view/component/VpSwipeRefreshLayout.kt b/app/src/main/java/com/skyd/imomoe/view/component/VpSwipeRefreshLayout.kt index 5b98863f..e64c99ac 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/VpSwipeRefreshLayout.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/VpSwipeRefreshLayout.kt @@ -8,8 +8,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlin.math.abs //解决外层SwipeRefreshLayout内有Vp的滑动冲突 -class VpSwipeRefreshLayout(context: Context, attrs: AttributeSet) : - SwipeRefreshLayout(context, attrs) { +class VpSwipeRefreshLayout : SwipeRefreshLayout { private var startY = 0f private var startX = 0f @@ -17,6 +16,10 @@ class VpSwipeRefreshLayout(context: Context, attrs: AttributeSet) : private var isVpSlop = false private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { when (ev.action) { MotionEvent.ACTION_DOWN -> { diff --git a/app/src/main/java/com/skyd/imomoe/view/component/WrapLinearLayoutManager.kt b/app/src/main/java/com/skyd/imomoe/view/component/WrapLinearLayoutManager.kt index debced63..a8d5635b 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/WrapLinearLayoutManager.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/WrapLinearLayoutManager.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Recycler -import tv.danmaku.ijk.media.player.pragma.DebugLog +import com.skyd.imomoe.util.logD class WrapLinearLayoutManager(context: Context) : LinearLayoutManager(context) { @@ -13,7 +13,7 @@ class WrapLinearLayoutManager(context: Context) : LinearLayoutManager(context) { super.onLayoutChildren(recycler, state) } catch (e: IndexOutOfBoundsException) { e.printStackTrace() - DebugLog.d("WrapLinearLayoutManager", "捕获异常:" + e.message) + logD("WrapLinearLayoutManager", "捕获异常:" + e.message) } } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/ZoomView.kt b/app/src/main/java/com/skyd/imomoe/view/component/ZoomView.kt index 04682796..bc5dd679 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/ZoomView.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/ZoomView.kt @@ -1,10 +1,14 @@ package com.skyd.imomoe.view.component +import android.animation.AnimatorSet +import android.animation.ObjectAnimator import android.content.Context import android.os.Build import android.util.AttributeSet import android.view.MotionEvent +import android.view.animation.DecelerateInterpolator import android.widget.FrameLayout +import java.io.Serializable import kotlin.math.atan2 import kotlin.math.sqrt @@ -13,10 +17,12 @@ class ZoomView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { - private var scale = 1f // 伸缩比例 - private var mTranslationX = 0f // 移动X - private var mTranslationY = 0f // 移动Y - private var mRotation = 0f // 旋转角度 + var useAnimate: Boolean = true + + var scale = 1f // 伸缩比例 + var mTranslationX = 0f // 移动X + var mTranslationY = 0f // 移动Y + var mRotation = 0f // 旋转角度 // 移动过程中临时变量 private var actionX = 0f @@ -37,11 +43,18 @@ class ZoomView @JvmOverloads constructor( degree = 0f moveType = 0 - translationX = 0f - translationY = 0f - scaleX = 1f - scaleY = 1f - rotation = 0f + val rotation = ObjectAnimator.ofFloat(this, "rotation", rotation, 0f) + val translationX = ObjectAnimator.ofFloat(this, "translationX", translationX, 0f) + val translationY = ObjectAnimator.ofFloat(this, "translationY", translationY, 0f) + val scaleX = ObjectAnimator.ofFloat(this, "scaleX", scaleX, 1f) + val scaleY = ObjectAnimator.ofFloat(this, "scaleY", scaleY, 1f) + + AnimatorSet().apply { + playTogether(rotation, translationX, translationY, scaleX, scaleY) + duration = if (useAnimate) 260 else 0 + interpolator = DecelerateInterpolator() + start() + } } override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { @@ -127,4 +140,36 @@ class ZoomView @JvmOverloads constructor( init { isClickable = true } + + data class ZoomData( + val useAnimate: Boolean, + val scale: Float, + val mTranslationX: Float, + val mTranslationY: Float, + val mRotation: Float, + ) : Serializable +} + +fun ZoomView.getData(): ZoomView.ZoomData { + return ZoomView.ZoomData( + useAnimate = useAnimate, + scale = scale, + mTranslationX = mTranslationX, + mTranslationY = mTranslationY, + mRotation = mRotation, + ) +} + +fun ZoomView.setData(other: ZoomView.ZoomData?) { + other ?: return + useAnimate = other.useAnimate + scale = other.scale + mTranslationX = other.mTranslationX + mTranslationY = other.mTranslationY + mRotation = other.mRotation + rotation = mRotation + translationX = mTranslationX + translationY = mTranslationY + scaleX = scale + scaleY = scale } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/BannerView.kt b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/BannerView.kt index 08e0a76e..b014cf64 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/BannerView.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/BannerView.kt @@ -22,7 +22,7 @@ import kotlin.math.abs /** * Created by Sky_D on 2021-02-08. */ -open class BannerView(mContext: Context, attrs: AttributeSet?) : +class BannerView(mContext: Context, attrs: AttributeSet?) : RelativeLayout(mContext, attrs) { companion object { @@ -64,17 +64,6 @@ open class BannerView(mContext: Context, attrs: AttributeSet?) : private var mBannerPaddingTop: Int = 0 private var mBannerPaddingBottom: Int = 0 - init { - mViewPager2.layoutParams = - LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - addView(mViewPager2) - mViewPager2.offscreenPageLimit = mOffscreenPageLimit - // 初始化自定义属性 - initAttr(attrs) - - mViewPager2.registerOnPageChangeCallback(CycleOnPageChangeCallback()) - } - /** * 初始化自定义属性 * @param attributeSet 属性集合 @@ -224,7 +213,7 @@ open class BannerView(mContext: Context, attrs: AttributeSet?) : * @param interval 轮播间隔,单位:毫秒 */ fun startPlay(interval: Long) { - if (!autoPlay && mViewPager2.adapter?.itemCount ?: 0 > 1) { + if (!autoPlay && (mViewPager2.adapter?.itemCount ?: 0) > 1) { mAutoPlayInterval = interval mHandler.postDelayed(mAutoPlayRunnable, mAutoPlayInterval) autoPlay = true @@ -250,7 +239,7 @@ open class BannerView(mContext: Context, attrs: AttributeSet?) : * 切换到下一页(不会循环) */ private fun nextPage() { - if (mViewPager2.adapter?.itemCount ?: 0 > 1 && autoPlay) { + if ((mViewPager2.adapter?.itemCount ?: 0) > 1 && autoPlay) { setCurrentItem(getCurrentItem() + 1, true) } } @@ -312,6 +301,17 @@ open class BannerView(mContext: Context, attrs: AttributeSet?) : return super.onInterceptTouchEvent(event) } + init { + mViewPager2.layoutParams = + LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + addView(mViewPager2) + mViewPager2.offscreenPageLimit = mOffscreenPageLimit + // 初始化自定义属性 + initAttr(attrs) + + mViewPager2.registerOnPageChangeCallback(CycleOnPageChangeCallback()) + } + private inner class CycleOnPageChangeCallback : OnPageChangeCallback() { var stateChangedItemRealPosition = -1 diff --git a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/CycleBannerAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/CycleBannerAdapter.kt index 90b4fea4..52e7b405 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/CycleBannerAdapter.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/CycleBannerAdapter.kt @@ -2,18 +2,16 @@ package com.skyd.imomoe.view.component.bannerview.adapter import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID -import com.skyd.imomoe.view.adapter.SkinRvAdapter import com.skyd.imomoe.view.component.bannerview.BannerUtil.getPosition /** * Created by Sky_D on 2021-02-08. */ -abstract class CycleBannerAdapter : SkinRvAdapter() { +abstract class CycleBannerAdapter : RecyclerView.Adapter() { final override fun getItemViewType(position: Int): Int = getItemType(getPosition(position, getCount())) final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) onBind(holder, getPosition(position, getCount())) } diff --git a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/MyCycleBannerAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/MyCycleBannerAdapter.kt index 33180137..169677c9 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/MyCycleBannerAdapter.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/adapter/MyCycleBannerAdapter.kt @@ -1,74 +1,33 @@ package com.skyd.imomoe.view.component.bannerview.adapter -import android.app.Activity -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.skyd.imomoe.App -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.BaseBean -import com.skyd.imomoe.util.AnimeCover6ViewHolder -import com.skyd.imomoe.util.Util.getResColor -import com.skyd.imomoe.util.coil.CoilUtil.loadImage -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.ViewHolderUtil -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible +import com.skyd.imomoe.bean.AnimeCover6Bean +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeCover6Proxy /** * Created by Sky_D on 2021-02-08. */ class MyCycleBannerAdapter( - private val activity: Activity, - private val dataList: List + private val dataList: List ) : CycleBannerAdapter() { - override fun getItemType(position: Int): Int = - ViewHolderUtil.getItemViewType(dataList[position]) + private val proxy = AnimeCover6Proxy( + height = ViewGroup.LayoutParams.MATCH_PARENT, + width = ViewGroup.LayoutParams.MATCH_PARENT + ) override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): RecyclerView.ViewHolder { - val viewHolder = ViewHolderUtil.getViewHolder(parent, viewType) - //vp2的item必须是MATCH_PARENT的 - val layoutParams = viewHolder.itemView.layoutParams - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - viewHolder.itemView.layoutParams = layoutParams - return viewHolder + return proxy.onCreateViewHolder(parent, viewType) } + @Suppress("UNCHECKED_CAST") override fun onBind(holder: RecyclerView.ViewHolder, position: Int) { - val item = dataList[position] - - when (holder) { - is AnimeCover6ViewHolder -> { - if (item is AnimeCoverBean) { - holder.tvAnimeCover6Night.setBackgroundColor(activity.getResColor(R.color.transparent_skin)) - holder.ivAnimeCover6Cover.loadImage( - item.cover?.url ?: "", - referer = item.cover?.referer - ) - holder.tvAnimeCover6Title.text = item.title - holder.tvAnimeCover6Episode.text = item.episodeClickable?.title - if (item.describe.isNullOrEmpty()) { - holder.tvAnimeCover6Describe.gone() - } else { - holder.tvAnimeCover6Describe.visible() - holder.tvAnimeCover6Describe.text = item.describe - } - holder.itemView.setOnClickListener { - process(activity, item.actionUrl) - } - } - } - else -> { - holder.itemView.visibility = View.GONE - (App.context.resources.getString(R.string.unknown_view_holder) + position).showToast() - } - } + (proxy as VarietyAdapter.Proxy) + .onBindViewHolder(holder, dataList[position], position) } override fun getCount(): Int = dataList.size diff --git a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/indicator/DotIndicator.kt b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/indicator/DotIndicator.kt index 9909f94c..f00705bc 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/bannerview/indicator/DotIndicator.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/bannerview/indicator/DotIndicator.kt @@ -6,18 +6,26 @@ import android.graphics.Paint import android.view.View import android.widget.RelativeLayout import com.skyd.imomoe.R +import com.skyd.imomoe.ext.theme.getAttrColor import com.skyd.imomoe.util.Util.dp -import com.skyd.imomoe.util.Util.getResColor /** * Created by Sky_D on 2021-02-08. */ -class DotIndicator : View, Indicator { +class DotIndicator(context: Context) : View(context), Indicator { private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) //抗锯齿 private var mRadius: Float = 3f.dp private var mDotsPadding = 3.dp - private var mSelectedColor: Int = context.getResColor(R.color.main_color_2_skin) - private var mUnSelectedColor: Int = context.getResColor(R.color.foreground_white_skin) + var mSelectedColor: Int = context.getAttrColor(R.attr.colorPrimary) + set(value) { + field = value + invalidate() + } + var mUnSelectedColor: Int = context.getAttrColor(R.attr.background) + set(value) { + field = value + invalidate() + } private var mDotsCount = 0 private var mCurrentPosition = 0 @@ -26,20 +34,6 @@ class DotIndicator : View, Indicator { private val mMarginTop = 10.dp private val mMarginBottom = 0 - constructor(context: Context) : super(context) { - val layoutParams: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.WRAP_CONTENT, - RelativeLayout.LayoutParams.WRAP_CONTENT - ) - layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE) - layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE) - layoutParams.leftMargin = mMarginStart - layoutParams.rightMargin = mMarginEnd - layoutParams.topMargin = mMarginTop - layoutParams.bottomMargin = mMarginBottom - setLayoutParams(layoutParams) - } - override fun getView(): View { return this } @@ -85,4 +79,18 @@ class DotIndicator : View, Indicator { } } } + + init { + val layoutParams: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT + ) + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE) + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE) + layoutParams.leftMargin = mMarginStart + layoutParams.rightMargin = mMarginEnd + layoutParams.topMargin = mMarginTop + layoutParams.bottomMargin = mMarginBottom + setLayoutParams(layoutParams) + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/compose/AnimeLazyVerticalGrid.kt b/app/src/main/java/com/skyd/imomoe/view/component/compose/AnimeLazyVerticalGrid.kt new file mode 100644 index 00000000..fb11ef0b --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/compose/AnimeLazyVerticalGrid.kt @@ -0,0 +1,51 @@ +package com.skyd.imomoe.view.component.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.skyd.imomoe.view.adapter.compose.AnimeItemSpace.animeItemSpace +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import com.skyd.imomoe.view.adapter.compose.MAX_SPAN_SIZE +import com.skyd.imomoe.view.adapter.compose.animeShowSpan + +@Composable +fun AnimeLazyVerticalGrid( + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), + dataList: List, + adapter: LazyGridAdapter, + enableLandScape: Boolean = true, // 是否启用横屏使用另一套布局方案 + key: ((index: Int, item: Any) -> Any)? = null +) { + val listState = rememberLazyGridState() + val spanIndexArray: MutableList = remember { mutableListOf() } + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(MAX_SPAN_SIZE), + state = listState, + contentPadding = contentPadding + ) { + itemsIndexed( + items = dataList, + key = key, + span = { index, item -> + val spanIndex = maxLineSpan - maxCurrentLineSpan + if (spanIndexArray.size > index) spanIndexArray[index] = spanIndex + else spanIndexArray.add(spanIndex) + GridItemSpan(animeShowSpan(item, enableLandScape)) + } + ) { index, item -> + adapter.draw( + modifier = Modifier.animeItemSpace( + item = item, + spanSize = animeShowSpan(item), + spanIndex = spanIndexArray[index] + ), + index = index, + data = item + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/compose/AnimeTopBar.kt b/app/src/main/java/com/skyd/imomoe/view/component/compose/AnimeTopBar.kt new file mode 100644 index 00000000..e9fdfd9e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/compose/AnimeTopBar.kt @@ -0,0 +1,115 @@ +package com.skyd.imomoe.view.component.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.R +import com.skyd.imomoe.ext.plus +import com.skyd.imomoe.ext.screenIsLand + +enum class AnimeTopBarStyle { + Small, Large +} + +@Composable +fun AnimeTopBar( + modifier: Modifier = Modifier, + style: AnimeTopBarStyle = AnimeTopBarStyle.Small, + title: @Composable () -> Unit, + contentPadding: @Composable () -> PaddingValues = { + if (LocalContext.current.screenIsLand) { + WindowInsets.navigationBars.asPaddingValues() + } else { + PaddingValues() + } + WindowInsets.statusBars.asPaddingValues() + }, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null, +) { + val colors = when (style) { + AnimeTopBarStyle.Small -> TopAppBarDefaults.smallTopAppBarColors() + AnimeTopBarStyle.Large -> TopAppBarDefaults.largeTopAppBarColors() + } + val scrollFraction = scrollBehavior?.scrollFraction ?: 0f + val appBarContainerColor by colors.containerColor(scrollFraction) + val topBarModifier = Modifier.padding(contentPadding()) + Surface(modifier = modifier, color = appBarContainerColor) { + when (style) { + AnimeTopBarStyle.Small -> { + SmallTopAppBar( + modifier = topBarModifier, + title = title, + navigationIcon = navigationIcon, + actions = actions, + colors = colors, + scrollBehavior = scrollBehavior + ) + } + AnimeTopBarStyle.Large -> { + LargeTopAppBar( + modifier = topBarModifier, + title = title, + navigationIcon = navigationIcon, + actions = actions, + colors = colors, + scrollBehavior = scrollBehavior + ) + } + } + } +} + +@Composable +fun TopBarIcon( + painter: Painter, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + tint: Color = LocalContentColor.current, + contentDescription: String?, +) { + IconButton(onClick = onClick) { + Icon( + modifier = modifier.size(24.dp), + painter = painter, + tint = tint, + contentDescription = contentDescription + ) + } +} + +@Composable +fun TopBarIcon( + imageVector: ImageVector, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + tint: Color = LocalContentColor.current, + contentDescription: String?, +) { + IconButton(onClick = onClick) { + Icon( + modifier = modifier.size(24.dp), + imageVector = imageVector, + tint = tint, + contentDescription = contentDescription + ) + } +} + +@Composable +fun BackIcon(onClick: () -> Unit = {}) { + TopBarIcon( + painter = painterResource(id = R.drawable.ic_arrow_back_24), + contentDescription = stringResource(id = R.string.back), + onClick = onClick + ) +} diff --git a/app/src/main/java/com/skyd/imomoe/view/component/compose/Centered.kt b/app/src/main/java/com/skyd/imomoe/view/component/compose/Centered.kt new file mode 100644 index 00000000..749e64d3 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/compose/Centered.kt @@ -0,0 +1,21 @@ +package com.skyd.imomoe.view.component.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun Centered( + modifier: Modifier = Modifier, + propagateMinConstraints: Boolean = false, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier, + propagateMinConstraints = propagateMinConstraints, + contentAlignment = Alignment.Center, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/compose/Dialog.kt b/app/src/main/java/com/skyd/imomoe/view/component/compose/Dialog.kt new file mode 100644 index 00000000..8dbb7117 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/compose/Dialog.kt @@ -0,0 +1,157 @@ +package com.skyd.imomoe.view.component.compose + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties +import com.skyd.imomoe.R + +@Composable +fun MessageDialog( + title: String = stringResource(id = R.string.warning), + message: String, + @DrawableRes icon: Int = 0, + properties: DialogProperties = DialogProperties(), + negativeText: String = stringResource(id = R.string.cancel), + positiveText: String = stringResource(id = R.string.ok), + onDismissRequest: (() -> Unit)? = null, + onNegative: (() -> Unit)? = null, + onPositive: (() -> Unit)? = null, +) { + MessageDialog( + title = title, + message = message, + icon = if (icon == 0) null else painterResource(id = icon), + properties = properties, + negativeText = negativeText, + positiveText = positiveText, + onDismissRequest = onDismissRequest, + onNegative = onNegative, + onPositive = onPositive + ) +} + +@Composable +fun MessageDialog( + title: String = stringResource(id = R.string.warning), + message: String, + icon: ImageVector? = null, + properties: DialogProperties = DialogProperties(), + negativeText: String = stringResource(id = R.string.cancel), + positiveText: String = stringResource(id = R.string.ok), + onDismissRequest: (() -> Unit)? = null, + onNegative: (() -> Unit)? = null, + onPositive: (() -> Unit)? = null, +) { + MessageDialog( + title = title, + message = message, + icon = if (icon == null) null else rememberVectorPainter(icon), + properties = properties, + negativeText = negativeText, + positiveText = positiveText, + onDismissRequest = onDismissRequest, + onNegative = onNegative, + onPositive = onPositive + ) +} + +@Composable +fun MessageDialog( + title: String = stringResource(id = R.string.warning), + message: String, + icon: Painter? = null, + properties: DialogProperties = DialogProperties(), + negativeText: String = stringResource(id = R.string.cancel), + positiveText: String = stringResource(id = R.string.ok), + onDismissRequest: (() -> Unit)? = null, + onNegative: (() -> Unit)? = null, + onPositive: (() -> Unit)? = null, +) { + val dismissButton: @Composable (() -> Unit) = { + TextButton( + onClick = { + onNegative?.invoke() + } + ) { + Text(text = negativeText) + } + } + val iconLambda: @Composable (() -> Unit)? = if (icon != null) { + { Icon(painter = icon, contentDescription = null) } + } else null + AlertDialog( + icon = if (icon != null) iconLambda else null, + title = { + Text(text = title) + }, + text = { + Text( + modifier = Modifier.verticalScroll(rememberScrollState()), + text = message + ) + }, + confirmButton = { + TextButton( + onClick = { + onPositive?.invoke() + } + ) { + Text(text = positiveText) + } + }, + dismissButton = if (onNegative == null) null else dismissButton, + onDismissRequest = { + onDismissRequest?.invoke() + }, + properties = properties + ) +} + +@Composable +fun WaitingDialog( + message: String, + properties: DialogProperties = DialogProperties(), + negativeText: String = stringResource(id = R.string.cancel), + positiveText: String = stringResource(id = R.string.ok), + onDismissRequest: (() -> Unit)? = null, + onNegative: (() -> Unit)? = null, + onPositive: (() -> Unit)? = null, +) { + val confirmButton: @Composable (() -> Unit) = { + TextButton( + onClick = { + onPositive?.invoke() + } + ) { + Text(text = positiveText) + } + } + val dismissButton: @Composable (() -> Unit) = { + TextButton( + onClick = { + onNegative?.invoke() + } + ) { + Text(text = negativeText) + } + } + AlertDialog( + onDismissRequest = { onDismissRequest?.invoke() }, + text = { Text(text = message) }, + icon = { CircularProgressIndicator() }, + confirmButton = if (onPositive == null) { + {} + } else confirmButton, + dismissButton = if (onNegative == null) null else dismissButton, + properties = properties + ) +} diff --git a/app/src/main/java/com/skyd/imomoe/view/component/compose/ImageTextPlaceholder.kt b/app/src/main/java/com/skyd/imomoe/view/component/compose/ImageTextPlaceholder.kt new file mode 100644 index 00000000..c825c0ba --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/compose/ImageTextPlaceholder.kt @@ -0,0 +1,49 @@ +package com.skyd.imomoe.view.component.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.skyd.imomoe.R + +@Composable +fun ImageTextPlaceholder( + modifier: Modifier = Modifier, + painter: Painter = painterResource(id = R.drawable.ic_sentiment_very_dissatisfied_24), + message: String, + onClick: (() -> Unit)? = null +) { + Centered( + modifier = modifier.fillMaxSize() + ) { + Column( + modifier = Modifier + .run { if (onClick != null) clickable(onClick = onClick) else this } + .fillMaxWidth(fraction = 0.5f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.fillMaxWidth(fraction = 0.6f), + painter = painter, + contentScale = ContentScale.Crop, + contentDescription = null + ) + Text( + modifier = Modifier.padding(top = 20.dp), + text = message, + style = MaterialTheme.typography.titleLarge + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/compose/ProgressTextPlaceholder.kt b/app/src/main/java/com/skyd/imomoe/view/component/compose/ProgressTextPlaceholder.kt new file mode 100644 index 00000000..e07d6546 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/compose/ProgressTextPlaceholder.kt @@ -0,0 +1,34 @@ +package com.skyd.imomoe.view.component.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ProgressTextPlaceholder( + modifier: Modifier = Modifier, + message: String, + onClick: (() -> Unit)? = null +) { + Centered( + modifier = modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.run { if (onClick != null) clickable(onClick = onClick) else this }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + Text( + modifier = Modifier.padding(top = 20.dp), + text = message, + style = MaterialTheme.typography.titleLarge + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuLoader.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuLoader.kt deleted file mode 100644 index 4b74735c..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuLoader.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.skyd.imomoe.view.component.player - -import android.net.Uri -import master.flame.danmaku.danmaku.loader.ILoader -import master.flame.danmaku.danmaku.loader.IllegalDataException -import java.io.InputStream -import java.net.URL -import kotlin.jvm.Throws - -class AnimeDanmakuLoader private constructor() : ILoader { - private var dataSource: AnimeJSONSource? = null - override fun getDataSource(): AnimeJSONSource? { - return dataSource - } - - @Throws(IllegalDataException::class) - fun load(url: URL) { - dataSource = try { - AnimeJSONSource(url) - } catch (e: Exception) { - throw IllegalDataException(e) - } - } - - @Throws(IllegalDataException::class) - override fun load(uri: String) { - dataSource = try { - AnimeJSONSource(Uri.parse(uri)) - } catch (e: Exception) { - throw IllegalDataException(e) - } - } - - @Throws(IllegalDataException::class) - override fun load(`in`: InputStream) { - dataSource = try { - AnimeJSONSource(`in`) - } catch (e: Exception) { - throw IllegalDataException(e) - } - } - - companion object { - val instance: AnimeDanmakuLoader by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { - AnimeDanmakuLoader() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuLoaderFactory.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuLoaderFactory.kt deleted file mode 100644 index 9924e630..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuLoaderFactory.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.skyd.imomoe.view.component.player - -import master.flame.danmaku.danmaku.loader.ILoader -import master.flame.danmaku.danmaku.loader.android.AcFunDanmakuLoader -import master.flame.danmaku.danmaku.loader.android.BiliDanmakuLoader - -class AnimeDanmakuLoaderFactory { - companion object { - var TAG_BILI = "bili" - var TAG_ACFUN = "acfun" - var TAG_ANIME = "anime" - fun create(tag: String): ILoader? { - return when { - TAG_BILI.equals(tag, ignoreCase = true) -> BiliDanmakuLoader.instance() - TAG_ACFUN.equals(tag, ignoreCase = true) -> AcFunDanmakuLoader.instance() - TAG_ANIME.equals(tag, ignoreCase = true) -> AnimeDanmakuLoader.instance - else -> AnimeDanmakuLoader.instance - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuParser.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuParser.kt deleted file mode 100644 index 7fc8ddeb..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeDanmakuParser.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.skyd.imomoe.view.component.player - -import android.graphics.Color -import com.skyd.imomoe.BuildConfig -import com.skyd.imomoe.util.Text.shield -import master.flame.danmaku.danmaku.model.android.Danmakus -import master.flame.danmaku.danmaku.parser.BaseDanmakuParser -import org.json.JSONArray -import org.json.JSONException -import java.util.* - -class AnimeDanmakuParser : BaseDanmakuParser() { - public override fun parse(): Danmakus { - if (mDataSource != null && mDataSource is AnimeJSONSource) { - val jsonSource = mDataSource as AnimeJSONSource - return doParse(jsonSource.data()) - } - return Danmakus() - } - - /** - * @param danmakuListData 弹幕数据 - * 传入的数组内包含普通弹幕,会员弹幕,锁定弹幕。 - * @return 转换后的Danmakus - */ - private fun doParse(danmakuListData: JSONArray?): Danmakus { - var danmakus = Danmakus() - if (danmakuListData == null || danmakuListData.length() == 0) { - return danmakus - } - danmakus = _parse(danmakuListData, danmakus) - return danmakus - } - - private fun _parse(jsonArray: JSONArray?, danmakus: Danmakus): Danmakus { - if (jsonArray == null || jsonArray.length() == 0) { - return danmakus - } - for (i in 0 until jsonArray.length()) { - try { - val array = jsonArray.getJSONArray(i) - val text = array.getString(4) - // 如果此条弹幕应该屏蔽,则直接continue到下一条 - if (text.shield()) continue - val type = getType(array.getString(1)) // 弹幕类型 - val time = (array.getDouble(0) * 1000).toLong() // 出现时间 - val color = getColor(array.getString(2)) - var textSize = 27.5f - if (array.length() >= 8) { - textSize = array.getString(7).replace("px", "").toFloat() - } - val item = mContext.mDanmakuFactory.createDanmaku(type, mContext) - if (item != null) { - item.time = time - item.textSize = 0.7f * textSize * (mDispDensity - 0.6f) - item.textColor = color - item.textShadowColor = if (color <= Color.BLACK) Color.WHITE else Color.BLACK - item.index = i - item.flags = mContext.mGlobalFlagValues - item.timer = mTimer - item.text = text - danmakus.addItem(item) - } - } catch (e: JSONException) { - e.printStackTrace() - } catch (e: NumberFormatException) { - e.printStackTrace() - } - } - return danmakus - } - - companion object { - fun getColor(s: String): Int { - val strColor = s.toLowerCase(Locale.ROOT) - try { - if (strColor.startsWith("#")) { - Color.parseColor(s) - } else if (strColor.startsWith("rgb")) { - val rgbArray = strColor.replace("rgb(", "") - .replace(")", "").split(",") - if (rgbArray.size == 3) Color.rgb( - rgbArray[0].trim().toInt(), - rgbArray[1].trim().toInt(), - rgbArray[2].trim().toInt() - ) - } - } catch (e: IllegalArgumentException) { - e.printStackTrace() - } - return Color.WHITE - } - } - - private fun getType(s: String): Int { - return when (s) { - "top" -> 4 - else -> 1 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeJSONSource.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeJSONSource.kt deleted file mode 100644 index 6368f1a4..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeJSONSource.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.skyd.imomoe.view.component.player - -import android.net.Uri -import master.flame.danmaku.danmaku.parser.IDataSource -import master.flame.danmaku.danmaku.util.IOUtils -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import java.io.File -import java.io.FileInputStream -import java.io.InputStream -import java.net.URL -import kotlin.jvm.Throws - -class AnimeJSONSource : IDataSource { - private var mJSONArray: JSONArray? = null - var danmuCount: Long = 0 - var code = 0 - var msg: String? = null - private var mInput: InputStream? = null - - constructor(json: String) { - init(json) - } - - constructor(`in`: InputStream?) { - init(`in`) - } - - @Throws(JSONException::class) - private fun init(`in`: InputStream?) { - if (`in` == null) throw NullPointerException("input stream cannot be null!") - mInput = `in` - val json = IOUtils.getString(mInput) - init(json) - } - - constructor(url: URL) { - init(url.openStream()) - } - - constructor(file: File) { - init(FileInputStream(file)) - } - - constructor(uri: Uri) { - val scheme = uri.scheme - if (IDataSource.SCHEME_HTTP_TAG.equals(scheme, ignoreCase = true) - || IDataSource.SCHEME_HTTPS_TAG.equals(scheme, ignoreCase = true) - ) { - init(URL(uri.toString()).openStream()) - } else if (IDataSource.SCHEME_FILE_TAG.equals(scheme, ignoreCase = true)) { - init(FileInputStream(uri.path)) - } - } - - @Throws(JSONException::class) - private fun init(json: String) { - if (json.isNotBlank()) { - val o = JSONObject(json) - danmuCount = o.getLong("danum") - code = o.getInt("code") - msg = o.getString("msg") - mJSONArray = o.getJSONArray("danmuku") - } - } - - override fun data(): JSONArray? { - return mJSONArray - } - - override fun release() { - IOUtils.closeQuietly(mInput) - mInput = null - mJSONArray = null - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeVideoPlayer.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeVideoPlayer.kt index 66294ce4..e276a3d2 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeVideoPlayer.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeVideoPlayer.kt @@ -2,17 +2,28 @@ package com.skyd.imomoe.view.component.player import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.annotation.TargetApi import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.Matrix +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Parcelable import android.util.AttributeSet import android.view.* import android.view.View.OnClickListener import android.widget.* +import androidx.annotation.WorkerThread +import androidx.core.content.ContextCompat.startActivity import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButtonToggleGroup +import com.shuyu.gsyvideoplayer.GSYVideoManager import com.shuyu.gsyvideoplayer.utils.CommonUtil import com.shuyu.gsyvideoplayer.utils.Debuger import com.shuyu.gsyvideoplayer.utils.GSYVideoType @@ -20,25 +31,25 @@ import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer import com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer import com.shuyu.gsyvideoplayer.video.base.GSYVideoPlayer import com.shuyu.gsyvideoplayer.video.base.GSYVideoView -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeEpisodeDataBean import com.skyd.imomoe.bean.BaseBean +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.ext.theme.getAttrColor import com.skyd.imomoe.util.Util.dp -import com.skyd.imomoe.util.Util.getResColor import com.skyd.imomoe.util.Util.getResDrawable import com.skyd.imomoe.util.Util.getScreenBrightness +import com.skyd.imomoe.util.Util.getScreenHeight +import com.skyd.imomoe.util.Util.getScreenWidth import com.skyd.imomoe.util.Util.openVideoByExternalPlayer -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible +import com.skyd.imomoe.util.showToast import com.skyd.imomoe.view.activity.DlnaActivity -import com.skyd.imomoe.view.adapter.SkinRvAdapter +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.VideoSpeed1Proxy import com.skyd.imomoe.view.component.ZoomView -import com.skyd.imomoe.view.component.textview.TypefaceTextView -import com.skyd.skin.SkinManager -import tv.danmaku.ijk.media.exo2.IjkExo2MediaPlayer -import tv.danmaku.ijk.media.player.IjkMediaPlayer +import com.skyd.imomoe.view.component.getData +import com.skyd.imomoe.view.component.setData +import com.skyd.imomoe.view.listener.dsl.setOnSeekBarChangeListener +import kotlinx.coroutines.* import java.io.File import java.io.Serializable import kotlin.math.abs @@ -47,101 +58,151 @@ import kotlin.math.abs open class AnimeVideoPlayer : StandardGSYVideoPlayer { companion object { val mScaleStrings = listOf( - Pair("默认比例", GSYVideoType.SCREEN_TYPE_DEFAULT), - Pair("16:9", GSYVideoType.SCREEN_TYPE_16_9), - Pair("4:3", GSYVideoType.SCREEN_TYPE_4_3), - Pair("全屏", GSYVideoType.SCREEN_TYPE_FULL), - Pair("拉伸全屏", GSYVideoType.SCREEN_MATCH_FULL) + "默认比例" to GSYVideoType.SCREEN_TYPE_DEFAULT, + "16:9" to GSYVideoType.SCREEN_TYPE_16_9, + "4:3" to GSYVideoType.SCREEN_TYPE_4_3, + "全屏" to GSYVideoType.SCREEN_TYPE_FULL, + "拉伸全屏" to GSYVideoType.SCREEN_MATCH_FULL ) const val NO_REVERSE = 0 const val HORIZONTAL_REVERSE = 1 const val VERTICAL_REVERSE = 2 + + // 夜间屏幕最大Alpha + const val NIGHT_SCREEN_MAX_ALPHA: Int = 0xAA + + val coroutineScope by lazy(LazyThreadSafetyMode.NONE) { + CoroutineScope(Dispatchers.Default) + } } + // 番剧名称(不是每一集的名称) + var animeTitle: String = "" + + /** + * 进度记忆最小时间,默认5秒后的进度才记忆 + */ + var playPositionMemoryTimeLimit = 5000L + + var playPositionMemoryStore: PlayPositionMemoryDataStore? = null + private var playPositionViewJob: Job? = null + + // 预跳转进度 + private var preSeekPlayPosition: Long? = null + // 正在双指缩放移动 private var doublePointerZoomingMoving = false - private var mDownloadButton: ImageView? = null - private var initFirstLoad = true - //记住切换数据源类型 + // 记住切换数据源类型 private var mScaleIndex = 0 - //4:3 16:9等 - private var mMoreScaleTextView: TextView? = null + // 4:3 16:9等 + private var tvMoreScale: TextView? = null - //倍速按钮 - private var mSpeedTextView: TextView? = null - private var mSpeedRecyclerView: RecyclerView? = null + // 倍速按钮 + private var tvSpeed: TextView? = null + private var rvSpeed: RecyclerView? = null - //速度 + // 速度 private var mPlaySpeed = 1f - //投屏按钮 - private var mClingImageView: ImageView? = null + // 下一集按钮 + private var ivNextEpisode: ImageView? = null - //分享按钮 - private var mShareImageView: ImageView? = null + // 如何播放下一集 + var onPlayNextEpisode: () -> Unit = {} + set(value) { + ivNextEpisode?.setOnClickListener { value() } + field = value + } - //更多按钮 - private var mMoreImageView: ImageView? = null + // 进度记忆组 + private var vgPlayPosition: ViewGroup? = null - //下一集按钮 - private var mNextImageView: ImageView? = null + // 进度文字 + private var tvPlayPosition: TextView? = null - //选集 - private var mEpisodeTextView: TextView? = null + // 关闭进度提示ImageView + private var ivClosePlayPositionTip: ImageView? = null + + // 选集 + private var tvEpisode: TextView? = null private var mEpisodeTextViewVisibility: Int = View.VISIBLE private var mEpisodeButtonOnClickListener: OnClickListener? = null - private var mEpisodeRecyclerView: RecyclerView? = null - private var mEpisodeAdapter: EpisodeRecyclerViewAdapter? = null + var rvEpisode: RecyclerView? = null + private var mEpisodeAdapter: VarietyAdapter? = null // 设置 - private var mSettingContainer: ViewGroup? = null - private var mSettingImageView: ImageView? = null + protected var vgSettingContainer: ViewGroup? = null + private var ivSetting: ImageView? = null // 镜像RadioGroup - private var mReverseRadioGroup: RadioGroup? = null + private var rgReverse: RadioGroup? = null private var mReverseValue: Int? = null private var mTextureViewTransform: Int = NO_REVERSE // 底部进度条CheckBox - private var mBottomProgressCheckBox: CheckBox? = null - private var mBottomProgressCheckBoxValue: Boolean = true + private var cbBottomProgress: CheckBox? = null - //底部进度调 - private var mBottomProgress: ProgressBar? = null + // 一次/循环/顺序播放 + private var tgSwitchVideoMode: MaterialButtonToggleGroup? = null - // 外部播放器打开 - private var mOpenByExternalPlayerTextView: TextView? = null + // 播放音频时常驻的封面 + private var ivMediaThumb: ImageView? = null - // 硬解码CheckBox - private var mMediaCodecCheckBox: CheckBox? = null + // 底部进度调 + private var pbBottomProgress: ProgressBar? = null + + // 外部播放器打开 + private var tvOpenByExternalPlayer: TextView? = null // 右侧弹出栏 - private var mRightContainer: ViewGroup? = null + protected open var vgRightContainer: ViewGroup? = null // 按住高速播放的tv - private var mTouchDownHighSpeedTextView: TextView? = null + private var tvTouchDownHighSpeed: TextView? = null private var mLongPressing: Boolean = false // 还原屏幕 - private var mRestoreScreenTextView: TextView? = null + private var tvRestoreScreen: TextView? = null + + // 投屏 + private var tvDlna: TextView? = null // 屏幕已经双指放大移动了 private var mDoublePointerZoomMoved: Boolean = false // 屏幕已经双指放大移动了 - private var mBiggerSurface: ViewGroup? = null + private var vgBiggerSurface: ViewGroup? = null // 控件没有显示 private var mUiCleared: Boolean = true // 显示系统时间 - private var mSystemTimeTextView: TextView? = null + private var tcSystemTime: TextClock? = null + + // top阴影 + private var viewTopContainerShadow: View? = null + + // 夜间屏幕View + private var viewNightScreen: View? = null + + // 夜间屏幕seekbar + private var sbNightScreen: SeekBar? = null + + // 夜间屏幕SeekBar值 + private var mNightScreenSeekBarProgress: Int = 0 + + // 全屏手动滑动下拉状态栏的起始偏移位置 + protected open var mStatusBarOffset: Int = 50.dp + + open var storeStateCurrentState: Int? = null + + protected open var surfaceContainer: View? = null constructor(context: Context) : super(context) @@ -152,41 +213,56 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { override fun getLayoutId() = if (mIfCurrentIsFullscreen) R.layout.layout_anime_video_player_land else R.layout.layout_anime_video_player - @SuppressLint("ClickableViewAccessibility") + @SuppressLint("ClickableViewAccessibility", "SetTextI18n") override fun init(context: Context?) { super.init(context) - mDownloadButton = findViewById(R.id.iv_download) - mMoreScaleTextView = findViewById(R.id.tv_more_scale) - mSpeedTextView = findViewById(R.id.tv_speed) -// mClingImageView = findViewById(R.id.iv_cling) - mRightContainer = findViewById(R.id.layout_right) - mSpeedRecyclerView = findViewById(R.id.rv_right) - mEpisodeRecyclerView = findViewById(R.id.rv_right) - mShareImageView = findViewById(R.id.iv_share) - mNextImageView = findViewById(R.id.iv_next) - mEpisodeTextView = findViewById(R.id.tv_episode) - mSettingImageView = findViewById(R.id.iv_setting) - mSettingContainer = findViewById(R.id.layout_setting) - mReverseRadioGroup = findViewById(R.id.rg_reverse) - mBottomProgressCheckBox = findViewById(R.id.cb_bottom_progress) - mBottomProgress = super.mBottomProgressBar - mMoreImageView = findViewById(R.id.iv_more) - mOpenByExternalPlayerTextView = findViewById(R.id.tv_open_by_external_player) -// mMediaCodecCheckBox = findViewById(R.id.cb_media_codec) - mRestoreScreenTextView = findViewById(R.id.tv_restore_screen) - mTouchDownHighSpeedTextView = findViewById(R.id.tv_touch_down_high_speed) - mBiggerSurface = findViewById(R.id.bigger_surface) - mSystemTimeTextView = findViewById(R.id.tv_system_time) - - mRightContainer?.gone() - mSettingContainer?.gone() - mTouchDownHighSpeedTextView?.gone() - - mBiggerSurface?.setOnClickListener(this) - mBiggerSurface?.setOnTouchListener(this) - - mRestoreScreenTextView?.setOnClickListener { + tvMoreScale = findViewById(R.id.tv_more_scale) + tvSpeed = findViewById(R.id.tv_speed) + vgRightContainer = findViewById(R.id.layout_right) + rvSpeed = findViewById(R.id.rv_right) + rvEpisode = findViewById(R.id.rv_right) + ivNextEpisode = findViewById(R.id.iv_next) + tvEpisode = findViewById(R.id.tv_episode) + ivSetting = findViewById(R.id.iv_setting) + vgSettingContainer = findViewById(R.id.layout_setting) + rgReverse = findViewById(R.id.rg_reverse) + cbBottomProgress = findViewById(R.id.cb_bottom_progress) + tgSwitchVideoMode = findViewById(R.id.tg_switch_video_mode) + pbBottomProgress = super.mBottomProgressBar + tvOpenByExternalPlayer = findViewById(R.id.tv_open_by_external_player) + tvRestoreScreen = findViewById(R.id.tv_restore_screen) + tvTouchDownHighSpeed = findViewById(R.id.tv_touch_down_high_speed) + vgBiggerSurface = findViewById(R.id.bigger_surface) + tcSystemTime = findViewById(R.id.tc_system_time) + viewTopContainerShadow = findViewById(R.id.view_top_container_shadow) + viewNightScreen = findViewById(R.id.view_player_night_screen) + sbNightScreen = findViewById(R.id.sb_player_night_screen) + tvDlna = findViewById(R.id.tv_dlna) + vgPlayPosition = findViewById(R.id.ll_play_position_view) + tvPlayPosition = findViewById(R.id.tv_play_position_time) + ivClosePlayPositionTip = findViewById(R.id.iv_close_play_position_tip) + ivMediaThumb = findViewById(R.id.iv_media_thumb) + surfaceContainer = findViewById(R.id.surface_container) + + vgRightContainer?.gone() + vgSettingContainer?.gone() + tvTouchDownHighSpeed?.gone() + vgPlayPosition?.gone() + + vgBiggerSurface?.setOnClickListener(this) + vgBiggerSurface?.setOnTouchListener(this) + + ivClosePlayPositionTip?.setOnClickListener { + playPositionViewJob?.cancel() + vgPlayPosition?.gone(true, 200L) + } + vgPlayPosition?.setOnClickListener { + preSeekPlayPosition?.also { if (it > 0L) seekTo(it) } + vgPlayPosition?.gone(true, 200L) + } + + tvRestoreScreen?.setOnClickListener { mTextureViewContainer?.run { if (this is ZoomView) restore() else { @@ -200,38 +276,56 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { it.gone() } } - mSpeedTextView?.setOnClickListener { - mRightContainer?.let { - val adapter = SpeedAdapter( - listOf( - SpeedBean("speed", "", "0.5"), - SpeedBean("speed", "", "0.75"), - SpeedBean("speed", "", "1"), - SpeedBean("speed", "", "1.25"), - SpeedBean("speed", "", "1.5"), - SpeedBean("speed", "", "2") + tvSpeed?.setOnClickListener { + vgRightContainer?.let { + val adapter = VarietyAdapter( + mutableListOf(VideoSpeed1Proxy(onBindViewHolder = { holder, data, _, _ -> + if (data.title.toFloat() == speed) { + holder.tvTitle.setTextColor(mContext.getAttrColor(R.attr.colorPrimary)) + } + holder.tvTitle.text = data.title + holder.tvTitle.setOnClickListener { + if (data.title == "1") { + tvSpeed?.text = mContext.getString(R.string.play_speed) + } else { + tvSpeed?.text = "${data.title}X" + } + mPlaySpeed = data.title.toFloat() + setSpeed(mPlaySpeed, true) + vgRightContainer?.gone() + //因为右侧界面显示时,不在xx秒后隐藏界面,所以要恢复xx秒后隐藏控制界面 + startDismissControlViewTimer() + } + true + })) + ).apply { + dataList = mutableListOf( + Speed1Bean("", "0.5"), + Speed1Bean("", "0.75"), + Speed1Bean("", "1"), + Speed1Bean("", "1.25"), + Speed1Bean("", "1.5"), + Speed1Bean("", "2") ) - ) - mSpeedRecyclerView?.layoutManager = LinearLayoutManager(context) - mSpeedRecyclerView?.adapter = adapter - adapter.notifyDataSetChanged() + } + rvSpeed?.layoutManager = LinearLayoutManager(context) + rvSpeed?.adapter = adapter } showRightContainer() } - mEpisodeTextView?.setOnClickListener { - mRightContainer?.let { - mEpisodeRecyclerView?.layoutManager = LinearLayoutManager(context) - mEpisodeRecyclerView?.adapter = mEpisodeAdapter + tvEpisode?.setOnClickListener { + vgRightContainer?.let { + rvEpisode?.layoutManager = LinearLayoutManager(context) + rvEpisode?.adapter = mEpisodeAdapter mEpisodeAdapter?.notifyDataSetChanged() - mEpisodeRecyclerView?.scrollToPosition(mEpisodeAdapter?.currentIndex ?: 0) } showRightContainer() } - mSettingImageView?.setOnClickListener { showSettingContainer() } - mReverseValue = mReverseRadioGroup?.getChildAt(0)?.id - mReverseRadioGroup?.children?.forEach { + ivSetting?.setOnClickListener { showSettingContainer() } + mReverseValue = rgReverse?.getChildAt(0)?.id + rgReverse?.children?.forEach { (it as RadioButton).apply { - setOnCheckedChangeListener { buttonView, isChecked -> + setOnCheckedChangeListener { _, isChecked -> if (!isChecked) return@setOnCheckedChangeListener mReverseValue = id when (id) { @@ -242,32 +336,42 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { } } } - mBottomProgressCheckBox?.setOnCheckedChangeListener { buttonView, isChecked -> + cbBottomProgress?.setOnCheckedChangeListener { _, isChecked -> + setBottomProgressBarVisibility(isChecked) + sharedPreferences().editor { + putBoolean("showPlayerBottomProgressbar", isChecked) + } + } + cbBottomProgress?.isChecked = sharedPreferences() + .getBoolean("showPlayerBottomProgressbar", false) + + tgSwitchVideoMode?.addOnButtonCheckedListener { _, checkedId, isChecked -> if (isChecked) { - mBottomProgress?.let { - mBottomProgressBar = it - it.visible() - } - } else { - mBottomProgressBar?.let { - mBottomProgress = it - it.gone() - mBottomProgressBar = null + when (checkedId) { + R.id.btn_play_once -> switchVideoMode = SwitchVideoMode.Once + R.id.btn_play_next -> switchVideoMode = SwitchVideoMode.Next + R.id.btn_play_repeat_one -> switchVideoMode = SwitchVideoMode.RepeatOne } } - mBottomProgressCheckBoxValue = isChecked } - mBottomProgressCheckBox?.isChecked = mBottomProgressBar != null + + tgSwitchVideoMode?.check( + when (switchVideoMode) { + SwitchVideoMode.Once -> R.id.btn_play_once + SwitchVideoMode.Next -> R.id.btn_play_next + SwitchVideoMode.RepeatOne -> R.id.btn_play_repeat_one + } + ) //重置视频比例 GSYVideoType.setShowType(mScaleStrings[mScaleIndex].second) changeTextureViewShowType() if (mTextureView != null) mTextureView.requestLayout() - mMoreScaleTextView?.text = mScaleStrings[mScaleIndex].first + tvMoreScale?.text = mScaleStrings[mScaleIndex].first //切换视频比例 - mMoreScaleTextView?.setOnClickListener(OnClickListener { + tvMoreScale?.setOnClickListener(OnClickListener { startDismissControlViewTimer() //重新开始ui消失时间计时 if (!mHadPlay) { return@OnClickListener @@ -276,27 +380,54 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { resolveTypeUI() }) - mClingImageView?.setOnClickListener { - mContext.startActivity( - Intent(mContext, DlnaActivity::class.java) - .putExtra("url", mUrl) - .putExtra("title", mTitle) - ) - mOriginUrl - } - - mOpenByExternalPlayerTextView?.setOnClickListener { + tvOpenByExternalPlayer?.setOnClickListener { if (!openVideoByExternalPlayer(mContext, mUrl)) mContext.getString(R.string.matched_app_not_found).showToast() } + + sbNightScreen?.setOnSeekBarChangeListener { + onProgressChanged { seekBar, progress, _ -> + seekBar ?: return@onProgressChanged + mNightScreenSeekBarProgress = progress + viewNightScreen?.setBackgroundColor((NIGHT_SCREEN_MAX_ALPHA * progress / seekBar.max) shl 24) + } + } + + tvDlna?.setOnClickListener { + val url = getUrl() + if (url == null) { + mContext.getString(R.string.please_wait_video_loaded).showToast() + return@setOnClickListener + } + startActivity( + mContext, Intent(mContext, DlnaActivity::class.java) + .putExtra("url", url) + .putExtra("title", getTitle()), null + ) + } } - fun getUrl(): String = mUrl + fun getUrl(): String? = mUrl fun getTitle(): String = mTitle + private fun setBottomProgressBarVisibility(show: Boolean) { + if (show) { + pbBottomProgress?.let { + mBottomProgressBar = it + it.visible() + } + } else { + mBottomProgressBar?.let { + pbBottomProgress = it + it.gone() + mBottomProgressBar = null + } + } + } + private fun showSettingContainer() { - mSettingContainer?.let { + vgSettingContainer?.let { hideAllWidget() it.translationX = 150f.dp it.visible() @@ -307,21 +438,26 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { animator.start() //取消xx秒后隐藏控制界面 cancelDismissControlViewTimer() - if (mReverseValue == null) mReverseValue = mReverseRadioGroup?.getChildAt(0)?.id + if (mReverseValue == null) mReverseValue = rgReverse?.getChildAt(0)?.id mReverseValue?.let { id -> findViewById(id).isChecked = true } - mBottomProgressCheckBox?.isChecked = mBottomProgressCheckBoxValue + cbBottomProgress?.isChecked = sharedPreferences() + .getBoolean("showPlayerBottomProgressbar", false) + } + } -// mMediaCodecCheckBox?.isChecked = GSYVideoType.isMediaCodec() -// mMediaCodecCheckBox?.setOnCheckedChangeListener { buttonView, isChecked -> -// if (isChecked) GSYVideoType.enableMediaCodec() -// else GSYVideoType.disableMediaCodec() -// startPlayLogic() -// } + fun setTopContainer(top: ViewGroup?) { + mTopContainer = top + viewTopContainerShadow = if (top == null) { + viewTopContainerShadow?.visible() + null + } else { + findViewById(R.id.view_top_container_shadow) } + restartTimerTask() } private fun showRightContainer() { - mRightContainer?.let { + vgRightContainer?.let { hideAllWidget() it.translationX = 150f.dp it.visible() @@ -335,13 +471,14 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { override fun hideAllWidget() { super.hideAllWidget() - setViewShowState(mRightContainer, INVISIBLE) - setViewShowState(mSettingContainer, INVISIBLE) - setViewShowState(mRestoreScreenTextView, View.GONE) +// setViewShowState(vgRightContainer, INVISIBLE) +// setViewShowState(vgSettingContainer, INVISIBLE) + setViewShowState(tvRestoreScreen, View.GONE) + setViewShowState(viewTopContainerShadow, View.INVISIBLE) } override fun onClickUiToggle(e: MotionEvent?) { - mRightContainer?.let { + vgRightContainer?.let { //如果右侧栏显示,则隐藏 if (it.visibility == View.VISIBLE) { it.gone() @@ -350,7 +487,7 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { return } } - mSettingContainer?.let { + vgSettingContainer?.let { // 如果显示,则隐藏 if (it.visibility == View.VISIBLE) { it.gone() @@ -381,30 +518,38 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { actionBar, statusBar ) as AnimeVideoPlayer + player.seekOnStart = seekOnStart player.mScaleIndex = mScaleIndex - player.mSpeedTextView?.text = mSpeedTextView?.text + player.tvSpeed?.text = tvSpeed?.text player.mFullscreenButton.visibility = mFullscreenButton.visibility player.mEpisodeTextViewVisibility = mEpisodeTextViewVisibility - player.mEpisodeTextView?.visibility = mEpisodeTextViewVisibility + player.tvEpisode?.visibility = mEpisodeTextViewVisibility player.mEpisodeAdapter = mEpisodeAdapter player.mTextureViewTransform = mTextureViewTransform player.mReverseValue = mReverseValue - player.mBottomProgressCheckBoxValue = mBottomProgressCheckBoxValue player.mPlaySpeed = mPlaySpeed - if (player.mBottomProgressBar != null) player.mBottomProgress = player.mBottomProgressBar - if (!player.mBottomProgressCheckBoxValue) player.mBottomProgressBar = null + player.sbNightScreen?.progress = mNightScreenSeekBarProgress + player.onPlayNextEpisode = onPlayNextEpisode + player.animeTitle = animeTitle + player.storeStateCurrentState = storeStateCurrentState + + if (player.mBottomProgressBar != null) player.pbBottomProgress = player.mBottomProgressBar + player.setBottomProgressBarVisibility( + sharedPreferences().getBoolean("showPlayerBottomProgressbar", false) + ) touchSurfaceUp() player.setRestoreScreenTextViewVisibility() player.resolveTypeUI() + player.supportDisplayCutouts() return player } private fun setRestoreScreenTextViewVisibility() { if (mUiCleared) { - mRestoreScreenTextView?.gone() + tvRestoreScreen?.gone() } else { - if (mDoublePointerZoomMoved) mRestoreScreenTextView?.visible() - else mRestoreScreenTextView?.gone() + if (mDoublePointerZoomMoved) tvRestoreScreen?.visible() + else tvRestoreScreen?.gone() } } @@ -423,21 +568,29 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { super.resolveNormalVideoShow(oldF, vp, gsyVideoPlayer) if (gsyVideoPlayer != null) { val player = gsyVideoPlayer as AnimeVideoPlayer + seekOnStart = player.seekOnStart mScaleIndex = player.mScaleIndex mFullscreenButton.visibility = player.mFullscreenButton.visibility - mSpeedTextView?.text = player.mSpeedTextView?.text + tvSpeed?.text = player.tvSpeed?.text mEpisodeTextViewVisibility = player.mEpisodeTextViewVisibility - mEpisodeTextView?.visibility = mEpisodeTextViewVisibility + tvEpisode?.visibility = mEpisodeTextViewVisibility mEpisodeAdapter = player.mEpisodeAdapter mTextureViewTransform = player.mTextureViewTransform mReverseValue = player.mReverseValue - mBottomProgressCheckBoxValue = player.mBottomProgressCheckBoxValue mPlaySpeed = player.mPlaySpeed - if (mBottomProgressBar != null) mBottomProgress = mBottomProgressBar - if (!mBottomProgressCheckBoxValue) mBottomProgressBar = null + mNightScreenSeekBarProgress = player.sbNightScreen?.progress ?: 0 + onPlayNextEpisode = player.onPlayNextEpisode + animeTitle = player.animeTitle + storeStateCurrentState = player.storeStateCurrentState + + if (mBottomProgressBar != null) pbBottomProgress = mBottomProgressBar + setBottomProgressBarVisibility( + sharedPreferences().getBoolean("showPlayerBottomProgressbar", false) + ) player.touchSurfaceUp() setRestoreScreenTextViewVisibility() resolveTypeUI() + supportDisplayCutouts() } } @@ -458,15 +611,32 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { if (!mHadPlay) { return } - mMoreScaleTextView?.text = mScaleStrings[mScaleIndex].first + tvMoreScale?.text = mScaleStrings[mScaleIndex].first GSYVideoType.setShowType(mScaleStrings[mScaleIndex].second) changeTextureViewShowType() if (mTextureView != null) mTextureView.requestLayout() setSpeed(mPlaySpeed, true) - mTouchDownHighSpeedTextView?.gone() + tvTouchDownHighSpeed?.gone() mLongPressing = false } + override fun setSpeed(speed: Float, soundTouch: Boolean) { + super.setSpeed(speed, soundTouch) + onSpeedChanged(speed) + } + + override fun setSpeed(speed: Float) { + super.setSpeed(speed) + onSpeedChanged(speed) + } + + /** + * 视频播放速度改变后回调 + */ + protected open fun onSpeedChanged(speed: Float) { + + } + /** * 需要在尺寸发生变化的时候重新处理 */ @@ -514,13 +684,19 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { cachePath: File?, title: String? ): Boolean { - val result = super.setUp(url, cacheWithPlay, cachePath, title) - mTitleTextView?.let { - if (it is TypefaceTextView) { - it.setIsFocused(true) + runCatching { + if (mContext.getMediaMime(Uri.parse(url))?.startsWith("audio/") == true) { + mContext.getMediaAlbumArt(Uri.parse(url))?.let { + ivMediaThumb?.visible() + ivMediaThumb?.setImageBitmap(it) + } + } else { + ivMediaThumb?.gone() } + }.onFailure { + ivMediaThumb?.gone() } - return result + return super.setUp(url, cacheWithPlay, cachePath, title) } override fun updateStartImage() { @@ -531,13 +707,13 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { imageView.setImageDrawable(getResDrawable(R.drawable.ic_pause_white_24)) } GSYVideoView.CURRENT_STATE_ERROR -> { - imageView.setImageDrawable(getResDrawable(R.drawable.ic_play_white_24)) + imageView.setImageDrawable(getResDrawable(R.drawable.ic_play_24)) } GSYVideoView.CURRENT_STATE_AUTO_COMPLETE -> { - imageView.setImageDrawable(getResDrawable(R.drawable.ic_refresh_white_24)) + imageView.setImageDrawable(getResDrawable(R.drawable.ic_refresh_24)) } else -> { - imageView.setImageDrawable(getResDrawable(R.drawable.ic_play_white_24)) + imageView.setImageDrawable(getResDrawable(R.drawable.ic_play_24)) } } } else { @@ -545,7 +721,7 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { } } - override fun onBrightnessSlide(percent: Float) { + public override fun onBrightnessSlide(percent: Float) { val activity = mContext as Activity val lpa = activity.window.attributes val mBrightnessData = lpa.screenBrightness @@ -568,43 +744,86 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { //正常 override fun changeUiToNormal() { super.changeUiToNormal() + viewTopContainerShadow?.visible() initFirstLoad = true mUiCleared = false } override fun changeUiToPauseShow() { + // 防止锁定后从桌面进入仍然显示界面的问题 + if (mLockCurScreen && mNeedLockFull) { + mLockScreen?.visible() + return + } super.changeUiToPauseShow() + viewTopContainerShadow?.visible() mUiCleared = false } override fun changeUiToClear() { super.changeUiToClear() + viewTopContainerShadow?.invisible() mUiCleared = true + + if (vgPlayPosition?.isVisible == true) ivClosePlayPositionTip?.callOnClick() } //准备中 override fun changeUiToPreparingShow() { super.changeUiToPreparingShow() + viewTopContainerShadow?.visible() mUiCleared = false } //播放中 override fun changeUiToPlayingShow() { - super.changeUiToPlayingShow() -// if (initFirstLoad) { -// mBottomContainer.gone() -// mStartButton.gone() -// } initFirstLoad = false + // 防止锁定后从桌面进入仍然显示界面的问题 + if (mLockCurScreen && mNeedLockFull) { + mLockScreen?.visible() + return + } + super.changeUiToPlayingShow() + viewTopContainerShadow?.visible() mUiCleared = false } //自动播放结束 override fun changeUiToCompleteShow() { super.changeUiToCompleteShow() + viewTopContainerShadow?.visible() mBottomContainer.gone() - mTouchDownHighSpeedTextView?.gone() + tvTouchDownHighSpeed?.gone() mUiCleared = false + + if (vgPlayPosition?.isVisible == true) ivClosePlayPositionTip?.callOnClick() + } + + override fun changeUiToError() { + super.changeUiToError() + viewTopContainerShadow?.invisible() + + if (vgPlayPosition?.isVisible == true) ivClosePlayPositionTip?.callOnClick() + } + + override fun changeUiToPrepareingClear() { + super.changeUiToPrepareingClear() + viewTopContainerShadow?.invisible() + } + + override fun changeUiToPlayingBufferingClear() { + super.changeUiToPlayingBufferingClear() + viewTopContainerShadow?.invisible() + } + + override fun changeUiToCompleteClear() { + super.changeUiToCompleteClear() + viewTopContainerShadow?.invisible() + } + + override fun changeUiToPlayingBufferingShow() { + super.changeUiToPlayingBufferingShow() + viewTopContainerShadow?.visible() } override fun onVideoPause() { @@ -623,7 +842,7 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { mVideoAllCallBack.let { if (it is MyVideoAllCallBack) it.onVideoResume() } - } catch (e: java.lang.Exception) { + } catch (e: Exception) { e.printStackTrace() } } @@ -631,30 +850,68 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { public override fun clickStartIcon() { super.clickStartIcon() + + // 下面是处理完点击后的逻辑 + if (mCurrentState == CURRENT_STATE_PLAYING) { + onVideoResume() + } } override fun onClick(v: View) { super.onClick(v) - val i = v.id - // bigger_surface代替原有的surface_container执行点击动作 - if (i == R.id.bigger_surface && mCurrentState == GSYVideoView.CURRENT_STATE_ERROR) { - if (mVideoAllCallBack != null) { - Debuger.printfLog("onClickStartError") - mVideoAllCallBack.onClickStartError(mOriginUrl, mTitle, this) - } - prepareVideo() - } else if (i == R.id.bigger_surface) { - if (mVideoAllCallBack != null && isCurrentMediaListener) { - if (mIfCurrentIsFullscreen) { - Debuger.printfLog("onClickBlankFullscreen") - mVideoAllCallBack.onClickBlankFullscreen(mOriginUrl, mTitle, this) + when (v.id) { + // bigger_surface代替原有的surface_container执行点击动作 + R.id.bigger_surface -> { + vgSettingContainer?.gone() + vgRightContainer?.gone() + if (mCurrentState == GSYVideoView.CURRENT_STATE_ERROR) { + if (mVideoAllCallBack != null) { + Debuger.printfLog("onClickStartError") + mVideoAllCallBack.onClickStartError(mOriginUrl, mTitle, this) + } + prepareVideo() } else { - Debuger.printfLog("onClickBlank") - mVideoAllCallBack.onClickBlank(mOriginUrl, mTitle, this) + if (mVideoAllCallBack != null && isCurrentMediaListener) { + if (mIfCurrentIsFullscreen) { + Debuger.printfLog("onClickBlankFullscreen") + mVideoAllCallBack.onClickBlankFullscreen(mOriginUrl, mTitle, this) + } else { + Debuger.printfLog("onClickBlank") + mVideoAllCallBack.onClickBlank(mOriginUrl, mTitle, this) + } + } + startDismissControlViewTimer() } } - startDismissControlViewTimer() + R.id.thumb -> { + vgSettingContainer?.gone() + vgRightContainer?.gone() + } + } + } + + /** + * 双击的时候调用此方法 + */ + override fun touchDoubleUp(e: MotionEvent?) { + // 处理双击前的逻辑 + val oldUiVisibilityState = mBottomContainer?.visibility ?: VISIBLE + + // 处理双击 + super.touchDoubleUp(e) + + // 下面是处理完双击后的逻辑 + if (mCurrentState == CURRENT_STATE_PLAYING) { // 若双击后是播放状态 + //双击前Ui是什么可见性状态,则双击后Ui还是什么可见性状态,避免双击后Ui突然显示出来 + if (oldUiVisibilityState == VISIBLE) changeUiToPlayingShow() + else changeUiToPlayingClear() +// cancelDismissControlViewTimer() + } else if (mCurrentState == CURRENT_STATE_PAUSE) { // 若双击后是暂停状态 + //双击前Ui是什么可见性状态,则双击后Ui还是什么可见性状态,避免双击后Ui突然显示出来 + if (oldUiVisibilityState == VISIBLE) changeUiToPauseShow() + else changeUiToPauseClear() +// cancelDismissControlViewTimer() } } @@ -666,14 +923,29 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { mLongPressing = true // 此处不能设置mPlaySpeed setSpeed(2f, true) - mTouchDownHighSpeedTextView?.text = + tvTouchDownHighSpeed?.text = mContext.getString(R.string.touch_down_high_speed, "2") - mTouchDownHighSpeedTextView?.visible() + tvTouchDownHighSpeed?.visible() } } } + override fun touchSurfaceMoveFullLogic(absDeltaX: Float, absDeltaY: Float) { + // 全屏下拉任务栏 + if (absDeltaY > mThreshold && absDeltaY > absDeltaX && mDownY <= mStatusBarOffset) { + cancelProgressTimer() + return + } + super.touchSurfaceMoveFullLogic(absDeltaX, absDeltaY) + } + override fun onTouch(v: View?, event: MotionEvent): Boolean { + if (mIfCurrentIsFullscreen && mLockCurScreen && mNeedLockFull) { + onClickUiToggle(event) + startDismissControlViewTimer() + return true + } + // ---长按逻辑开始 if (event.pointerCount == 1) { if (event.action == MotionEvent.ACTION_UP) { @@ -681,7 +953,7 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { if (mLongPressing) { mLongPressing = false setSpeed(mPlaySpeed, true) - mTouchDownHighSpeedTextView?.gone() + tvTouchDownHighSpeed?.gone() return false } } @@ -695,14 +967,14 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { // 不让super的代码执行,表明正在双指放大移动旋转 doublePointerZoomingMoving = true mDoublePointerZoomMoved = true - if (!mUiCleared) mRestoreScreenTextView?.visible() + if (!mUiCleared) tvRestoreScreen?.visible() // 下面用bigger_surface代替原有的surface_container执行手势动作 return false } } // 当正在双指操作时,禁止执行super的代码 if (doublePointerZoomingMoving) { - mRestoreScreenTextView?.visible() + tvRestoreScreen?.visible() // 如果双指松开,则标志不是在移动 if (event.action == MotionEvent.ACTION_UP) { doublePointerZoomingMoving = false @@ -784,32 +1056,124 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { mInnerHandler.postDelayed({ backToNormal() }, delay.toLong()) } - fun setEpisodeButtonOnClickListener(listener: OnClickListener) { - mEpisodeButtonOnClickListener = listener + /** + * 准备好视频,开始查找进度 + */ + override fun onPrepared() { + super.onPrepared() + if (sharedPreferences().getBoolean("restorePlaySpeed", false)) { + setSpeed(sharedPreferences().getFloat("playSpeed", 1f), true) + } + playPositionViewJob?.cancel() + playPositionMemoryStore?.apply { + coroutineScope.launch { + getPlayPosition(mOriginUrl)?.also { + preSeekPlayPosition = it + playPositionViewJob = launch(Dispatchers.Main) { + // 若用户没有设置自动跳转 或者 看完了,才显示提示 + if (!sharedPreferences() + .getBoolean("autoJumpToLastPosition", true) || it == -1L + ) { + tvPlayPosition?.text = positionFormat(it) + vgPlayPosition?.visible() + //展示5秒 + delay(5000) + vgPlayPosition?.gone(true, 200L) + } + } + } + } + } } - fun setEpisodeAdapter(adapter: EpisodeRecyclerViewAdapter) { - mEpisodeAdapter = adapter + /** + * 1.退出界面时记忆进度 + */ + override fun onDetachedFromWindow() { + storePlayPosition() + sharedPreferences().editor { putFloat("playSpeed", mPlaySpeed) } + super.onDetachedFromWindow() } - fun getShareButton() = mShareImageView + /** + * 2.切换选集时记忆进度 + */ + override fun setUp( + url: String?, + cacheWithPlay: Boolean, + cachePath: File?, + title: String?, + changeState: Boolean + ): Boolean { + if (url != mOriginUrl) { + vgPlayPosition?.gone() + storePlayPosition() + } + + return super.setUp(url, cacheWithPlay, cachePath, title, changeState) + } - fun getMoreButton() = mMoreImageView + /** + * 1.退出界面时记忆进度 + * 2.切换选集时记忆进度 + * + * @param position 小于0代表播放完毕(已看完),大于等于0代表正常进度 + * + * 注意:记忆单位是每个视频而不是一部番剧;一部番剧里面的每集都有记录,并非只记录最后看的那一集 + */ + private fun storePlayPosition(position: Long = gsyVideoManager.currentPosition) { + val url = mOriginUrl ?: return + val duration = gsyVideoManager.duration + var newPosition = position + // 若还剩10s结束,则直接标记为“看完” + if (newPosition > 0 && abs(newPosition - duration) <= 10000L) newPosition = -1L + // 进度为负(已经播放完) 或 当前进度大于最小限制且小于最大限制(播放完时不记录),则记录 + if (newPosition < 0 || (newPosition > playPositionMemoryTimeLimit && duration > 0)) { + playPositionMemoryStore?.apply { + coroutineScope.launch { + putPlayPosition(url, newPosition) + } + } + } + } - fun getEpisodeButton() = mEpisodeTextView + /** + * 切集是 onCompletion,onAutoComplete 就是正常播放结束的 + * https://github.com/CarGuo/GSYVideoPlayer/issues/2983#issuecomment-708278308 + */ + override fun onAutoCompletion() { + super.onAutoCompletion() + // 播放完毕 + storePlayPosition(-1L) + + // 不要使用自带的mLooping属性,自带的不支持动态设置 + when (switchVideoMode) { + SwitchVideoMode.Once -> {} + SwitchVideoMode.Next -> { + onPlayNextEpisode() + } + SwitchVideoMode.RepeatOne -> { + startPlayLogic() + } + } + } - fun getDownloadButton() = mDownloadButton + fun setEpisodeButtonOnClickListener(listener: OnClickListener) { + mEpisodeButtonOnClickListener = listener + } - fun getBottomContainer() = mBottomContainer + fun setEpisodeAdapter(adapter: VarietyAdapter) { + mEpisodeAdapter = adapter + } - fun getClingButton() = mClingImageView + fun getEpisodeButton() = tvEpisode - fun getNextButton() = mNextImageView + fun getBottomContainer() = mBottomContainer - fun getRightContainer() = mRightContainer + fun getRightContainer() = vgRightContainer fun setEpisodeButtonVisibility(visibility: Int) { - mEpisodeTextView?.visibility = visibility + tvEpisode?.visibility = visibility mEpisodeTextViewVisibility = visibility } @@ -818,71 +1182,178 @@ open class AnimeVideoPlayer : StandardGSYVideoPlayer { else super.cancelDismissControlViewTimer() } - class SpeedBean( - override var type: String, - override var actionUrl: String, - var title: String - ) : BaseBean, Serializable + /** + * 适配刘海屏,防止重要内容落入刘海内被遮挡 + */ + open fun supportDisplayCutouts() { + if (currentPlayer == this && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val decorView: View = activity.window.decorView + decorView.post { + val displayCutout = decorView.rootWindowInsets?.displayCutout ?: return@post + mTopContainer?.updateSafeInset(displayCutout) + mBottomContainer?.updateSafeInset(displayCutout) + } + } + } - inner class SpeedAdapter(val list: List) : SkinRvAdapter() { + @TargetApi(28) + protected fun View.updateSafeInset(displayCutout: DisplayCutout) { + val location = IntArray(2) + getLocationOnScreen(location) + val left = location[0] + val right = location[0] + width + val top = location[1] + val bottom = location[1] + height + + var leftSolved = false + var rightSolved = false + + updatePadding(left = 0, right = 0, top = 0, bottom = 0) + if (!inSafeInset(displayCutout)) { + // left + if (left + paddingLeft < displayCutout.safeInsetLeft) { + updatePadding(left = displayCutout.safeInsetLeft) + leftSolved = true + } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return RightRecyclerViewViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_player_list_item_1, parent, false) - ).apply { SkinManager.setSkin(itemView) } - } + // right + if (right - paddingRight > getScreenWidth(true) - displayCutout.safeInsetRight) { + updatePadding(right = displayCutout.safeInsetRight) + rightSolved = true + } - @SuppressLint("SetTextI18n") - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = list[position] + // top + if (top + paddingTop < displayCutout.safeInsetTop) { + if (!leftSolved && !rightSolved) { + updatePadding(top = displayCutout.safeInsetTop) + } + } - when (holder) { - is RightRecyclerViewViewHolder -> { - if (item.type == "speed") { - if (item.title.toFloat() == speed) { - holder.tvTitle.setTextColor(context.getResColor(R.color.unchanged_main_color_2_skin)) - } - holder.tvTitle.text = item.title - holder.itemView.setOnClickListener { - if (item.title == "1") { - mSpeedTextView?.text = App.context.getString(R.string.play_speed) - } else { - mSpeedTextView?.text = item.title + "X" - } - mPlaySpeed = item.title.toFloat() - setSpeed(mPlaySpeed, true) - mRightContainer?.gone() - //因为右侧界面显示时,不在xx秒后隐藏界面,所以要恢复xx秒后隐藏控制界面 - startDismissControlViewTimer() - } - } + // bottom + if (bottom - paddingBottom > getScreenHeight(true) - displayCutout.safeInsetBottom) { + if (!leftSolved && !rightSolved) { + updatePadding(bottom = displayCutout.safeInsetBottom) } } } - - override fun getItemCount(): Int = list.size } - abstract class EpisodeRecyclerViewAdapter( - private val activity: Activity, - private val dataList: List, - ) : SkinRvAdapter() { - - abstract val currentIndex: Int - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return RightRecyclerViewViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_player_list_item_1, parent, false) - ).apply { SkinManager.setSkin(itemView) } + @TargetApi(28) + protected fun View.inSafeInset(displayCutout: DisplayCutout): Boolean { + displayCutout.boundingRects.forEach { + if (overlap(it)) return false } + return true + } - override fun getItemCount(): Int = dataList.size + override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets { + return super.onApplyWindowInsets(insets).also { + supportDisplayCutouts() + } } + class Speed1Bean( + override var route: String, + var title: String + ) : BaseBean, Serializable + class RightRecyclerViewViewHolder(view: View) : RecyclerView.ViewHolder(view) { val tvTitle = view as TextView } + + interface PlayPositionMemoryDataStore { + + suspend fun getPlayPosition(url: String): Long? + + /** + * @param position 播放进度毫秒,可用GSYVideoViewBridge::currentPosition获取 + */ + @WorkerThread + suspend fun putPlayPosition(url: String, position: Long) + + @WorkerThread + suspend fun deletePlayPosition(url: String) + + fun positionFormat(position: Long): String + } + + override fun onSaveInstanceState(): Parcelable? { + return Bundle().apply { + putParcelable("super", super.onSaveInstanceState()) + putString("mUrl", mUrl) + putString("mOriginUrl", mOriginUrl) + putBoolean("mIfCurrentIsFullscreen", mIfCurrentIsFullscreen) + putString("animeTitle", animeTitle) + putString("mTitle", mTitle) + putLong("currentPosition", GSYVideoManager.instance().currentPosition) + putBoolean("mActionBar", mActionBar) + putBoolean("mStatusBar", mStatusBar) + // 存储播放状态信息,注意需要是currentPlayer的(因为全屏的播放器在恢复后不会走onRestoreInstanceState) + putInt( + "mCurrentState", + (currentPlayer as? AnimeVideoPlayer)?.storeStateCurrentState ?: mCurrentState + ) + // 存储亮度信息,注意不是mBrightnessData + putFloat("brightness", mContext.activity.window.attributes.screenBrightness) + // 存储缩放信息,注意需要是currentPlayer的(因为全屏的播放器在恢复后不会走onRestoreInstanceState) + putSerializable( + "zoomData", + ((currentPlayer as? AnimeVideoPlayer)?.surfaceContainer as? ZoomView)?.getData() + ) + } + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is Bundle) { + runCatching { + super.onRestoreInstanceState(state.getParcelable("super")) + }.onFailure { + if (it !is IllegalArgumentException) it.printStackTrace() + } + // 恢复mUrl和mTitle并装载视频 + val mUrl = state.getString("mUrl") + val mTitle = state.getString("mTitle") + if (!mUrl.isNullOrBlank() && !mTitle.isNullOrBlank()) { + setUp(mUrl, false, mTitle) + } + + // 恢复animeTitle + animeTitle = state.getString("animeTitle").orEmpty() + + // 恢复亮度信息,注意不是mBrightnessData + val brightness = state.getFloat("brightness", -1f) + if (brightness >= 0) { + ((currentPlayer as? AnimeVideoPlayer) ?: this).onBrightnessSlide(brightness) + } + + // 恢复播放状态和播放位置 + val currentPosition = state.getLong("currentPosition") + if (currentPosition > 0L) seekOnStart = currentPosition + when (state.getInt("mCurrentState", -1)) { + CURRENT_STATE_PREPAREING, CURRENT_STATE_PLAYING, CURRENT_STATE_PLAYING_BUFFERING_START -> { + startPlayLogic() + } + else -> { + startPlayLogic() + onVideoPause() + } + } + + // 恢复是否全屏 + val mIfCurrentIsFullscreen = state.getBoolean("mIfCurrentIsFullscreen") + if (mIfCurrentIsFullscreen) { + startWindowFullscreen( + mContext, + state.getBoolean("mActionBar"), + state.getBoolean("mStatusBar") + ) + } + + // 恢复缩放信息,注意在“恢复是否全屏”之后执行,且需要是currentPlayer的 + // (因为全屏的播放器在恢复后不会走onRestoreInstanceState) + ((currentPlayer as? AnimeVideoPlayer)?.surfaceContainer as? ZoomView) + ?.setData(state.getSerializable("zoomData") as? ZoomView.ZoomData) + + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeVideoPositionMemoryStore.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeVideoPositionMemoryStore.kt new file mode 100644 index 00000000..a723f23c --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/AnimeVideoPositionMemoryStore.kt @@ -0,0 +1,24 @@ +package com.skyd.imomoe.view.component.player + +import com.shuyu.gsyvideoplayer.utils.CommonUtil +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.database.entity.PlayRecordEntity +import com.skyd.imomoe.database.getOfflineDatabase + +object AnimeVideoPositionMemoryStore : AnimeVideoPlayer.PlayPositionMemoryDataStore { + + private val dao = getOfflineDatabase().playRecordDao() + + override suspend fun getPlayPosition(url: String): Long? = dao.query(url)?.position + + override suspend fun putPlayPosition(url: String, position: Long) { + dao.insert(PlayRecordEntity(url, position)) + } + + override suspend fun deletePlayPosition(url: String) = dao.delete(url) + + override fun positionFormat(position: Long): String = + if (position < 0L) appContext.getString(R.string.episode_play_completed) + else CommonUtil.stringForTime(position) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/DanmakuAdapter.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/DanmakuAdapter.kt deleted file mode 100644 index 78fca480..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/DanmakuAdapter.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.skyd.imomoe.view.component.player - -import android.graphics.Color -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.style.BackgroundColorSpan -import android.text.style.ImageSpan -import master.flame.danmaku.controller.IDanmakuView -import master.flame.danmaku.danmaku.model.BaseDanmaku -import master.flame.danmaku.danmaku.model.android.BaseCacheStuffer -import master.flame.danmaku.danmaku.util.IOUtils -import java.io.IOException -import java.io.InputStream -import java.net.URL - -class DanmakuAdapter(private val mDanmakuView: IDanmakuView?) : BaseCacheStuffer.Proxy() { - private var mDrawable: Drawable? = null - override fun prepareDrawing(danmaku: BaseDanmaku, fromWorkerThread: Boolean) { - if (danmaku.text is Spanned) { // 根据你的条件检查是否需要需要更新弹幕 - // FIXME 这里只是简单启个线程来加载远程url图片,请使用你自己的异步线程池,最好加上你的缓存池 - object : Thread() { - override fun run() { - val url = "http://www.bilibili.com/favicon.ico" - var inputStream: InputStream? = null - var drawable = mDrawable - if (drawable == null) { - try { - inputStream = URL(url).openConnection().getInputStream() - drawable = BitmapDrawable.createFromStream(inputStream, "bitmap") - mDrawable = drawable - } catch (e: IOException) { - e.printStackTrace() - } finally { - IOUtils.closeQuietly(inputStream) - } - } - drawable?.let { - it.setBounds(0, 0, 100, 100) - danmaku.text = createSpannable(it) - mDanmakuView?.invalidateDanmaku(danmaku, false) - } - } - }.start() - } - } - - override fun releaseResource(danmaku: BaseDanmaku) { - // TODO 重要:清理含有ImageSpan的text中的一些占用内存的资源 例如drawable - } - - private fun createSpannable(drawable: Drawable): SpannableStringBuilder { - val text = "bitmap" - val spannableStringBuilder = SpannableStringBuilder(text) - val span = ImageSpan(drawable) //ImageSpan.ALIGN_BOTTOM); - spannableStringBuilder.setSpan(span, 0, text.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) - spannableStringBuilder.append("图文混排") - spannableStringBuilder.setSpan( - BackgroundColorSpan(Color.parseColor("#8A2233B1")), - 0, - spannableStringBuilder.length, - Spannable.SPAN_INCLUSIVE_INCLUSIVE - ) - return spannableStringBuilder - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/DanmakuVideoPlayer.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/DanmakuVideoPlayer.kt index eb291d8d..f45e2587 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/DanmakuVideoPlayer.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/DanmakuVideoPlayer.kt @@ -1,88 +1,74 @@ package com.skyd.imomoe.view.component.player -import android.animation.ValueAnimator +import android.app.Activity import android.content.Context -import android.graphics.Color import android.util.AttributeSet -import android.view.KeyEvent import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.ImageView import android.widget.TextView -import android.widget.Toast -import com.google.gson.Gson -import com.shuyu.gsyvideoplayer.utils.Debuger +import androidx.fragment.app.FragmentActivity +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.kuaishou.akdanmaku.data.DanmakuItemData.Companion.DANMAKU_STYLE_ICON_UP import com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer import com.shuyu.gsyvideoplayer.video.base.GSYVideoPlayer import com.shuyu.gsyvideoplayer.video.base.GSYVideoView import com.skyd.imomoe.R -import com.skyd.imomoe.bean.SendDanmuBean -import com.skyd.imomoe.bean.SendDanmuResultBean -import com.skyd.imomoe.net.RetrofitManager -import com.skyd.imomoe.net.service.DanmuService -import com.skyd.imomoe.util.Text.shield -import com.skyd.imomoe.util.Util.hideKeyboard -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.html.SnifferVideo.AC -import com.skyd.imomoe.util.html.SnifferVideo.KEY -import com.skyd.imomoe.util.html.SnifferVideo.REFEREER_URL -import com.skyd.imomoe.util.html.SnifferVideo.VIDEO_ID -import com.skyd.imomoe.util.visible -import com.skyd.imomoe.view.component.player.AnimeDanmakuLoaderFactory.Companion.TAG_ANIME -import com.skyd.imomoe.view.component.player.AnimeDanmakuLoaderFactory.Companion.create +import com.skyd.imomoe.config.Api +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.component.player.danmaku.DanmakuManager +import com.skyd.imomoe.view.component.player.danmaku.DanmakuType +import com.skyd.imomoe.view.component.player.danmaku.anime.AnimeDanmakuRepository +import com.skyd.imomoe.view.component.player.danmaku.anime.AnimeDanmakuRepository.Companion.toDanmakuItemData +import com.skyd.imomoe.view.component.player.danmaku.bili.BilibiliDanmakuRepository +import com.skyd.imomoe.view.fragment.dialog.DanmakuSettingDialogFragment +import com.skyd.imomoe.view.fragment.dialog.SendDanmakuFontDialogFragment import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import master.flame.danmaku.controller.DrawHandler -import master.flame.danmaku.controller.IDanmakuView -import master.flame.danmaku.danmaku.loader.IllegalDataException -import master.flame.danmaku.danmaku.model.BaseDanmaku -import master.flame.danmaku.danmaku.model.DanmakuTimer -import master.flame.danmaku.danmaku.model.IDisplayer -import master.flame.danmaku.danmaku.model.android.DanmakuContext -import master.flame.danmaku.danmaku.model.android.Danmakus -import master.flame.danmaku.danmaku.model.android.SpannedCacheStuffer -import master.flame.danmaku.danmaku.parser.BaseDanmakuParser -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.RequestBody.Companion.toRequestBody -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.withContext import java.net.URL -import kotlin.math.abs -/** - * 注意:这只是一个例子,演示如何集合弹幕,需要完善如弹出输入弹幕等的,可以自行完善。 - * 注意:b站的弹幕so只有v5 v7 x86、没有64,所以记得配置上ndk过滤。 - */ -open class DanmakuVideoPlayer : AnimeVideoPlayer { - private lateinit var mDanmakuUrl: String - private var mParser: BaseDanmakuParser? = null //解析器对象 - private lateinit var danmakuView: IDanmakuView //弹幕view - private var danmakuContext: DanmakuContext? = null - var danmakuStartSeekPosition: Long = -1 - - // 由于seek后弹幕时间不精确,因此标记是否需要校准 - private var needCorrectSeekPosition = false - // 是否在显示弹幕 - var mDanmakuShow = true +open class DanmakuVideoPlayer : AnimeVideoPlayer { + companion object { + const val ANIME_DANMAKU_URL = Api.DANMAKU_URL + } - // 请求弹幕相关参数 - var mDanmuParamMap = HashMap() - private set + private lateinit var danmakuManager: DanmakuManager // 弹幕输入文本框 - private var mDanmakuInputEditText: EditText? = null + private var etDanmakuInput: EditText? = null // 弹幕开关 - private var mShowDanmakuImageView: ImageView? = null + private var ivShowDanmaku: ImageView? = null + + // 弹幕设置 + private var ivDanmakuSetting: ImageView? = null + + // 发送弹幕样式按钮 + private var ivSendDanmakuFont: ImageView? = null + + private var vgDanmakuController: ViewGroup? = null + + // 自定义弹幕链接 + private var tvInputCustomDanmakuUrl: TextView? = null - private var mDanmuController: ViewGroup? = null + // 弹幕进度-2s + private var tvRewindDanmakuProgress: TextView? = null + // 弹幕进度恢复正常 + private var tvResetDanmakuProgress: TextView? = null + + // 弹幕进度+2s + private var tvForwardDanmakuProgress: TextView? = null + + // 弹幕进度delta + private var mDanmakuProgressDelta: Long = 0L + + @Suppress("unused") constructor(context: Context, fullFlag: Boolean?) : super(context, fullFlag) constructor(context: Context) : super(context) @@ -91,41 +77,33 @@ open class DanmakuVideoPlayer : AnimeVideoPlayer { override fun init(context: Context?) { super.init(context) - danmakuView = findViewById(R.id.danmaku_view) - mShowDanmakuImageView = findViewById(R.id.iv_show_danmu) - mDanmakuInputEditText = findViewById(R.id.et_input_danmu) - mDanmuController = findViewById(R.id.cl_danmu_controller) - mDanmakuInputEditText?.gone() - mShowDanmakuImageView?.gone() - // 设置高度是0 - hideBottomDanmuController() - - mDanmakuInputEditText?.setOnEditorActionListener(object : TextView.OnEditorActionListener { - override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean { - if (actionId == EditorInfo.IME_ACTION_SEND) { - val text = v.text.toString() - if (text.isBlank()) { - mContext.resources.getString(R.string.please_input_danmu_text).showToast() - return false - } - mDanmakuInputEditText?.setText("") - SendDanmuBean( - "DIYgod", "rgb(255, 255, 255)", - mDanmuParamMap[VIDEO_ID] ?: "", - mDanmuParamMap[REFEREER_URL] ?: "", - "27.5px", text, currentPlayer.currentPositionWhenPlaying / 1000.0, - "right" - ).let { - sendDanmaku(mDanmuParamMap[AC] ?: "", mDanmuParamMap[KEY] ?: "", it) - } - return true - + danmakuManager = DanmakuManager(findViewById(R.id.danmaku_view)) + ivShowDanmaku = findViewById(R.id.iv_show_danmaku) + ivDanmakuSetting = findViewById(R.id.iv_danmaku_setting) + ivSendDanmakuFont = findViewById(R.id.iv_send_danmaku_font) + etDanmakuInput = findViewById(R.id.et_input_danmaku) + vgDanmakuController = findViewById(R.id.vg_danmaku_controller) + tvInputCustomDanmakuUrl = findViewById(R.id.tv_input_custom_danmaku_url) + tvRewindDanmakuProgress = findViewById(R.id.tv_player_rewind_danmaku_progress) + tvResetDanmakuProgress = findViewById(R.id.tv_player_reset_danmaku_progress) + tvForwardDanmakuProgress = findViewById(R.id.tv_player_forward_danmaku_progress) + vgDanmakuController?.gone() + + etDanmakuInput?.setOnEditorActionListener { v, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEND) { + val text = v.text.toString() + if (text.isBlank()) { + mContext.getString(R.string.please_input_danmaku_text).showToast() + return@setOnEditorActionListener false } - return true + v.text = "" + v.hideKeyboard() + sendDanmaku(text) } - }) + true + } - mDanmakuInputEditText?.setOnFocusChangeListener { v, hasFocus -> + etDanmakuInput?.setOnFocusChangeListener { v, hasFocus -> if (mIfCurrentIsFullscreen) { if (hasFocus) cancelDismissControlViewTimer() else { @@ -137,62 +115,161 @@ open class DanmakuVideoPlayer : AnimeVideoPlayer { } } - mDanmakuUrl = "" - initDanmaku() + ivSendDanmakuFont?.setOnClickListener { + val fragmentActivity = + mContext.activity as? FragmentActivity ?: return@setOnClickListener + SendDanmakuFontDialogFragment( + danmakuMode = DanmakuManager.sendDanmakuMode, + danmakuColor = DanmakuManager.sendDanmakuColor + ) { danmakuMode, danmakuColor -> + DanmakuManager.sendDanmakuMode = danmakuMode + DanmakuManager.sendDanmakuColor = danmakuColor + }.show(fragmentActivity.supportFragmentManager, SendDanmakuFontDialogFragment.TAG) + } - mShowDanmakuImageView?.setOnClickListener { + ivShowDanmaku?.setOnClickListener { startDismissControlViewTimer() - mDanmakuShow = !mDanmakuShow + DanmakuManager.showDanmaku = !DanmakuManager.showDanmaku resolveDanmakuShow() } + + ivDanmakuSetting?.setOnClickListener { + val fragmentActivity = + mContext.activity as? FragmentActivity ?: return@setOnClickListener + DanmakuSettingDialogFragment( + filter = DanmakuManager.showDanmakuType, + allowOverlap = DanmakuManager.allowOverlap, + danmakuAlpha = DanmakuManager.alpha, + danmakuScale = DanmakuManager.danmakuScale, + danmakuBold = DanmakuManager.danmakuBold, + onDanmakuFilterChanged = { danmakuManager.switchTypeFilter(it) }, + onAllowOverlapChanged = { danmakuManager.allowOverlap(it) }, + onDanmakuAlphaChanged = { danmakuManager.danmakuAlpha(it) }, + onDanmakuScaleChanged = { danmakuManager.danmakuScale(it) }, + onDanmakuBoldChanged = { danmakuManager.danmakuBold(it) } + ).show(fragmentActivity.supportFragmentManager, DanmakuSettingDialogFragment.TAG) + } + + tvInputCustomDanmakuUrl?.setOnClickListener { + (mContext as? Activity)?.showInputDialog( + hint = mContext.getString(R.string.input_danmaku_url) + ) { _, _, text -> + try { + val url = URL(text.toString()).toString() + if (url.contains("bili", true)) { + DanmakuManager.enableDanmaku = true + setDanmakuUrl(url, DanmakuType.BilibiliType()) + } + } catch (e: Exception) { + mContext.getString(R.string.website_format_error).showToast() + e.printStackTrace() + } + } + vgSettingContainer?.invisible() + } + + tvRewindDanmakuProgress?.setOnClickListener { + if (mDanmakuProgressDelta < -60000L) { + mContext.getString(R.string.cannot_rewind_over_10s).showToast() + return@setOnClickListener + } + mDanmakuProgressDelta -= 2000L + seekDanmaku(currentPlayer.currentPositionWhenPlaying) + } + + tvForwardDanmakuProgress?.setOnClickListener { + if (mDanmakuProgressDelta > 60000L) { + mContext.getString(R.string.cannot_forward_over_10s).showToast() + return@setOnClickListener + } + mDanmakuProgressDelta += 2000L + seekDanmaku(currentPlayer.currentPositionWhenPlaying) + } + + tvResetDanmakuProgress?.setOnClickListener { + mDanmakuProgressDelta = 0L + seekDanmaku(currentPlayer.currentPositionWhenPlaying) + } + } + + override fun onCompletion() { + super.onCompletion() + stopDanmaku() + } + + override fun onAutoCompletion() { + super.onAutoCompletion() + stopDanmaku() } +// override fun onBufferingUpdate(percent: Int) { +// super.onBufferingUpdate(percent) +// pauseDanmaku() +// } + override fun onPrepared() { super.onPrepared() - onPrepareDanmaku(this) + if (DanmakuManager.enableDanmaku) { + setDanmakuUrl() + seekDanmaku(0L) +// playDanmaku() + } } override fun onVideoPause() { super.onVideoPause() - danmakuOnPause() + pauseDanmaku() } override fun onVideoResume(seek: Boolean) { super.onVideoResume(seek) - danmakuOnResume() + playDanmaku() } - override fun clickStartIcon() { - super.clickStartIcon() - if (mCurrentState == GSYVideoView.CURRENT_STATE_PLAYING) { - danmakuOnResume() - } else if (mCurrentState == GSYVideoView.CURRENT_STATE_PAUSE) { - danmakuOnPause() - } + override fun onSeekComplete() { + super.onSeekComplete() + // 虽然此方法叫做onSeekComplete,但是这时候多半还没有缓冲完(为GSYPlayer库设计缺陷) + // 因此不能开始弹幕播放,要传入pauseDanmaku = true,强行暂停播放 + seekDanmaku(gsyVideoManager.currentPosition, true) } - override fun onCompletion() { - releaseDanmaku(this) + override fun changeUiToPlayingShow() { + super.changeUiToPlayingShow() + // 弥补上述onSeekComplete方法内的缺陷 + playDanmaku() } - override fun onSeekComplete() { - super.onSeekComplete() - val time = mProgressBar.progress / 100.0 * duration - // 如果已经初始化过的,直接seek到对于位置 -// Log.e("---", "$time ${mProgressBar.progress} $duration") -// Log.e("---", "$mCurrentPosition ${mProgressBar.progress} $duration") - if (mHadPlay && danmakuView.isPrepared) { - resolveDanmakuSeek(this, time.toLong()) - needCorrectSeekPosition = true - } else if (mHadPlay && !danmakuView.isPrepared) { - // 如果没有初始化过的,记录位置等待 - danmakuStartSeekPosition = time.toLong() - } + override fun changeUiToPauseShow() { + super.changeUiToPauseShow() + pauseDanmaku() + } + + override fun changeUiToPlayingBufferingShow() { + super.changeUiToPlayingBufferingShow() + pauseDanmaku() + } + + override fun changeUiToCompleteShow() { + super.changeUiToCompleteShow() + stopDanmaku() + } + + override fun changeUiToPreparingShow() { + super.changeUiToPreparingShow() + pauseDanmaku() } - override fun cloneParams(from: GSYBaseVideoPlayer, to: GSYBaseVideoPlayer) { - (to as DanmakuVideoPlayer).mDanmakuUrl = (from as DanmakuVideoPlayer).mDanmakuUrl - super.cloneParams(from, to) + override fun changeUiToError() { + super.changeUiToError() + pauseDanmaku() + } + + /** + * 视频播放速度改变后回调 + */ + override fun onSpeedChanged(speed: Float) { + super.onSpeedChanged(speed) + danmakuManager.danmakuPlayer.updatePlaySpeed(speed) } /** @@ -206,15 +283,15 @@ open class DanmakuVideoPlayer : AnimeVideoPlayer { ): GSYBaseVideoPlayer { val player = super.startWindowFullscreen(context, actionBar, statusBar) as DanmakuVideoPlayer - // 设置弹幕信息Map - player.mDanmuParamMap.clear() - player.mDanmuParamMap.putAll(mDanmuParamMap) - // 对弹幕设置偏移记录 - player.danmakuStartSeekPosition = currentPositionWhenPlaying.toLong() - player.mDanmakuShow = mDanmakuShow - player.mShowDanmakuImageView?.visibility = mShowDanmakuImageView?.visibility ?: View.GONE - player.mDanmakuInputEditText?.visibility = mDanmakuInputEditText?.visibility ?: View.GONE - onPrepareDanmaku(player) + player.vgDanmakuController?.visibility = vgDanmakuController?.visibility ?: View.GONE + + // 重建一个DanmakuPlayer,以便清除上次播放的弹幕 + player.danmakuManager.recreatePlayer() + player.resolveDanmakuShow() + player.updatePlayerDanmakuState() + player.seekDanmaku(currentPositionWhenPlaying) + pauseDanmaku() + return player } @@ -230,83 +307,88 @@ open class DanmakuVideoPlayer : AnimeVideoPlayer { super.resolveNormalVideoShow(oldF, vp, gsyVideoPlayer) gsyVideoPlayer?.let { val player = it as DanmakuVideoPlayer - // 设置弹幕信息Map - mDanmuParamMap.clear() - mDanmuParamMap.putAll(player.mDanmuParamMap) - mDanmakuShow = player.mDanmakuShow - mShowDanmakuImageView?.visibility = player.mShowDanmakuImageView?.visibility ?: View.GONE - mDanmakuInputEditText?.visibility = player.mDanmakuInputEditText?.visibility ?: View.GONE - if (player.mDanmakuInputEditText?.visibility == View.VISIBLE) showBottomDanmakuController() - else hideBottomDanmuController() - - if (player.danmakuView.isPrepared) { - resolveDanmakuSeek(this, player.currentPositionWhenPlaying.toLong()) - resolveDanmakuShow() - releaseDanmaku(player) - } - } - } + vgDanmakuController?.visibility = player.vgDanmakuController?.visibility ?: View.GONE - protected fun danmakuOnPause() { - if (danmakuView.isPrepared) { - danmakuView.pause() + // 重建一个DanmakuPlayer,以便清除上次播放的弹幕 + danmakuManager.recreatePlayer() + resolveDanmakuShow() + updatePlayerDanmakuState() + seekDanmaku(player.currentPositionWhenPlaying) + player.pauseDanmaku() } } - protected fun danmakuOnResume() { - if (danmakuView.isPrepared && danmakuView.isPaused) { - danmakuView.resume() - } + fun getDanmakuControllerHeight(): Int { + vgDanmakuController?.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ) + return vgDanmakuController?.height ?: 0 } - fun setDanmaKuUrl(url: String, paramMap: HashMap? = null) { - if (paramMap != null) { - mDanmuParamMap.clear() - mDanmuParamMap.putAll(paramMap) - } - mDanmakuUrl = url - if (!danmakuView.isPrepared) { - onPrepareDanmaku(currentPlayer as DanmakuVideoPlayer) - } + /** + * 将old状态赋值给new + */ + fun updatePlayerDanmakuState() { + if (!DanmakuManager.enableDanmaku) return + danmakuManager.danmakuPlayer.updateData(DanmakuManager.danmakuDataList) + danmakuManager.playDanmaku() + onDanmakuStart() } - private fun initDanmaku() { - // 设置最大显示行数 - val maxLinesPair = HashMap() - maxLinesPair[BaseDanmaku.TYPE_SCROLL_RL] = 6 // 滚动弹幕最大显示6行 - // 设置是否禁止重叠 - val overlappingEnablePair = HashMap() - overlappingEnablePair[BaseDanmaku.TYPE_SCROLL_RL] = true - overlappingEnablePair[BaseDanmaku.TYPE_FIX_TOP] = true - val danmakuAdapter = DanmakuAdapter(danmakuView) - danmakuContext = DanmakuContext.create() - danmakuContext?.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3f) - ?.setDuplicateMergingEnabled(false)?.setScrollSpeedFactor(1.2f)?.setScaleTextSize(1.2f) - ?.setCacheStuffer(SpannedCacheStuffer(), danmakuAdapter) // 图文混排使用SpannedCacheStuffer - ?.setMaximumLines(maxLinesPair) - ?.preventOverlapping(overlappingEnablePair) - //todo 这是为了demo效果,实际上需要去掉这个,外部传输文件进来 - danmakuView.setCallback(object : DrawHandler.Callback { - override fun updateTimer(timer: DanmakuTimer) { - if (abs(currentPlayer.speed - 1f) >= 0.001f) timer.update( - currentPlayer.currentPositionWhenPlaying.toLong() - )/* else if (needCorrectSeekPosition) { - currentPlayer.currentPositionWhenPlaying.toLong() - needCorrectSeekPosition = false - }*/ - } - - override fun drawingFinished() {} - override fun danmakuShown(danmaku: BaseDanmaku) {} - override fun prepared() { - if (danmakuStartSeekPosition != -1L) { - resolveDanmakuSeek(this@DanmakuVideoPlayer, danmakuStartSeekPosition) - danmakuStartSeekPosition = -1 + fun setDanmakuUrl( + url: String = ANIME_DANMAKU_URL, + danmakuType: DanmakuType<*> = DanmakuType.AnimeType(), + autoPlayIfVideoIsPlaying: Boolean = true // 调用此方法后若视频在播放,则自动播放弹幕 + ) { + if (url.isEmpty()) return + + DanmakuManager.danmakuDataList.clear() + // 重建一个DanmakuPlayer,以便清除上次播放的弹幕 + (currentPlayer as DanmakuVideoPlayer).danmakuManager.recreatePlayer() + + coroutineScope.launch(Dispatchers.IO) { + runCatching { + var success = false + val dataList: MutableList = arrayListOf() + DanmakuManager.danmakuType = danmakuType + + when (danmakuType) { + is DanmakuType.AnimeType -> { + danmakuType.repository = AnimeDanmakuRepository(animeTitle, mTitle).apply { + dataList += parse() + } + success = true + } + is DanmakuType.BilibiliType -> { + danmakuType.repository = BilibiliDanmakuRepository(url).apply { + dataList += parse() + } + success = true + } } - resolveDanmakuShow() + DanmakuManager.danmakuUrl = url + if (success) { + withContext(Dispatchers.Main) { + (currentPlayer as DanmakuVideoPlayer).also { + it.danmakuManager.danmakuPlayer.updateData(dataList) + DanmakuManager.danmakuDataList.clear() + DanmakuManager.danmakuDataList += dataList + it.updatePlayerDanmakuState() + if (autoPlayIfVideoIsPlaying && + it.mCurrentState == GSYVideoView.CURRENT_STATE_PLAYING + ) { + it.playDanmaku() + } + it.seekDanmaku(currentPlayer.currentPositionWhenPlaying) + } + } + } + }.onFailure { + it.printStackTrace() + it.message?.showToast() } - }) - danmakuView.enableDanmakuDrawingCache(true) + } } /** @@ -314,176 +396,126 @@ open class DanmakuVideoPlayer : AnimeVideoPlayer { */ private fun resolveDanmakuShow() { post { - if (mDanmakuShow) { - if (!danmakuView.isShown) danmakuView.show() - mShowDanmakuImageView?.isSelected = true - } else { - if (danmakuView.isShown) danmakuView.hide() - mShowDanmakuImageView?.isSelected = false - } + danmakuManager.setDanmakuVisibility(DanmakuManager.showDanmaku) + ivShowDanmaku?.isSelected = DanmakuManager.showDanmaku } } /** * 开始播放弹幕 */ - private fun onPrepareDanmaku(gsyVideoPlayer: DanmakuVideoPlayer) { - if (mDanmakuUrl.isBlank()) return - mDanmakuInputEditText?.visible() - mShowDanmakuImageView?.visible() - showBottomDanmakuController() - // 使用的弹幕url进行显示,要网络请求,因此在io线程执行(与danmakuView.prepare需要顺序执行) - GlobalScope.launch(Dispatchers.IO) { - gsyVideoPlayer.mParser = createParser(gsyVideoPlayer.mDanmakuUrl) - gsyVideoPlayer.danmakuView.let { danmakuView -> - if (!danmakuView.isPrepared && gsyVideoPlayer.mParser != null) { - danmakuView.prepare(gsyVideoPlayer.mParser, gsyVideoPlayer.danmakuContext) - } - } + private fun onDanmakuStart() { + vgDanmakuController?.visible() + tvRewindDanmakuProgress?.visible() + tvResetDanmakuProgress?.visible() + tvForwardDanmakuProgress?.visible() + if (DanmakuManager.danmakuType is DanmakuType.AnimeType) { + etDanmakuInput?.enable() + etDanmakuInput?.hint = mContext.getString(R.string.send_a_danmaku) + } else { + etDanmakuInput?.disable() + etDanmakuInput?.hint = mContext.getString(R.string.send_a_danmaku_is_disabled) + } + mVideoAllCallBack.let { + if (it is MyVideoAllCallBack) it.onDanmakuStart() } - - // 若不加下面的if,则切换横竖屏后不管是否暂停,弹幕都会自动播放 - if (gsyVideoPlayer.currentState == CURRENT_STATE_PLAYING) gsyVideoPlayer.danmakuView.resume() - else gsyVideoPlayer.danmakuView.pause() } /** - * 弹幕偏移 + * 播放弹幕,要保证只在次方法内调用mDanmakuPlayer.start(config) */ - private fun resolveDanmakuSeek(gsyVideoPlayer: DanmakuVideoPlayer, time: Long) { - gsyVideoPlayer.danmakuView.let { danmakuView -> - if (mHadPlay && danmakuView.isPrepared) danmakuView.seekTo(time) - } + protected open fun playDanmaku() { + if (DanmakuManager.danmakuUrl.isBlank() || !DanmakuManager.enableDanmaku) return + // 若不加下面的if,则切换横竖屏后不管是否暂停,弹幕都会自动播放 + if (currentPlayer.isInPlayingState) danmakuManager.playDanmaku() + onDanmakuStart() } /** - * 创建解析器对象,解析弹幕url - * - * @param url - * @return + * 发送弹幕 */ - private fun createParser(url: String): BaseDanmakuParser { - if (url.isBlank()) { - return object : BaseDanmakuParser() { - override fun parse(): Danmakus { - return Danmakus() + protected open fun sendDanmaku( + content: String, + time: Long = gsyVideoManager.currentPosition + ) { + coroutineScope.launch { + runCatching { + when (val danmakuType = DanmakuManager.danmakuType) { + is DanmakuType.AnimeType -> { + danmakuType.repository?.send( + content = content, + time = time, + mode = DanmakuManager.sendDanmakuMode, + color = DanmakuManager.sendDanmakuColor + )?.let { + val data = it.toDanmakuItemData(DANMAKU_STYLE_ICON_UP) + withContext(Dispatchers.Main) { + danmakuManager.danmakuPlayer.send(data) + } + } + } + else -> { + context.getString(R.string.danmaku_video_player_unsupport_send_danmaku) + .showToast() + } } + }.onFailure { + it.printStackTrace() + it.message?.showToast() } } - val loader = create(TAG_ANIME) - try { - if (loader is AnimeDanmakuLoader) loader.load(URL(url)) - else loader?.load(url) - } catch (e: IllegalDataException) { - e.printStackTrace() - } - return AnimeDanmakuParser().apply { - load(loader?.dataSource) - } } /** - * 释放弹幕控件 + * 暂停弹幕 */ - private fun releaseDanmaku(danmakuVideoPlayer: DanmakuVideoPlayer?) { - danmakuVideoPlayer?.danmakuView?.let { danmakuView -> - Debuger.printfError("release Danmaku!") - danmakuView.release() - } + protected open fun pauseDanmaku() { + danmakuManager.danmakuPlayer.pause() } /** - * 显示非全屏模式下播放器下方弹幕控制部分 + * 停止弹幕 */ - private fun showBottomDanmakuController() { - mDanmuController?.let { danmuController -> - if (danmuController.layoutParams.height == 0) { - danmuController.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT - danmuController.requestLayout() - post { - ValueAnimator.ofInt(height, height + danmuController.height).setDuration(500) - .apply { - addUpdateListener { animation -> - layoutParams.height = animation.animatedValue as Int - requestLayout() - } - start() - } - } - } - } + protected open fun stopDanmaku() { + danmakuManager.danmakuPlayer.stop() + // 加此句是因为stop后会seek 0,又会播放 + danmakuManager.danmakuPlayer.pause() } /** - * 隐藏非全屏模式下播放器下方弹幕控制部分 + * 弹幕偏移 + * @param pauseDanmaku 传入true则不管当前状态都会停止播放弹幕 */ - private fun hideBottomDanmuController() { - mDanmuController?.let { danmuController -> - if (danmuController.layoutParams.height != 0) { - if (danmuController.height > 0) { - post { - ValueAnimator.ofInt(height, height - danmuController.height) - .setDuration(500) - .apply { - addUpdateListener { animation -> - layoutParams.height = animation.animatedValue as Int - requestLayout() - } - start() - } - } - } - danmuController.layoutParams.height = 0 - danmuController.requestLayout() - } + private fun seekDanmaku(time: Long, pauseDanmaku: Boolean = false) { + // 若mDanmakuProgressDelta<0即左移了,并且总进度也小于0,则重置mDanmakuProgressDelta + if (time + mDanmakuProgressDelta < 0L) { + mDanmakuProgressDelta = 0L - time } + danmakuManager.danmakuPlayer.seekTo(time + mDanmakuProgressDelta) + // 由于上面一条语句会导致弹幕开始播放,因此要判断是否暂停 + if (pauseDanmaku || mCurrentState == GSYVideoView.CURRENT_STATE_PAUSE || + mCurrentState == GSYVideoView.CURRENT_STATE_PLAYING_BUFFERING_START + ) + pauseDanmaku() + if (mCurrentState == GSYVideoView.CURRENT_STATE_AUTO_COMPLETE || + mCurrentState == GSYVideoView.CURRENT_STATE_ERROR || + mCurrentState == GSYVideoView.CURRENT_STATE_NORMAL + ) + stopDanmaku() } /** - * 发送弹幕 - * @param isLive 是否是直播弹幕 + * 释放弹幕控件 */ - private fun sendDanmaku( - ac: String, - key: String, - sendDanmuBean: SendDanmuBean, - isLive: Boolean = false - ) { - val danmaku = danmakuContext?.mDanmakuFactory?.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL) - danmaku ?: return - sendDanmuBean.text.apply { - // 检测是否有被屏蔽字符,若有则不进行发送 - if (shield()) { - mContext.getString(R.string.danmu_exist_shield_content).showToast(Toast.LENGTH_LONG) - return - } - danmaku.text = this - } - danmaku.isLive = isLive - danmaku.time = (sendDanmuBean.time * 1000).toLong() - danmaku.textSize = 0.7f * sendDanmuBean.size.replace("px", "").toFloat() * - (mParser?.displayer?.density ?: 1 - 0.6f) - val color = AnimeDanmakuParser.getColor(sendDanmuBean.color) - danmaku.textColor = color - danmaku.textShadowColor = if (color <= Color.BLACK) Color.WHITE else Color.BLACK - danmaku.underlineColor = Color.GREEN - danmakuView.addDanmaku(danmaku) - - val request = RetrofitManager.instance.create(DanmuService::class.java) ?: return - val json = Gson().toJson(sendDanmuBean) - .toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) - request.sendDanmu(ac, key, json).enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - mContext.getString(R.string.send_danmu_failed, t.message).showToast() - t.printStackTrace() - } + private fun releaseDanmaku() { + danmakuManager.danmakuPlayer.release() + } - override fun onResponse( - call: Call, - response: Response - ) { - response.body()?.message?.showToast() - } - }) + /** + * 调用此方法释放整个视频播放器 + */ + override fun release() { + super.release() + releaseDanmaku() } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/DetailPlayerActivity.java b/app/src/main/java/com/skyd/imomoe/view/component/player/DetailPlayerActivity.java deleted file mode 100644 index f2a9def0..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/DetailPlayerActivity.java +++ /dev/null @@ -1,319 +0,0 @@ -package com.skyd.imomoe.view.component.player; - -import android.content.res.Configuration; -import android.os.Bundle; -import android.view.View; -import com.skyd.skin.core.SkinBaseActivity; -import com.shuyu.gsyvideoplayer.GSYVideoManager; -import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder; -import com.shuyu.gsyvideoplayer.utils.OrientationOption; -import com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer; - -import org.jetbrains.annotations.NotNull; - -import static com.shuyu.gsyvideoplayer.video.base.GSYVideoView.CURRENT_STATE_PAUSE; - -/** - * 详情模式播放页面基础类 - */ -public abstract class DetailPlayerActivity extends SkinBaseActivity implements MyVideoAllCallBack { - - protected boolean isPlay; - - // 是否是在onPause方法里自动暂停的 - protected boolean isPause; - - protected AnimeOrientationUtils orientationUtils; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - /** - * 选择普通模式 - */ - public void initVideo() { - //外部辅助的旋转,帮助全屏 - orientationUtils = new AnimeOrientationUtils(this, getGSYVideoPlayer(), getOrientationOption()); - //初始化不打开外部的旋转 - orientationUtils.setEnable(false); - if (getGSYVideoPlayer().getFullscreenButton() != null) { - getGSYVideoPlayer().getFullscreenButton().setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showFull(); - clickForFullScreen(); - } - }); - } - // 退出全屏监听,避免平板退出全屏后变成竖屏 - getGSYVideoPlayer().setBackFromFullScreenListener(view -> { - onBackPressed(); - }); - } - - /** - * 选择builder模式 - */ - public void initVideoBuilderMode() { - initVideo(); - getGSYVideoOptionBuilder(). - setVideoAllCallBack(this) - .build(getGSYVideoPlayer()); - } - - public void showFull() { - if (orientationUtils.getIsLand() != 1) { - //直接横屏 - orientationUtils.resolveByClick(); - } - //第一个true是否需要隐藏actionbar,第二个true是否需要隐藏statusBar - getGSYVideoPlayer().startWindowFullscreen(DetailPlayerActivity.this, hideActionBarWhenFull(), hideStatusBarWhenFull()); - - } - - @Override - public void onBackPressed() { - if (orientationUtils != null) { - orientationUtils.backToProtVideo2(); - } - if (GSYVideoManager.backFromWindowFull(this)) { - return; - } - super.onBackPressed(); - } - - - @Override - protected void onPause() { - super.onPause(); - if (getGSYVideoPlayer().getCurrentPlayer().getCurrentState() != CURRENT_STATE_PAUSE) { - getGSYVideoPlayer().getCurrentPlayer().onVideoPause(); - if (orientationUtils != null) { - orientationUtils.setIsPause(true); - } - isPause = true; - } - } - - @Override - protected void onResume() { - super.onResume(); - if (isPause) { - getGSYVideoPlayer().getCurrentPlayer().onVideoResume(); - if (orientationUtils != null) { - orientationUtils.setIsPause(false); - } - isPause = false; - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (isPlay) { - getGSYVideoPlayer().getCurrentPlayer().release(); - } - if (orientationUtils != null) - orientationUtils.releaseListener(); - } - - /** - * orientationUtils 和 detailPlayer.onConfigurationChanged 方法是用于触发屏幕旋转的 - */ - @Override - public void onConfigurationChanged(@NotNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - //如果旋转了就全屏 - if (isPlay && !isPause) { - getGSYVideoPlayer().onConfigurationChanged(this, newConfig, orientationUtils, hideActionBarWhenFull(), hideStatusBarWhenFull()); - } - } - - @Override - public void onStartPrepared(String url, Object... objects) { - videoPlayStatusChanged(true); - } - - @Override - public void onPrepared(String url, Object... objects) { - - if (orientationUtils == null) { - throw new NullPointerException("initVideo() or initVideoBuilderMode() first"); - } - //开始播放了才能旋转和全屏 - orientationUtils.setEnable(getDetailOrientationRotateAuto() && !isAutoFullWithSize()); - isPlay = true; - isPause = false; - videoPlayStatusChanged(true); - } - - @Override - public void onClickStartIcon(String url, Object... objects) { - - } - - @Override - public void onClickStartError(String url, Object... objects) { - - } - - @Override - public void onClickStop(String url, Object... objects) { - videoPlayStatusChanged(false); - } - - @Override - public void onClickStopFullscreen(String url, Object... objects) { - videoPlayStatusChanged(false); - } - - @Override - public void onClickResume(String url, Object... objects) { - videoPlayStatusChanged(true); - } - - @Override - public void onClickResumeFullscreen(String url, Object... objects) { - videoPlayStatusChanged(true); - } - - @Override - public void onClickSeekbar(String url, Object... objects) { - - } - - @Override - public void onClickSeekbarFullscreen(String url, Object... objects) { - - } - - @Override - public void onAutoComplete(String url, Object... objects) { - videoPlayStatusChanged(false); - } - - @Override - public void onEnterFullscreen(String url, Object... objects) { - - } - - @Override - public void onQuitFullscreen(String url, Object... objects) { - if (orientationUtils != null) { - orientationUtils.backToProtVideo(); - } - } - - @Override - public void onQuitSmallWidget(String url, Object... objects) { - - } - - @Override - public void onEnterSmallWidget(String url, Object... objects) { - - } - - @Override - public void onTouchScreenSeekVolume(String url, Object... objects) { - - } - - @Override - public void onTouchScreenSeekPosition(String url, Object... objects) { - - } - - @Override - public void onTouchScreenSeekLight(String url, Object... objects) { - - } - - @Override - public void onPlayError(String url, Object... objects) { - videoPlayStatusChanged(false); - } - - @Override - public void onClickStartThumb(String url, Object... objects) { - - } - - @Override - public void onClickBlank(String url, Object... objects) { - - } - - @Override - public void onClickBlankFullscreen(String url, Object... objects) { - - } - - @Override - public void onComplete(String url, Object... objects) { - videoPlayStatusChanged(false); - } - - public boolean hideActionBarWhenFull() { - return true; - } - - public boolean hideStatusBarWhenFull() { - return true; - } - - /** - * 可配置旋转 OrientationUtils - */ - public OrientationOption getOrientationOption() { - return null; - } - - /** - * 播放控件 - */ - public abstract T getGSYVideoPlayer(); - - /** - * 配置播放器 - */ - public abstract GSYVideoOptionBuilder getGSYVideoOptionBuilder(); - - /** - * 点击了全屏 - */ - public abstract void clickForFullScreen(); - - /** - * 是否启动旋转横屏,true表示启动 - */ - public abstract boolean getDetailOrientationRotateAuto(); - - /** - * 是否根据视频尺寸,自动选择竖屏全屏或者横屏全屏,注意,这时候默认旋转无效 - */ - public boolean isAutoFullWithSize() { - return false; - } - - @Override - public void onVideoPause() { - videoPlayStatusChanged(false); - } - - @Override - public void onVideoResume() { - videoPlayStatusChanged(true); - } - - /** - * 视频播放状态变化 - * - * @param playing false:未在播放(包括播放失败暂停等等);true:正在播放(包括正在准备加载、缓冲等等) - */ - protected void videoPlayStatusChanged(boolean playing) { - - } -} diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/DetailPlayerActivity.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/DetailPlayerActivity.kt new file mode 100644 index 00000000..3d031daa --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/DetailPlayerActivity.kt @@ -0,0 +1,271 @@ +package com.skyd.imomoe.view.component.player + +import android.content.res.Configuration +import androidx.viewbinding.ViewBinding +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder +import com.shuyu.gsyvideoplayer.utils.OrientationOption +import com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer +import com.shuyu.gsyvideoplayer.video.base.GSYVideoView +import com.skyd.imomoe.view.activity.BaseActivity + +/** + * 详情模式播放页面基础类 + */ +abstract class DetailPlayerActivity : BaseActivity(), + MyVideoAllCallBack { + protected open var isPlay = false + + // 是否是在onPause方法里自动暂停的 + protected open var isPause = false + protected open var orientationUtils: AnimeOrientationUtils? = null + + protected open var onPausePosition: Long = -1 + protected open var onPauseState: Int = -1 + + // 是否播放过视频 + protected open var startedPlayVideo: Boolean = false + + // 是否在后台 + protected open var activityInBackground: Boolean = false + + /** + * 选择普通模式 + */ + protected open fun initVideo() { + //外部辅助的旋转,帮助全屏 + orientationUtils = + AnimeOrientationUtils(this, getGSYVideoPlayer(), orientationOption).apply { + // 初始化不打开外部的旋转 + isEnable = false + } + // 锁定后不随屏幕旋转而旋转视频 + getGSYVideoPlayer().setLockClickListener { _, lock -> + orientationUtils?.isEnable = !lock + getGSYVideoPlayer().currentPlayer.isRotateViewAuto = !lock + } + if (getGSYVideoPlayer().fullscreenButton != null) { + getGSYVideoPlayer().fullscreenButton.setOnClickListener { + showFull() + clickForFullScreen() + } + } + // 退出全屏监听,避免平板退出全屏后变成竖屏 + getGSYVideoPlayer().setBackFromFullScreenListener { onBackPressed() } + } + + /** + * 选择builder模式 + */ + fun initVideoBuilderMode() { + initVideo() + gsyVideoOptionBuilder.setVideoAllCallBack(this).build(getGSYVideoPlayer()) + } + + protected open fun showFull() { + if (orientationUtils?.isLand != 1) { + //直接横屏 + orientationUtils?.resolveByClick() + } + //第一个true是否需要隐藏actionbar,第二个true是否需要隐藏statusBar + getGSYVideoPlayer().startWindowFullscreen( + this@DetailPlayerActivity, + hideActionBarWhenFull(), + hideStatusBarWhenFull() + ) + } + + override fun onBackPressed() { + orientationUtils?.backToProtVideo2() + if (GSYVideoManager.backFromWindowFull(this)) { + return + } + super.onBackPressed() + } + + override fun onPause() { + super.onPause() + val player = getGSYVideoPlayer().currentPlayer + + activityInBackground = true + + onPauseState = player.currentState + onPausePosition = player.currentPositionWhenPlaying + + if (player is AnimeVideoPlayer) { + player.storeStateCurrentState = player.currentState + } + + if (player.currentState != GSYVideoView.CURRENT_STATE_PAUSE) { + player.onVideoPause() + orientationUtils?.setIsPause(true) + isPause = true + } + } + + override fun onResume() { + super.onResume() + activityInBackground = false + + getGSYVideoPlayer().currentPlayer.apply { + if (this is AnimeVideoPlayer) { + storeStateCurrentState = null + } + if (currentState == GSYVideoView.CURRENT_STATE_NORMAL && + onPausePosition != -1L && + onPauseState != -1 && + startedPlayVideo + ) { + seekOnStart = onPausePosition + startPlayLogic() + isPause = false + if (onPauseState == GSYVideoView.CURRENT_STATE_PAUSE) { + onVideoPause() + isPause = true + } + } else { + if (isPause) { + getGSYVideoPlayer().currentPlayer.onVideoResume() + orientationUtils?.setIsPause(false) + isPause = false + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + if (isPlay) { + getGSYVideoPlayer().currentPlayer.release() + } + orientationUtils?.releaseListener() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + //如果旋转了就全屏 + if (isPlay && !isPause) { + getGSYVideoPlayer().onConfigurationChanged( + this, + newConfig, + orientationUtils, + hideActionBarWhenFull(), + hideStatusBarWhenFull() + ) + } + } + + override fun onStartPrepared(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(true) + } + + override fun onPrepared(url: String?, vararg objects: Any?) { + orientationUtils.let { + if (it == null) { + throw NullPointerException("initVideo() or initVideoBuilderMode() first") + } + // 开始播放了才能旋转和全屏 + it.isEnable = detailOrientationRotateAuto && !isAutoFullWithSize + startedPlayVideo = true + isPlay = true + isPause = false + videoPlayStatusChanged(true) + } + } + + override fun onClickStartIcon(url: String?, vararg objects: Any?) {} + override fun onClickStartError(url: String?, vararg objects: Any?) {} + override fun onClickStop(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(false) + } + + override fun onClickStopFullscreen(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(false) + } + + override fun onClickResume(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(true) + } + + override fun onClickResumeFullscreen(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(true) + } + + override fun onClickSeekbar(url: String?, vararg objects: Any?) {} + override fun onClickSeekbarFullscreen(url: String?, vararg objects: Any?) {} + override fun onAutoComplete(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(false) + } + + override fun onEnterFullscreen(url: String?, vararg objects: Any?) {} + override fun onQuitFullscreen(url: String?, vararg objects: Any?) { + orientationUtils?.backToProtVideo() + } + + override fun onQuitSmallWidget(url: String?, vararg objects: Any?) {} + override fun onEnterSmallWidget(url: String?, vararg objects: Any?) {} + override fun onTouchScreenSeekVolume(url: String?, vararg objects: Any?) {} + override fun onTouchScreenSeekPosition(url: String?, vararg objects: Any?) {} + override fun onTouchScreenSeekLight(url: String?, vararg objects: Any?) {} + override fun onPlayError(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(false) + } + + override fun onClickStartThumb(url: String?, vararg objects: Any?) {} + override fun onClickBlank(url: String?, vararg objects: Any?) {} + override fun onClickBlankFullscreen(url: String?, vararg objects: Any?) {} + override fun onComplete(url: String?, vararg objects: Any?) { + videoPlayStatusChanged(false) + } + + protected open fun hideActionBarWhenFull(): Boolean = true + + protected open fun hideStatusBarWhenFull(): Boolean = true + + /** + * 可配置旋转 OrientationUtils + */ + protected open val orientationOption: OrientationOption? + get() = null + + /** + * 播放控件 + */ + abstract fun getGSYVideoPlayer(): T + + /** + * 配置播放器 + */ + abstract val gsyVideoOptionBuilder: GSYVideoOptionBuilder + + /** + * 点击了全屏 + */ + abstract fun clickForFullScreen() + + /** + * 是否启动旋转横屏,true表示启动 + */ + abstract val detailOrientationRotateAuto: Boolean + + /** + * 是否根据视频尺寸,自动选择竖屏全屏或者横屏全屏,注意,这时候默认旋转无效 + */ + protected open val isAutoFullWithSize: Boolean + get() = false + + override fun onVideoPause() { + videoPlayStatusChanged(false) + } + + override fun onVideoResume() { + videoPlayStatusChanged(true) + } + + /** + * 视频播放状态变化 + * + * @param playing false:未在播放(包括播放失败暂停等等);true:正在播放(包括正在准备加载、缓冲等等) + */ + protected open fun videoPlayStatusChanged(playing: Boolean) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/MyVideoAllCallBack.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/MyVideoAllCallBack.kt index affc0fd5..581c6e80 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/player/MyVideoAllCallBack.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/MyVideoAllCallBack.kt @@ -8,4 +8,6 @@ interface MyVideoAllCallBack : VideoAllCallBack { fun onVideoResume() fun onVideoSizeChanged() + + fun onDanmakuStart() } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/PlayerCore.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/PlayerCore.kt new file mode 100644 index 00000000..153f002c --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/PlayerCore.kt @@ -0,0 +1,104 @@ +package com.skyd.imomoe.view.component.player + +import android.app.Activity +import com.shuyu.gsyvideoplayer.player.IPlayerManager +import com.shuyu.gsyvideoplayer.player.PlayerFactory +import com.shuyu.gsyvideoplayer.player.SystemPlayerManager +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.skyd.imomoe.R +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.sharedPreferences +import com.skyd.imomoe.ext.showListDialog +import com.skyd.imomoe.util.logI +import tv.danmaku.ijk.media.exo2.Exo2PlayerManager + + +object PlayerCore { + class Core(val coreName: String, val playManager: Class) : CharSequence { + override val length: Int + get() = playManager.name.length + + override fun get(index: Int): Char = playManager.name[index] + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return playManager.name.subSequence(startIndex, endIndex) + } + + override fun toString(): String = coreName + + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + is String -> other == playManager.name + is Core -> other.coreName == this.coreName && other.playManager == this.playManager + else -> false + } + } + + override fun hashCode(): Int { + var result = coreName.hashCode() + result = 31 * result + playManager.hashCode() + return result + } + } + + private infix fun String.to(that: Class): Core = Core(this, that) + + val playerCores: List = listOf( + "ExoPlayer内核 (默认)" to Exo2PlayerManager::class.java, + "系统内核" to SystemPlayerManager::class.java + ) + + var playerCore: Core = (playerCores.firstOrNull { + it.equals(sharedPreferences().getString("playerCore", null) + .run { this ?: Exo2PlayerManager::class.java.name }) + } ?: playerCores.first()).also { PlayerFactory.setPlayManager(it.playManager) } + set(value) { + if (value == field) return + sharedPreferences().editor { putString("playerCore", value.playManager.name) } + field = value + PlayerFactory.setPlayManager(value.playManager) + } + + // 保证这个函数内调用一次applyMediaCodec + fun onAppCreate() { + applyMediaCodec() + logI("PlayerCore initialized: ${playerCore.coreName} ${playerCore.playManager.name}") + } + + fun Activity.selectPlayerCore(onPositive: ((Core) -> Unit)? = null) { + var initialSelection = 0 + playerCores.forEachIndexed { index, s -> + if (s == playerCore) initialSelection = index + } + showListDialog( + title = getString(R.string.select_player_core), + items = playerCores, + checkedItem = initialSelection, + ) { _, _, itemIndex -> + playerCore = playerCores[itemIndex] + onPositive?.invoke(playerCores[itemIndex]) + } + } + + /** + * 设置硬解码,只有ijk内核生效 + */ + fun setMediaCodec(enable: Boolean) { + if (GSYVideoType.isMediaCodec() == enable && GSYVideoType.isMediaCodecTexture() == enable) { + return + } + if (enable) { + GSYVideoType.enableMediaCodec() + GSYVideoType.enableMediaCodecTexture() + } else { + GSYVideoType.disableMediaCodec() + GSYVideoType.disableMediaCodecTexture() + } + sharedPreferences().editor { putBoolean("mediaCodec", enable) } + } + + fun applyMediaCodec() { + setMediaCodec(sharedPreferences().getBoolean("mediaCodec", false)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuManager.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuManager.kt new file mode 100644 index 00000000..6304eb7a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuManager.kt @@ -0,0 +1,168 @@ +package com.skyd.imomoe.view.component.player.danmaku + +import android.graphics.Color +import androidx.annotation.IntRange +import com.google.gson.Gson +import com.kuaishou.akdanmaku.DanmakuConfig +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.kuaishou.akdanmaku.ecs.component.filter.* +import com.kuaishou.akdanmaku.render.SimpleRenderer +import com.kuaishou.akdanmaku.ui.DanmakuPlayer +import com.kuaishou.akdanmaku.ui.DanmakuView +import com.skyd.imomoe.ext.editor +import com.skyd.imomoe.ext.sharedPreferences +import com.skyd.imomoe.view.fragment.dialog.DanmakuSettingDialogFragment +import com.skyd.imomoe.view.fragment.dialog.DanmakuSettingDialogFragment.ShowDanmakuType + +class DanmakuManager(val danmakuView: DanmakuView) { + companion object { + var danmakuUrl: String = "" + val colorFilter = TextColorFilter() + var dataFilters = emptyMap() + val danmakuDataList: MutableList = mutableListOf() + + // 是否使用弹幕功能(直接不请求弹幕数据)(播放本地视频时可能禁止弹幕功能) + var enableDanmaku: Boolean = true + + // 是否在显示弹幕(已经启用,并且会请求弹幕数据) + var showDanmaku = true + + // 弹幕源类型 + var danmakuType: DanmakuType<*>? = null + + var showDanmakuType: ShowDanmakuType = Gson().fromJson( + sharedPreferences().getString("showDanmakuType", null), + ShowDanmakuType::class.java + ) ?: ShowDanmakuType() + private set(value) { + sharedPreferences().editor { putString("showDanmakuType", Gson().toJson(value)) } + field = value + } + + var allowOverlap: Boolean = sharedPreferences().getBoolean("allowDanmakuOverlap", true) + private set(value) { + sharedPreferences().editor { putBoolean("allowDanmakuOverlap", value) } + field = value + } + + @IntRange(from = 0, to = 100) + var alpha: Int = sharedPreferences().getInt("danmakuAlpha", 100) + private set(value) { + sharedPreferences().editor { putInt("danmakuAlpha", value) } + field = value + } + + @IntRange(from = DanmakuSettingDialogFragment.MIN_DANMAKU_SCALE.toLong()) + var danmakuScale: Int = sharedPreferences().getInt("danmakuScale", 130) + private set(value) { + sharedPreferences().editor { putInt("danmakuScale", value) } + field = value + } + + var danmakuBold: Boolean = sharedPreferences().getBoolean("danmakuBold", true) + private set(value) { + sharedPreferences().editor { putBoolean("danmakuBold", value) } + field = value + } + + private fun createDataFilters(): List = + listOf( + TypeFilter(), + colorFilter, + UserIdFilter(), + GuestFilter(), + BlockedTextFilter { it == 0L }, + DuplicateMergedFilter() + ) + + private fun createLayoutFilters(): List = emptyList() + + private var config = DanmakuConfig( + allowOverlap = allowOverlap, + alpha = alpha / 100f, + textSizeScale = danmakuScale / 100f, + bold = danmakuBold + ).apply { + dataFilter = createDataFilters() + dataFilters = dataFilter.associateBy { it.filterParams } + layoutFilter = createLayoutFilters() + } + + // 发送弹幕样式 + var sendDanmakuMode: DanmakuMode = DanmakuMode.Scroll + + // 发送弹幕颜色 + var sendDanmakuColor: Int = Color.WHITE + } + + var danmakuPlayer: DanmakuPlayer = DanmakuPlayer(SimpleRenderer()).apply { + bindView(danmakuView) + } + private set + + fun recreatePlayer() { + danmakuPlayer.release() + danmakuPlayer = DanmakuPlayer(SimpleRenderer()).apply { + bindView(danmakuView) + } + } + + fun playDanmaku() { + danmakuPlayer.start(config) + } + + fun danmakuBold(danmakuBold: Boolean) { + DanmakuManager.danmakuBold = danmakuBold + config = config.copy(bold = danmakuBold) + danmakuPlayer.updateConfig(config) + } + + fun danmakuScale( + @IntRange(from = DanmakuSettingDialogFragment.MIN_DANMAKU_SCALE.toLong()) + danmakuScale: Int + ) { + DanmakuManager.danmakuScale = danmakuScale + config = config.copy(textSizeScale = danmakuScale / 100f) + danmakuPlayer.updateConfig(config) + } + + fun danmakuAlpha(@IntRange(from = 0, to = 100) alpha: Int) { + DanmakuManager.alpha = alpha + config = config.copy(alpha = alpha / 100f) + danmakuPlayer.updateConfig(config) + } + + fun allowOverlap(allowOverlap: Boolean) { + DanmakuManager.allowOverlap = allowOverlap + config = config.copy(allowOverlap = allowOverlap) + danmakuPlayer.updateConfig(config) + } + + fun switchTypeFilter(filter: ShowDanmakuType) { + showDanmakuType = filter + (dataFilters[DanmakuFilters.FILTER_TYPE_TYPE] as? TypeFilter)?.let { typeFilter -> + filter.toList().forEach { + if (it.second != -1) { + if (it.first) typeFilter.removeFilterItem(it.second) + else typeFilter.addFilterItem(it.second) + } else { + colorFilter.filterColor.clear() + if (!it.first) { + colorFilter.filterColor.add(0xFFFFFF) + } + } + config.updateFilter() + danmakuPlayer.updateConfig(config) + } + } + } + + fun setDanmakuVisibility(visible: Boolean) { + config = config.copy(visibility = visible) + danmakuPlayer.updateConfig(config) + } + + init { + switchTypeFilter(showDanmakuType) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuMode.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuMode.kt new file mode 100644 index 00000000..9baf233e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuMode.kt @@ -0,0 +1,13 @@ +package com.skyd.imomoe.view.component.player.danmaku + +enum class DanmakuMode { + Scroll, Top, Bottom +} + +fun DanmakuMode.string(): String { + return when (this) { + DanmakuMode.Scroll -> "scroll" + DanmakuMode.Top -> "top" + DanmakuMode.Bottom -> "bottom" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuRepository.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuRepository.kt new file mode 100644 index 00000000..3cfbbe84 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuRepository.kt @@ -0,0 +1,7 @@ +package com.skyd.imomoe.view.component.player.danmaku + +import com.kuaishou.akdanmaku.data.DanmakuItemData + +interface DanmakuRepository { + suspend fun parse(): List +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuType.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuType.kt new file mode 100644 index 00000000..47bcd36e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/DanmakuType.kt @@ -0,0 +1,15 @@ +package com.skyd.imomoe.view.component.player.danmaku + +import com.skyd.imomoe.view.component.player.danmaku.anime.AnimeDanmakuRepository +import com.skyd.imomoe.view.component.player.danmaku.bili.BilibiliDanmakuRepository + +sealed class DanmakuType(var repository: T? = null) { + class AnimeType( + repository: AnimeDanmakuRepository? = null + ) : DanmakuType(repository) + + class BilibiliType( + repository: BilibiliDanmakuRepository? = null + ) : DanmakuType(repository) +} + diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/anime/AnimeDanmakuRepository.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/anime/AnimeDanmakuRepository.kt new file mode 100644 index 00000000..3244c514 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/anime/AnimeDanmakuRepository.kt @@ -0,0 +1,119 @@ +package com.skyd.imomoe.view.component.player.danmaku.anime + +import android.graphics.Color +import android.widget.Toast +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.kuaishou.akdanmaku.data.DanmakuItemData.Companion.DANMAKU_STYLE_NONE +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.danmaku.DanmakuData +import com.skyd.imomoe.ext.shield +import com.skyd.imomoe.net.RetrofitManager +import com.skyd.imomoe.net.service.DanmakuService +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.component.player.danmaku.DanmakuMode +import com.skyd.imomoe.view.component.player.danmaku.DanmakuRepository +import com.skyd.imomoe.view.component.player.danmaku.string +import kotlin.math.roundToLong + +class AnimeDanmakuRepository( + private val animeName: String, + val episode: String, +) : DanmakuRepository { + var data: DanmakuData? = null + + override suspend fun parse(): List { + return RetrofitManager + .get() + .create(DanmakuService::class.java) + .getDanmaku(animeName = animeName, episode = episode) + .also { + if (it.code != 200) { + it.msg.showToast() + } + }.data + .also { this.data = it } + ?.data.orEmpty() + .map { it.toDanmakuItemData() } + } + + suspend fun send( + content: String, + time: Long, // 毫秒时间戳 + episodeId: String = data?.episode?.id.orEmpty(), + mode: DanmakuMode = DanmakuMode.Scroll, + color: Int = Color.WHITE, + ): DanmakuData.Data? { + if (episodeId.isBlank()) { + "invalid episodeId: $episodeId, send failed!".showToast(Toast.LENGTH_LONG) + return null + } + if (content.shield()) { + appContext.getString(R.string.danmaku_exist_shield_content).showToast(Toast.LENGTH_LONG) + return null + } + return RetrofitManager + .get() + .create(DanmakuService::class.java) + .sendDanmaku( + content, + time / 1000.0, + episodeId, + mode.string(), + String.format("#%06X", 0xFFFFFF and color) + ) + .also { + if (it.code != 200) { + it.msg.showToast() + } + } + .data + ?.apply { + data?.data?.add(this) + } + } + + companion object { + fun DanmakuData.Data.toDanmakuItemData(danmakuStyle: Int = DANMAKU_STYLE_NONE): DanmakuItemData { + return DanmakuItemData( + content = this.content, + danmakuId = this.id.hashCode().toLong(), + textColor = parseColor(this.color ?: "#FFFFFF"), + position = (this.time * 1000L).roundToLong(), + textSize = 20, + danmakuStyle = danmakuStyle, + mode = getMode(this.type), + ) + } + + private fun parseColor(s: String): Int { + runCatching { + if (s.startsWith("rgb")) { + val rgb = s.replace("rgb", "") + .replace("(", "") + .replace(")", "") + .split(",") + return Color.rgb( + rgb[0].trim().toInt(), + rgb[1].trim().toInt(), + rgb[2].trim().toInt() + ) + } else if (s.startsWith("#")) { + return Color.parseColor(s) + } + }.onFailure { + it.printStackTrace() + } + return Color.WHITE + } + + private fun getMode(s: String): Int { + return when (s) { + "scroll" -> DanmakuItemData.DANMAKU_MODE_ROLLING + "top" -> DanmakuItemData.DANMAKU_MODE_CENTER_TOP + "bottom" -> DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM + else -> DanmakuItemData.DANMAKU_MODE_ROLLING + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/bili/BilibiliDanmakuRepository.kt b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/bili/BilibiliDanmakuRepository.kt new file mode 100644 index 00000000..5b23bb97 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/player/danmaku/bili/BilibiliDanmakuRepository.kt @@ -0,0 +1,169 @@ +package com.skyd.imomoe.view.component.player.danmaku.bili + +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.skyd.imomoe.ext.string +import com.skyd.imomoe.net.RetrofitManager +import com.skyd.imomoe.net.service.DanmakuService +import com.skyd.imomoe.util.Util.toEncodedUrl +import com.skyd.imomoe.view.component.player.danmaku.DanmakuRepository +import org.xml.sax.Attributes +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler +import org.xml.sax.helpers.XMLReaderFactory +import java.io.IOException +import java.util.* +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream + +class BilibiliDanmakuRepository(val url: String) : DanmakuRepository { + init { + System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver") + } + + override suspend fun parse(): List { + val inputStream = InflaterInputStream( + RetrofitManager.get().create(DanmakuService::class.java) + .getCustomizeDanmaku(url.toEncodedUrl()).byteStream(), + Inflater(true) + ) + val data: String = inputStream.string() + if (data.isBlank()) return ArrayList() + try { + val xmlReader = XMLReaderFactory.createXMLReader() + val contentHandler = XmlContentHandler() + xmlReader.contentHandler = contentHandler + xmlReader.parse(InputSource(data.byteInputStream())) + return contentHandler.result + } catch (e: SAXException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + return ArrayList() + } + + class XmlContentHandler : DefaultHandler() { + lateinit var result: MutableList + var item: DanmakuItemData? = null + var completed = false + + override fun startDocument() { + result = ArrayList() + } + + override fun endDocument() { + completed = true + } + + override fun startElement( + uri: String, + localName: String, + qName: String, + attributes: Attributes + ) { + var tagName = if (localName.isNotEmpty()) localName else qName + tagName = tagName.lowercase(Locale.getDefault()).trim { it <= ' ' } + if (tagName == "d") { + // 我从未见过如此厚颜无耻之猴 + // 0:时间(弹幕出现时间) + // 1:类型(1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕) + // 2:字号 + // 3:颜色 + // 4:时间戳 ? + // 5:弹幕池id + // 6:用户hash + // 7:弹幕id + val pValue = attributes.getValue("p") + // parse p value to danmaku + val values = pValue.split(",").toTypedArray() + + if (values.isNotEmpty()) { + try { + item = DanmakuItemData( + content = "", + danmakuId = values[7].toLong(), + textSize = getTextSize(values[2].toInt()).toInt(), + textColor = values[3].toInt(), + position = (values[0].toFloat() * 1000).toLong(), + mode = getType(values[1].toInt()) + ) + } catch (e: NumberFormatException) { + e.printStackTrace() + } + } + } + } + + override fun endElement(uri: String, localName: String, qName: String) { + item?.let { + val tagName = if (localName.isNotEmpty()) localName else qName + if (tagName.equals("d", ignoreCase = true)) { + result.add(it) + } + item = null + } + } + + override fun characters(ch: CharArray, start: Int, length: Int) { + item?.let { + item = DanmakuItemData( + content = decodeXmlString(String(ch, start, length)), + danmakuId = it.danmakuId, + textSize = it.textSize, + textColor = it.textColor, + position = it.position, + mode = it.mode + ) + } + } + + private fun decodeXmlString(s: String): String { + var result = s + if (result.contains("&")) { + result = result.replace("&", "&") + } + if (result.contains(""")) { + result = result.replace(""", "\"") + } + if (result.contains(">")) { + result = result.replace(">", ">") + } + if (result.contains("<")) { + result = result.replace("<", "<") + } + return result + } + + companion object { + /** + * 获取真实的字体大小px + * 弹幕库限制字体最小12f最大25f + * @param n 12非常小,16特小,18小,25中,36大,45很大,64特别大 + * @return 以px为单位的字体大小 + */ + private fun getTextSize(n: Int): Float { + return when (n) { + 12 -> 12f + 16 -> 14f + 18 -> 17f + 25 -> 19f + 36 -> 21f + 45 -> 23f + 64 -> 25f + else -> 19f + } + } + + private fun getType(s: Int): Int { + // 类型(1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕) + return when (s) { + 1 -> DanmakuItemData.DANMAKU_MODE_ROLLING + 4 -> DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM + 5 -> DanmakuItemData.DANMAKU_MODE_CENTER_TOP + else -> DanmakuItemData.DANMAKU_MODE_ROLLING + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/preference/BasePreferenceFragment.kt b/app/src/main/java/com/skyd/imomoe/view/component/preference/BasePreferenceFragment.kt new file mode 100644 index 00000000..ec1ed43a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/component/preference/BasePreferenceFragment.kt @@ -0,0 +1,21 @@ +package com.skyd.imomoe.view.component.preference + +import androidx.preference.PreferenceFragmentCompat +import com.skyd.imomoe.util.eventbus.EventBusSubscriber +import org.greenrobot.eventbus.EventBus + + +abstract class BasePreferenceFragment : PreferenceFragmentCompat() { + protected var isFirstLoadData = true + + override fun onStart() { + super.onStart() + if (this is EventBusSubscriber) EventBus.getDefault().register(this) + } + + override fun onStop() { + super.onStop() + if (this is EventBusSubscriber && EventBus.getDefault().isRegistered(this)) + EventBus.getDefault().unregister(this) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/component/textview/TypefaceTextView.kt b/app/src/main/java/com/skyd/imomoe/view/component/textview/TypefaceTextView.kt deleted file mode 100644 index cd9473f2..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/textview/TypefaceTextView.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.skyd.imomoe.view.component.textview - -import android.content.Context -import android.graphics.Typeface -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatTextView -import com.skyd.imomoe.R - -class TypefaceTextView : AppCompatTextView { - var isFocused: Boolean? = null - - fun setIsFocused(isFocused: Boolean?) { - this.isFocused = isFocused - } - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - attrs?.let { - val typedArray = context.obtainStyledAttributes(it, R.styleable.TypefaceTextView, 0, 0) - val typefaceType = typedArray.getInt(R.styleable.TypefaceTextView_typeface, 0) - typeface = getTypeface(typefaceType) - typedArray.recycle() - } - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - attrs?.let { - val typedArray = context.obtainStyledAttributes(it, R.styleable.TypefaceTextView, 0, 0) - val typefaceType = typedArray.getInt(R.styleable.TypefaceTextView_typeface, 0) - typeface = - getTypeface( - typefaceType - ) - typedArray.recycle() - } - } - - override fun isFocused(): Boolean { - isFocused?.let { - return it - } - return super.isFocused() - } - - companion object { - fun getTypeface(typefaceType: Int?) = when (typefaceType) { - TypefaceUtil.BPR_TYPEFACE -> TypefaceUtil.getBPRTypeface() - else -> Typeface.DEFAULT - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/textview/TypefaceUtil.kt b/app/src/main/java/com/skyd/imomoe/view/component/textview/TypefaceUtil.kt deleted file mode 100644 index faae24fc..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/component/textview/TypefaceUtil.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.skyd.imomoe.view.component.textview - -import android.graphics.Typeface -import com.skyd.imomoe.App - -object TypefaceUtil { - const val BPR_TYPEFACE = 1 - - private var bPRTypeface: Typeface? = null - - fun getBPRTypeface(): Typeface { - bPRTypeface?.let { - return it - } - return try { - Typeface.createFromAsset(App.context.assets, "fonts/BPreplay.otf") - } catch (e: RuntimeException) { - Typeface.DEFAULT - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetProvider.kt b/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetProvider.kt index f7de4d67..372849d6 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetProvider.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetProvider.kt @@ -10,13 +10,12 @@ import android.net.Uri import android.os.Build import android.widget.RemoteViews import com.google.gson.Gson -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.RouteProcessor +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.AnimeCover10Bean +import com.skyd.imomoe.route.Router.route import com.skyd.imomoe.util.Util.getWeekday -import com.skyd.imomoe.util.Util.showToast +import com.skyd.imomoe.util.showToast import java.util.* @@ -28,10 +27,9 @@ class EverydayAnimeWidgetProvider : AppWidgetProvider() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == VIEW_CLICK_ACTION) { - val item = Gson().fromJson(intent.getStringExtra(ITEM), AnimeCoverBean::class.java) - if (item.episodeClickable?.actionUrl.equals(item.actionUrl)) - startPlayActivity(context, item.episodeClickable?.actionUrl) - else startPlayActivity(context, item.episodeClickable?.actionUrl + item.actionUrl) + val item = Gson().fromJson(intent.getStringExtra(ITEM), AnimeCover10Bean::class.java) + val route = item.episodeClickable?.route.orEmpty().ifBlank { item.route } + startPlayActivity(context, route) } else if (intent.action == REFRESH_ACTION) { val mgr: AppWidgetManager = AppWidgetManager.getInstance(context) val cn = ComponentName(context, EverydayAnimeWidgetProvider::class.java) @@ -46,7 +44,7 @@ class EverydayAnimeWidgetProvider : AppWidgetProvider() { mgr.getAppWidgetIds(cn), R.id.lv_widget_everyday_anime ) - App.context.getString(R.string.update_widget).showToast() + appContext.getString(R.string.update_widget).showToast() } super.onReceive(context, intent) } @@ -111,7 +109,7 @@ class EverydayAnimeWidgetProvider : AppWidgetProvider() { private fun startPlayActivity(context: Context, actionUrl: String?) { actionUrl ?: return - (DataSourceManager.getRouterProcessor() ?: RouteProcessor()).process(context, actionUrl) + actionUrl.route(context) } companion object { diff --git a/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetService.kt b/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetService.kt index 14b0cdac..d5643309 100644 --- a/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetService.kt +++ b/app/src/main/java/com/skyd/imomoe/view/component/widget/everydayanime/EverydayAnimeWidgetService.kt @@ -3,16 +3,18 @@ package com.skyd.imomoe.view.component.widget.everydayanime import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import android.widget.RemoteViews import android.widget.RemoteViewsService import android.widget.RemoteViewsService.RemoteViewsFactory import com.google.gson.Gson import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean +import com.skyd.imomoe.bean.AnimeCover10Bean import com.skyd.imomoe.model.DataSourceManager import com.skyd.imomoe.model.impls.EverydayAnimeWidgetModel import com.skyd.imomoe.model.interfaces.IEverydayAnimeWidgetModel import com.skyd.imomoe.util.Util +import com.skyd.imomoe.util.showToast import java.util.* class EverydayAnimeService : RemoteViewsService() { @@ -27,7 +29,7 @@ internal class EverydayAnimeRemoteViewsFactory( ) : RemoteViewsFactory { private val model = DataSourceManager.create(IEverydayAnimeWidgetModel::class.java) ?: EverydayAnimeWidgetModel() - private val mWidgetItems: MutableList = ArrayList() + private val mWidgetItems: MutableList = ArrayList() override fun onCreate() { @@ -43,7 +45,12 @@ internal class EverydayAnimeRemoteViewsFactory( val item = mWidgetItems[position] val rv = RemoteViews(mContext.packageName, R.layout.item_anime_cover_10) rv.setTextViewText(R.id.tv_anime_cover_10_title, item.title) - rv.setTextViewText(R.id.tv_anime_cover_10_episode, item.episodeClickable?.title) + if (item.episodeClickable?.title.isNullOrBlank()) { + rv.setViewVisibility(R.id.tv_anime_cover_10_episode, View.GONE) + } else { + rv.setViewVisibility(R.id.tv_anime_cover_10_episode, View.VISIBLE) + rv.setTextViewText(R.id.tv_anime_cover_10_episode, item.episodeClickable?.title) + } val extras = Bundle() // 传Serializable的对象获取为null,原因未知,只能传转成json之后的了 @@ -78,17 +85,22 @@ internal class EverydayAnimeRemoteViewsFactory( // from the network, etc., it is ok to do it here, synchronously. The widget will remain // in its current state while work is being done here, so you don't need to worry about // locking up the widget. - val list = getEverydayAnimeData() - if (list.size != 7) return - mWidgetItems.clear() - mWidgetItems.addAll( - list[Util.getRealDayOfWeek( - Calendar.getInstance(Locale.getDefault()).get(Calendar.DAY_OF_WEEK) - ) - 1].toMutableList() - ) + try { + val list = getEverydayAnimeData() + if (list.size != 7) return + mWidgetItems.clear() + mWidgetItems.addAll( + list[Util.getRealDayOfWeek( + Calendar.getInstance(Locale.getDefault()).get(Calendar.DAY_OF_WEEK) + ) - 1].toMutableList() + ) + } catch (e: Exception) { + e.printStackTrace() + e.message?.showToast() + } } - private fun getEverydayAnimeData(): MutableList> { + private fun getEverydayAnimeData(): MutableList> { return model.getEverydayAnimeData() } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/AnimeShowFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/AnimeShowFragment.kt index 81d60dc0..1e992e31 100644 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/AnimeShowFragment.kt +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/AnimeShowFragment.kt @@ -2,50 +2,83 @@ package com.skyd.imomoe.view.fragment import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.view.ViewStub import android.widget.Toast -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.GetDataEnum import com.skyd.imomoe.databinding.FragmentAnimeShowBinding -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.smartNotifyDataSetChanged -import com.skyd.imomoe.view.adapter.AnimeShowAdapter -import com.skyd.imomoe.view.adapter.SerializableRecycledViewPool +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.ext.screenIsLand +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.Banner1ViewHolder +import com.skyd.imomoe.util.showToast import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize.Companion.MAX_SPAN_SIZE +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.* import com.skyd.imomoe.viewmodel.AnimeShowViewModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class AnimeShowFragment : BaseFragment() { - private var partUrl: String = "" - private lateinit var viewModel: AnimeShowViewModel - private lateinit var adapter: AnimeShowAdapter + private val viewModel: AnimeShowViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter( + mutableListOf( + AnimeCover1Proxy(), + AnimeCover3Proxy(), + AnimeCover4Proxy(), + AnimeCover5Proxy(), + Banner1Proxy(), + Header1Proxy() + ) + ).apply { + onViewAttachedToWindow = { + when (it) { + is Banner1ViewHolder -> it.banner1.startPlay(5000) + } + } + + onViewDetachedFromWindow = { + when (it) { + is Banner1ViewHolder -> it.banner1.stopPlay() + } + } + } + } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProvider(this).get(AnimeShowViewModel::class.java) val arguments = arguments - try { - partUrl = arguments?.getString("partUrl") ?: "" - viewModel.viewPool = - arguments?.getSerializable("viewPool") as SerializableRecycledViewPool - viewModel.childViewPool = - arguments.getSerializable("childViewPool") as SerializableRecycledViewPool - } catch (e: Exception) { - e.printStackTrace() - e.message?.showToast(Toast.LENGTH_LONG) + runCatching { + if (viewModel.partUrl.isBlank()) { + viewModel.partUrl = arguments?.getString("partUrl").orEmpty() + } + }.onFailure { + it.printStackTrace() + it.message?.showToast(Toast.LENGTH_LONG) + } + mBinding.apply { + if (requireContext().screenIsLand) { + rvAnimeShowFragment.addFitsSystemWindows(right = true, bottom = true) + } + rvAnimeShowFragment.adapter = adapter + rvAnimeShowFragment.layoutManager = GridLayoutManager(activity, MAX_SPAN_SIZE).apply { + spanSizeLookup = AnimeShowSpanSize(adapter) + } + rvAnimeShowFragment.addItemDecoration(AnimeShowItemDecoration()) } } - override fun getBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentAnimeShowBinding = FragmentAnimeShowBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentAnimeShowBinding.inflate(inflater, container, false) override fun onResume() { super.onResume() @@ -56,53 +89,36 @@ class AnimeShowFragment : BaseFragment() { } private fun initData() { - val childViewPool = viewModel.childViewPool - adapter = if (childViewPool == null) { - AnimeShowAdapter(this, viewModel.animeShowList) - } else { - AnimeShowAdapter(this, viewModel.animeShowList, childViewPool) - } - mBinding.run { - rvAnimeShowFragment.layoutManager = GridLayoutManager(activity, 4) - .apply { - spanSizeLookup = AnimeShowSpanSize(adapter) - } - rvAnimeShowFragment.addItemDecoration(AnimeShowItemDecoration()) - rvAnimeShowFragment.setHasFixedSize(true) - rvAnimeShowFragment.adapter = adapter srlAnimeShowFragment.setOnRefreshListener { - viewModel.getAnimeShowData(partUrl) + viewModel.getAnimeShowData() } srlAnimeShowFragment.setOnLoadMoreListener { - viewModel.pageNumberBean?.let { - viewModel.getAnimeShowData(it.actionUrl, isRefresh = false) - return@setOnLoadMoreListener - } - mBinding.srlAnimeShowFragment.finishLoadMore() - getString(R.string.no_more_info).showToast() + viewModel.loadMoreAnimeShowData() } } - - viewModel.viewPool?.let { - mBinding.rvAnimeShowFragment.setRecycledViewPool(it) - } - - viewModel.mldGetAnimeShowList.observe(viewLifecycleOwner, { - mBinding.srlAnimeShowFragment.closeHeaderOrFooter() - adapter.smartNotifyDataSetChanged(it.first, it.second, viewModel.animeShowList) - when (it.first) { - GetDataEnum.REFRESH, GetDataEnum.LOAD_MORE -> hideLoadFailedTip() - GetDataEnum.FAILED -> { - showLoadFailedTip(getString(R.string.load_data_failed_click_to_retry)) { - viewModel.getAnimeShowData(partUrl) - hideLoadFailedTip() + viewModel.animeShowList.collectWithLifecycle(viewLifecycleOwner) { data -> + when (data) { + is DataState.Empty -> refresh() + is DataState.Success -> { + adapter.dataList = data.data + hideLoadFailedTip() + mBinding.srlAnimeShowFragment.closeHeaderOrFooter() + } + is DataState.Error -> { + adapter.dataList = emptyList() + showLoadFailedTip { + viewModel.getAnimeShowData() } + mBinding.srlAnimeShowFragment.closeHeaderOrFooter() + } + is DataState.Loading -> { + mBinding.srlAnimeShowFragment.autoLoadMoreAnimationOnly() } + else -> {} } - }) - refresh() + } } fun refresh(): Boolean { @@ -110,8 +126,4 @@ class AnimeShowFragment : BaseFragment() { } override fun getLoadFailedTipView(): ViewStub = mBinding.layoutAnimeShowFragmentLoadFailed - - companion object { - const val TAG = "AnimeShowFragment" - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/BaseComposeFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/BaseComposeFragment.kt new file mode 100644 index 00000000..a19d14c2 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/BaseComposeFragment.kt @@ -0,0 +1,34 @@ +package com.skyd.imomoe.view.fragment + +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import com.google.android.material.composethemeadapter3.Mdc3Theme +import com.skyd.imomoe.util.eventbus.EventBusSubscriber +import org.greenrobot.eventbus.EventBus + + +abstract class BaseComposeFragment : Fragment() { + protected var isFirstLoadData = true + + fun setContentBase(content: @Composable () -> Unit): View = + ComposeView(requireContext()).apply { + setContent { + Mdc3Theme { + content.invoke() + } + } + } + + override fun onStart() { + super.onStart() + if (this is EventBusSubscriber) EventBus.getDefault().register(this) + } + + override fun onStop() { + super.onStop() + if (this is EventBusSubscriber && EventBus.getDefault().isRegistered(this)) + EventBus.getDefault().unregister(this) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/BaseFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/BaseFragment.kt index 79622366..4517a4f8 100644 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/BaseFragment.kt +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/BaseFragment.kt @@ -1,7 +1,6 @@ package com.skyd.imomoe.view.fragment import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,14 +9,14 @@ import android.widget.TextView import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import com.skyd.imomoe.R +import com.skyd.imomoe.ext.gone +import com.skyd.imomoe.ext.visible import com.skyd.imomoe.util.eventbus.EventBusSubscriber -import com.skyd.imomoe.util.gone -import com.skyd.imomoe.util.visible -import com.skyd.skin.core.SkinBaseFragment +import com.skyd.imomoe.util.logE import org.greenrobot.eventbus.EventBus -abstract class BaseFragment : SkinBaseFragment() { +abstract class BaseFragment : Fragment() { protected var isFirstLoadData = true private var binding: VB? = null protected val mBinding get() = binding!! @@ -54,7 +53,10 @@ abstract class BaseFragment : SkinBaseFragment() { protected open fun getLoadFailedTipView(): ViewStub? = null - protected open fun showLoadFailedTip(text: String, onClickListener: View.OnClickListener?) { + protected open fun showLoadFailedTip( + text: String = getString(R.string.load_data_failed_click_to_retry), + onClickListener: View.OnClickListener? = null + ) { val loadFailedTipViewStub = getLoadFailedTipView() ?: return if (loadFailedTipViewStub.parent != null) { loadFailedTipView = loadFailedTipViewStub.inflate() @@ -62,10 +64,10 @@ abstract class BaseFragment : SkinBaseFragment() { tvImageTextTip1.text = text if (onClickListener != null) loadFailedTipView.setOnClickListener(onClickListener) } else { - if (this::loadFailedTipView.isInitialized) { + if (::loadFailedTipView.isInitialized) { loadFailedTipView.visible() } else { - Log.e("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") + logE("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") } } } @@ -76,7 +78,7 @@ abstract class BaseFragment : SkinBaseFragment() { if (this::loadFailedTipView.isInitialized) { loadFailedTipView.gone() } else { - Log.e("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") + logE("showLoadFailedTip", "layout_image_text_tip_1 isn't initialized") } } } diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/DataSourceMarketFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/DataSourceMarketFragment.kt new file mode 100644 index 00000000..1eab6226 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/DataSourceMarketFragment.kt @@ -0,0 +1,215 @@ +package com.skyd.imomoe.view.fragment + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.arialyy.aria.core.task.DownloadTask +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.DataSource2Bean +import com.skyd.imomoe.databinding.FragmentDataSourceMarketBinding +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.route.Router.buildRouteUri +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.route.processor.UrlMapActivityProcessor +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.market.DataSourceDownloadService +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.DataSource2Proxy +import com.skyd.imomoe.view.listener.dsl.requestPermissions +import com.skyd.imomoe.viewmodel.DataSourceMarketViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow + +class DataSourceMarketFragment : BaseFragment() { + companion object { + val needRefresh: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + } + + private val viewModel: DataSourceMarketViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter(mutableListOf(DataSource2Proxy( + onActionClickListener = { _, data, _ -> + XXPermissions.with(this) + .permission( + Permission.MANAGE_EXTERNAL_STORAGE, + Permission.NOTIFICATION_SERVICE + ) + .requestPermissions { + onGranted { permissions, all -> + if (!all) { + if (permissions?.contains(Permission.MANAGE_EXTERNAL_STORAGE) == false) { + getString(R.string.no_storage_can_not_download).showToast() + } + if (permissions?.contains(Permission.NOTIFICATION_SERVICE) == false) { + getString(R.string.no_notification_service).showToast() + } + return@onGranted + } + if (DataSourceManager.customDataSourceInfo?.get("name") == data.name || + DataSourceManager.dataSourceFileName.substringBeforeLast(".") == data.name + ) { + showMessageDialog( + icon = R.drawable.ic_warning_2_24, + message = getString(R.string.data_source_market_restart_after_downloaded) + ) { _, _ -> + startDownload(data) + } + } else { + startDownload(data) + } + } + onDenied { permissions, _ -> + if (permissions?.contains(Permission.MANAGE_EXTERNAL_STORAGE) == false) { + getString(R.string.no_storage_can_not_download).showToast() + } + if (permissions?.contains(Permission.NOTIFICATION_SERVICE) == false) { + getString(R.string.no_notification_service).showToast() + } + } + } + } + ))) + } + + private var binder: DataSourceDownloadService.DataSourceDownloadBinder? = null + + private fun startDownload(data: DataSource2Bean) { + requireActivity().startService( + Intent(activity, DataSourceDownloadService::class.java) + .putExtra( + DataSourceDownloadService.DOWNLOAD_URL_KEY, + data.downloadUrl + ) + .putExtra( + DataSourceDownloadService.DATA_SOURCE_TITLE, + data.name + ) + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mBinding.root.addFitsSystemWindows(right = true, bottom = true) + } + + override fun onResume() { + super.onResume() + if (isFirstLoadData) { + initData() + isFirstLoadData = false + } + } + + private fun initData() { + mBinding.apply { + rvDataSourceMarketFragment.layoutManager = LinearLayoutManager(activity) + rvDataSourceMarketFragment.adapter = adapter + + srlDataSourceMarketFragment.setOnRefreshListener { viewModel.getDataSourceMarketList() } + srlDataSourceMarketFragment.setEnableLoadMore(false) + } + + viewModel.dataSourceMarketList.collectWithLifecycle(viewLifecycleOwner) { + when (it) { + is DataState.Refreshing -> { + mBinding.srlDataSourceMarketFragment.autoRefreshAnimationOnly() + } + is DataState.Success -> { + adapter.dataList = it.data + mBinding.srlDataSourceMarketFragment.finishRefresh() + } + else -> { + adapter.dataList = emptyList() + mBinding.srlDataSourceMarketFragment.finishRefresh() + } + } + } + + needRefresh.collectWithLifecycle(viewLifecycleOwner) { + if (it) mBinding.srlDataSourceMarketFragment.autoRefresh() + } + + viewModel.askAddUrlMap.collectWithLifecycle(viewLifecycleOwner) { + if (it) { + showMessageDialog( + title = getString(R.string.ask), + message = getString(R.string.data_source_market_ask_add_url_map), + onNegative = { dialog, _ -> dialog.dismiss() } + ) { _, _ -> + UrlMapActivityProcessor.route.buildRouteUri { + appendQueryParameter( + UrlMapActivityProcessor.JSON_DATA, + requireActivity().getRawString(R.raw.github_url_map) + ) + appendQueryParameter( + UrlMapActivityProcessor.AUTO_ADD_AND_FINISH, "true" + ) + appendQueryParameter( + UrlMapActivityProcessor.ENABLED, "true" + ) + }.route(requireActivity()) + } + } + } + + requireActivity().bindService( + Intent(activity, DataSourceDownloadService::class.java), + serviceConnection, + Context.BIND_AUTO_CREATE + ) + } + + private val serviceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, service: IBinder?) { + binder = (service as? DataSourceDownloadService.DataSourceDownloadBinder)?.apply { +// viewModel.initList(notCompleteList, dataSourceTitleMap) + + onTaskRunningEvent.collectWithLifecycle(this@DataSourceMarketFragment) { task -> + viewModel.onTaskRunning(task.downloadEntity) + } + onTaskCompleteEvent.collectWithLifecycle(this@DataSourceMarketFragment) { task -> + viewModel.onTaskComplete(task.downloadEntity, dataSourceTitleMap) + } + onTaskCancelEvent.collectWithLifecycle(this@DataSourceMarketFragment) { task -> + viewModel.onTaskCancel(task.downloadEntity, dataSourceTitleMap) + } + onTaskStopEvent.collectWithLifecycle(this@DataSourceMarketFragment) { task -> + viewModel.onTaskCancel(task.downloadEntity, dataSourceTitleMap) + } + onTaskFailEvent.collectWithLifecycle(this@DataSourceMarketFragment) { task -> + viewModel.onTaskCancel(task.downloadEntity, dataSourceTitleMap) + } + onTaskResumeEvent.collectWithLifecycle(this@DataSourceMarketFragment) { + } + onTaskPreEvent.collectWithLifecycle(this@DataSourceMarketFragment, onTaskPreStart) + onTaskStartEvent.collectWithLifecycle(this@DataSourceMarketFragment, onTaskPreStart) + } + } + + override fun onServiceDisconnected(name: ComponentName) { + binder = null + } + } + + private val onTaskPreStart: suspend CoroutineScope.(data: DownloadTask) -> Unit = { task -> + val binder = this@DataSourceMarketFragment.binder + if (binder != null) { + viewModel.onTaskPreStart(task.downloadEntity, binder.dataSourceTitleMap) + } + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentDataSourceMarketBinding.inflate(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/EverydayAnimeFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/EverydayAnimeFragment.kt index 268adf58..fd637e31 100644 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/EverydayAnimeFragment.kt +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/EverydayAnimeFragment.kt @@ -1,109 +1,112 @@ package com.skyd.imomoe.view.fragment import android.animation.ObjectAnimator -import android.app.Activity -import android.graphics.Rect import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewStub -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.tabs.TabLayout +import androidx.fragment.app.viewModels import com.google.android.material.tabs.TabLayoutMediator -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.config.Const.ViewHolderTypeInt import com.skyd.imomoe.databinding.FragmentEverydayAnimeBinding -import com.skyd.imomoe.util.GridRecyclerView1ViewHolder +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.ext.hideToolbarWhenCollapsed +import com.skyd.imomoe.state.DataState import com.skyd.imomoe.util.eventbus.EventBusSubscriber import com.skyd.imomoe.util.eventbus.MessageEvent import com.skyd.imomoe.util.eventbus.RefreshEvent -import com.skyd.imomoe.view.adapter.AnimeShowAdapter -import com.skyd.imomoe.view.adapter.SkinRvAdapter +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeCover12Proxy +import com.skyd.imomoe.view.adapter.variety.proxy.GridRecyclerView1Proxy import com.skyd.imomoe.view.component.WrapLinearLayoutManager +import com.skyd.imomoe.view.listener.dsl.addOnTabSelectedListener import com.skyd.imomoe.viewmodel.EverydayAnimeViewModel +import dagger.hilt.android.AndroidEntryPoint import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +@AndroidEntryPoint class EverydayAnimeFragment : BaseFragment(), EventBusSubscriber { - private lateinit var viewModel: EverydayAnimeViewModel - private lateinit var adapter: Vp2Adapter + private val viewModel: EverydayAnimeViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter( + mutableListOf( + GridRecyclerView1Proxy( + onBindViewHolder = { holder, data, _ -> + val rvLayoutParams = holder.rvGridRecyclerView1.layoutParams + rvLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + holder.rvGridRecyclerView1.layoutManager = + WrapLinearLayoutManager(requireActivity()) + holder.rvGridRecyclerView1.layoutParams = rvLayoutParams + holder.rvGridRecyclerView1.isNestedScrollingEnabled = true + val adapter = VarietyAdapter( + mutableListOf(AnimeCover12Proxy()) + ).apply { dataList = data } + holder.rvGridRecyclerView1.adapter = adapter + }, + height = ViewGroup.LayoutParams.MATCH_PARENT, + width = ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + ) + } private var offscreenPageLimit = 1 private var selectedTabIndex = -1 - private var lastRefreshTime: Long = System.currentTimeMillis() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - viewModel = ViewModelProvider(this).get(EverydayAnimeViewModel::class.java) - return super.onCreateView(inflater, container, savedInstanceState) - } + private var lastRefreshTime: Long = 0L - override fun getBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentEverydayAnimeBinding = + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentEverydayAnimeBinding.inflate(inflater, container, false) - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) mBinding.run { - vp2EverydayAnimeFragment.setOffscreenPageLimit(offscreenPageLimit) + ablEverydayAnimeFragment.addFitsSystemWindows(right = true, top = true) + vp2EverydayAnimeFragment.addFitsSystemWindows(right = true) + vp2EverydayAnimeFragment.offscreenPageLimit = offscreenPageLimit srlEverydayAnimeFragment.setOnRefreshListener { refresh() } - tlEverydayAnimeFragment.addOnTabSelectedListener(object : - TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - selectedTabIndex = tab.position - } + tlEverydayAnimeFragment.addOnTabSelectedListener { + onTabSelected { tab -> selectedTabIndex = tab?.position ?: return@onTabSelected } + } + vp2EverydayAnimeFragment.adapter = adapter - override fun onTabUnselected(tab: TabLayout.Tab) { - } + ablEverydayAnimeFragment.hideToolbarWhenCollapsed(tbEverydayAnimeFragment) - override fun onTabReselected(tab: TabLayout.Tab) { - } + val tabLayoutMediator = TabLayoutMediator( + mBinding.tlEverydayAnimeFragment, + mBinding.vp2EverydayAnimeFragment + ) { tab, position -> + tab.text = viewModel.tabList.value.readOrNull().orEmpty()[position].title + } + tabLayoutMediator.attach() + } - }) + viewModel.header.collectWithLifecycle(viewLifecycleOwner) { data -> + mBinding.tbEverydayAnimeFragment.title = data } - viewModel.mldHeader.observe(viewLifecycleOwner, { - mBinding.tvEverydayAnimeFragmentTitle.text = it.title - }) - - viewModel.mldEverydayAnimeList.observe(viewLifecycleOwner, { - mBinding.srlEverydayAnimeFragment.isRefreshing = false - - if (it != null) { - viewModel.everydayAnimeList.apply { - clear() - if (this@EverydayAnimeFragment::adapter.isInitialized) - adapter.notifyDataSetChanged() - addAll(it) - if (this@EverydayAnimeFragment::adapter.isInitialized) - adapter.notifyDataSetChanged() + viewModel.everydayAnimeList.collectWithLifecycle(viewLifecycleOwner) { data -> + when (data) { + is DataState.Refreshing -> { + mBinding.srlEverydayAnimeFragment.isRefreshing = true + } + is DataState.Error -> { + mBinding.srlEverydayAnimeFragment.isRefreshing = false + showLoadFailedTip { + mBinding.srlEverydayAnimeFragment.isRefreshing = true + refresh() + } } - val selectedTabIndex = this.selectedTabIndex - activity?.let { it1 -> + is DataState.Success -> { + mBinding.srlEverydayAnimeFragment.isRefreshing = false + val selectedTabIndex = this@EverydayAnimeFragment.selectedTabIndex //先隐藏 - ObjectAnimator.ofFloat(mBinding.llEverydayAnimeFragment, "alpha", 1f, 0f) + ObjectAnimator.ofFloat(mBinding.vp2EverydayAnimeFragment, "alpha", 1f, 0f) .setDuration(270).start() - //添加rv - if (!this::adapter.isInitialized) { - adapter = Vp2Adapter(it1, viewModel.everydayAnimeList) - mBinding.vp2EverydayAnimeFragment.setAdapter(adapter) - } - val tabLayoutMediator = TabLayoutMediator( - mBinding.tlEverydayAnimeFragment, - mBinding.vp2EverydayAnimeFragment.getViewPager() - ) { tab, position -> - tab.text = viewModel.tabList[position].title - } - tabLayoutMediator.attach() + adapter.dataList = data.data val tabCount = adapter.itemCount mBinding.vp2EverydayAnimeFragment.post { @@ -119,27 +122,22 @@ class EverydayAnimeFragment : BaseFragment(), Even ) } //设置完数据后显示,避免闪烁 - ObjectAnimator.ofFloat(mBinding.llEverydayAnimeFragment, "alpha", 0f, 1f) + ObjectAnimator.ofFloat(mBinding.vp2EverydayAnimeFragment, "alpha", 0f, 1f) .setDuration(270).start() } - } - hideLoadFailedTip() - } else { - viewModel.everydayAnimeList.apply { - val count = size - clear() - if (this@EverydayAnimeFragment::adapter.isInitialized) - adapter.notifyItemRangeRemoved(0, count) - } - showLoadFailedTip(getString(R.string.load_data_failed_click_to_retry)) { - viewModel.getEverydayAnimeData() hideLoadFailedTip() } + else -> { + mBinding.srlEverydayAnimeFragment.isRefreshing = true + } } - }) + } - mBinding.srlEverydayAnimeFragment.isRefreshing = true - viewModel.getEverydayAnimeData() + if (viewModel.tabList.value is DataState.Empty || + viewModel.everydayAnimeList.value is DataState.Empty + ) { + refresh() + } } @Subscribe(threadMode = ThreadMode.MAIN) @@ -154,63 +152,10 @@ class EverydayAnimeFragment : BaseFragment(), Even private fun refresh() { //避免刷新间隔太短 if (System.currentTimeMillis() - lastRefreshTime > 500) { - mBinding.srlEverydayAnimeFragment.isRefreshing = true lastRefreshTime = System.currentTimeMillis() viewModel.getEverydayAnimeData() - } else { - mBinding.srlEverydayAnimeFragment.isRefreshing = false } } override fun getLoadFailedTipView(): ViewStub = mBinding.layoutEverydayAnimeFragmentLoadFailed - - class Vp2Adapter( - private var activity: Activity, - private var list: List>, - private var showRankNumber: BooleanArray = BooleanArray(0) - ) : SkinRvAdapter() { - - //必须四个参数都不是-1才生效 - var childPadding = Rect(-1, -1, -1, -1) - - override fun getItemViewType(position: Int): Int = ViewHolderTypeInt.GRID_RECYCLER_VIEW_1 - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - val viewHolder = super.onCreateViewHolder(parent, viewType) - //vp2的item必须是MATCH_PARENT的 - val layoutParams = viewHolder.itemView.layoutParams - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - viewHolder.itemView.layoutParams = layoutParams - return viewHolder - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = list[position] - when (holder) { - is GridRecyclerView1ViewHolder -> { - val rvLayoutParams = holder.rvGridRecyclerView1.layoutParams - rvLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - holder.rvGridRecyclerView1.layoutManager = WrapLinearLayoutManager(activity) - holder.rvGridRecyclerView1.layoutParams = rvLayoutParams - holder.rvGridRecyclerView1.isNestedScrollingEnabled = true - val adapter = AnimeShowAdapter.GridRecyclerView1Adapter(activity, item) - adapter.padding = childPadding - if (showRankNumber.isNotEmpty() && showRankNumber[position]) - adapter.showRankNumber = true - holder.rvGridRecyclerView1.adapter = adapter - adapter.notifyDataSetChanged() - } - } - } - - override fun getItemCount(): Int = list.size - } - - companion object { - const val TAG = "EverydayAnimeFragment" - } } diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/HomeFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/HomeFragment.kt index 894f7e46..8b29e3df 100644 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/HomeFragment.kt +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/HomeFragment.kt @@ -6,47 +6,29 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewStub -import android.widget.Toast import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator -import com.hjq.permissions.OnPermissionCallback -import com.hjq.permissions.Permission -import com.hjq.permissions.XXPermissions import com.skyd.imomoe.R import com.skyd.imomoe.databinding.FragmentHomeBinding -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.util.Util.process -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.clickScale -import com.skyd.imomoe.util.eventbus.EventBusSubscriber -import com.skyd.imomoe.util.eventbus.MessageEvent -import com.skyd.imomoe.util.eventbus.RefreshEvent -import com.skyd.imomoe.util.eventbus.SelectHomeTabEvent -import com.skyd.imomoe.view.activity.* +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.route.Router.route +import com.skyd.imomoe.route.processor.SearchActivityProcessor +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.view.activity.AnimeDownloadActivity +import com.skyd.imomoe.view.activity.ClassifyActivity +import com.skyd.imomoe.view.activity.FavoriteActivity +import com.skyd.imomoe.view.activity.RankActivity +import com.skyd.imomoe.view.listener.dsl.addOnTabSelectedListener import com.skyd.imomoe.viewmodel.HomeViewModel -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode - - -class HomeFragment : BaseFragment(), EventBusSubscriber { - private lateinit var viewModel: HomeViewModel - private lateinit var adapter: VpAdapter - private var currentTab = -1 - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - viewModel = ViewModelProvider(this).get(HomeViewModel::class.java) - adapter = VpAdapter(this) - return super.onCreateView(inflater, container, savedInstanceState) - } +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class HomeFragment : BaseFragment() { + private val viewModel: HomeViewModel by viewModels() + private val adapter: VpAdapter by lazy { VpAdapter() } override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding = FragmentHomeBinding.inflate(inflater, container, false) @@ -54,165 +36,98 @@ class HomeFragment : BaseFragment(), EventBusSubscriber { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // 清除缓存,以免换肤后颜色错误 - viewModel.childViewPool.clear() - viewModel.viewPool.clear() - mBinding.run { - vp2HomeFragment.setAdapter(adapter) + ablHomeFragment.addFitsSystemWindows(right = true, top = true) + + vp2HomeFragment.adapter = adapter val tabLayoutMediator = TabLayoutMediator( tlHomeFragment, vp2HomeFragment.getViewPager() ) { tab, position -> - if (position < viewModel.allTabList.size) - tab.text = viewModel.allTabList[position].title - } - tabLayoutMediator.attach() - - ivHomeFragmentRank.setOnClickListener { - it.clickScale(0.8f, 70) - activity?.let { it1 -> - it1.startActivity(Intent(it1, RankActivity::class.java)) - it1.overridePendingTransition(R.anim.anl_push_left_in, R.anim.anl_stay) + val list = viewModel.allTabList.value.readOrNull().orEmpty() + if (position < list.size) { + tab.text = list[position].title } } + tabLayoutMediator.attach() - ivHomeFragmentClassify.setOnClickListener { - it.clickScale(0.8f, 70) - startActivity(Intent(activity, ClassifyActivity::class.java)) - } - - tvHomeFragmentHeaderSearch.setOnClickListener { - activity?.let { - val const = DataSourceManager.getConst() ?: com.skyd.imomoe.model.impls.Const() - process(it, const.actionUrl.ANIME_SEARCH() + "") - it.overridePendingTransition(R.anim.anl_push_top_in, R.anim.anl_stay) + tbHomeFragment.apply { + setNavigationOnClickListener { + startActivity(Intent(requireActivity(), RankActivity::class.java)) + requireActivity().overridePendingTransition( + R.anim.anl_push_left_in, + R.anim.anl_stay + ) } - } - ivHomeFragmentAnimeDownload.setOnClickListener { - it.clickScale(0.8f, 70) - XXPermissions.with(this@HomeFragment).permission(Permission.MANAGE_EXTERNAL_STORAGE) - .request(object : OnPermissionCallback { - override fun onGranted(permissions: MutableList?, all: Boolean) { + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.menu_item_home_fragment_classify -> { + startActivity(Intent(activity, ClassifyActivity::class.java)) + true + } + R.id.menu_item_home_fragment_download -> { startActivity(Intent(activity, AnimeDownloadActivity::class.java)) + true } - - override fun onDenied(permissions: MutableList?, never: Boolean) { - super.onDenied(permissions, never) - "无存储权限,无法播放本地缓存视频".showToast(Toast.LENGTH_LONG) + R.id.menu_item_home_fragment_favorite -> { + startActivity(Intent(activity, FavoriteActivity::class.java)) + true } + else -> false } - ) - } - - ivHomeFragmentFavorite.setOnClickListener { - it.clickScale(0.8f, 70) - startActivity(Intent(activity, FavoriteActivity::class.java)) - } - tlHomeFragment.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - currentTab = tab.position - } - - override fun onTabUnselected(tab: TabLayout.Tab) { - //当选项卡变成未选中状态时调用 } + } - override fun onTabReselected(tab: TabLayout.Tab) { - adapter.refresh(currentTab) + btnHomeFragmentSearch.setOnClickListener { + activity?.let { + SearchActivityProcessor.route.route(it) + it.overridePendingTransition(R.anim.anl_push_top_in, R.anim.anl_stay) } - }) - } + } - viewModel.mldGetAllTabList.observe(viewLifecycleOwner, Observer { - adapter.clearAllFragment() - if (!it) { - showLoadFailedTip(getString(R.string.load_data_failed_click_to_retry), - View.OnClickListener { - viewModel.getAllTabData() - hideLoadFailedTip() - }) - getString(R.string.get_home_tab_data_failed).showToast(Toast.LENGTH_LONG) - } else { - hideLoadFailedTip() - viewModel.allTabList.size.let { size -> - if (size > 0) mBinding.vp2HomeFragment.setOffscreenPageLimit(size) - } - for (i in viewModel.allTabList.indices) { - val fragment = AnimeShowFragment() - val bundle = Bundle() - bundle.putString("partUrl", viewModel.allTabList[i].actionUrl) - bundle.putSerializable("viewPool", viewModel.viewPool) - bundle.putSerializable("childViewPool", viewModel.childViewPool) - fragment.arguments = bundle - adapter.addFragment(fragment) - } + tlHomeFragment.addOnTabSelectedListener { + onTabSelected { viewModel.currentTab = it!!.position } } - adapter.notifyDataSetChanged() - }) - viewModel.getAllTabData() - } + ablHomeFragment.hideToolbarWhenCollapsed(tbHomeFragment) + } - // priority = 1比MainActivity的高,以便在找不到相应子页面时拦截SelectHomeTabEvent - // 使得不会切换到MainActivity页面 - @Subscribe(threadMode = ThreadMode.MAIN, priority = 1) - override fun onMessageEvent(event: MessageEvent) { - when (event) { - is RefreshEvent -> { - // 如果获取首页信息成功了,则刷新每个tab内容,否则重新获取主页信息 - if (viewModel.mldGetAllTabList.value == true) - adapter.refresh(currentTab) - else viewModel.getAllTabData() - } - is SelectHomeTabEvent -> { - var tabPosition = -1 - viewModel.allTabList.forEachIndexed { index, tabBean -> - if (tabBean.actionUrl == event.actionUrl) { - tabPosition = index - return@forEachIndexed + viewModel.allTabList.collectWithLifecycle(viewLifecycleOwner) + { data -> + when (data) { + is DataState.Success -> { + hideLoadFailedTip() + if (data.data.isNotEmpty()) { + mBinding.vp2HomeFragment.offscreenPageLimit = data.data.size } } - if (tabPosition >= 0 && tabPosition < mBinding.tlHomeFragment.tabCount) - mBinding.vp2HomeFragment.getViewPager() - .apply { post { currentItem = tabPosition } } - else { - EventBus.getDefault().cancelEventDelivery(event) - getString(R.string.unknown_route, event.actionUrl).showToast() + is DataState.Error -> { + showLoadFailedTip { + viewModel.getAllTabData() + } } + else -> {} } + adapter.notifyDataSetChanged() } } override fun getLoadFailedTipView(): ViewStub = mBinding.layoutHomeFragmentLoadFailed - class VpAdapter : FragmentStateAdapter { - - constructor(fragmentActivity: FragmentActivity) : super(fragmentActivity) - - constructor(fragment: Fragment) : super(fragment) - - private val fragments = mutableListOf() - - fun clearAllFragment() { - fragments.clear() - } + inner class VpAdapter : FragmentStateAdapter(this) { - fun addFragment(fragment: AnimeShowFragment) { - fragments.add(fragment) - } + override fun getItemCount() = viewModel.allTabList.value.readOrNull().orEmpty().size - fun refresh(position: Int) { - fragments[position].refresh() + override fun createFragment(position: Int): Fragment { + val fragment = AnimeShowFragment() + val bundle = Bundle() + bundle.putString( + "partUrl", + viewModel.allTabList.value.readOrNull().orEmpty()[position].route + ) + fragment.arguments = bundle + return fragment } - - override fun getItemCount() = fragments.size - - override fun createFragment(position: Int) = fragments[position] - } - - companion object { - const val TAG = "HomeFragment" } } diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/LocalDataSourceFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/LocalDataSourceFragment.kt new file mode 100644 index 00000000..8f0bbf66 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/LocalDataSourceFragment.kt @@ -0,0 +1,102 @@ +package com.skyd.imomoe.view.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.DataSource1Bean +import com.skyd.imomoe.databinding.FragmentLocalDataSourceBinding +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.view.activity.ConfigDataSourceActivity +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.DataSource1Proxy +import com.skyd.imomoe.viewmodel.LocalDataSourceViewModel + +class LocalDataSourceFragment : BaseFragment() { + private val viewModel: LocalDataSourceViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter(mutableListOf(DataSource1Proxy( + onClickListener = { _, data, _ -> + val configDataSourceActivity = activity ?: return@DataSource1Proxy + if (data.selected) { + configDataSourceActivity.showSnackbar(getString(R.string.the_data_source_is_using_now)) + } else { + if (configDataSourceActivity is ConfigDataSourceActivity) { + configDataSourceActivity.setDataSource(data.file.name) + } else { + configDataSourceActivity.showSnackbar(getString(R.string.activity_is_not_config_data_source_activity)) + } + } + }, + onLongClickListener = { holder, data, _ -> + showItemMenu(holder.itemView, data) + true + } + ))) + } + + fun getDataSourceList() = viewModel.getDataSourceList() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mBinding.root.addFitsSystemWindows(right = true, bottom = true) + } + + override fun onResume() { + super.onResume() + if (isFirstLoadData) { + initData() + isFirstLoadData = false + } + } + + private fun initData() { + mBinding.apply { + rvLocalDataSourceFragment.layoutManager = LinearLayoutManager(activity) + rvLocalDataSourceFragment.adapter = adapter + } + + viewModel.dataSourceList.collectWithLifecycle(viewLifecycleOwner) { data -> + when (data) { + is DataState.Success -> { + adapter.dataList = data.data + } + else -> {} + } + } + + dataSourceDirectoryChanged.collectWithLifecycle(viewLifecycleOwner) { data -> + viewModel.getDataSourceList() + } + } + + private fun showItemMenu(v: View, data: DataSource1Bean) { + val popup = PopupMenu(requireContext(), v) + popup.menuInflater.inflate(R.menu.menu_local_data_source_fragment_item, popup.menu) + + popup.setOnMenuItemClickListener { menuItem: MenuItem -> + when (menuItem.itemId) { + R.id.menu_item_local_data_source_fragment_delete_item -> { + val activity = this.activity ?: return@setOnMenuItemClickListener true + if (activity is ConfigDataSourceActivity) { + activity.deleteDataSource(data) + } else { + activity.showSnackbar(getString(R.string.activity_is_not_config_data_source_activity)) + } + true + } + else -> false + } + } + popup.show() + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentLocalDataSourceBinding.inflate(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/MoreDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/MoreDialogFragment.kt deleted file mode 100644 index f6c8a509..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/MoreDialogFragment.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.skyd.imomoe.view.fragment - -import android.os.Bundle -import android.util.LruCache -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.skyd.imomoe.R -import com.skyd.imomoe.databinding.FragmentMoreDialogBinding - -open class MoreDialogFragment : BottomSheetDialogFragment() { - private var _binding: FragmentMoreDialogBinding? = null - private val mBinding get() = _binding!! - private lateinit var listeners: Array - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NORMAL, R.style.BottomSheetDialogTheme) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - _binding = FragmentMoreDialogBinding.inflate(inflater, container, false) - return mBinding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - mBinding.run { - tvCancelMore.setOnClickListener(listeners[0]) - tvDlna.setOnClickListener(listeners[1]) - tvOpenInOtherPlayer.setOnClickListener(listeners[2]) - } - } - - fun setOnClickListener(listeners: Array): BottomSheetDialogFragment { - this.listeners = listeners - return this - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/MoreFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/MoreFragment.kt index e31fa622..dbc5b002 100644 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/MoreFragment.kt +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/MoreFragment.kt @@ -1,79 +1,147 @@ package com.skyd.imomoe.view.fragment +import android.content.Context import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.GridLayoutManager +import androidx.compose.foundation.layout.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.skyd.imomoe.R -import com.skyd.imomoe.bean.MoreBean -import com.skyd.imomoe.config.Const.ViewHolderTypeString -import com.skyd.imomoe.config.Const.ActionUrl.Companion.ANIME_LAUNCH_ACTIVITY -import com.skyd.imomoe.config.Const.ActionUrl.Companion.ANIME_SKIP_BY_WEBSITE -import com.skyd.imomoe.databinding.FragmentMoreBinding -import com.skyd.imomoe.view.activity.AboutActivity -import com.skyd.imomoe.view.activity.HistoryActivity -import com.skyd.imomoe.view.activity.SettingActivity -import com.skyd.imomoe.view.activity.SkinActivity -import com.skyd.imomoe.view.adapter.MoreAdapter +import com.skyd.imomoe.bean.More1Bean +import com.skyd.imomoe.ext.plus +import com.skyd.imomoe.ext.screenIsLand +import com.skyd.imomoe.route.Router.buildRouteUri +import com.skyd.imomoe.route.processor.ConfigDataSourceActivityProcessor +import com.skyd.imomoe.route.processor.JumpByUrlProcessor +import com.skyd.imomoe.route.processor.StartActivityProcessor +import com.skyd.imomoe.view.activity.* +import com.skyd.imomoe.view.adapter.compose.LazyGridAdapter +import com.skyd.imomoe.view.adapter.compose.proxy.More1Proxy +import com.skyd.imomoe.view.component.compose.AnimeLazyVerticalGrid +import com.skyd.imomoe.view.component.compose.AnimeTopBar +import com.skyd.imomoe.view.component.compose.TopBarIcon -class MoreFragment : BaseFragment() { - private val list: MutableList = ArrayList() - private val adapter: MoreAdapter = MoreAdapter(this, list) - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMoreBinding = - FragmentMoreBinding.inflate(inflater, container, false) - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) +class MoreFragment : BaseComposeFragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return setContentBase { + MoreScreen() + } + } +} - list.add( - MoreBean( - ViewHolderTypeString.MORE_1, - "$ANIME_LAUNCH_ACTIVITY/${HistoryActivity::class.qualifiedName}", - getString(R.string.watch_history), - R.drawable.ic_history_white_24 - ) - ) - list.add( - MoreBean( - ViewHolderTypeString.MORE_1, - ANIME_SKIP_BY_WEBSITE, - getString(R.string.skip_by_website), - R.drawable.ic_insert_link_white_24 - ) - ) - list.add( - MoreBean( - ViewHolderTypeString.MORE_1, - "$ANIME_LAUNCH_ACTIVITY/${SkinActivity::class.qualifiedName}", - getString(R.string.skin_center), - R.drawable.ic_skin_white_32_skin +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun MoreScreen() { + val context = LocalContext.current + Scaffold( + topBar = { + AnimeTopBar( + title = { + Text(text = stringResource(R.string.more)) + }, + navigationIcon = { + TopBarIcon( + painter = painterResource(id = R.drawable.ic_beans_24), + contentDescription = null + ) + }, + contentPadding = { + if (LocalContext.current.screenIsLand) { + PaddingValues( + end = WindowInsets.navigationBars.asPaddingValues() + .calculateEndPadding(LocalLayoutDirection.current) + ) + } else { + PaddingValues() + } + WindowInsets.statusBars.asPaddingValues() + } ) - ) - list.add( - MoreBean( - ViewHolderTypeString.MORE_1, - "$ANIME_LAUNCH_ACTIVITY/${SettingActivity::class.qualifiedName}", - getString(R.string.setting), - R.drawable.ic_settings_white_24 - ) - ) - list.add( - MoreBean( - ViewHolderTypeString.MORE_1, - "$ANIME_LAUNCH_ACTIVITY/${AboutActivity::class.qualifiedName}", - getString(R.string.about), - R.drawable.ic_info_white_24 + } + ) { + val adapter = remember { LazyGridAdapter(mutableListOf(More1Proxy())) } + val dataList = remember { initData(context) } + AnimeLazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + dataList = dataList, + adapter = adapter, + contentPadding = it + PaddingValues(vertical = 6.dp) + PaddingValues( + end = WindowInsets.navigationBars.asPaddingValues() + .calculateEndPadding(LocalLayoutDirection.current) ) ) - - mBinding.run { - rvMoreFragment.layoutManager = GridLayoutManager(activity, 2) - rvMoreFragment.adapter = adapter - } - } - - companion object { - const val TAG = "MoreFragment" } } + +private fun initData(context: Context): List { + val list: MutableList = ArrayList() + list += More1Bean( + StartActivityProcessor.route.buildRouteUri { + appendQueryParameter("cls", HistoryActivity::class.qualifiedName) + }.toString(), + context.getString(R.string.watch_history), + R.drawable.ic_history_24 + ) + list += More1Bean( + JumpByUrlProcessor.route, + context.getString(R.string.skip_by_website), + R.drawable.ic_insert_link_24 + ) + list += More1Bean( + StartActivityProcessor.route.buildRouteUri { + appendQueryParameter("cls", SkinActivity::class.qualifiedName) + }.toString(), + context.getString(R.string.skin_center), + R.drawable.ic_skin_32 + ) + list += More1Bean( + StartActivityProcessor.route.buildRouteUri { + appendQueryParameter("cls", DownloadManagerActivity::class.qualifiedName) + }.toString(), + context.getString(R.string.download_manager), + R.drawable.ic_cloud_download_24 + ) + list += More1Bean( + ConfigDataSourceActivityProcessor.route.buildRouteUri { + appendQueryParameter("selectPageIndex", "1") + }.toString(), + context.getString(R.string.data_source_market), + R.drawable.ic_plugin_24 + ) + list += More1Bean( + StartActivityProcessor.route.buildRouteUri { + appendQueryParameter("cls", SettingActivity::class.qualifiedName) + }.toString(), + context.getString(R.string.setting), + R.drawable.ic_settings_24 + ) + list += More1Bean( + StartActivityProcessor.route.buildRouteUri { + appendQueryParameter("cls", BackupRestoreActivity::class.qualifiedName) + }.toString(), + context.getString(R.string.backup_and_restore), + R.drawable.ic_cloud_done_24 + ) + list += More1Bean( + StartActivityProcessor.route.buildRouteUri { + appendQueryParameter("cls", AboutActivity::class.qualifiedName) + }.toString(), + context.getString(R.string.about), + R.drawable.ic_info_24 + ) + return list +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/RankFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/RankFragment.kt index 34c74f4f..61f6fd50 100644 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/RankFragment.kt +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/RankFragment.kt @@ -2,85 +2,82 @@ package com.skyd.imomoe.view.fragment import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager -import com.skyd.imomoe.R -import com.skyd.imomoe.bean.GetDataEnum import com.skyd.imomoe.databinding.FragmentRankBinding -import com.skyd.imomoe.util.Util.showToast -import com.skyd.imomoe.util.smartNotifyDataSetChanged -import com.skyd.imomoe.view.adapter.AnimeShowAdapter +import com.skyd.imomoe.ext.addFitsSystemWindows +import com.skyd.imomoe.ext.collectWithLifecycle +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeCover11Proxy +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeCover3Proxy import com.skyd.imomoe.viewmodel.RankListViewModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class RankFragment : BaseFragment() { - private var partUrl: String = "" - private lateinit var viewModel: RankListViewModel - private lateinit var adapter: AnimeShowAdapter.GridRecyclerView1Adapter + private val viewModel: RankListViewModel by viewModels() + private val adapter: VarietyAdapter by lazy { + VarietyAdapter(mutableListOf(AnimeCover3Proxy(), AnimeCover11Proxy())) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(RankListViewModel::class.java) - val arguments = arguments - try { - partUrl = arguments?.getString("partUrl") ?: "" + val arguments = arguments + if (viewModel.partUrl.isBlank()) { + viewModel.partUrl = arguments?.getString("partUrl").orEmpty() + } } catch (e: Exception) { e.printStackTrace() e.message?.showToast(Toast.LENGTH_LONG) } } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - adapter = AnimeShowAdapter.GridRecyclerView1Adapter(requireActivity(), viewModel.rankList) - adapter.showRankNumber = true mBinding.run { - rvRankFragment.layoutManager = GridLayoutManager(activity, 4) - .apply { - spanSizeLookup = AnimeShowSpanSize(adapter) - } + rvRankFragment.addFitsSystemWindows(right = true, bottom = true) + rvRankFragment.layoutManager = GridLayoutManager( + activity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { spanSizeLookup = AnimeShowSpanSize(adapter) } rvRankFragment.addItemDecoration(AnimeShowItemDecoration()) - rvRankFragment.setHasFixedSize(true) rvRankFragment.adapter = adapter - srlRankFragment.setOnRefreshListener { - viewModel.getRankListData(partUrl) - } - srlRankFragment.setOnLoadMoreListener { - viewModel.pageNumberBean?.let { - viewModel.getRankListData(it.actionUrl, isRefresh = false) - return@setOnLoadMoreListener - } - mBinding.srlRankFragment.finishLoadMore() - getString(R.string.no_more_info).showToast() - } + srlRankFragment.setOnRefreshListener { viewModel.getRankListData() } + srlRankFragment.setOnLoadMoreListener { viewModel.loadMoreRankListData() } } - viewModel.mldRankData.observe(viewLifecycleOwner, Observer { - mBinding.srlRankFragment.closeHeaderOrFooter() - adapter.smartNotifyDataSetChanged(it.first, it.second, viewModel.rankList) - - when (it.first) { - GetDataEnum.REFRESH, GetDataEnum.LOAD_MORE -> hideLoadFailedTip() - GetDataEnum.FAILED -> { - showLoadFailedTip(getString(R.string.load_data_failed_click_to_retry)) { - viewModel.getRankListData(partUrl) - hideLoadFailedTip() + viewModel.mldRankData.collectWithLifecycle(viewLifecycleOwner) { data -> + when (data) { + is DataState.Empty -> mBinding.srlRankFragment.autoRefresh() + is DataState.Success -> { + adapter.dataList = data.data + hideLoadFailedTip() + mBinding.srlRankFragment.closeHeaderOrFooter() + } + is DataState.Error -> { + showLoadFailedTip { + viewModel.getRankListData() } + mBinding.srlRankFragment.closeHeaderOrFooter() + } + is DataState.Loading -> { + mBinding.srlRankFragment.autoLoadMoreAnimationOnly() } + else -> {} } - }) - - mBinding.srlRankFragment.autoRefresh() + } } override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRankBinding = FragmentRankBinding.inflate(inflater, container, false) - } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/SettingFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/SettingFragment.kt new file mode 100644 index 00000000..d70ae6af --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/SettingFragment.kt @@ -0,0 +1,284 @@ +package com.skyd.imomoe.view.fragment + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.preference.CheckBoxPreference +import androidx.preference.Preference +import com.shuyu.gsyvideoplayer.player.IjkPlayerManager +import com.skyd.imomoe.R +import com.skyd.imomoe.config.Const +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.ext.theme.selectDarkMode +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.net.DnsServer.selectDnsServer +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.util.compare.EpisodeTitleSort +import com.skyd.imomoe.util.compare.EpisodeTitleSort.selectEpisodeTitleSortMode +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.util.update.AppUpdateHelper +import com.skyd.imomoe.util.update.AppUpdateStatus +import com.skyd.imomoe.view.activity.ConfigDataSourceActivity +import com.skyd.imomoe.view.activity.UrlMapActivity +import com.skyd.imomoe.view.component.player.PlayerCore +import com.skyd.imomoe.view.component.player.PlayerCore.selectPlayerCore +import com.skyd.imomoe.view.component.preference.BasePreferenceFragment +import com.skyd.imomoe.viewmodel.SettingViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class SettingFragment : BasePreferenceFragment() { + private val viewModel: SettingViewModel by viewModels() + private var selfUpdateCheck = false + + @Inject + lateinit var appUpdateHelper: AppUpdateHelper + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // 清理历史记录 + viewModel.deleteAllHistory.collectWithLifecycle(viewLifecycleOwner) { data -> + data.second.showToast() + } + viewModel.allHistoryCount.collectWithLifecycle(viewLifecycleOwner) { data -> + findPreference("delete_all_history")?.summary = + getString(R.string.delete_all_history_summary, data) + } + + // 清理缓存文件 + viewModel.cacheSize.collectWithLifecycle(viewLifecycleOwner) { data -> + findPreference("clear_cache")?.summary = + getString(R.string.clear_cache_summary, data) + } + viewModel.clearAllCache.collectWithLifecycle(viewLifecycleOwner) { data -> + data.second.showToast() + } + + appUpdateHelper.getUpdateStatus().collectWithLifecycle(viewLifecycleOwner) { + val text1: String = when (it) { + AppUpdateStatus.UNCHECK -> { + getString(R.string.uncheck_update) +// appUpdateHelper.checkUpdate() + } + AppUpdateStatus.CHECKING -> { + getString(R.string.checking_update) + } + AppUpdateStatus.DATED -> { + if (selfUpdateCheck) appUpdateHelper.noticeUpdate(requireActivity()) + getString(R.string.find_new_version) + } + AppUpdateStatus.VALID -> { + getString(R.string.is_latest_version).apply { + if (selfUpdateCheck) showToast() + } + } + AppUpdateStatus.LATER -> { + getString(R.string.delay_update) + } + AppUpdateStatus.ERROR -> { + getString(R.string.check_update_failed).apply { + if (selfUpdateCheck) showToast() + } + } + } + findPreference("update")?.title = + getString(R.string.check_update_summary, text1) + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.root_preferences, rootKey) + + findPreference("download_path")?.apply { + summary = Const.DownloadAnime.animeFilePath + setOnPreferenceClickListener { + showMessageDialog( + title = getString(R.string.attention), + message = "由于新版Android存储机制变更,因此新缓存的动漫将存储在App的私有路径," + + "以前缓存的动漫依旧能够观看,其后面将有“旧”字样。新缓存的动漫与以前缓存的互不影响。" + + "\n\n注意:新缓存的动漫将在App被卸载或数据被清除后丢失。", + onPositive = { dialog, _ -> dialog.dismiss() } + ) + false + } + } + + findPreference("delete_all_history")?.apply { + setOnPreferenceClickListener { + showMessageDialog( + title = getString(R.string.warning), + message = getString(R.string.confirm_delete_all_history), + icon = R.drawable.ic_delete_24, + positiveText = getString(R.string.delete), + onPositive = { _, _ -> viewModel.deleteAllHistory() }, + onNegative = { dialog, _ -> dialog.dismiss() } + ) + false + } + } + + findPreference("clear_cache")?.apply { + setOnPreferenceClickListener { + showMessageDialog( + title = getString(R.string.warning), + message = "确定清理所有缓存?不包括缓存视频", + icon = R.drawable.ic_sd_storage_24, + positiveText = getString(R.string.clean), + onPositive = { _, _ -> viewModel.clearAllCache() }, + onNegative = { dialog, _ -> dialog.dismiss() } + ) + false + } + } + + findPreference("dark_mode_follow_system")?.apply { + setOnPreferenceClickListener { + requireActivity().selectDarkMode() + false + } + } + + findPreference("disable_screenshot")?.apply { + isChecked = disableScreenshot + setOnPreferenceChangeListener { _, newValue -> + disableScreenshot = newValue as? Boolean ?: false + true + } + } + + findPreference("update")?.apply { + summary = getString(R.string.current_version, Util.getAppVersionName()) + setOnPreferenceClickListener { + selfUpdateCheck = true + when (appUpdateHelper.getUpdateStatus().value) { + AppUpdateStatus.CHECKING -> { + "已在检查,请稍等...".showToast() + } + else -> appUpdateHelper.checkUpdate() + } + false + } + } + + findPreference("custom_data_source")?.apply { + setOnPreferenceClickListener { + startActivity(Intent(activity, ConfigDataSourceActivity::class.java)) + false + } + title = getString( + R.string.custom_data_source, + (DataSourceManager.customDataSourceInfo?.get("name") + ?: DataSourceManager.dataSourceFileName.substringBeforeLast(".")).let { + if (it == DataSourceManager.DEFAULT_DATA_SOURCE) + getString(R.string.default_data_source) + else it + }) + } + + findPreference("dns_server")?.apply { + setOnPreferenceClickListener { + activity?.selectDnsServer() + false + } + } + + findPreference("show_player_bottom_progressbar")?.apply { + isChecked = sharedPreferences().getBoolean("showPlayerBottomProgressbar", false) + setOnPreferenceChangeListener { _, newValue -> + sharedPreferences().editor { + putBoolean("showPlayerBottomProgressbar", newValue as? Boolean ?: false) + } + true + } + } + + findPreference("auto_jump_to_last_position")?.apply { + isChecked = sharedPreferences().getBoolean("autoJumpToLastPosition", true) + setOnPreferenceChangeListener { _, newValue -> + sharedPreferences().editor { + putBoolean("autoJumpToLastPosition", newValue as? Boolean ?: true) + } + true + } + } + + findPreference("store_play_speed")?.apply { + isChecked = sharedPreferences().getBoolean("restorePlaySpeed", false) + setOnPreferenceChangeListener { _, newValue -> + sharedPreferences().editor { + putBoolean("restorePlaySpeed", newValue as? Boolean ?: false) + } + true + } + } + + findPreference("episode_title_sort_mode")?.apply { + summary = getString( + R.string.episode_title_sort_mode_summary, + EpisodeTitleSort.episodeTitleSortMode + ) + setOnPreferenceClickListener { + activity?.selectEpisodeTitleSortMode { + summary = getString(R.string.episode_title_sort_mode_summary, it) + } + false + } + } + + findPreference("player_core")?.apply { + summary = getString(R.string.current_player_core, PlayerCore.playerCore.coreName) + setOnPreferenceClickListener { + activity?.selectPlayerCore { + summary = getString(R.string.current_player_core, it.coreName) + findPreference("media_codec")?.isVisible = + PlayerCore.playerCore.playManager == IjkPlayerManager::class.java + } + false + } + } + + findPreference("media_codec")?.apply { + setOnPreferenceChangeListener { _, newValue -> + PlayerCore.setMediaCodec(newValue as? Boolean ?: false) + true + } + isVisible = PlayerCore.playerCore.playManager == IjkPlayerManager::class.java + } + + findPreference("enable_danmaku_in_local_video")?.apply { + isChecked = sharedPreferences().getBoolean("enableDanmakuInLocalVideo", false) + setOnPreferenceChangeListener { _, newValue -> + if (newValue == true) { + showMessageDialog( + title = getString(R.string.warning), + message = getString(R.string.enable_danmaku_in_local_video_warning), + icon = R.drawable.ic_forum_24, + cancelable = false, + onNegative = { _, _ -> + isChecked = newValue != true + sharedPreferences().editor { + putBoolean("enableDanmakuInLocalVideo", newValue != true) + } + } + ) { dialog, _ -> + dialog.dismiss() + } + } + sharedPreferences().editor { + putBoolean("enableDanmakuInLocalVideo", newValue as? Boolean ?: false) + } + true + } + } + + findPreference("url_map")?.apply { + setOnPreferenceClickListener { + startActivity(Intent(activity, UrlMapActivity::class.java)) + false + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/ShareDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/ShareDialogFragment.kt deleted file mode 100644 index 7fb463e3..00000000 --- a/app/src/main/java/com/skyd/imomoe/view/fragment/ShareDialogFragment.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.skyd.imomoe.view.fragment - -import android.app.Activity -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.skyd.imomoe.R -import com.skyd.imomoe.databinding.FragmentShareDialogBinding -import com.skyd.imomoe.util.Share.SHARE_LINK -import com.skyd.imomoe.util.Share.SHARE_QQ -import com.skyd.imomoe.util.Share.SHARE_WECHAT -import com.skyd.imomoe.util.Share.SHARE_WEIBO -import com.skyd.imomoe.util.Share.share - -open class ShareDialogFragment : BottomSheetDialogFragment() { - private var _binding: FragmentShareDialogBinding? = null - private val mBinding get() = _binding!! - private lateinit var shareContent: String - private lateinit var attachedActivity: Activity - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setStyle(STYLE_NORMAL, R.style.BottomSheetDialogTheme) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - _binding = FragmentShareDialogBinding.inflate(inflater, container, false) - return mBinding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - activity?.let { act -> - attachedActivity = act - mBinding.tvToQq.setOnClickListener { - share(attachedActivity, shareContent, SHARE_QQ) - dismiss() - } - mBinding.tvToWechat.setOnClickListener { - share(attachedActivity, shareContent, SHARE_WECHAT) - dismiss() - } - mBinding.tvToWeibo.setOnClickListener { - share(attachedActivity, shareContent, SHARE_WEIBO) - dismiss() - } - mBinding.tvCopyLink.setOnClickListener { - share(attachedActivity, shareContent, SHARE_LINK) - dismiss() - } - mBinding.tvCancelShare.setOnClickListener { - dismiss() - } - } - } - - fun setShareContent(shareContent: String): BottomSheetDialogFragment { - this.shareContent = shareContent - return this - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/WebDavFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/WebDavFragment.kt new file mode 100644 index 00000000..b222c86e --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/WebDavFragment.kt @@ -0,0 +1,258 @@ +package com.skyd.imomoe.view.fragment + +import android.content.DialogInterface +import android.os.Build +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.fragment.app.viewModels +import androidx.preference.Preference +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.skyd.imomoe.R +import com.skyd.imomoe.ext.* +import com.skyd.imomoe.util.showToast +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.RestoreFile1Proxy +import com.skyd.imomoe.view.component.BottomSheetRecyclerView +import com.skyd.imomoe.view.component.preference.BasePreferenceFragment +import com.skyd.imomoe.viewmodel.WebDavViewModel + +class WebDavFragment : BasePreferenceFragment() { + private val viewModel: WebDavViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + findPreference("backup_app_database_to_cloud")?.title = getString( + R.string.backup_app_database_to_cloud, "" + ) + + viewModel.backup.collectWithLifecycle(viewLifecycleOwner) { + if (it.first == WebDavViewModel.TYPE_APP_DATABASE_DIR) { + findPreference("backup_app_database_to_cloud")?.title = getString( + R.string.backup_app_database_to_cloud, + if (it.second) getString(R.string.backup_succeed) + else getString(R.string.backup_failed) + ) + } + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.webdav_preferences, rootKey) + + findPreference("webdav_server_address")?.apply { + val address = sharedPreferences().getString("webdav_server_address", null).orEmpty() + summary = address + setOnPreferenceClickListener { + var url: String + showInputDialog( + hint = getString(R.string.webdav_server_address), + prefill = address + ) { _, _, text -> + url = text.toString() + if (url.matches(Regex("^[a-zA-z]+://[^\\s]+"))) { + if (!url.endsWith("/")) url += "/" + sharedPreferences().editor { putString("webdav_server_address", url) } + summary = url + } else { + showMessageDialog( + onPositive = { dialog, _ -> dialog.dismiss() }, + message = getString(R.string.wrong_webdav_server_address_format) + ) + } + } + false + } + } + + findPreference("webdav_account")?.apply { + val account = sharedPreferences() + .getString("webdav_account", null).orEmpty() + summary = account + setOnPreferenceClickListener { + showInputDialog( + hint = getString(R.string.webdav_account), + prefill = account + ) { _, _, text -> + sharedPreferences().editor { putString("webdav_account", text.toString()) } + summary = text + } + false + } + } + + findPreference("webdav_password")?.apply { + setOnPreferenceClickListener { + showInputDialog( + hint = getString(R.string.webdav_password), + ) { _, _, text -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + secretSharedPreferences().editor { + putString("webdav_password", text.toString()) + } + } else viewModel.pwd = text.toString() + summary = getString(R.string.webdav_password_set) + } + false + } + summary = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (secretSharedPreferences().getString("webdav_password", null) != null) { + getString(R.string.webdav_password_set) + } else getString(R.string.webdav_password_not_set) + } else { + if (viewModel.pwd.isNotEmpty()) getString(R.string.webdav_password_set) + else getString(R.string.webdav_password_not_set) + } + } + + findPreference("backup_app_database_to_cloud")?.apply { + setOnPreferenceClickListener { + val credential = getWebDAVCredential() + if (!checkWebDAVCredential( + credential.first, + credential.second, + credential.third + ) + ) { + return@setOnPreferenceClickListener false + } + viewModel.backup( + credential.first, credential.second, credential.third, + WebDavViewModel.TYPE_APP_DATABASE_DIR + ) + title = getString( + R.string.backup_app_database_to_cloud, + getString(R.string.backing_up) + ) + false + } + } + + findPreference("restore_app_database_from_cloud")?.apply { + setOnPreferenceClickListener { + val credential = getWebDAVCredential() + if (!checkWebDAVCredential( + credential.first, + credential.second, + credential.third + ) + ) { + return@setOnPreferenceClickListener false + } + viewModel.getFileList( + credential.first, credential.second, credential.third, + WebDavViewModel.TYPE_APP_DATABASE_DIR + ) + showBottomSheetDialog( + WebDavViewModel.TYPE_APP_DATABASE_DIR, + R.string.restore_app_database_bottom_sheet_dialog_tip + ).show() + false + } + } + } + + private fun getWebDAVCredential(): Triple { + val url = sharedPreferences().getString("webdav_server_address", null).orEmpty() + val account = sharedPreferences().getString("webdav_account", null).orEmpty() + val password = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + secretSharedPreferences().getString("webdav_password", null).orEmpty() + } else { + viewModel.pwd + } + return Triple(url, account, password) + } + + private fun checkWebDAVCredential( + url: String, + account: String, + password: String + ): Boolean { + if (url.isBlank()) { + getString(R.string.webdav_server_not_set).showToast() + return false + } + if (account.isBlank()) { + getString(R.string.webdav_account_not_set).showToast() + return false + } + if (password.isBlank()) { + getString(R.string.webdav_password_not_set).showToast() + return false + } + return true + } + + private fun showBottomSheetDialog( + type: String, + @StringRes tipRes: Int = -1 + ): BottomSheetDialog { + val bottomSheetDialog = + BottomSheetDialog(requireActivity(), R.style.BottomSheetDialogTheme) + val contentView = View.inflate(requireActivity(), R.layout.dialog_bottom_sheet_1, null) + bottomSheetDialog.setContentView(contentView) + val tips = contentView.findViewById(R.id.tv_dialog_bottom_sheet_1_tips) + if (tipRes != -1) tips.text = getString(tipRes) + val recyclerView = + contentView.findViewById(R.id.rv_dialog_bottom_sheet_1) + recyclerView.layoutManager = LinearLayoutManager(requireActivity()) + recyclerView.post { recyclerView.scrollToPosition(0) } + val adapter = VarietyAdapter( + mutableListOf(RestoreFile1Proxy(onClickListener = { _, data, _ -> + askRestore(type) { dialog, _ -> + viewModel.restore(data.path, getWebDAVCredential(), type) + getString(R.string.restoring_data).showToast() + dialog.dismiss() + } + }, onLongClickListener = { _, data, _ -> + askDelete(type) { _, _ -> + viewModel.delete(data, getWebDAVCredential(), type) + } + true + })) + ) + val job = viewModel.fileList.collectWithLifecycle(this) { + adapter.dataList = viewModel.fileMap[it].orEmpty() + } + bottomSheetDialog.setOnDismissListener { + job.cancel() + } + recyclerView.adapter = adapter + return bottomSheetDialog + } + + /** + * 弹出询问是否覆盖恢复数据对话框 + */ + private fun askRestore( + type: String, + onPositive: (dialog: DialogInterface, which: Int) -> Unit + ) { + val message = when (type) { + WebDavViewModel.TYPE_APP_DATABASE_DIR -> { + getString(R.string.restore_app_database_warning) + } + else -> null + } + showMessageDialog(onPositive = onPositive, message = message) + } + + /** + * 弹出询问是否删除对话框 + */ + private fun askDelete( + type: String, + onPositive: (dialog: DialogInterface, which: Int) -> Unit + ) { + val message = when (type) { + WebDavViewModel.TYPE_APP_DATABASE_DIR -> { + getString(R.string.delete_remote_file_warning) + } + else -> null + } + showMessageDialog(onPositive = onPositive, message = message) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/BaseBottomSheetDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/BaseBottomSheetDialogFragment.kt new file mode 100644 index 00000000..f0451fcc --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/BaseBottomSheetDialogFragment.kt @@ -0,0 +1,37 @@ +package com.skyd.imomoe.view.fragment.dialog + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +abstract class BaseBottomSheetDialogFragment : BottomSheetDialogFragment() { + private var binding: VB? = null + protected val mBinding get() = binding!! + var onDismissListener: ((DialogInterface) -> Unit)? = null + + protected abstract fun getBinding(inflater: LayoutInflater, container: ViewGroup?): VB + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = getBinding(inflater, container) + this.binding = binding + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDismissListener?.invoke(dialog) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/BaseDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/BaseDialogFragment.kt new file mode 100644 index 00000000..bbbfe2c8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/BaseDialogFragment.kt @@ -0,0 +1,37 @@ +package com.skyd.imomoe.view.fragment.dialog + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +abstract class BaseDialogFragment : DialogFragment() { + private var binding: VB? = null + protected val mBinding get() = binding!! + var onDismissListener: ((DialogInterface) -> Unit)? = null + + protected abstract fun getBinding(inflater: LayoutInflater, container: ViewGroup?): VB + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = getBinding(inflater, container) + this.binding = binding + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDismissListener?.invoke(dialog) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/DanmakuSettingDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/DanmakuSettingDialogFragment.kt new file mode 100644 index 00000000..144a8d1f --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/DanmakuSettingDialogFragment.kt @@ -0,0 +1,140 @@ +package com.skyd.imomoe.view.fragment.dialog + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.annotation.IntRange +import com.kuaishou.akdanmaku.data.DanmakuItemData +import com.skyd.imomoe.databinding.FragmentDanmakuSettingDialogBinding +import com.skyd.imomoe.ext.percentage +import com.skyd.imomoe.view.listener.dsl.setOnSeekBarChangeListener +import java.io.Serializable + + +class DanmakuSettingDialogFragment( + private val fillParentWidth: Boolean = false, + private var filter: ShowDanmakuType = ShowDanmakuType(), + private var allowOverlap: Boolean = true, + @IntRange(from = 0, to = 100) + private var danmakuAlpha: Int = 100, + @IntRange(from = MIN_DANMAKU_SCALE.toLong()) + private var danmakuScale: Int = MIN_DANMAKU_SCALE + 60, + private var danmakuBold: Boolean = true, + private val onDanmakuFilterChanged: ((filter: ShowDanmakuType) -> Unit)? = null, + private val onAllowOverlapChanged: ((allowOverlap: Boolean) -> Unit)? = null, + private val onDanmakuAlphaChanged: ((danmakuAlpha: Int) -> Unit)? = null, + private val onDanmakuScaleChanged: ((danmakuScale: Int) -> Unit)? = null, + private val onDanmakuBoldChanged: ((danmakuBold: Boolean) -> Unit)? = null, +) : BaseDialogFragment() { + companion object { + const val TAG = "DanmakuSettingDialogFragment" + const val MIN_DANMAKU_SCALE: Int = 70 + } + + override fun onStart() { + super.onStart() + if (fillParentWidth) { + dialog?.window?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT + ) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + dialog?.window?.apply { + // make dialog itself transparent + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + // background dim + setDimAmount(0f) + } + + mBinding.apply { + chipDanmakuTypeScroll.isChecked = !filter.scroll + chipDanmakuTypeTop.isChecked = !filter.top + chipDanmakuTypeBottom.isChecked = !filter.bottom + chipDanmakuTypeColor.isChecked = !filter.color + + switchAllowOverlap.isChecked = allowOverlap + + switchBoldDanmaku.isChecked = danmakuBold + + sbDanmakuTextSizeScale.progress = danmakuScale - MIN_DANMAKU_SCALE + tvDanmakuTextSizeScale.text = danmakuScale.percentage + + sbDanmakuAlpha.progress = danmakuAlpha + tvDanmakuAlpha.text = danmakuAlpha.percentage + + chipDanmakuTypeScroll.setOnCheckedChangeListener { _, isChecked -> + filter.scroll = !isChecked + onDanmakuFilterChanged?.invoke(filter) + } + chipDanmakuTypeTop.setOnCheckedChangeListener { _, isChecked -> + filter.top = !isChecked + onDanmakuFilterChanged?.invoke(filter) + } + chipDanmakuTypeBottom.setOnCheckedChangeListener { _, isChecked -> + filter.bottom = !isChecked + onDanmakuFilterChanged?.invoke(filter) + } + chipDanmakuTypeColor.setOnCheckedChangeListener { _, isChecked -> + filter.color = !isChecked + onDanmakuFilterChanged?.invoke(filter) + } + + switchAllowOverlap.setOnCheckedChangeListener { _, isChecked -> + allowOverlap = isChecked + onAllowOverlapChanged?.invoke(allowOverlap) + } + + switchBoldDanmaku.setOnCheckedChangeListener { _, isChecked -> + danmakuBold = isChecked + onDanmakuBoldChanged?.invoke(danmakuBold) + } + + sbDanmakuAlpha.setOnSeekBarChangeListener { + onProgressChanged { seekBar, progress, _ -> + seekBar ?: return@onProgressChanged + danmakuAlpha = progress + tvDanmakuAlpha.text = danmakuAlpha.percentage + onDanmakuAlphaChanged?.invoke(danmakuAlpha) + } + } + + sbDanmakuTextSizeScale.setOnSeekBarChangeListener { + onProgressChanged { seekBar, progress, _ -> + seekBar ?: return@onProgressChanged + danmakuScale = progress + MIN_DANMAKU_SCALE + tvDanmakuTextSizeScale.text = danmakuScale.percentage + onDanmakuScaleChanged?.invoke(danmakuScale) + } + } + } + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentDanmakuSettingDialogBinding.inflate(layoutInflater) + + class ShowDanmakuType( + // 显示是true,不显示是false + var scroll: Boolean = true, + var top: Boolean = true, + var bottom: Boolean = true, + var color: Boolean = true + ) : Serializable { + fun toList(): List> { + return arrayListOf( + scroll to DanmakuItemData.DANMAKU_MODE_ROLLING, + top to DanmakuItemData.DANMAKU_MODE_CENTER_TOP, + bottom to DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM, + color to -1 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/EpisodeDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/EpisodeDialogFragment.kt new file mode 100644 index 00000000..a615f788 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/EpisodeDialogFragment.kt @@ -0,0 +1,92 @@ +package com.skyd.imomoe.view.fragment.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.skyd.imomoe.R +import com.skyd.imomoe.bean.AnimeEpisode1Bean +import com.skyd.imomoe.databinding.FragmentEpisodeDialogBinding +import com.skyd.imomoe.util.AnimeEpisode1ViewHolder +import com.skyd.imomoe.view.adapter.decoration.AnimeShowItemDecoration +import com.skyd.imomoe.view.adapter.spansize.AnimeShowSpanSize +import com.skyd.imomoe.view.adapter.variety.VarietyAdapter +import com.skyd.imomoe.view.adapter.variety.proxy.AnimeEpisode1Proxy + +open class EpisodeDialogFragment( + private val backgroundDim: Boolean = true, + private val offsetFromTop: Int? = null, + private val afterViewCreated: (EpisodeDialogFragment.() -> Unit)? = null +) : BaseBottomSheetDialogFragment() { + companion object { + const val TAG = "EpisodeDialogFragment" + } + + private var onEpisodeClick: (( + holder: AnimeEpisode1ViewHolder, + data: AnimeEpisode1Bean, + index: Int + ) -> Unit)? = null + + private val adapter = VarietyAdapter( + mutableListOf(AnimeEpisode1Proxy(onClickListener = { holder, data, index -> + onEpisodeClick?.invoke(holder, data, index) + }, width = ViewGroup.LayoutParams.MATCH_PARENT)) + ) + + var title: String + get() = mBinding.tvTitleEpisodeDialogFragment.text.toString() + set(value) { + mBinding.tvTitleEpisodeDialogFragment.text = value + } + + var dataList: List + get() = adapter.dataList + set(value) { + adapter.dataList = value + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle( + STYLE_NORMAL, + if (backgroundDim) R.style.BottomSheetDialogTheme + else R.style.BottomSheetDialogTheme_NoBackgroundDim + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mBinding.apply { + btnDismissEpisodeDialogFragment.setOnClickListener { dismiss() } + rvTitleEpisodeDialogFragment.layoutManager = GridLayoutManager( + activity, + AnimeShowSpanSize.MAX_SPAN_SIZE + ).apply { spanSizeLookup = AnimeShowSpanSize(adapter) } + if (rvTitleEpisodeDialogFragment.itemDecorationCount == 0) { + rvTitleEpisodeDialogFragment.addItemDecoration(AnimeShowItemDecoration()) + } + rvTitleEpisodeDialogFragment.adapter = adapter + } + if (offsetFromTop != null) { + (dialog as? BottomSheetDialog)?.behavior?.maxHeight = + requireActivity().findViewById(android.R.id.content).height - offsetFromTop + } + afterViewCreated?.invoke(this) + } + + fun onEpisodeClick( + listener: (( + holder: AnimeEpisode1ViewHolder, + data: AnimeEpisode1Bean, + index: Int + ) -> Unit)? = null + ) { + onEpisodeClick = listener + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentEpisodeDialogBinding.inflate(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/MoreDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/MoreDialogFragment.kt new file mode 100644 index 00000000..1020dcac --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/MoreDialogFragment.kt @@ -0,0 +1,47 @@ +package com.skyd.imomoe.view.fragment.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.skyd.imomoe.R +import com.skyd.imomoe.databinding.FragmentMoreDialogBinding + +open class MoreDialogFragment : BaseBottomSheetDialogFragment() { + companion object { + const val TAG = "MoreDialogFragment" + } + + private var onCancelButtonClick: ((v: View) -> Unit)? = null + private var onDlnaButtonClick: ((v: View) -> Unit)? = null + private var onOpenInOtherPlayerButtonClick: ((v: View) -> Unit)? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.BottomSheetDialogTheme) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mBinding.apply { + tvCancelMore.setOnClickListener { onCancelButtonClick?.invoke(it) } + tvDlna.setOnClickListener { onDlnaButtonClick?.invoke(it) } + tvOpenInOtherPlayer.setOnClickListener { onOpenInOtherPlayerButtonClick?.invoke(it) } + } + } + + fun onCancelButtonClick(listener: ((v: View) -> Unit)? = null) { + onCancelButtonClick = listener + } + + fun onDlnaButtonClick(listener: ((v: View) -> Unit)? = null) { + onDlnaButtonClick = listener + } + + fun onOpenInOtherPlayerButtonClick(listener: ((v: View) -> Unit)? = null) { + onOpenInOtherPlayerButtonClick = listener + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentMoreDialogBinding.inflate(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/SendDanmakuFontDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/SendDanmakuFontDialogFragment.kt new file mode 100644 index 00000000..cd4a9ecc --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/SendDanmakuFontDialogFragment.kt @@ -0,0 +1,79 @@ +package com.skyd.imomoe.view.fragment.dialog + +import android.content.DialogInterface +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.skyd.imomoe.R +import com.skyd.imomoe.databinding.FragmentSendDanmakuFontDialogBinding +import com.skyd.imomoe.view.component.player.danmaku.DanmakuMode +import vadiole.colorpicker.ColorModel +import vadiole.colorpicker.ColorPickerDialog + + +class SendDanmakuFontDialogFragment( + var danmakuMode: DanmakuMode = DanmakuMode.Scroll, + var danmakuColor: Int = Color.WHITE, + private val callback: ((DanmakuMode, Int) -> Unit)? = null +) : BaseDialogFragment() { + companion object { + const val TAG = "SendDanmakuFontDialogFragment" + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + dialog?.window?.apply { + // make dialog itself transparent + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + // background dim + setDimAmount(0f) + } + + mBinding.apply { + tgDanmakuMode.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (isChecked) { + when (checkedId) { + R.id.btn_danmaku_mode_scroll -> danmakuMode = DanmakuMode.Scroll + R.id.btn_danmaku_mode_top -> danmakuMode = DanmakuMode.Top + R.id.btn_danmaku_mode_bottom -> danmakuMode = DanmakuMode.Bottom + } + } + } + + tgDanmakuMode.check( + when (danmakuMode) { + DanmakuMode.Scroll -> R.id.btn_danmaku_mode_scroll + DanmakuMode.Top -> R.id.btn_danmaku_mode_top + DanmakuMode.Bottom -> R.id.btn_danmaku_mode_bottom + } + ) + cvDanmakuColor.setCardBackgroundColor(danmakuColor) + cvDanmakuColor.setOnClickListener { + ColorPickerDialog.Builder() + .setInitialColor(danmakuColor) + .setColorModel(ColorModel.HSV) + .setColorModelSwitchEnabled(true) + .setButtonOkText(R.string.ok) + .setButtonCancelText(R.string.cancel) + .onColorSelected { color: Int -> + danmakuColor = color + cvDanmakuColor.setCardBackgroundColor(color) + } + .create() + .show(childFragmentManager, "color_picker") + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + callback?.invoke(danmakuMode, danmakuColor) + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSendDanmakuFontDialogBinding.inflate(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/ShareDialogFragment.kt b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/ShareDialogFragment.kt new file mode 100644 index 00000000..709b9630 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/fragment/dialog/ShareDialogFragment.kt @@ -0,0 +1,61 @@ +package com.skyd.imomoe.view.fragment.dialog + +import android.app.Activity +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.skyd.imomoe.R +import com.skyd.imomoe.databinding.FragmentShareDialogBinding +import com.skyd.imomoe.util.Share.SHARE_LINK +import com.skyd.imomoe.util.Share.SHARE_QQ +import com.skyd.imomoe.util.Share.SHARE_WECHAT +import com.skyd.imomoe.util.Share.SHARE_WEIBO +import com.skyd.imomoe.util.Share.share +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +open class ShareDialogFragment : BaseBottomSheetDialogFragment() { + @Inject + lateinit var activity: Activity + private lateinit var shareContent: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.BottomSheetDialogTheme) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + mBinding.tvToQq.setOnClickListener { + share(activity, shareContent, SHARE_QQ) + dismiss() + } + mBinding.tvToWechat.setOnClickListener { + share(activity, shareContent, SHARE_WECHAT) + dismiss() + } + mBinding.tvToWeibo.setOnClickListener { + share(activity, shareContent, SHARE_WEIBO) + dismiss() + } + mBinding.tvCopyLink.setOnClickListener { + share(activity, shareContent, SHARE_LINK) + dismiss() + } + mBinding.tvCancelShare.setOnClickListener { + dismiss() + } + } + + fun setShareContent(shareContent: String): BottomSheetDialogFragment { + this.shareContent = shareContent + return this + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentShareDialogBinding.inflate(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnItemSelectedListener.kt b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnItemSelectedListener.kt new file mode 100644 index 00000000..f97734dc --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnItemSelectedListener.kt @@ -0,0 +1,35 @@ +package com.skyd.imomoe.view.listener.dsl + +import android.view.View +import android.widget.AdapterView +import androidx.appcompat.widget.AppCompatSpinner + +fun AppCompatSpinner.setOnItemSelectedListener(init: OnItemSelectedListener.() -> Unit) { + val listener = OnItemSelectedListener() + listener.init() + this.onItemSelectedListener = listener +} + +private typealias OnItemSelected = (parent: AdapterView<*>?, view: View?, position: Int, id: Long) -> Unit +private typealias OnNothingSelected = (parent: AdapterView<*>?) -> Unit + +class OnItemSelectedListener : AdapterView.OnItemSelectedListener { + private var onItemSelected: OnItemSelected? = null + private var onNothingSelected: OnNothingSelected? = null + + fun onItemSelected(itemSelected: OnItemSelected?) { + this.onItemSelected = itemSelected + } + + fun onNothingSelected(nothingSelected: OnNothingSelected?) { + this.onNothingSelected = nothingSelected + } + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + onItemSelected?.invoke(parent, view, position, id) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + onNothingSelected?.invoke(parent) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnPermissionsCallback.kt b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnPermissionsCallback.kt new file mode 100644 index 00000000..0315d5f5 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnPermissionsCallback.kt @@ -0,0 +1,63 @@ +package com.skyd.imomoe.view.listener.dsl + +import com.hjq.permissions.XXPermissions + +fun XXPermissions.requestPermissions(init: OnPermissionsCallback.() -> Unit) { + val listener = OnPermissionsCallback() + listener.init() + this.request(listener) +} + +fun XXPermissions.requestSinglePermission(init: OnSinglePermissionCallback.() -> Unit) { + val listener = OnSinglePermissionCallback() + listener.init() + this.request(listener) +} + +private typealias OnGranted = (permissions: MutableList?, all: Boolean) -> Unit +private typealias OnDenied = (permissions: MutableList?, never: Boolean) -> Unit + +class OnPermissionsCallback : com.hjq.permissions.OnPermissionCallback { + private var onGranted: OnGranted? = null + private var onDenied: OnDenied? = null + + fun onGranted(onGranted: OnGranted?) { + this.onGranted = onGranted + } + + fun onDenied(denied: OnDenied?) { + this.onDenied = denied + } + + override fun onGranted(permissions: MutableList?, all: Boolean) { + onGranted?.invoke(permissions, all) + } + + override fun onDenied(permissions: MutableList?, never: Boolean) { + onDenied?.invoke(permissions, never) + } +} + +private typealias OnSingleGranted = () -> Unit +private typealias OnSingleDenied = (never: Boolean) -> Unit + +class OnSinglePermissionCallback : com.hjq.permissions.OnPermissionCallback { + private var onSingleGranted: OnSingleGranted? = null + private var onSingleDenied: OnSingleDenied? = null + + fun onGranted(onSingleGranted: OnSingleGranted?) { + this.onSingleGranted = onSingleGranted + } + + fun onDenied(onSingleDenied: OnSingleDenied?) { + this.onSingleDenied = onSingleDenied + } + + override fun onGranted(permissions: MutableList?, all: Boolean) { + onSingleGranted?.invoke() + } + + override fun onDenied(permissions: MutableList?, never: Boolean) { + onSingleDenied?.invoke(never) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnSeekBarChangeListener.kt b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnSeekBarChangeListener.kt new file mode 100644 index 00000000..4ea6fc5d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnSeekBarChangeListener.kt @@ -0,0 +1,43 @@ +package com.skyd.imomoe.view.listener.dsl + +import android.widget.SeekBar + +fun SeekBar.setOnSeekBarChangeListener(init: OnSeekBarChangeListener.() -> Unit) { + val listener = OnSeekBarChangeListener() + listener.init() + this.setOnSeekBarChangeListener(listener) +} + +private typealias OnProgressChanged = (seekBar: SeekBar?, progress: Int, fromUser: Boolean) -> Unit +private typealias OnStartTrackingTouch = (seekBar: SeekBar?) -> Unit +private typealias OnStopTrackingTouch = (seekBar: SeekBar?) -> Unit + +class OnSeekBarChangeListener : SeekBar.OnSeekBarChangeListener { + private var onProgressChanged: OnProgressChanged? = null + private var onStartTrackingTouch: OnStartTrackingTouch? = null + private var onStopTrackingTouch: OnStopTrackingTouch? = null + + fun onProgressChanged(onProgressChanged: OnProgressChanged?) { + this.onProgressChanged = onProgressChanged + } + + fun onStartTrackingTouch(onStartTrackingTouch: OnStartTrackingTouch?) { + this.onStartTrackingTouch = onStartTrackingTouch + } + + fun onStopTrackingTouch(onStopTrackingTouch: OnStopTrackingTouch?) { + this.onStopTrackingTouch = onStopTrackingTouch + } + + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + onProgressChanged?.invoke(seekBar, progress, fromUser) + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + onStartTrackingTouch?.invoke(seekBar) + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + onStopTrackingTouch?.invoke(seekBar) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnTabSelectedListener.kt b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnTabSelectedListener.kt new file mode 100644 index 00000000..08b5866a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/view/listener/dsl/OnTabSelectedListener.kt @@ -0,0 +1,43 @@ +package com.skyd.imomoe.view.listener.dsl + +import com.google.android.material.tabs.TabLayout + +fun TabLayout.addOnTabSelectedListener(init: OnTabSelectedListener.() -> Unit) { + val listener = OnTabSelectedListener() + listener.init() + this.addOnTabSelectedListener(listener) +} + +private typealias OnTabSelected = (tab: TabLayout.Tab?) -> Unit +private typealias OnTabUnselected = (tab: TabLayout.Tab?) -> Unit +private typealias OnTabReselected = (tab: TabLayout.Tab?) -> Unit + +class OnTabSelectedListener : TabLayout.OnTabSelectedListener { + private var onTabSelected: OnTabSelected? = null + private var onTabUnselected: OnTabUnselected? = null + private var onTabReselected: OnTabReselected? = null + + fun onTabSelected(onTabSelected: OnTabSelected?) { + this.onTabSelected = onTabSelected + } + + fun onTabUnselected(onTabUnselected: OnTabUnselected?) { + this.onTabUnselected = onTabUnselected + } + + fun onTabReselected(onTabReselected: OnTabReselected?) { + this.onTabReselected = onTabReselected + } + + override fun onTabSelected(tab: TabLayout.Tab?) { + onTabSelected?.invoke(tab) + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + onTabUnselected?.invoke(tab) + } + + override fun onTabReselected(tab: TabLayout.Tab?) { + onTabReselected?.invoke(tab) + } +} diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDetailViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDetailViewModel.kt index cb2cb76f..c1de0ff1 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDetailViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDetailViewModel.kt @@ -1,47 +1,80 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.* -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.AnimeDetailModel +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.FavoriteAnimeBean +import com.skyd.imomoe.bean.ImageBean +import com.skyd.imomoe.database.getAppDataBase +import com.skyd.imomoe.ext.request import com.skyd.imomoe.model.interfaces.IAnimeDetailModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject +@HiltViewModel +class AnimeDetailViewModel @Inject constructor( + private val animeDetailModel: IAnimeDetailModel +) : ViewModel() { + var cover: ImageBean = ImageBean("", "", "") + var title: String = "" + var animeDetailList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + var partUrl: String = "" + var favorite: MutableStateFlow = MutableStateFlow(false) -class AnimeDetailViewModel : ViewModel() { - private val animeDetailModel: IAnimeDetailModel by lazy { - DataSourceManager.create(IAnimeDetailModel::class.java) ?: AnimeDetailModel() + fun getAnimeDetailData() { + queryFavorite() + request(request = { animeDetailModel.getAnimeDetailData(partUrl) }, success = { + cover = it.first + title = it.second + animeDetailList.tryEmit(DataState.Success(it.third)) + refreshAnimeCover() // 更新数据库中番剧封面地址 + }, error = { + animeDetailList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) + } + + // 查询是否追番 + fun queryFavorite() { + request(request = { + getAppDataBase().favoriteAnimeDao().getFavoriteAnime(partUrl) + }, success = { favorite.tryEmit(it != null) }) + } + + // 取消追番 + fun deleteFavorite() { + request(request = { + getAppDataBase().favoriteAnimeDao().deleteFavoriteAnime(partUrl) + }, success = { + appContext.getString(R.string.remove_favorite_succeed).showToast() + favorite.tryEmit(false) + }) } - var cover: ImageBean = ImageBean("", "", "", "") - var title: String = "" - var animeDetailList: MutableList = ArrayList() - var mldAnimeDetailList: MutableLiveData>> = - MutableLiveData() - //www.yhdm.io - fun getAnimeDetailData(partUrl: String) { - viewModelScope.launch(Dispatchers.IO) { - try { - animeDetailModel.getAnimeDetailData(partUrl).apply { - cover = first - title = second - mldAnimeDetailList.postValue(Pair(GetDataEnum.REFRESH, third)) - } - } catch (e: Exception) { - mldAnimeDetailList.postValue(Pair(GetDataEnum.FAILED, ArrayList())) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() - } - } + // 追番 + fun insertFavorite() { + request(request = { + getAppDataBase().favoriteAnimeDao().insertFavoriteAnime( + FavoriteAnimeBean( + "", + partUrl, + title, + System.currentTimeMillis(), + cover + ) + ) + }, success = { + appContext.getString(R.string.favorite_succeed).showToast() + favorite.tryEmit(true) + }) } - companion object { - const val TAG = "AnimeDetailViewModel" + fun refreshAnimeCover() { + request(request = { + getAppDataBase().favoriteAnimeDao().updateFavoriteAnimeCover(partUrl, cover) + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDownloadViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDownloadViewModel.kt index f905a4d8..f4eff532 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDownloadViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeDownloadViewModel.kt @@ -1,31 +1,41 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.bean.AnimeCoverBean +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.AnimeCover7Bean import com.skyd.imomoe.config.Const import com.skyd.imomoe.database.entity.AnimeDownloadEntity import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.util.comparator.EpisodeTitleComparator -import com.skyd.imomoe.util.MD5.getMD5 -import com.skyd.imomoe.util.Util.getDirectorySize -import com.skyd.imomoe.util.Util.getFileSize -import com.skyd.imomoe.util.Util.getFormatSize -import com.skyd.imomoe.util.downloadanime.AnimeDownloadHelper.Companion.deleteAnimeFromXml -import com.skyd.imomoe.util.downloadanime.AnimeDownloadHelper.Companion.getAnimeFromXml -import com.skyd.imomoe.util.downloadanime.AnimeDownloadHelper.Companion.save2Xml -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.ext.formatSize +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.ext.toMD5 +import com.skyd.imomoe.route.Router.buildRouteUri +import com.skyd.imomoe.route.processor.EpisodeDownloadProcessor +import com.skyd.imomoe.route.processor.PlayDownloadM3U8Processor +import com.skyd.imomoe.route.processor.PlayDownloadProcessor +import com.skyd.imomoe.util.compare.EpisodeTitleSort.sortEpisodeTitle +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadHelper.deleteAnimeFromXml +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadHelper.getAnimeFromXml +import com.skyd.imomoe.util.download.downloadanime.AnimeDownloadHelper.save2Xml +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import java.io.File class AnimeDownloadViewModel : ViewModel() { - var animeCoverList: MutableList = ArrayList() - var mldAnimeCoverList: MutableLiveData = MutableLiveData() + var mode = 0 //0是默认的,是番剧;1是番剧每一集 + var actionBarTitle = "" + var directoryName = "" + var path = 0 + val animeCoverList: MutableStateFlow = + MutableStateFlow(AnimeDownloadUiState.None) + val delete: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) fun getAnimeCover() { - viewModelScope.launch(Dispatchers.IO) { + request(request = { val files = arrayOf(File(Const.DownloadAnime.animeFilePath).listFiles(), Const.DownloadAnime.run { new = false @@ -33,7 +43,7 @@ class AnimeDownloadViewModel : ViewModel() { new = true f }) - animeCoverList.clear() + val list: MutableList = ArrayList() for (i: Int in 0..1) { files[i]?.let { for (file in it) { @@ -42,30 +52,39 @@ class AnimeDownloadViewModel : ViewModel() { //查找文件名不以.temp结尾的文件 !s.endsWith(".temp") && !s.endsWith(".xml") }?.size - animeCoverList.add( - animeCoverList.size, AnimeCoverBean( - Const.ViewHolderTypeString.ANIME_COVER_7, - Const.ActionUrl.ANIME_ANIME_DOWNLOAD_EPISODE + "/" + file.name, - "", - file.name, - null, - "", - size = getFormatSize(getDirectorySize(file).toDouble()), + list.add( + AnimeCover7Bean( + EpisodeDownloadProcessor.route.buildRouteUri { + appendQueryParameter("directoryName", file.name) + appendQueryParameter( + "type", + (if (i == 0) 0 else 1).toString() + ) + appendQueryParameter("animeTitle", file.name) + }.toString(), + title = file.name, + size = file.formatSize(), episodeCount = episodeCount.toString() + "P", - path = if (i == 0) 0 else 1 + path = file.path, + pathType = if (i == 0) 0 else 1 ) ) } } } } - mldAnimeCoverList.postValue(true) - } + list + }, success = { + animeCoverList.tryEmit(AnimeDownloadUiState.Success(it)) + }, error = { + animeCoverList.tryEmit(AnimeDownloadUiState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } - fun getAnimeCoverEpisode(directoryName: String, path: Int = 0) { + fun getAnimeCoverEpisode() { //不支持重命名文件 - viewModelScope.launch(Dispatchers.IO) { + request(request = { val animeFilePath = if (path == 0) Const.DownloadAnime.animeFilePath else { Const.DownloadAnime.new = false @@ -74,8 +93,7 @@ class AnimeDownloadViewModel : ViewModel() { p } val files = File(animeFilePath + directoryName).listFiles() - animeCoverList.clear() - files?.let { + if (files != null) { val animeList = getAnimeFromXml(directoryName, animeFilePath) // xml里的文件名 @@ -83,7 +101,7 @@ class AnimeDownloadViewModel : ViewModel() { // 文件夹下的文件名 val filesName: MutableList = ArrayList() // 获取文件夹下的文件名 - for (file in it) filesName.add(file.name) + for (file in files) filesName.add(file.name) //数据库中的数据 val animeMd5InDB = getAppDataBase().animeDownloadDao().getAnimeDownloadMd5List() // 先删除xml里被用户删除的视频,再获取xml里的文件名(保证xml里的文件名都是存在的文件) @@ -102,12 +120,12 @@ class AnimeDownloadViewModel : ViewModel() { } } // 没有在xml里的视频 - for (file in it) { + for (file in files) { if (file.name !in animeFilesName) { // 试图从数据库中取出不在xml里的视频的数据,如果没找到则是null val unsavedAnime: AnimeDownloadEntity? = getAppDataBase().animeDownloadDao() - .getAnimeDownload(getMD5(file) ?: "") + .getAnimeDownload(file.toMD5() ?: "") if (unsavedAnime != null && unsavedAnime.fileName == null) { unsavedAnime.fileName = file.name getAppDataBase().animeDownloadDao() @@ -120,40 +138,76 @@ class AnimeDownloadViewModel : ViewModel() { } } + val list: MutableList = ArrayList() for (anime in animeList) { - val fileName = - animeFilePath + directoryName.substring(1, directoryName.length) + - "/" + anime.fileName - animeCoverList.add( - AnimeCoverBean( - Const.ViewHolderTypeString.ANIME_COVER_7, - (if (fileName.endsWith(".m3u8", true)) - Const.ActionUrl.ANIME_ANIME_DOWNLOAD_M3U8 - else Const.ActionUrl.ANIME_ANIME_DOWNLOAD_PLAY) - + "/" + fileName, - "", - anime.title, - null, - "", - size = getFormatSize( - getFileSize( - File( - animeFilePath + - directoryName + "/" + anime.fileName - ) - ).toDouble() - ), - path = path + val fileName = animeFilePath + directoryName + "/" + anime.fileName + var route = if (fileName.endsWith(".m3u8", true)) { + PlayDownloadM3U8Processor.route + } else { + PlayDownloadProcessor.route + } + route = route.buildRouteUri { + appendQueryParameter("filePath", fileName) + appendQueryParameter("animeTitle", actionBarTitle) + appendQueryParameter("episodeTitle", anime.title) + }.toString() + list.add( + AnimeCover7Bean( + route, + title = anime.title, + size = File(animeFilePath + directoryName + "/" + anime.fileName).formatSize(), + path = fileName, + pathType = path ) ) } - animeCoverList.sortWith(EpisodeTitleComparator()) + list.sortEpisodeTitle() + } else { + emptyList() } - mldAnimeCoverList.postValue(true) - } + }, success = { + animeCoverList.tryEmit(AnimeDownloadUiState.Success(it)) + }, error = { + animeCoverList.tryEmit(AnimeDownloadUiState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) + } + + private var deleteJob: Job? = null + + fun cancelDelete() { + deleteJob?.cancel() } - companion object { - const val TAG = "AnimeDownloadViewModel" + fun delete(path: String) { + deleteJob = request(request = { + val file = File(path) + file.deleteRecursively() + }, success = { + delete.tryEmit(if (it) DeleteUiState.Success(path) else DeleteUiState.Failed(path)) + }, error = { + delete.tryEmit(DeleteUiState.Failed(path)) + it.message?.showToast() + }, finish = { deleteJob = null }) } +} + +sealed interface DeleteUiState { + object None : DeleteUiState + data class Success(val message: String = "") : DeleteUiState + data class Failed(val message: String = "") : DeleteUiState + + fun getMessageData(): String { + return if (this is Success) message else if (this is Failed) message else "" + } +} + +sealed interface AnimeDownloadUiState { + object None : AnimeDownloadUiState + data class Success(override val dataList: List) : WithData(dataList) + data class Error(val message: String = "") : AnimeDownloadUiState + data class Refreshing(override val dataList: List? = null) : WithData(dataList) + + abstract class WithData(open val dataList: List? = null) : AnimeDownloadUiState + fun readOrNull(): List? = (this as? WithData)?.dataList } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeShowViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeShowViewModel.kt index aaa17091..96358118 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeShowViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/AnimeShowViewModel.kt @@ -1,61 +1,55 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.GetDataEnum -import com.skyd.imomoe.bean.IAnimeShowBean +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.AnimeShowModel +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.ext.tryEmitError +import com.skyd.imomoe.ext.tryEmitLoadMore import com.skyd.imomoe.model.interfaces.IAnimeShowModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import com.skyd.imomoe.view.adapter.SerializableRecycledViewPool -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject -class AnimeShowViewModel : ViewModel() { - private val animeShowModel: IAnimeShowModel by lazy { - DataSourceManager.create(IAnimeShowModel::class.java) ?: AnimeShowModel() - } - var childViewPool: SerializableRecycledViewPool? = null - var viewPool: SerializableRecycledViewPool? = null - var animeShowList: MutableList = ArrayList() - var mldGetAnimeShowList: MutableLiveData>> = - MutableLiveData() // value:-1错误;0重新获取;1刷新 - var pageNumberBean: PageNumberBean? = null - - private var isRequesting = false +@HiltViewModel +class AnimeShowViewModel @Inject constructor( + private val animeShowModel: IAnimeShowModel +) : ViewModel() { + var partUrl: String = "" + val animeShowList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + private var pageNumberBean: PageNumberBean? = null - //http://www.yhdm.io版本 - fun getAnimeShowData(partUrl: String, isRefresh: Boolean = true) { - viewModelScope.launch(Dispatchers.IO) { - try { - if (isRequesting) return@launch - isRequesting = true - pageNumberBean = null - animeShowModel.getAnimeShowData(partUrl).apply { - pageNumberBean = second - mldGetAnimeShowList.postValue( - Pair( - if (isRefresh) GetDataEnum.REFRESH else GetDataEnum.LOAD_MORE, first - ) - ) - isRequesting = false - } - } catch (e: Exception) { - mldGetAnimeShowList.postValue(Pair(GetDataEnum.FAILED, ArrayList())) - isRequesting = false - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() - } - } + fun getAnimeShowData() { + pageNumberBean = null + animeShowList.tryEmit(DataState.Refreshing) + request(request = { animeShowModel.getAnimeShowData(partUrl) }, success = { + pageNumberBean = it.second + animeShowList.tryEmit(DataState.Success(it.first.toMutableList())) + }, error = { + animeShowList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } - companion object { - const val TAG = "AnimeShowViewModel" + fun loadMoreAnimeShowData() { + val partUrl = pageNumberBean?.route + val oldData = animeShowList.value + animeShowList.tryEmit(DataState.Loading) + if (partUrl == null) { + animeShowList.tryEmit(oldData) + appContext.getString(R.string.no_more_info).showToast() + return + } + request(request = { animeShowModel.getAnimeShowData(partUrl) }, success = { + pageNumberBean = it.second + animeShowList.tryEmitLoadMore(oldData, it.first.toMutableList()) + }, error = { + animeShowList.tryEmitError(oldData, it.message) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/ClassifyViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/ClassifyViewModel.kt index 89cab089..5268c221 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/ClassifyViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/ClassifyViewModel.kt @@ -1,35 +1,34 @@ package com.skyd.imomoe.viewmodel import android.app.Activity -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.ClassifyBean -import com.skyd.imomoe.bean.GetDataEnum import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.ClassifyModel +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.ext.tryEmitError +import com.skyd.imomoe.ext.tryEmitLoadMore import com.skyd.imomoe.model.interfaces.IClassifyModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject -class ClassifyViewModel : ViewModel() { - private val classifyModel: IClassifyModel by lazy { - DataSourceManager.create(IClassifyModel::class.java) ?: ClassifyModel() - } - var isRequesting = false - var classifyTabList: MutableList = ArrayList() //上方分类数据 - var mldClassifyTabList: MutableLiveData, GetDataEnum>> = - MutableLiveData() - var classifyList: MutableList = ArrayList() //下方tv数据 - var mldClassifyList: MutableLiveData>> = - MutableLiveData() - var pageNumberBean: PageNumberBean? = null +@HiltViewModel +class ClassifyViewModel @Inject constructor( + private val classifyModel: IClassifyModel +) : ViewModel() { + var classifyTabTitle: String = "" //如 地区 + var classifyTitle: String = "" //如 大陆 + var currentPartUrl: String = "" + private var isRequesting = false + val classifyTabList: MutableStateFlow>> = + MutableStateFlow(DataState.Empty) + val classifyList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + private var pageNumberBean: PageNumberBean? = null fun setActivity(activity: Activity) { classifyModel.setActivity(activity) @@ -40,41 +39,48 @@ class ClassifyViewModel : ViewModel() { } fun getClassifyTabData() { - viewModelScope.launch(Dispatchers.IO) { - try { - mldClassifyTabList.postValue( - Pair(classifyModel.getClassifyTabData(), GetDataEnum.REFRESH) - ) - } catch (e: Exception) { - classifyTabList.clear() - mldClassifyTabList.postValue(Pair(ArrayList(), GetDataEnum.FAILED)) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() - } - } + classifyTabList.tryEmit(DataState.Refreshing) + request(request = { classifyModel.getClassifyTabData() }, success = { + classifyTabList.tryEmit(DataState.Success(it)) + }, error = { + classifyTabList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } - fun getClassifyData(partUrl: String, isRefresh: Boolean = true) { - viewModelScope.launch(Dispatchers.IO) { - try { - if (isRequesting) return@launch - isRequesting = true - classifyModel.getClassifyData(partUrl).apply { - pageNumberBean = second - mldClassifyList.postValue( - Pair(if (isRefresh) GetDataEnum.REFRESH else GetDataEnum.LOAD_MORE, first) - ) - } - } catch (e: Exception) { - pageNumberBean = null - mldClassifyList.postValue(Pair(GetDataEnum.FAILED, ArrayList())) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() - } - } + fun getClassifyData(partUrl: String) { + if (isRequesting) return + isRequesting = true + classifyList.tryEmit(DataState.Refreshing) + request(request = { classifyModel.getClassifyData(partUrl) }, success = { + pageNumberBean = it.second + classifyList.tryEmit(DataState.Success(it.first)) + }, error = { + pageNumberBean = null + classifyList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }, finish = { isRequesting = false }) } - companion object { - const val TAG = "ClassifyViewModel" + fun loadMoreClassifyData() { + if (isRequesting) return + isRequesting = true + val oldData = classifyList.value + classifyList.tryEmit(DataState.Loading) + val partUrl = pageNumberBean?.route + if (partUrl == null) { + classifyList.tryEmit(oldData) + appContext.getString(R.string.no_more_info).showToast() + isRequesting = false + return + } + request(request = { classifyModel.getClassifyData(partUrl) }, success = { + pageNumberBean = it.second + classifyList.tryEmitLoadMore(oldData = oldData, newData = it.first) + }, error = { + pageNumberBean = null + classifyList.tryEmitError(oldData, it.message) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }, finish = { isRequesting = false }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/ConfigDataSourceViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/ConfigDataSourceViewModel.kt new file mode 100644 index 00000000..719fb10a --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/ConfigDataSourceViewModel.kt @@ -0,0 +1,31 @@ +package com.skyd.imomoe.viewmodel + +import androidx.lifecycle.ViewModel +import com.skyd.imomoe.bean.DataSource1Bean +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.net.RetrofitManager +import com.skyd.imomoe.util.Util +import kotlinx.coroutines.flow.MutableSharedFlow + + +class ConfigDataSourceViewModel : ViewModel() { + var deleteSource: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + + fun resetDataSource() = setDataSource(DataSourceManager.DEFAULT_DATA_SOURCE) + + fun clearDataSourceCache() { + DataSourceManager.clearCache() + RetrofitManager.setInstanceNull() + } + + fun setDataSource(name: String) { + DataSourceManager.setDataSourceNameSynchronously(name) + DataSourceManager.clearCache() + RetrofitManager.setInstanceNull() + Util.restartApp() + } + + fun deleteDataSource(bean: DataSource1Bean) { + deleteSource.tryEmit(bean.file.delete()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/DataSourceMarketViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/DataSourceMarketViewModel.kt new file mode 100644 index 00000000..8cc7c29d --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/DataSourceMarketViewModel.kt @@ -0,0 +1,162 @@ +package com.skyd.imomoe.viewmodel + +import androidx.lifecycle.ViewModel +import com.arialyy.aria.core.download.DownloadEntity +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.bean.DataSource1Bean +import com.skyd.imomoe.bean.DataSourceRepositoryBean +import com.skyd.imomoe.ext.dataSourceDirectoryChanged +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.net.RetrofitManager +import com.skyd.imomoe.net.service.DataSourceService +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow + + +class DataSourceMarketViewModel : ViewModel() { + var dataSourceMarketList: MutableStateFlow>> = + MutableStateFlow(DataState.Empty) + var localDataSourceMap = hashMapOf() + val askAddUrlMap: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + + init { + getDataSourceMarketList() + } + + fun getDataSourceMarketList( + interfaceVersion: String = com.skyd.imomoe.model.interfaces.interfaceVersion + ) { + dataSourceMarketList.tryEmit(DataState.Refreshing) + request(request = { + RetrofitManager.get().create(DataSourceService::class.java).getDataSourceJson() + }, success = { + localDataSourceMap.clear() + DataSourceManager.getDataSourceList(DataSourceManager.getJarDirectory()) + .forEach { item -> + localDataSourceMap[item.name] = item + } + it.dataSourceList.forEach { item -> + item.interfaceVersion = interfaceVersion + val local = localDataSourceMap[item.name] + if (local == null) { + item.status = DataSourceRepositoryBean.Status.NONE + } else if ((local.versionCode ?: -1) < item.versionCode) { + item.status = DataSourceRepositoryBean.Status.OUTDATED + } else if ((local.versionCode ?: -1) == item.versionCode) { + item.status = DataSourceRepositoryBean.Status.NEWEST + } + } + dataSourceMarketList.tryEmit(DataState.Success(it.dataSourceList)) + }, error = { + if (it.message?.contains("timeout") == true) { + askAddUrlMap.tryEmit(true) + } + dataSourceMarketList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) + } + + fun onTaskPreStart( + entity: DownloadEntity, + dataSourceTitleMap: HashMap + ) { + request(request = { + updateStatus( + dataSourceTitleMap[entity.url], + DataSourceRepositoryBean.Status.DOWNLOADING + ) + }, success = { + dataSourceMarketList.tryEmit(DataState.Success(it)) + }, error = { + it.message?.showToast() + }) + } + + fun onTaskRunning(entity: DownloadEntity) { + } + + fun onTaskComplete( + entity: DownloadEntity, + dataSourceTitleMap: HashMap + ) { + request(request = { + val dataSourceTitle = dataSourceTitleMap[entity.url] + updateStatus( + dataSourceTitleMap[entity.url], + DataSourceRepositoryBean.Status.NEWEST + ).apply { + appContext.getString(R.string.data_source_market_download_complete, dataSourceTitle) + .showToast() + dataSourceDirectoryChanged.tryEmit(true) + } + }, success = { + dataSourceMarketList.tryEmit(DataState.Success(it)) + if (DataSourceManager.customDataSourceInfo?.get("name") == dataSourceTitleMap[entity.url] || + DataSourceManager.dataSourceFileName.substringBeforeLast(".") == dataSourceTitleMap[entity.url] + ) { + DataSourceManager.clearCache() + RetrofitManager.setInstanceNull() + Util.restartApp() + } + }, error = { + it.message?.showToast() + }) + } + + fun onTaskCancel( + entity: DownloadEntity, + dataSourceTitleMap: HashMap + ) { + request(request = { + var dataList = dataSourceMarketList.value.readOrNull().orEmpty() + dataList = dataList.toMutableList().map { + var result: Any = it + if (it is DataSourceRepositoryBean && it.name == dataSourceTitleMap[entity.url]) { + val local = localDataSourceMap[it.name] + val status: DataSourceRepositoryBean.Status = if (local == null) { + DataSourceRepositoryBean.Status.NONE + } else if ((local.versionCode ?: -1) < it.versionCode) { + DataSourceRepositoryBean.Status.OUTDATED + } else if ((local.versionCode ?: -1) == it.versionCode) { + DataSourceRepositoryBean.Status.NEWEST + } else DataSourceRepositoryBean.Status.NONE + // 若最新数据有变化,则new一个新的bean替换之前的bean + // 注意:此处必须要new,不能直接更改之前的bean,否则Diff检测不出差异(旧数据被更改) + result = (it.clone() as DataSourceRepositoryBean).apply { + this.status = status + } + } + result + } + dataList + }, success = { + dataSourceMarketList.tryEmit(DataState.Success(it)) + }, error = { + it.message?.showToast() + }) + } + + private fun updateStatus( + dataSourceTitle: String?, + status: DataSourceRepositoryBean.Status + ): List { + var dataList = dataSourceMarketList.value.readOrNull().orEmpty() + dataList = dataList.toMutableList().map { + var result: Any = it + if (it is DataSourceRepositoryBean && it.name == dataSourceTitle) { + // 若最新数据有变化,则new一个新的bean替换之前的bean + // 注意:此处必须要new,不能直接更改之前的bean,否则Diff检测不出差异(旧数据被更改) + result = (it.clone() as DataSourceRepositoryBean).apply { + this.status = status + } + } + result + } + return dataList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/DlnaViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/DlnaViewModel.kt new file mode 100644 index 00000000..ab93ea0f --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/DlnaViewModel.kt @@ -0,0 +1,89 @@ +package com.skyd.imomoe.viewmodel + +import androidx.lifecycle.ViewModel +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.util.Util +import com.skyd.imomoe.util.dlna.Utils.isLocalMediaAddress +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.flow.MutableStateFlow +import org.fourthline.cling.model.meta.Device + + +class DlnaViewModel : ViewModel() { + val uiState: MutableStateFlow = MutableStateFlow(DlnaUiState.None) + + fun initData(url: String, title: String) { + uiState.tryEmit(DlnaUiState.Initializing(url = url, title = title)) + request(request = { + // 视频不是本地文件 + if (!url.isLocalMediaAddress()) { + Util.getRedirectUrl(url) + } else url + }, success = { + uiState.tryEmit(DlnaUiState.Initialized(url = it, title = title)) + }, error = { + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) + } + + fun addDevice(device: Device<*, *, *>) { + when (val currentState = uiState.value) { + is DlnaUiState.Initialized -> { + uiState.tryEmit( + DlnaUiState.Searching( + url = currentState.url, + title = currentState.title, + dataList = listOf(device) + ) + ) + } + is DlnaUiState.Searching -> { + uiState.tryEmit( + DlnaUiState.Searching( + url = currentState.url, + title = currentState.title, + dataList = currentState.dataList.toMutableList() + device + ) + ) + } + else -> {} + } + } + + fun removeDevice(device: Device<*, *, *>) { + when (val currentState = uiState.value) { + is DlnaUiState.Searching -> { + uiState.tryEmit( + DlnaUiState.Searching( + url = currentState.url, + title = currentState.title, + dataList = currentState.dataList.toMutableList() - device + ) + ) + } + else -> {} + } + } +} + +sealed class DlnaUiState(open val url: String, open val title: String) { + object None : DlnaUiState("", "") + + data class Initializing( + override val url: String, override val title: String + ) : DlnaUiState(url, title) + + data class Initialized( + override val url: String, override val title: String + ) : DlnaUiState(url, title) + + data class Searching( + override val url: String, + override val title: String, + val dataList: List + ) : DlnaUiState(url, title) + + fun readOrNull(): List? = (this as? Searching)?.dataList +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/DownloadManagerViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/DownloadManagerViewModel.kt new file mode 100644 index 00000000..888a22a8 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/DownloadManagerViewModel.kt @@ -0,0 +1,163 @@ +package com.skyd.imomoe.viewmodel + +import androidx.lifecycle.ViewModel +import com.arialyy.aria.core.download.DownloadEntity +import com.skyd.imomoe.bean.AnimeDownload1Bean +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.flow.MutableStateFlow + + +class DownloadManagerViewModel : ViewModel() { + val downloadDataList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + + fun initList( + notCompleteList: List, + animeTitleEpisodeMap: HashMap> + ) { + downloadDataList.tryEmit(DataState.Refreshing) + request(request = { + val dataList = ArrayList() + notCompleteList.forEach { entity -> + val p = animeTitleEpisodeMap[entity.url] + if (p != null) { + // 首次初始化列表 + dataList.add( + AnimeDownload1Bean.create( + title = p.first, + episode = p.second, + entity = entity + ) + ) + } + } + dataList + }, success = { + downloadDataList.tryEmit(DataState.Success(it)) + }, error = { + downloadDataList.tryEmit(DataState.Error(it.message.orEmpty())) + it.message?.showToast() + }) + } + + fun onTaskPreStart( + entity: DownloadEntity, + animeTitleEpisodeMap: HashMap> + ) { + request(request = { + var contain = false + val dataList = downloadDataList.value.readOrNull().orEmpty().toMutableList() + // 根据url和下载任务id查找是否已经添加 + dataList.forEach { + if (it is AnimeDownload1Bean) { + if (it.url == entity.url && it.id == entity.id) { + contain = true + return@forEach + } + } else if (it is DownloadEntity) { + if (it.url == entity.url && it.id == entity.id) { + contain = true + return@forEach + } + } + } + // 没有添加则添加 + if (!contain) { + dataList.apply { + val p = animeTitleEpisodeMap[entity.url] ?: return@apply + add( + AnimeDownload1Bean.create( + title = p.first, + episode = p.second, + entity = entity + ) + ) + } + } + dataList + }, success = { + downloadDataList.tryEmit(DataState.Success(it)) + }, error = { + it.message?.showToast() + }) + } + + fun onTaskRunning(entity: DownloadEntity) { + request(request = { + var dataList = downloadDataList.value.readOrNull().orEmpty() + dataList = dataList.toMutableList().map { + var result: Any = it + if (it is AnimeDownload1Bean && it.url == entity.url) { + // 若最新数据有变化,则new一个新的bean替换之前的bean + // 注意:此处必须要new,不能直接更改之前的bean,否则Diff检测不出差异(旧数据被更改) + if (it != entity) { + result = AnimeDownload1Bean.create(it.title, it.episode, entity = entity) + } + } + result + } + dataList + }, success = { + downloadDataList.tryEmit(DataState.Success(it)) + }) + // onTaskRunning Toast显示错误体验不佳 + } + + fun onTaskComplete(entity: DownloadEntity) { + request(request = { + var dataList = downloadDataList.value.readOrNull().orEmpty() + dataList = dataList.filter { + if (it is AnimeDownload1Bean) { + // 如果是下载完成任务的id,则从list中移除 + it.id != entity.id + } else true + } + dataList + }, success = { + downloadDataList.tryEmit(DataState.Success(it)) + }, error = { + it.message?.showToast() + }) + } + + fun onTaskCancel(entity: DownloadEntity) { + request(request = { + var dataList = downloadDataList.value.readOrNull().orEmpty() + dataList = dataList.filter { + // 如果是取消任务的id,则从list中移除 + if (it is AnimeDownload1Bean) { + it.id != entity.id + } else true + } + dataList + }, success = { + downloadDataList.tryEmit(DataState.Success(it)) + }, error = { + it.message?.showToast() + }) + } + + fun onTaskStateChanged(entity: DownloadEntity) { + request(request = { + var dataList = downloadDataList.value.readOrNull().orEmpty() + dataList = dataList.toMutableList().map { + var result: Any = it + if (result is AnimeDownload1Bean && result.id == entity.id) { + // 若最新数据有变化,则new一个新的bean替换之前的bean + // 注意:此处必须要new,不能直接更改之前的bean,否则Diff检测不出差异(旧数据被更改) + if (it != entity) { + result = + AnimeDownload1Bean.create(result.title, result.episode, entity = entity) + } + } + result + } + dataList + }, success = { + downloadDataList.tryEmit(DataState.Success(it)) + }, error = { + it.message?.showToast() + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/EverydayAnimeViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/EverydayAnimeViewModel.kt index 841ae474..f7acd186 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/EverydayAnimeViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/EverydayAnimeViewModel.kt @@ -1,68 +1,50 @@ package com.skyd.imomoe.viewmodel import android.widget.Toast -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.AnimeShowBean +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.TabBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.EverydayAnimeModel +import com.skyd.imomoe.ext.request import com.skyd.imomoe.model.interfaces.IEverydayAnimeModel +import com.skyd.imomoe.state.DataState import com.skyd.imomoe.util.Util.getRealDayOfWeek -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import java.util.* +import javax.inject.Inject -class EverydayAnimeViewModel : ViewModel() { - private val everydayAnimeModel: IEverydayAnimeModel by lazy { - DataSourceManager.create(IEverydayAnimeModel::class.java) ?: EverydayAnimeModel() - } - var header: AnimeShowBean = AnimeShowBean( - "", "", "", "", - "", null, "", null - ) +@HiltViewModel +class EverydayAnimeViewModel @Inject constructor( + private val everydayAnimeModel: IEverydayAnimeModel +) : ViewModel() { var selectedTabIndex = -1 - var mldHeader: MutableLiveData = MutableLiveData() - var tabList: MutableList = ArrayList() - var mldTabList: MutableLiveData> = MutableLiveData() - var everydayAnimeList: MutableList> = ArrayList() - var mldEverydayAnimeList: MutableLiveData>?> = - MutableLiveData() + val header: MutableStateFlow = + MutableStateFlow(appContext.getString(R.string.everyday_anime_list)) + val everydayAnimeList: MutableStateFlow>>> = + MutableStateFlow(DataState.Empty) + val tabList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) fun getEverydayAnimeData() { - viewModelScope.launch(Dispatchers.IO) { - try { - everydayAnimeModel.getEverydayAnimeData().apply { - if (first.size != second.size) throw Exception("tabs count != tabList count") - selectedTabIndex = getRealDayOfWeek( - Calendar.getInstance(Locale.getDefault()) - .get(Calendar.DAY_OF_WEEK) - ) - 1 - header = third - tabList.clear() - tabList.addAll(first) - mldTabList.postValue(tabList) - mldEverydayAnimeList.postValue(second) - mldHeader.postValue(header) - } - } catch (e: Exception) { - selectedTabIndex = -1 - tabList.clear() - mldEverydayAnimeList.postValue(null) - e.printStackTrace() - "${App.context.getString(R.string.get_data_failed)}\n${e.message}" - .showToastOnIOThread(Toast.LENGTH_LONG) + tabList.tryEmit(DataState.Refreshing) + everydayAnimeList.tryEmit(DataState.Refreshing) + request(request = { + everydayAnimeModel.getEverydayAnimeData().apply { + if (first.size != second.size) throw Exception("tabs count != tabList count") } - } - } - - companion object { - const val TAG = "EverydayAnimeViewModel" + }, success = { + selectedTabIndex = getRealDayOfWeek( + Calendar.getInstance(Locale.getDefault()).get(Calendar.DAY_OF_WEEK) + ) - 1 + tabList.tryEmit(DataState.Success(it.first)) + everydayAnimeList.tryEmit(DataState.Success(it.second)) + header.tryEmit(it.third.ifBlank { appContext.getString(R.string.everyday_anime_list) }) + }, error = { + selectedTabIndex = -1 + everydayAnimeList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast(Toast.LENGTH_LONG) + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/FavoriteViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/FavoriteViewModel.kt index bd406aa6..b22bb076 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/FavoriteViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/FavoriteViewModel.kt @@ -1,41 +1,43 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.FavoriteAnimeBean +import com.skyd.imomoe.appContext import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.flow.MutableStateFlow class FavoriteViewModel : ViewModel() { - var favoriteList: MutableList = ArrayList() - var mldFavoriteList: MutableLiveData = MutableLiveData() + val uiState: MutableStateFlow = + MutableStateFlow(FavoriteUiState.Refreshing()) + + init { + getFavoriteData() + } fun getFavoriteData() { - viewModelScope.launch(Dispatchers.IO) { - try { - favoriteList.clear() - favoriteList.addAll(getAppDataBase().favoriteAnimeDao().getFavoriteAnimeList()) - favoriteList.sortWith(Comparator { o1, o2 -> - // 负数表示按时间戳从大到小排列 - -o1.time.compareTo(o2.time) - }) - mldFavoriteList.postValue(true) - } catch (e: Exception) { - favoriteList.clear() - mldFavoriteList.postValue(false) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() + uiState.tryEmit(FavoriteUiState.Refreshing()) + request(request = { + getAppDataBase().favoriteAnimeDao().getFavoriteAnimeList() + }, success = { + it.sortWith { o1, o2 -> + // 负数表示按时间戳从大到小排列 + -o1.time.compareTo(o2.time) } - } + uiState.tryEmit(FavoriteUiState.Success(it)) + }, error = { + uiState.tryEmit(FavoriteUiState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } +} - companion object { - const val TAG = "FavoriteViewModel" - } +sealed interface FavoriteUiState { + data class Success(override val dataList: List) : WithData(dataList) + data class Error(val message: String = "") : FavoriteUiState + data class Refreshing(override val dataList: List? = null) : WithData(dataList) + + abstract class WithData(open val dataList: List? = null) : FavoriteUiState } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/HistoryViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/HistoryViewModel.kt index d5e7e8f6..7bab7724 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/HistoryViewModel.kt @@ -1,73 +1,63 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.HistoryBean import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.flow.MutableStateFlow class HistoryViewModel : ViewModel() { - var historyList: MutableList = ArrayList() - var mldHistoryList: MutableLiveData = MutableLiveData() - var mldDeleteHistory: MutableLiveData = MutableLiveData() - var mldDeleteAllHistory: MutableLiveData = MutableLiveData() + val uiState: MutableStateFlow = MutableStateFlow(HistoryUiState.Refreshing()) + + init { + getHistoryList() + } fun getHistoryList() { - viewModelScope.launch(Dispatchers.IO) { - try { - historyList.clear() - historyList.addAll(getAppDataBase().historyDao().getHistoryList()) - historyList.sortWith(Comparator { o1, o2 -> - // 负数表示按时间戳从大到小排列 - -o1.time.compareTo(o2.time) - }) - mldHistoryList.postValue(true) - } catch (e: Exception) { - historyList.clear() - mldHistoryList.postValue(false) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() + val currentState = uiState.value + val oldList = if (currentState is HistoryUiState.Success) currentState.dataList else null + uiState.tryEmit(HistoryUiState.Refreshing(oldList)) + request(request = { getAppDataBase().historyDao().getHistoryList() }, success = { + it.sortWith { o1, o2 -> + // 负数表示按时间戳从大到小排列 + -o1.time.compareTo(o2.time) } - } + uiState.tryEmit(HistoryUiState.Success(it)) + }, error = { + uiState.tryEmit(HistoryUiState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } fun deleteHistory(historyBean: HistoryBean) { - viewModelScope.launch(Dispatchers.IO) { - try { - getAppDataBase().historyDao().deleteHistory(historyBean.animeUrl) - val index = historyList.indexOf(historyBean) - historyList.removeAt(index) - mldDeleteHistory.postValue(index) - } catch (e: Exception) { - mldDeleteHistory.postValue(-1) - e.printStackTrace() - (App.context.getString(R.string.delete_failed) + "\n" + e.message).showToastOnIOThread() - } - } + request(request = { + getAppDataBase().historyDao().deleteHistory(historyBean.animeUrl) + getHistoryList() + }, error = { + "${appContext.getString(R.string.delete_failed)}\n${it.message}".showToast() + }) } fun deleteAllHistory() { - viewModelScope.launch(Dispatchers.IO) { - try { - getAppDataBase().historyDao().deleteAllHistory() - val itemCount: Int = historyList.size - historyList.clear() - mldDeleteAllHistory.postValue(itemCount) - } catch (e: Exception) { - mldDeleteAllHistory.postValue(0) - e.printStackTrace() - (App.context.getString(R.string.delete_failed) + "\n" + e.message).showToastOnIOThread() - } - } + val currentState = uiState.value + if (currentState is HistoryUiState.Success && currentState.dataList.isEmpty()) return + request(request = { + getAppDataBase().historyDao().deleteAllHistory() + getHistoryList() + }, error = { + "${appContext.getString(R.string.delete_failed)}\n${it.message}".showToast() + }) } +} - companion object { - const val TAG = "HistoryViewModel" - } +sealed interface HistoryUiState { + data class Success(override val dataList: List) : WithData(dataList) + data class Error(val message: String = "") : HistoryUiState + data class Refreshing(override val dataList: List? = null) : WithData(dataList) + + abstract class WithData(open val dataList: List? = null) : HistoryUiState } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/HomeViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/HomeViewModel.kt index 629fa9bc..930c59a2 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/HomeViewModel.kt @@ -1,51 +1,37 @@ package com.skyd.imomoe.viewmodel import android.widget.Toast -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.TabBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.HomeModel +import com.skyd.imomoe.ext.request import com.skyd.imomoe.model.interfaces.IHomeModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import com.skyd.imomoe.view.adapter.SerializableRecycledViewPool -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject -class HomeViewModel : ViewModel() { - private val homeModel: IHomeModel by lazy { - DataSourceManager.create(IHomeModel::class.java) ?: HomeModel() - } - val childViewPool = SerializableRecycledViewPool() - val viewPool = SerializableRecycledViewPool() - var allTabList: MutableList = ArrayList() - var mldGetAllTabList: MutableLiveData = MutableLiveData() - - fun getAllTabData() { - viewModelScope.launch(Dispatchers.IO) { - try { - homeModel.getAllTabData().apply { - allTabList.clear() - allTabList.addAll(this) - mldGetAllTabList.postValue(true) - } +@HiltViewModel +class HomeViewModel @Inject constructor( + private val homeModel: IHomeModel +) : ViewModel() { + val allTabList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + var currentTab = -1 - } catch (e: Exception) { - allTabList.clear() - mldGetAllTabList.postValue(false) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread( - Toast.LENGTH_LONG - ) - } - } + init { + getAllTabData() } - companion object { - const val TAG = "HomeViewModel" + fun getAllTabData() { + allTabList.tryEmit(DataState.Refreshing) + request(request = { homeModel.getAllTabData() }, success = { + allTabList.tryEmit(DataState.Success(it)) + }, error = { + allTabList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast(Toast.LENGTH_LONG) + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/LocalDataSourceViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/LocalDataSourceViewModel.kt new file mode 100644 index 00000000..04f66112 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/LocalDataSourceViewModel.kt @@ -0,0 +1,26 @@ +package com.skyd.imomoe.viewmodel + +import androidx.lifecycle.ViewModel +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.model.DataSourceManager +import com.skyd.imomoe.state.DataState +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow + + +class LocalDataSourceViewModel : ViewModel() { + var dataSourceList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + var customMainUrl: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + + init { + getDataSourceList() + } + + fun getDataSourceList(directoryPath: String = DataSourceManager.getJarDirectory()) { + request(request = { + dataSourceList.tryEmit( + DataState.Success(DataSourceManager.getDataSourceList(directoryPath)) + ) + }, error = { dataSourceList.tryEmit(DataState.Error(it.message.orEmpty())) }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/MonthAnimeViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/MonthAnimeViewModel.kt index 203ad99d..e2a246a8 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/MonthAnimeViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/MonthAnimeViewModel.kt @@ -1,50 +1,54 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.MonthAnimeModel +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.ext.tryEmitError +import com.skyd.imomoe.ext.tryEmitLoadMore import com.skyd.imomoe.model.interfaces.IMonthAnimeModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject -class MonthAnimeViewModel : ViewModel() { - private val monthAnimeModel: IMonthAnimeModel by lazy { - DataSourceManager.create(IMonthAnimeModel::class.java) ?: MonthAnimeModel() - } - var monthAnimeList: MutableList = ArrayList() - var mldMonthAnimeList: MutableLiveData = MutableLiveData() - var pageNumberBean: PageNumberBean? = null - var newPageIndex: Pair? = null +@HiltViewModel +class MonthAnimeViewModel @Inject constructor( + private val monthAnimeModel: IMonthAnimeModel +) : ViewModel() { + var partUrl: String = "" + val monthAnimeList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + private var pageNumberBean: PageNumberBean? = null - fun getMonthAnimeData(partUrl: String, isRefresh: Boolean = true) { - viewModelScope.launch(Dispatchers.IO) { - try { - monthAnimeModel.getMonthAnimeData(partUrl).apply { - if (isRefresh) monthAnimeList.clear() - val positionStart = monthAnimeList.size - monthAnimeList.addAll(first) - pageNumberBean = second - newPageIndex = Pair(positionStart, monthAnimeList.size - positionStart) - mldMonthAnimeList.postValue(true) - } - } catch (e: Exception) { - monthAnimeList.clear() - mldMonthAnimeList.postValue(false) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() - } - } + fun getMonthAnimeData(partUrl: String) { + monthAnimeList.tryEmit(DataState.Refreshing) + request(request = { monthAnimeModel.getMonthAnimeData(partUrl) }, success = { + pageNumberBean = it.second + monthAnimeList.tryEmit(DataState.Success(it.first)) + }, error = { + monthAnimeList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } - companion object { - const val TAG = "MonthAnimeViewModel" + fun loadMoreMonthAnimeData() { + val partUrl = pageNumberBean?.route + val oldData = monthAnimeList.value + monthAnimeList.tryEmit(DataState.Loading) + if (partUrl == null) { + monthAnimeList.tryEmit(oldData) + appContext.getString(R.string.no_more_info).showToast() + return + } + request(request = { monthAnimeModel.getMonthAnimeData(partUrl) }, success = { + pageNumberBean = it.second + monthAnimeList.tryEmitLoadMore(oldData, it.first) + }, error = { + monthAnimeList.tryEmitError(oldData, it.message) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/PlayViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/PlayViewModel.kt index 02e678f4..7519a3ac 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/PlayViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/PlayViewModel.kt @@ -1,38 +1,49 @@ package com.skyd.imomoe.viewmodel import android.app.Activity -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.* -import com.skyd.imomoe.config.Const.ViewHolderTypeString import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.PlayModel +import com.skyd.imomoe.ext.request import com.skyd.imomoe.model.interfaces.IPlayModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.compare.EpisodeTitleSort.sortEpisodeTitle +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class PlayViewModel : ViewModel() { - private val playModel: IPlayModel by lazy { - DataSourceManager.create(IPlayModel::class.java) ?: PlayModel() - } - var playBean: PlayBean? = null +@HiltViewModel +class PlayViewModel @Inject constructor( + private val playModel: IPlayModel +) : ViewModel() { + var isFirstTimeToPlay = true + lateinit var playBean: PlayBean var partUrl: String = "" - var animeCover: ImageBean = ImageBean("", "", "", "") - var mldAnimeCover: MutableLiveData = MutableLiveData() - var mldPlayBean: MutableLiveData = MutableLiveData() - var playBeanDataList: MutableList = ArrayList() - val episodesList: MutableList = ArrayList() - var currentEpisodeIndex = 0 - val mldEpisodesList: MutableLiveData = MutableLiveData() - val animeEpisodeDataBean = AnimeEpisodeDataBean("animeEpisode1", "", "") - val mldAnimeEpisodeDataRefreshed: MutableLiveData = MutableLiveData() - val mldGetAnimeEpisodeData: MutableLiveData = MutableLiveData() + var lastPlayPosition: Long = 0L + val animeCover: MutableStateFlow = MutableStateFlow(null) + val playDataList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + + // 当前播放集数的索引 + var currentEpisodeIndex: MutableStateFlow = MutableStateFlow(0) + + // 当前播放的集数 + val animeEpisodeDataBean = AnimeEpisodeDataBean("", "") + val episodesList: MutableStateFlow>> = + MutableStateFlow(DataState.Empty) + val playAnotherEpisodeEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + val animeDownloadUrl: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + val loadingEpisodeData: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + val favorite: MutableStateFlow = MutableStateFlow(false) fun setActivity(activity: Activity) { playModel.setActivity(activity) @@ -42,144 +53,208 @@ class PlayViewModel : ViewModel() { playModel.clearActivity() } - fun refreshAnimeEpisodeData(partUrl: String, currentEpisodeIndex: Int, title: String = "") { - viewModelScope.launch(Dispatchers.IO) { - try { - this@PlayViewModel.partUrl = partUrl - playModel.refreshAnimeEpisodeData(partUrl, animeEpisodeDataBean).apply { - if (this) { - animeEpisodeDataBean.title = title - mldAnimeEpisodeDataRefreshed.postValue(true) - } else { - throw RuntimeException("html play class not found") - } - } - } catch (e: Exception) { - e.printStackTrace() - animeEpisodeDataBean.actionUrl = "animeEpisode1" - animeEpisodeDataBean.title = "" - animeEpisodeDataBean.videoUrl = "" - mldAnimeEpisodeDataRefreshed.postValue(false) - "${App.context.getString(R.string.get_data_failed)}\n${e.message}".showToastOnIOThread() - } - this@PlayViewModel.currentEpisodeIndex = currentEpisodeIndex + /** + * @return true if has next episode, false else. + */ + fun playNextEpisode(): Boolean { + val list = episodesList.value.readOrNull().orEmpty() + if (currentEpisodeIndex.value + 1 in list.indices) { + playAnotherEpisode( + list[currentEpisodeIndex.value + 1].route, + currentEpisodeIndex.value + 1 + ) + return true } + return false } - fun getAnimeEpisodeUrlData(partUrl: String, position: Int) { - viewModelScope.launch(Dispatchers.IO) { - try { -// this@PlayViewModel.partUrl = partUrl - playModel.getAnimeEpisodeUrlData(partUrl).apply { - this ?: throw RuntimeException("getAnimeEpisodeUrlData return null") - episodesList[position].videoUrl = this - mldEpisodesList.postValue(true) - mldGetAnimeEpisodeData.postValue(position) - } - } catch (e: Exception) { - e.printStackTrace() - "${App.context.getString(R.string.get_data_failed)}\n${e.message}".showToastOnIOThread() + // 播放另一集(页面切换到另一集,因此partUrl要更新) + fun playAnotherEpisode(partUrl: String, currentEpisodeIndex: Int) { + this.partUrl = partUrl + loadingEpisodeData.tryEmit(partUrl) + request(request = { + playModel.playAnotherEpisode(partUrl).let { + it ?: throw RuntimeException("html play class not found") } - } + }, success = { + animeEpisodeDataBean.route = it.route.ifBlank { partUrl } + animeEpisodeDataBean.title = it.title + animeEpisodeDataBean.videoUrl = it.videoUrl + playAnotherEpisodeEvent.tryEmit(true) + }, error = { + animeEpisodeDataBean.route = "animeEpisode1" + animeEpisodeDataBean.title = "" + animeEpisodeDataBean.videoUrl = "" + playAnotherEpisodeEvent.tryEmit(false) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }, finish = { this.currentEpisodeIndex.tryEmit(currentEpisodeIndex) }) } - fun getPlayData(partUrl: String) { - viewModelScope.launch(Dispatchers.IO) { - try { - this@PlayViewModel.partUrl = partUrl - playModel.getPlayData(partUrl, animeEpisodeDataBean).apply { - playBeanDataList.clear() - episodesList.clear() - playBeanDataList.addAll(first) - episodesList.addAll(second) - playBean = third - mldPlayBean.postValue(playBean) - mldEpisodesList.postValue(true) - } - } catch (e: Exception) { - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() + fun getAnimeDownloadUrl(partUrl: String, position: Int) { + request(request = { + playModel.getAnimeDownloadUrl(partUrl).let { + it ?: throw RuntimeException("getAnimeEpisodeUrlData return null") } - } + }, success = { + val episode = episodesList.value.readOrNull().orEmpty()[position] + episode.videoUrl = it + animeDownloadUrl.tryEmit(episode) + }, error = { + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } - // 更新追番集数数据 - fun updateFavoriteData( - detailPartUrl: String, - lastEpisodeUrl: String, - lastEpisode: String, - time: Long - ) { - viewModelScope.launch(Dispatchers.IO) { - try { - val favoriteAnimeDao = getAppDataBase().favoriteAnimeDao() - val favoriteAnimeBean = favoriteAnimeDao.getFavoriteAnime(detailPartUrl) - if (favoriteAnimeBean != null) { - favoriteAnimeBean.lastEpisode = lastEpisode - favoriteAnimeBean.lastEpisodeUrl = lastEpisodeUrl - favoriteAnimeBean.time = time - favoriteAnimeDao.updateFavoriteAnime(favoriteAnimeBean) + fun getPlayData() { + request(request = { + if (animeCover.value == null) { + animeCover.tryEmit(playModel.getAnimeCoverImageBean(partUrl)) + } + playModel.getPlayData(partUrl, animeEpisodeDataBean) + }, success = { + viewModelScope.launch { + if (animeEpisodeDataBean.route.isBlank()) { + animeEpisodeDataBean.route = partUrl } - } catch (e: Exception) { - e.printStackTrace() + val list = episodesList.value.readOrNull().orEmpty().toMutableList() + list.clear() + list.addAll(it.second) + list.sortEpisodeTitle() + playBean = it.third + run loop@{ + list.forEachIndexed { index, item -> + if (animeEpisodeDataBean == item || + item.route.isNotBlank() && animeEpisodeDataBean.route == item.route + ) { + currentEpisodeIndex.tryEmit(index) + return@loop + } + } + } + episodesList.tryEmit(DataState.Success(list)) + playDataList.tryEmit(DataState.Success(it.first)) + queryFavorite() } + }, error = { + playDataList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) + } + + // 更新追番集数数据 + fun updateFavoriteData() { + if (playBean.detailPartUrl.isNotBlank()) { + request(request = { + getAppDataBase().favoriteAnimeDao().getFavoriteAnime(playBean.detailPartUrl) + }, success = { + it ?: return@request + it.lastEpisode = animeEpisodeDataBean.title + it.lastEpisodeUrl = partUrl + it.time = System.currentTimeMillis() + request({ getAppDataBase().favoriteAnimeDao().updateFavoriteAnime(it) }) + }) + } else { + appContext.getString(R.string.delete_favorite_failed_in_play_activity).showToast() } } // 插入观看历史记录 - fun insertHistoryData(detailPartUrl: String) { - viewModelScope.launch(Dispatchers.IO) { - try { - if (animeCover.url.isBlank()) { - playModel.getAnimeCoverImageBean(detailPartUrl).apply { - this ?: return@apply - getAppDataBase().historyDao().insertHistory( - HistoryBean( - ViewHolderTypeString.ANIME_COVER_9, "", detailPartUrl, - playBean?.title?.title ?: "", - System.currentTimeMillis(), - this, - partUrl, - animeEpisodeDataBean.title - ) - ) - } - } else { - getAppDataBase().historyDao().insertHistory( + fun insertHistoryData() { + request(request = { + animeCover.value.let { + if (it == null) { + playModel.getAnimeCoverImageBean(partUrl).run { + val cover = this ?: ImageBean("", "", "") HistoryBean( - ViewHolderTypeString.ANIME_COVER_9, "", detailPartUrl, - playBean?.title?.title ?: "", + "", playBean.detailPartUrl, + playBean.title.title, System.currentTimeMillis(), - animeCover, + cover, partUrl, animeEpisodeDataBean.title ) + } + } else { + HistoryBean( + "", playBean.detailPartUrl, + playBean.title.title, + System.currentTimeMillis(), + it, + partUrl, + animeEpisodeDataBean.title ) } - } catch (e: Exception) { - e.printStackTrace() } - } + }, success = { + request(request = { getAppDataBase().historyDao().insertHistory(it) }) + }) } - fun getAnimeCoverImageBean(detailPartUrl: String) { - viewModelScope.launch(Dispatchers.IO) { - try { - playModel.getAnimeCoverImageBean(detailPartUrl).apply { - this ?: return@apply - animeCover.url = url - animeCover.referer = referer - mldAnimeCover.postValue(true) - } - } catch (e: Exception) { - mldAnimeCover.postValue(false) - e.printStackTrace() - "${App.context.getString(R.string.get_data_failed)}\n${e.message}".showToastOnIOThread() + fun getAnimeCoverImageBean() { + request(request = { + playModel.getAnimeCoverImageBean(partUrl) + }, success = { + it ?: return@request + val cover = animeCover.value + if (cover == null) { + animeCover.tryEmit(ImageBean("", it.url, it.referer)) + } else { + cover.url = it.url + cover.referer = it.referer } + }, error = { + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) + } + + + // 查询是否追番 + fun queryFavorite() { + if (playBean.detailPartUrl.isNotBlank()) { + request(request = { + getAppDataBase().favoriteAnimeDao().getFavoriteAnime(playBean.detailPartUrl) + }, success = { favorite.tryEmit(it != null) }) + } else { + appContext.getString(R.string.delete_favorite_failed_in_play_activity).showToast() + } + } + + // 取消追番 + fun deleteFavorite() { + if (playBean.detailPartUrl.isNotBlank()) { + request(request = { + getAppDataBase().favoriteAnimeDao().deleteFavoriteAnime(playBean.detailPartUrl) + }, success = { + appContext.getString(R.string.remove_favorite_succeed).showToast() + favorite.tryEmit(false) + }) + } else { + appContext.getString(R.string.delete_favorite_failed_in_play_activity).showToast() } } - companion object { - const val TAG = "PlayViewModel" + // 追番 + fun insertFavorite() { + val cover = animeCover.value // 番剧封面 + if (this::playBean.isInitialized && cover != null && playBean.detailPartUrl.isNotBlank()) { + val title = playBean.title.title // 番剧名,非集数名 + request(request = { + getAppDataBase().favoriteAnimeDao().insertFavoriteAnime( + FavoriteAnimeBean( + "", + playBean.detailPartUrl, + title, + System.currentTimeMillis(), + cover, + lastEpisodeUrl = partUrl, + lastEpisode = animeEpisodeDataBean.title + ) + ) + }, success = { + appContext.getString(R.string.favorite_succeed).showToast() + favorite.tryEmit(true) + }) + } else { + appContext.getString(R.string.insert_favorite_failed_in_play_activity).showToast() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/RankListViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/RankListViewModel.kt index 424f191c..44c4685a 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/RankListViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/RankListViewModel.kt @@ -1,58 +1,54 @@ package com.skyd.imomoe.viewmodel import android.widget.Toast -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.GetDataEnum +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.PageNumberBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.RankListModel +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.ext.tryEmitError +import com.skyd.imomoe.ext.tryEmitLoadMore import com.skyd.imomoe.model.interfaces.IRankListModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.util.* -import kotlin.collections.ArrayList +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject -class RankListViewModel : ViewModel() { - private val rankModel: IRankListModel by lazy { - DataSourceManager.create(IRankListModel::class.java) ?: RankListModel() - } - var isRequesting = false - var rankList: MutableList = Collections.synchronizedList(ArrayList()) - var pageNumberBean: PageNumberBean? = null - var mldRankData: MutableLiveData>> = - MutableLiveData() - - fun getRankListData(partUrl: String, isRefresh: Boolean = true) { - viewModelScope.launch(Dispatchers.IO) { - try { - if (isRequesting) return@launch - isRequesting = true +@HiltViewModel +class RankListViewModel @Inject constructor( + private val rankModel: IRankListModel +) : ViewModel() { + var partUrl: String = "" + val mldRankData: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + private var pageNumberBean: PageNumberBean? = null - rankModel.getRankListData(partUrl).apply { - pageNumberBean = second - mldRankData.postValue( - Pair( - if (isRefresh) GetDataEnum.REFRESH else GetDataEnum.LOAD_MORE, - first.toMutableList() - ) - ) - isRequesting = false - } - } catch (e: Exception) { - mldRankData.postValue(Pair(GetDataEnum.FAILED, ArrayList())) - isRequesting = false - e.printStackTrace() - e.message?.showToastOnIOThread(Toast.LENGTH_LONG) - } - } + fun getRankListData() { + request(request = { rankModel.getRankListData(partUrl) }, success = { + pageNumberBean = it.second + mldRankData.tryEmit(DataState.Success(it.first.toMutableList())) + }, error = { + mldRankData.tryEmit(DataState.Error(it.message.orEmpty())) + it.message?.showToast(Toast.LENGTH_LONG) + }) } - companion object { - const val TAG = "RankViewModel" + fun loadMoreRankListData() { + val partUrl = pageNumberBean?.route + val oldData = mldRankData.value + mldRankData.tryEmit(DataState.Loading) + if (partUrl == null) { + mldRankData.tryEmit(oldData) + appContext.getString(R.string.no_more_info).showToast() + return + } + request(request = { rankModel.getRankListData(partUrl) }, success = { + pageNumberBean = it.second + mldRankData.tryEmitLoadMore(oldData, it.first.toMutableList()) + }, error = { + mldRankData.tryEmitError(oldData, it.message) + it.message?.showToast(Toast.LENGTH_LONG) + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/RankViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/RankViewModel.kt index dc8ac3f1..3d43971f 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/RankViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/RankViewModel.kt @@ -1,50 +1,36 @@ package com.skyd.imomoe.viewmodel import android.widget.Toast -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.skyd.imomoe.bean.TabBean -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.RankModel +import com.skyd.imomoe.ext.request import com.skyd.imomoe.model.interfaces.IRankModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.util.* +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject -class RankViewModel : ViewModel() { - private val rankModel: IRankModel by lazy { - DataSourceManager.create(IRankModel::class.java) ?: RankModel() - } +@HiltViewModel +class RankViewModel @Inject constructor( + private val rankModel: IRankModel +) : ViewModel() { var isRequesting = false - var tabList: MutableList = Collections.synchronizedList(ArrayList()) - var mldRankData: MutableLiveData = MutableLiveData() - - fun getRankTabData() { - viewModelScope.launch(Dispatchers.IO) { - try { - if (isRequesting) return@launch - isRequesting = true + val rankData: MutableStateFlow>> = MutableStateFlow(DataState.Empty) - rankModel.getRankTabData().apply { - tabList.clear() - tabList.addAll(this) - mldRankData.postValue(true) - isRequesting = false - } - } catch (e: Exception) { - mldRankData.postValue(false) - tabList.clear() - isRequesting = false - e.printStackTrace() - e.message?.showToastOnIOThread(Toast.LENGTH_LONG) - } - } + init { + getRankTabData() } - companion object { - const val TAG = "RankViewModel" + fun getRankTabData() { + if (isRequesting) return + isRequesting = true + request(request = { rankModel.getRankTabData() }, success = { + rankData.tryEmit(DataState.Success(it)) + }, error = { + rankData.tryEmit(DataState.Error(it.message.orEmpty())) + it.message?.showToast(Toast.LENGTH_LONG) + }, finish = { isRequesting = false }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/SearchViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/SearchViewModel.kt index fe3ddcd9..d30d51b8 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/SearchViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/SearchViewModel.kt @@ -1,122 +1,113 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.skyd.imomoe.App import com.skyd.imomoe.R -import com.skyd.imomoe.bean.AnimeCoverBean -import com.skyd.imomoe.bean.GetDataEnum +import com.skyd.imomoe.appContext import com.skyd.imomoe.bean.PageNumberBean import com.skyd.imomoe.bean.SearchHistoryBean import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.model.DataSourceManager -import com.skyd.imomoe.model.impls.SearchModel +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.ext.tryEmitError +import com.skyd.imomoe.ext.tryEmitLoadMore import com.skyd.imomoe.model.interfaces.ISearchModel -import com.skyd.imomoe.util.Util.showToastOnIOThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject -class SearchViewModel : ViewModel() { - private val searchModel: ISearchModel by lazy { - DataSourceManager.create(ISearchModel::class.java) ?: SearchModel() - } +@HiltViewModel +class SearchViewModel @Inject constructor( + private val searchModel: ISearchModel +) : ViewModel() { + var keyword = "" + val searchResultList: MutableStateFlow>> = MutableStateFlow(DataState.Empty) + val searchHistoryList: MutableStateFlow>> = + MutableStateFlow(DataState.Empty) + val deleteCompleted: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + private var pageNumberBean: PageNumberBean? = null - var searchResultList: MutableList = ArrayList() - var mldSearchResultList: MutableLiveData>> = - MutableLiveData() - var keyWord = "" - var searchHistoryList: MutableList = ArrayList() - var mldSearchHistoryList: MutableLiveData = MutableLiveData() - var mldInsertCompleted: MutableLiveData = MutableLiveData() - var mldUpdateCompleted: MutableLiveData = MutableLiveData() - var mldDeleteCompleted: MutableLiveData = MutableLiveData() - var pageNumberBean: PageNumberBean? = null + fun getSearchData(keyWord: String, partUrl: String = "") { + searchResultList.tryEmit(DataState.Refreshing) + request(request = { searchModel.getSearchData(keyWord, partUrl) }, success = { + pageNumberBean = it.second + this@SearchViewModel.keyword = keyWord + searchResultList.tryEmit(DataState.Success(it.first.toMutableList())) + }, error = { + searchResultList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) + } - fun getSearchData(keyWord: String, isRefresh: Boolean = true, partUrl: String = "") { - viewModelScope.launch(Dispatchers.IO) { - try { - searchModel.getSearchData(keyWord, partUrl).apply { - pageNumberBean = second - this@SearchViewModel.keyWord = keyWord - mldSearchResultList.postValue( - Pair(if (isRefresh) GetDataEnum.REFRESH else GetDataEnum.LOAD_MORE, first) - ) - } - } catch (e: Exception) { - mldSearchResultList.postValue(Pair(GetDataEnum.FAILED, ArrayList())) - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() - } + fun loadMoreSearchData() { + val partUrl = pageNumberBean?.route + val oldData = searchResultList.value + searchResultList.tryEmit(DataState.Loading) + if (partUrl == null) { + appContext.getString(R.string.no_more_info).showToast() + searchResultList.tryEmit(oldData) + return } + request(request = { searchModel.getSearchData(keyword, partUrl) }, success = { + pageNumberBean = it.second + searchResultList.tryEmitLoadMore(oldData, it.first.toMutableList()) + }, error = { + searchResultList.tryEmitError(oldData, it.message) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } fun getSearchHistoryData() { - viewModelScope.launch(Dispatchers.IO) { - try { - searchHistoryList.clear() - searchHistoryList.addAll(getAppDataBase().searchHistoryDao().getSearchHistoryList()) - } catch (e: Exception) { - e.printStackTrace() - (App.context.getString(R.string.get_data_failed) + "\n" + e.message).showToastOnIOThread() - } finally { - mldSearchHistoryList.postValue(true) - } - } + request(request = { + getAppDataBase().searchHistoryDao().getSearchHistoryList() + }, success = { + searchHistoryList.tryEmit(DataState.Success(it)) + }, error = { + searchHistoryList.tryEmit(DataState.Error(it.message.orEmpty())) + "${appContext.getString(R.string.get_data_failed)}\n${it.message}".showToast() + }) } fun insertSearchHistory(searchHistoryBean: SearchHistoryBean) { - viewModelScope.launch(Dispatchers.IO) { - try { - if (searchHistoryList.isEmpty()) searchHistoryList.addAll( - getAppDataBase().searchHistoryDao().getSearchHistoryList() - ) - val index = searchHistoryList.indexOf(searchHistoryBean) - if (index != -1) { - searchHistoryList.removeAt(index) - searchHistoryList.add(0, searchHistoryBean) - getAppDataBase().searchHistoryDao().deleteSearchHistory(searchHistoryBean.title) - getAppDataBase().searchHistoryDao().insertSearchHistory(searchHistoryBean) - } else { - searchHistoryList.add(0, searchHistoryBean) - getAppDataBase().searchHistoryDao().insertSearchHistory(searchHistoryBean) - } - } catch (e: Exception) { - e.printStackTrace() - } finally { - mldInsertCompleted.postValue(true) - } - } - } - - fun updateSearchHistory(searchHistoryBean: SearchHistoryBean, itemPosition: Int) { - viewModelScope.launch(Dispatchers.IO) { - try { - searchHistoryList[itemPosition] = searchHistoryBean - getAppDataBase().searchHistoryDao().updateSearchHistory(searchHistoryBean) - } catch (e: Exception) { - e.printStackTrace() - } finally { - mldUpdateCompleted.postValue(itemPosition) - } - } - } - - fun deleteSearchHistory(itemPosition: Int) { - viewModelScope.launch(Dispatchers.IO) { - try { - val searchHistoryBean = searchHistoryList.removeAt(itemPosition) - getAppDataBase().searchHistoryDao().deleteSearchHistory(searchHistoryBean.timeStamp) - } catch (e: Exception) { - e.printStackTrace() - } finally { - mldDeleteCompleted.postValue(itemPosition) + request(request = { + val list = getAppDataBase().searchHistoryDao().getSearchHistoryList().toMutableList() + val index = list.indexOf(searchHistoryBean) + if (index != -1) { + list.removeAt(index) + list.add(0, searchHistoryBean) + getAppDataBase().searchHistoryDao().deleteSearchHistory(searchHistoryBean.title) + getAppDataBase().searchHistoryDao().insertSearchHistory(searchHistoryBean) + } else { + list.add(0, searchHistoryBean) + getAppDataBase().searchHistoryDao().insertSearchHistory(searchHistoryBean) } - } + list + }, success = { + searchHistoryList.tryEmit(DataState.Success(it)) + }, error = { + searchHistoryList.tryEmit(DataState.Error(it.message.orEmpty())) + }) } - companion object { - const val TAG = "SearchViewModel" + fun deleteSearchHistory(searchHistoryBean: SearchHistoryBean) { + request(request = { + getAppDataBase().searchHistoryDao().deleteSearchHistory(searchHistoryBean.timeStamp) + }, success = { + searchHistoryList.tryEmit( + DataState.Success( + searchHistoryList.value + .readOrNull() + .orEmpty() + .toMutableList() + .apply { remove(searchHistoryBean) } + ) + ) + deleteCompleted.tryEmit(searchHistoryBean) + }, error = { + deleteCompleted.tryEmit(null) + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/SettingViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/SettingViewModel.kt index 67722ee2..6af7711f 100644 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/SettingViewModel.kt +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/SettingViewModel.kt @@ -1,71 +1,96 @@ package com.skyd.imomoe.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import coil.util.CoilUtils -import com.skyd.imomoe.App +import coil.Coil +import coil.annotation.ExperimentalCoilApi import com.skyd.imomoe.R +import com.skyd.imomoe.appContext import com.skyd.imomoe.database.getAppDataBase -import com.skyd.imomoe.util.Util.getDirectorySize -import com.skyd.imomoe.util.Util.getFormatSize -import com.skyd.imomoe.util.Util.showToastOnIOThread +import com.skyd.imomoe.database.getOfflineDatabase +import com.skyd.imomoe.ext.directorySize +import com.skyd.imomoe.ext.formatSize +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.net.okhttpClient import com.skyd.imomoe.util.coil.CoilUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.skyd.imomoe.util.showToast +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow class SettingViewModel : ViewModel() { - var mldDeleteAllHistory: MutableLiveData = MutableLiveData() - var mldClearAllCache: MutableLiveData = MutableLiveData() - var mldCacheSize: MutableLiveData = MutableLiveData() + val allHistoryCount: MutableStateFlow = MutableStateFlow(-1L) + val cacheSize: MutableStateFlow = MutableStateFlow("") + + val deleteAllHistory: MutableSharedFlow> = + MutableSharedFlow(extraBufferCapacity = 1) + val clearAllCache: MutableSharedFlow> = + MutableSharedFlow(extraBufferCapacity = 1) + + init { + getAllHistoryCount() + getCacheSize() + } fun deleteAllHistory() { - viewModelScope.launch(Dispatchers.IO) { - try { - getAppDataBase().historyDao().deleteAllHistory() - getAppDataBase().searchHistoryDao().deleteAllSearchHistory() - mldDeleteAllHistory.postValue(true) - } catch (e: Exception) { - mldDeleteAllHistory.postValue(false) - e.printStackTrace() - (App.context.getString(R.string.delete_failed) + "\n" + e.message).showToastOnIOThread() - } - } + request(request = { + getAppDataBase().historyDao().deleteAllHistory() + getAppDataBase().searchHistoryDao().deleteAllSearchHistory() + getOfflineDatabase().playRecordDao().deleteAll() + }, success = { + deleteAllHistory.tryEmit(true to appContext.getString(R.string.delete_all_history_succeed)) + getAllHistoryCount() + }, error = { + deleteAllHistory.tryEmit(false to appContext.getString(R.string.clear_cache_failed)) + "${appContext.getString(R.string.delete_failed)}\n${it.message}".showToast() + }) } - // 获取Glide磁盘缓存大小 + // 获取Coil磁盘缓存大小 + @OptIn(ExperimentalCoilApi::class) fun getCacheSize() { - viewModelScope.launch(Dispatchers.IO) { - mldCacheSize.postValue( - try { - getFormatSize( - getDirectorySize(CoilUtils.createDefaultCache(App.context).directory).toDouble() - ) - } catch (e: Exception) { - e.printStackTrace() - "获取缓存大小失败" - } - ) - } + Thread { + runCatching { + ((Coil.imageLoader(appContext).diskCache?.size ?: 0) + + (okhttpClient.cache?.directory?.directorySize() ?: 0)).formatSize() + }.onSuccess { + cacheSize.tryEmit(it) + }.onFailure { + it.printStackTrace() + cacheSize.tryEmit(appContext.getString(R.string.get_cache_size_failed)) + } + }.start() } - fun clearAllCache() { - viewModelScope.launch(Dispatchers.IO) { - try { - // Glide + Thread { + runCatching { CoilUtil.clearMemoryDiskCache() - mldClearAllCache.postValue(true) - } catch (e: Exception) { - mldClearAllCache.postValue(false) - e.printStackTrace() - (App.context.getString(R.string.delete_failed) + "\n" + e.message).showToastOnIOThread() + if (okhttpClient.cache?.directory?.exists() == true) { + okhttpClient.cache?.delete() + } + }.onSuccess { + clearAllCache.tryEmit(true to appContext.getString(R.string.clear_cache_succeed)) + }.onFailure { + it.printStackTrace() + clearAllCache.tryEmit(false to appContext.getString(R.string.clear_cache_failed)) + "${appContext.getString(R.string.delete_failed)}\n${it.message}".showToast() + }.also { + request(request = { + delay(1000) + getCacheSize() + }) } - } + }.start() } - companion object { - const val TAG = "SettingViewModel" + fun getAllHistoryCount() { + request(request = { + getAppDataBase().historyDao().getHistoryCount() + + getAppDataBase().searchHistoryDao().getSearchHistoryCount() + + getOfflineDatabase().playRecordDao().getPlayRecordCount() + }, success = { allHistoryCount.tryEmit(it) }, error = { + allHistoryCount.tryEmit(-1) + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/UpnpViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/UpnpViewModel.kt deleted file mode 100644 index d0d30ab4..00000000 --- a/app/src/main/java/com/skyd/imomoe/viewmodel/UpnpViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.skyd.imomoe.viewmodel - -import androidx.lifecycle.ViewModel -import org.fourthline.cling.model.meta.Device -import java.util.* - -class UpnpViewModel : ViewModel() { - var deviceList: MutableList?> = ArrayList() - - companion object { - const val TAG = "UpnpViewModel" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/UrlMapViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/UrlMapViewModel.kt new file mode 100644 index 00000000..b1ab03e4 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/UrlMapViewModel.kt @@ -0,0 +1,156 @@ +package com.skyd.imomoe.viewmodel + +import androidx.lifecycle.ViewModel +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.skyd.imomoe.database.entity.UrlMapEntity +import com.skyd.imomoe.database.getAppDataBase +import com.skyd.imomoe.ext.request +import com.skyd.imomoe.state.DataState +import com.skyd.imomoe.util.showToast +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + + +@HiltViewModel +class UrlMapViewModel @Inject constructor() : ViewModel() { + var urlMapList = MutableStateFlow>>(DataState.Empty) + val requestFinish: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + var autoAdd = false + var autoAddAndFinish = false + + init { + getUrlMapList() + } + + fun getUrlMapList() { + urlMapList.value = DataState.Refreshing + request(request = { getAppDataBase().urlMapDao().getAll() }, success = { + urlMapList.value = DataState.Success(it) + }, error = { + urlMapList.value = DataState.Error(it.message.orEmpty()) + }) + } + + fun setUrlMap(jsonData: String) { + request(request = { + val entityList = Gson().fromJson>( + jsonData, + object : TypeToken>() {}.type + ) + var solvedCount = 0 + entityList.forEach { + val onEachEntityFinish: (() -> Unit)? = if (autoAddAndFinish) { + { + solvedCount++ + if (solvedCount == entityList.size) { + requestFinish.tryEmit(true) + } + } + } else null + // Gson解析后不能保证不为空 + @Suppress("UselessCallOnNotNull") + if (!it.oldUrl.isNullOrBlank() && !it.newUrl.isNullOrBlank()) { + @Suppress("USELESS_ELVIS") + setUrlMap( + it.oldUrl, it.newUrl, it.enabled ?: true, + onFinish = onEachEntityFinish + ) + } + } + }, error = { + it.message?.showToast() + }) + } + + fun setUrlMap( + oldUrl: String, + newUrl: String, + enabled: Boolean = true, + onFinish: (() -> Unit)? = null + ) { + request(request = { + getAppDataBase().urlMapDao().setNewUrl(oldUrl, newUrl, enabled) + }, success = { + urlMapList.value = DataState.Success( + urlMapList.value.readOrNull().orEmpty().toMutableList().apply { + var i = -1 + run { + forEachIndexed { index, urlMapEntity -> + if (urlMapEntity.oldUrl == oldUrl) { + i = index + return@run + } + } + } + if (i != -1) { + removeAt(i) + add(i, UrlMapEntity(oldUrl = oldUrl, newUrl = newUrl, enabled = enabled)) + } else { + add(UrlMapEntity(oldUrl = oldUrl, newUrl = newUrl, enabled = enabled)) + } + } + ) + }, error = { + it.message?.showToast() + }, finish = onFinish) + } + + fun deleteUrlMap(oldUrl: String) { + request(request = { + getAppDataBase().urlMapDao().delete(oldUrl) + }, success = { + urlMapList.value = DataState.Success(urlMapList.value.readOrNull().orEmpty().filter { + it.oldUrl != oldUrl + }) + }, error = { + it.message?.showToast() + }) + } + + fun editUrlMap(old: Pair, new: Pair, enabled: Boolean = true) { + request(request = { + getAppDataBase().urlMapDao().delete(old.first) + getAppDataBase().urlMapDao().setNewUrl(new.first, new.second, enabled) + }, success = { + urlMapList.value = DataState.Success( + urlMapList.value.readOrNull().orEmpty().toMutableList().apply { + var i = -1 + run { + forEachIndexed { index, urlMapEntity -> + if (urlMapEntity.oldUrl == old.first && urlMapEntity.newUrl == old.second) { + i = index + return@run + } + } + } + if (i != -1) { + removeAt(i) + } + add(UrlMapEntity(oldUrl = new.first, newUrl = new.second, enabled = enabled)) + } + ) + }, error = { + it.message?.showToast() + }) + } + + fun enabledUrlMap(oldUrl: String, enable: Boolean) { + request(request = { + getAppDataBase().urlMapDao().enabled(oldUrl, enable) + }, success = { + urlMapList.value = DataState.Success(urlMapList.value.readOrNull().orEmpty().apply { + forEach { + if (it.oldUrl == oldUrl) { + it.enabled = enable + return@apply + } + } + }) + }, error = { + it.message?.showToast() + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/imomoe/viewmodel/WebDavViewModel.kt b/app/src/main/java/com/skyd/imomoe/viewmodel/WebDavViewModel.kt new file mode 100644 index 00000000..19c48308 --- /dev/null +++ b/app/src/main/java/com/skyd/imomoe/viewmodel/WebDavViewModel.kt @@ -0,0 +1,160 @@ +package com.skyd.imomoe.viewmodel + +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.sqlite.db.SimpleSQLiteQuery +import com.skyd.imomoe.R +import com.skyd.imomoe.appContext +import com.skyd.imomoe.database.getAppDataBase +import com.skyd.imomoe.util.currentDate +import com.skyd.imomoe.util.killApplicationProcess +import com.skyd.imomoe.util.showToast +import com.thegrizzlylabs.sardineandroid.DavResource +import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine +import kotlinx.coroutines.flow.MutableSharedFlow +import java.io.File + + +class WebDavViewModel : ViewModel() { + companion object { + const val APP_DIR = "Imomoe" + const val DATABASE_DIR = "${APP_DIR}/DataBase" + const val TYPE_APP_DATABASE_DIR = "${DATABASE_DIR}/AppDataBase" + val dataBaseName = mapOf(TYPE_APP_DATABASE_DIR to "app.db") + } + + var pwd: String = "" + val backup: MutableSharedFlow> = + MutableSharedFlow(extraBufferCapacity = 1) + val restore: MutableSharedFlow> = + MutableSharedFlow(extraBufferCapacity = 1) + val fileList: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + val fileMap: HashMap?> = hashMapOf() + + private fun getAppDatabaseFile(): File { + val version = getAppDataBase().openHelper.readableDatabase.version + getAppDataBase().utilDao().checkpoint(SimpleSQLiteQuery("pragma wal_checkpoint(full)")) + val dbName = dataBaseName[TYPE_APP_DATABASE_DIR] + return appContext.getDatabasePath(dbName).copyTo( + File( + appContext.getExternalFilesDir(null).toString() + + "/Backup/AppDatabase/${dbName}-${currentDate("yyyyMMdd-HHmmss")}-" + + "${(0..99999).random()}.${version}" + ), overwrite = true + ) + } + + fun getFileList(server: String, username: String, password: String, type: String) { + Thread { + try { + val sardine = OkHttpSardine() + sardine.setCredentials(username, password) + if (type == TYPE_APP_DATABASE_DIR) { + val version = getAppDataBase().openHelper.readableDatabase.version + val list = sardine.list("$server$TYPE_APP_DATABASE_DIR").filter { + !it.isDirectory && it.name.substringAfterLast(".").toInt() <= version + } + fileMap[type] = list + fileList.tryEmit(type) + } + } catch (e: Exception) { + fileMap[type] = null + fileList.tryEmit(type) + e.printStackTrace() + e.message?.showToast() + } + }.start() + } + + fun backup(server: String, username: String, password: String, type: String) { + Thread { + try { + val sardine = OkHttpSardine() + sardine.setCredentials(username, password) + if (type == TYPE_APP_DATABASE_DIR) { + val appDatabaseFile = getAppDatabaseFile() + if (!sardine.exists(server + APP_DIR)) { + sardine.createDirectory(server + APP_DIR) + } + if (!sardine.exists(server + DATABASE_DIR)) { + sardine.createDirectory(server + DATABASE_DIR) + } + if (!sardine.exists(server + TYPE_APP_DATABASE_DIR)) { + sardine.createDirectory(server + TYPE_APP_DATABASE_DIR) + } + sardine.put( + "$server$TYPE_APP_DATABASE_DIR/${appDatabaseFile.name}", + appDatabaseFile, + "*/*" + ) + appDatabaseFile.delete() + backup.tryEmit(type to true) + } + } catch (e: Exception) { + backup.tryEmit(type to false) + e.printStackTrace() + e.message?.showToast() + } + }.start() + } + + fun restore(partUrl: String, credential: Triple, type: String) { + restore(partUrl, credential.first, credential.second, credential.third, type) + } + + fun restore(partUrl: String, server: String, username: String, password: String, type: String) { + Thread { + try { + val url = server.toUri().let { it.scheme + "://" + it.host + partUrl } + val sardine = OkHttpSardine() + sardine.setCredentials(username, password) + if (type == TYPE_APP_DATABASE_DIR) { + val restoreDatabaseVersion = partUrl.substringAfterLast(".").toInt() + if (restoreDatabaseVersion > getAppDataBase().openHelper.readableDatabase.version) { + throw RuntimeException("the database version is newer, can't restore.") + } + val dbName = dataBaseName[TYPE_APP_DATABASE_DIR] + val shm = appContext.getDatabasePath("$dbName-shm") + val wal = appContext.getDatabasePath("$dbName-wal") + if (shm.exists()) if (!shm.delete()) throw RuntimeException("delete shm file failed") + if (wal.exists()) if (!wal.delete()) throw RuntimeException("delete wal file failed") + sardine.get(url).copyTo(appContext.getDatabasePath(dbName).outputStream()) + appContext.getString(R.string.restore_app_database_succeed).showToast() + // 因为删除了shm和wal文件,因此APP进程需要结束/重启一下 + appContext.killApplicationProcess() + } + } catch (e: Exception) { + restore.tryEmit(type to false) + e.printStackTrace() + e.message?.showToast() + } + }.start() + } + + fun delete(data: DavResource, credential: Triple, type: String) { + delete(data, credential.first, credential.second, credential.third, type) + } + + fun delete( + data: DavResource, server: String, username: String, password: String, type: String + ) { + Thread { + try { + val url = server.toUri().let { it.scheme + "://" + it.host + data.path } + val sardine = OkHttpSardine() + sardine.setCredentials(username, password) + sardine.delete(url) + val list = fileMap[type]?.toMutableList() + ?: throw IllegalArgumentException("$type list not exists") + if (list.remove(data)) fileMap[type] = list + else throw IllegalArgumentException("$type doesn't contain data: $data") + fileList.tryEmit(type) + appContext.getString(R.string.delete_succeed).showToast() + } catch (e: Exception) { + e.printStackTrace() + "${appContext.getString(R.string.delete_failed)}\n${e.message}".showToast() + } + }.start() + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v23/ic_akarin_padding.png b/app/src/main/res/drawable-v23/ic_akarin_padding.png new file mode 100644 index 00000000..8fdeb6ba Binary files /dev/null and b/app/src/main/res/drawable-v23/ic_akarin_padding.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_everyday_anime_widget.webp b/app/src/main/res/drawable-xxxhdpi/ic_everyday_anime_widget.webp index 2634ce83..8b12fb0c 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_everyday_anime_widget.webp and b/app/src/main/res/drawable-xxxhdpi/ic_everyday_anime_widget.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_click_download.webp b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_click_download.webp new file mode 100644 index 00000000..8e112e34 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_click_download.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_downloaded.webp b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_downloaded.webp new file mode 100644 index 00000000..bd312a68 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_downloaded.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_left_page.webp b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_left_page.webp new file mode 100644 index 00000000..53e8cd6c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_left_page.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_to_market.webp b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_to_market.webp new file mode 100644 index 00000000..deaabcc8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_to_market.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_use.webp b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_use.webp new file mode 100644 index 00000000..fea35743 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_new_use_data_source_step_use.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_spinner_bg_1_skin.9.png b/app/src/main/res/drawable-xxxhdpi/ic_spinner_bg_1_skin.9.png deleted file mode 100644 index 4ee2a1a8..00000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_spinner_bg_1_skin.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_ads_files.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_ads_files.webp new file mode 100644 index 00000000..18c11b5a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_ads_files.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_custom.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_custom.webp new file mode 100644 index 00000000..dc94b2ef Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_custom.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_delete.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_delete.webp new file mode 100644 index 00000000..a921108c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_delete.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_import_dialog.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_import_dialog.webp new file mode 100644 index 00000000..eb717743 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_import_dialog.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_import_success.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_import_success.webp new file mode 100644 index 00000000..ba1e56e0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_import_success.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_long_click.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_long_click.webp new file mode 100644 index 00000000..c8932989 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_long_click.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_more.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_more.webp new file mode 100644 index 00000000..9e9e5b9b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_more.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_open_ads.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_open_ads.webp new file mode 100644 index 00000000..2bd2a690 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_open_ads.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_reset_button.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_reset_button.webp new file mode 100644 index 00000000..9e0e01ba Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_reset_button.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_reset_dialog.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_reset_dialog.webp new file mode 100644 index 00000000..6bb8a853 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_reset_dialog.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_set.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_set.webp new file mode 100644 index 00000000..d304200b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_set.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_setting.webp b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_setting.webp new file mode 100644 index 00000000..53a9c60b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_use_data_source_step_setting.webp differ diff --git a/app/src/main/res/drawable/bg_circle_main_color_2_50_skin.xml b/app/src/main/res/drawable/bg_circle_main_color_2_50_skin.xml deleted file mode 100644 index 7c0d6208..00000000 --- a/app/src/main/res/drawable/bg_circle_main_color_2_50_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_access_time_24.xml b/app/src/main/res/drawable/ic_access_time_24.xml new file mode 100644 index 00000000..a9e22649 --- /dev/null +++ b/app/src/main/res/drawable/ic_access_time_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_access_time_main_color_24_skin.xml b/app/src/main/res/drawable/ic_access_time_main_color_24_skin.xml deleted file mode 100644 index dcd70e3b..00000000 --- a/app/src/main/res/drawable/ic_access_time_main_color_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_akarin.xml b/app/src/main/res/drawable/ic_akarin.xml new file mode 100644 index 00000000..4b03a666 --- /dev/null +++ b/app/src/main/res/drawable/ic_akarin.xml @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_akarin_padding.xml b/app/src/main/res/drawable/ic_akarin_padding.xml new file mode 100644 index 00000000..cf289cf3 --- /dev/null +++ b/app/src/main/res/drawable/ic_akarin_padding.xml @@ -0,0 +1,764 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_24.xml b/app/src/main/res/drawable/ic_arrow_back_24.xml new file mode 100644 index 00000000..afb4f765 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_ios_24.xml b/app/src/main/res/drawable/ic_arrow_back_ios_24.xml new file mode 100644 index 00000000..c52d8439 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_ios_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_ios_white_24.xml b/app/src/main/res/drawable/ic_arrow_back_ios_white_24.xml deleted file mode 100644 index 5d0949af..00000000 --- a/app/src/main/res/drawable/ic_arrow_back_ios_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_back_white_24.xml b/app/src/main/res/drawable/ic_arrow_back_white_24.xml deleted file mode 100644 index a01ccf77..00000000 --- a/app/src/main/res/drawable/ic_arrow_back_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_circle_down_24.xml b/app/src/main/res/drawable/ic_arrow_circle_down_24.xml new file mode 100644 index 00000000..8398d2cc --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_circle_down_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_circle_down_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_arrow_circle_down_main_color_2_24_skin.xml deleted file mode 100644 index ea8d4b52..00000000 --- a/app/src/main/res/drawable/ic_arrow_circle_down_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_circle_down_white_24_skin.xml b/app/src/main/res/drawable/ic_arrow_circle_down_white_24_skin.xml deleted file mode 100644 index 45a4a0a0..00000000 --- a/app/src/main/res/drawable/ic_arrow_circle_down_white_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_forward_ios_12.xml b/app/src/main/res/drawable/ic_arrow_forward_ios_12.xml new file mode 100644 index 00000000..3877e641 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward_ios_12.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward_ios_main_color_12_skin.xml b/app/src/main/res/drawable/ic_arrow_forward_ios_main_color_12_skin.xml deleted file mode 100644 index 221fb217..00000000 --- a/app/src/main/res/drawable/ic_arrow_forward_ios_main_color_12_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_right_24.xml b/app/src/main/res/drawable/ic_arrow_right_24.xml new file mode 100644 index 00000000..3ac04ece --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_arrow_right_main_color_2_24_skin.xml deleted file mode 100644 index 3add9375..00000000 --- a/app/src/main/res/drawable/ic_arrow_right_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_upward_24.xml b/app/src/main/res/drawable/ic_arrow_upward_24.xml new file mode 100644 index 00000000..5d4e8580 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_upward_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_bar_chart_24.xml b/app/src/main/res/drawable/ic_bar_chart_24.xml new file mode 100644 index 00000000..846d5270 --- /dev/null +++ b/app/src/main/res/drawable/ic_bar_chart_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_bar_chart_white_24_skin.xml b/app/src/main/res/drawable/ic_bar_chart_white_24_skin.xml deleted file mode 100644 index dd9921b8..00000000 --- a/app/src/main/res/drawable/ic_bar_chart_white_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_beans_24.xml b/app/src/main/res/drawable/ic_beans_24.xml new file mode 100644 index 00000000..4a5005b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_beans_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_beans_main_color_24_skin.xml b/app/src/main/res/drawable/ic_beans_main_color_24_skin.xml deleted file mode 100644 index 2b5a48d8..00000000 --- a/app/src/main/res/drawable/ic_beans_main_color_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_beans_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_beans_main_color_2_24_skin.xml deleted file mode 100644 index 940108c6..00000000 --- a/app/src/main/res/drawable/ic_beans_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_category_24.xml b/app/src/main/res/drawable/ic_category_24.xml new file mode 100644 index 00000000..8b27958e --- /dev/null +++ b/app/src/main/res/drawable/ic_category_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_category_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_category_main_color_2_24_skin.xml deleted file mode 100644 index 170306f5..00000000 --- a/app/src/main/res/drawable/ic_category_main_color_2_24_skin.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_check_circle_24.xml b/app/src/main/res/drawable/ic_check_circle_24.xml new file mode 100644 index 00000000..959b91f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear_24.xml b/app/src/main/res/drawable/ic_clear_24.xml new file mode 100644 index 00000000..3fb9e2e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear_main_color_24_skin.xml b/app/src/main/res/drawable/ic_clear_main_color_24_skin.xml deleted file mode 100644 index 9823f717..00000000 --- a/app/src/main/res/drawable/ic_clear_main_color_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_close_24.xml b/app/src/main/res/drawable/ic_close_24.xml new file mode 100644 index 00000000..3fb9e2e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_cloud_done_24.xml b/app/src/main/res/drawable/ic_cloud_done_24.xml new file mode 100644 index 00000000..9238906a --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_done_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_cloud_download_24.xml b/app/src/main/res/drawable/ic_cloud_download_24.xml new file mode 100644 index 00000000..a2272ef9 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_download_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_cloud_upload_24.xml b/app/src/main/res/drawable/ic_cloud_upload_24.xml new file mode 100644 index 00000000..928462e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_upload_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_danmaku_bottom_24.xml b/app/src/main/res/drawable/ic_danmaku_bottom_24.xml new file mode 100644 index 00000000..069d0738 --- /dev/null +++ b/app/src/main/res/drawable/ic_danmaku_bottom_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_danmaku_font_24.xml b/app/src/main/res/drawable/ic_danmaku_font_24.xml new file mode 100644 index 00000000..f6a257a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_danmaku_font_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_danmaku_scroll_24.xml b/app/src/main/res/drawable/ic_danmaku_scroll_24.xml new file mode 100644 index 00000000..1a03cccf --- /dev/null +++ b/app/src/main/res/drawable/ic_danmaku_scroll_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_danmaku_setting_24.xml b/app/src/main/res/drawable/ic_danmaku_setting_24.xml new file mode 100644 index 00000000..63f78c9d --- /dev/null +++ b/app/src/main/res/drawable/ic_danmaku_setting_24.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_danmaku_top_24.xml b/app/src/main/res/drawable/ic_danmaku_top_24.xml new file mode 100644 index 00000000..b9b7221b --- /dev/null +++ b/app/src/main/res/drawable/ic_danmaku_top_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_database_24.xml b/app/src/main/res/drawable/ic_database_24.xml new file mode 100644 index 00000000..7d46572e --- /dev/null +++ b/app/src/main/res/drawable/ic_database_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_24.xml b/app/src/main/res/drawable/ic_delete_24.xml new file mode 100644 index 00000000..5bf28a76 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_delete_main_color_2_24_skin.xml deleted file mode 100644 index a5574ba8..00000000 --- a/app/src/main/res/drawable/ic_delete_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_delete_white_24.xml b/app/src/main/res/drawable/ic_delete_white_24.xml deleted file mode 100644 index 756a6e1e..00000000 --- a/app/src/main/res/drawable/ic_delete_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_developer_board_24.xml b/app/src/main/res/drawable/ic_developer_board_24.xml new file mode 100644 index 00000000..c0c022ed --- /dev/null +++ b/app/src/main/res/drawable/ic_developer_board_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_dns_24.xml b/app/src/main/res/drawable/ic_dns_24.xml new file mode 100644 index 00000000..5ffa01c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_dns_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_east_24.xml b/app/src/main/res/drawable/ic_east_24.xml new file mode 100644 index 00000000..c7118b7c --- /dev/null +++ b/app/src/main/res/drawable/ic_east_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fast_forward_24.xml b/app/src/main/res/drawable/ic_fast_forward_24.xml new file mode 100644 index 00000000..191eb940 --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_forward_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fast_rewind_24.xml b/app/src/main/res/drawable/ic_fast_rewind_24.xml new file mode 100644 index 00000000..297692bf --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_rewind_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_format_list_bulleted_24.xml b/app/src/main/res/drawable/ic_format_list_bulleted_24.xml new file mode 100644 index 00000000..dc44c49c --- /dev/null +++ b/app/src/main/res/drawable/ic_format_list_bulleted_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_format_list_bulleted_white_24_skin.xml b/app/src/main/res/drawable/ic_format_list_bulleted_white_24_skin.xml deleted file mode 100644 index ab03aa4d..00000000 --- a/app/src/main/res/drawable/ic_format_list_bulleted_white_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_forum_24.xml b/app/src/main/res/drawable/ic_forum_24.xml new file mode 100644 index 00000000..e68382fe --- /dev/null +++ b/app/src/main/res/drawable/ic_forum_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen_24.xml b/app/src/main/res/drawable/ic_fullscreen_24.xml new file mode 100644 index 00000000..4f167331 --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen_exit_24.xml b/app/src/main/res/drawable/ic_fullscreen_exit_24.xml new file mode 100644 index 00000000..efafe012 --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen_exit_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen_exit_white_24.xml b/app/src/main/res/drawable/ic_fullscreen_exit_white_24.xml deleted file mode 100644 index 7d36af8d..00000000 --- a/app/src/main/res/drawable/ic_fullscreen_exit_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_fullscreen_white_24.xml b/app/src/main/res/drawable/ic_fullscreen_white_24.xml deleted file mode 100644 index 17586b22..00000000 --- a/app/src/main/res/drawable/ic_fullscreen_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_history_24.xml b/app/src/main/res/drawable/ic_history_24.xml new file mode 100644 index 00000000..6910c6bf --- /dev/null +++ b/app/src/main/res/drawable/ic_history_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_history_white_24.xml b/app/src/main/res/drawable/ic_history_white_24.xml deleted file mode 100644 index 0b7f3eba..00000000 --- a/app/src/main/res/drawable/ic_history_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_home_24.xml b/app/src/main/res/drawable/ic_home_24.xml new file mode 100644 index 00000000..d84512c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_main_color_24_skin.xml b/app/src/main/res/drawable/ic_home_main_color_24_skin.xml deleted file mode 100644 index 4b235942..00000000 --- a/app/src/main/res/drawable/ic_home_main_color_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_home_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_home_main_color_2_24_skin.xml deleted file mode 100644 index 82b0b8e0..00000000 --- a/app/src/main/res/drawable/ic_home_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_info_24.xml b/app/src/main/res/drawable/ic_info_24.xml new file mode 100644 index 00000000..760350b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_white_24.xml b/app/src/main/res/drawable/ic_info_white_24.xml deleted file mode 100644 index a953caff..00000000 --- a/app/src/main/res/drawable/ic_info_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_insert_drive_file_24.xml b/app/src/main/res/drawable/ic_insert_drive_file_24.xml new file mode 100644 index 00000000..68f41466 --- /dev/null +++ b/app/src/main/res/drawable/ic_insert_drive_file_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_insert_link_24.xml b/app/src/main/res/drawable/ic_insert_link_24.xml new file mode 100644 index 00000000..b73b2b9c --- /dev/null +++ b/app/src/main/res/drawable/ic_insert_link_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_insert_link_white_24.xml b/app/src/main/res/drawable/ic_insert_link_white_24.xml deleted file mode 100644 index c5515033..00000000 --- a/app/src/main/res/drawable/ic_insert_link_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_down_24.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down_24.xml new file mode 100644 index 00000000..6d706a77 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_down_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_down_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down_main_color_2_24_skin.xml deleted file mode 100644 index fac005e7..00000000 --- a/app/src/main/res/drawable/ic_keyboard_arrow_down_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_language_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_language_main_color_2_24_skin.xml deleted file mode 100644 index 63e4a93e..00000000 --- a/app/src/main/res/drawable/ic_language_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_link_24.xml b/app/src/main/res/drawable/ic_link_24.xml new file mode 100644 index 00000000..dcc5506d --- /dev/null +++ b/app/src/main/res/drawable/ic_link_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_link_34.xml b/app/src/main/res/drawable/ic_link_34.xml new file mode 100644 index 00000000..4a080dca --- /dev/null +++ b/app/src/main/res/drawable/ic_link_34.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_link_main_color_2_34_skin.xml b/app/src/main/res/drawable/ic_link_main_color_2_34_skin.xml deleted file mode 100644 index e3710b35..00000000 --- a/app/src/main/res/drawable/ic_link_main_color_2_34_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_live_tv_24.xml b/app/src/main/res/drawable/ic_live_tv_24.xml new file mode 100644 index 00000000..ffc08bf7 --- /dev/null +++ b/app/src/main/res/drawable/ic_live_tv_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_live_tv_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_live_tv_main_color_2_24_skin.xml deleted file mode 100644 index cfa670ab..00000000 --- a/app/src/main/res/drawable/ic_live_tv_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_looks_one_24.xml b/app/src/main/res/drawable/ic_looks_one_24.xml new file mode 100644 index 00000000..38b28a0d --- /dev/null +++ b/app/src/main/res/drawable/ic_looks_one_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_memory_24.xml b/app/src/main/res/drawable/ic_memory_24.xml new file mode 100644 index 00000000..d439e629 --- /dev/null +++ b/app/src/main/res/drawable/ic_memory_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert_24.xml b/app/src/main/res/drawable/ic_more_vert_24.xml new file mode 100644 index 00000000..82cc1a4d --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert_white_24.xml b/app/src/main/res/drawable/ic_more_vert_white_24.xml deleted file mode 100644 index 4f85be5c..00000000 --- a/app/src/main/res/drawable/ic_more_vert_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_music_note_white_24.xml b/app/src/main/res/drawable/ic_music_note_white_24.xml deleted file mode 100644 index 1b538899..00000000 --- a/app/src/main/res/drawable/ic_music_note_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_nights_stay_24.xml b/app/src/main/res/drawable/ic_nights_stay_24.xml new file mode 100644 index 00000000..35bf94f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_nights_stay_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_nights_stay_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_nights_stay_main_color_2_24_skin.xml deleted file mode 100644 index dd8c2350..00000000 --- a/app/src/main/res/drawable/ic_nights_stay_main_color_2_24_skin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_pause_24.xml b/app/src/main/res/drawable/ic_pause_24.xml new file mode 100644 index 00000000..1fc802ca --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_circle_24.xml b/app/src/main/res/drawable/ic_pause_circle_24.xml new file mode 100644 index 00000000..0b449ca9 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_circle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_circle_white_24.xml b/app/src/main/res/drawable/ic_pause_circle_white_24.xml deleted file mode 100644 index 2f9b52d8..00000000 --- a/app/src/main/res/drawable/ic_pause_circle_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_photo_24.xml b/app/src/main/res/drawable/ic_photo_24.xml new file mode 100644 index 00000000..8115ecf7 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_photo_main_color_3_24_skin.xml b/app/src/main/res/drawable/ic_photo_main_color_3_24_skin.xml deleted file mode 100644 index b0d3e7a4..00000000 --- a/app/src/main/res/drawable/ic_photo_main_color_3_24_skin.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_play_24.xml b/app/src/main/res/drawable/ic_play_24.xml new file mode 100644 index 00000000..d02a89b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_circle_24.xml b/app/src/main/res/drawable/ic_play_circle_24.xml new file mode 100644 index 00000000..f3fd463b --- /dev/null +++ b/app/src/main/res/drawable/ic_play_circle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_circle_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_play_circle_main_color_2_24_skin.xml deleted file mode 100644 index eec4ac9e..00000000 --- a/app/src/main/res/drawable/ic_play_circle_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_play_circle_white_24.xml b/app/src/main/res/drawable/ic_play_circle_white_24.xml deleted file mode 100644 index 7bdcf94d..00000000 --- a/app/src/main/res/drawable/ic_play_circle_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_play_white_24.xml b/app/src/main/res/drawable/ic_play_white_24.xml deleted file mode 100644 index 64c4c880..00000000 --- a/app/src/main/res/drawable/ic_play_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_player_24.xml b/app/src/main/res/drawable/ic_player_24.xml new file mode 100644 index 00000000..a55821cd --- /dev/null +++ b/app/src/main/res/drawable/ic_player_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add_24.xml b/app/src/main/res/drawable/ic_playlist_add_24.xml new file mode 100644 index 00000000..00f6dc8d --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_plugin_24.xml b/app/src/main/res/drawable/ic_plugin_24.xml new file mode 100644 index 00000000..6277d551 --- /dev/null +++ b/app/src/main/res/drawable/ic_plugin_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_power_24.xml b/app/src/main/res/drawable/ic_power_24.xml new file mode 100644 index 00000000..d9b9fc60 --- /dev/null +++ b/app/src/main/res/drawable/ic_power_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_power_white_24.xml b/app/src/main/res/drawable/ic_power_white_24.xml deleted file mode 100644 index 850a5f01..00000000 --- a/app/src/main/res/drawable/ic_power_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_progress_24.xml b/app/src/main/res/drawable/ic_progress_24.xml new file mode 100644 index 00000000..fe16c380 --- /dev/null +++ b/app/src/main/res/drawable/ic_progress_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_qq_34.xml b/app/src/main/res/drawable/ic_qq_34.xml new file mode 100644 index 00000000..e02fb36d --- /dev/null +++ b/app/src/main/res/drawable/ic_qq_34.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_qq_main_color_2_34_skin.xml b/app/src/main/res/drawable/ic_qq_main_color_2_34_skin.xml deleted file mode 100644 index ef33b859..00000000 --- a/app/src/main/res/drawable/ic_qq_main_color_2_34_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_refresh_24.xml b/app/src/main/res/drawable/ic_refresh_24.xml new file mode 100644 index 00000000..fd27daf6 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_white_24.xml b/app/src/main/res/drawable/ic_refresh_white_24.xml deleted file mode 100644 index 25d6c04c..00000000 --- a/app/src/main/res/drawable/ic_refresh_white_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_repeat_one_24.xml b/app/src/main/res/drawable/ic_repeat_one_24.xml new file mode 100644 index 00000000..054658e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_one_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_replay_24.xml b/app/src/main/res/drawable/ic_replay_24.xml new file mode 100644 index 00000000..49369f64 --- /dev/null +++ b/app/src/main/res/drawable/ic_replay_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_replay_white_24_skin.xml b/app/src/main/res/drawable/ic_replay_white_24_skin.xml deleted file mode 100644 index f89be424..00000000 --- a/app/src/main/res/drawable/ic_replay_white_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_restore_24.xml b/app/src/main/res/drawable/ic_restore_24.xml new file mode 100644 index 00000000..f1633691 --- /dev/null +++ b/app/src/main/res/drawable/ic_restore_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_right_32.xml b/app/src/main/res/drawable/ic_right_32.xml new file mode 100644 index 00000000..7d334235 --- /dev/null +++ b/app/src/main/res/drawable/ic_right_32.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_right_white_32_skin.xml b/app/src/main/res/drawable/ic_right_white_32_skin.xml deleted file mode 100644 index 6c0b3c26..00000000 --- a/app/src/main/res/drawable/ic_right_white_32_skin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_sakura_17.xml b/app/src/main/res/drawable/ic_sakura_17.xml deleted file mode 100644 index 1cbb06b8..00000000 --- a/app/src/main/res/drawable/ic_sakura_17.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_screenshot_24.xml b/app/src/main/res/drawable/ic_screenshot_24.xml new file mode 100644 index 00000000..0c066522 --- /dev/null +++ b/app/src/main/res/drawable/ic_screenshot_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sd_storage_24.xml b/app/src/main/res/drawable/ic_sd_storage_24.xml new file mode 100644 index 00000000..3cf43170 --- /dev/null +++ b/app/src/main/res/drawable/ic_sd_storage_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sd_storage_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_sd_storage_main_color_2_24_skin.xml deleted file mode 100644 index 34d1811b..00000000 --- a/app/src/main/res/drawable/ic_sd_storage_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_search_24.xml b/app/src/main/res/drawable/ic_search_24.xml new file mode 100644 index 00000000..cb799ffd --- /dev/null +++ b/app/src/main/res/drawable/ic_search_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_main_color_24_skin.xml b/app/src/main/res/drawable/ic_search_main_color_24_skin.xml deleted file mode 100644 index 1bd37a40..00000000 --- a/app/src/main/res/drawable/ic_search_main_color_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sentiment_very_dissatisfied_24.xml b/app/src/main/res/drawable/ic_sentiment_very_dissatisfied_24.xml new file mode 100644 index 00000000..0a83890b --- /dev/null +++ b/app/src/main/res/drawable/ic_sentiment_very_dissatisfied_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sentiment_very_dissatisfied_main_color_24_skin.xml b/app/src/main/res/drawable/ic_sentiment_very_dissatisfied_main_color_24_skin.xml deleted file mode 100644 index 1695a49f..00000000 --- a/app/src/main/res/drawable/ic_sentiment_very_dissatisfied_main_color_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings_24.xml b/app/src/main/res/drawable/ic_settings_24.xml new file mode 100644 index 00000000..2d216a2b --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_white_24.xml b/app/src/main/res/drawable/ic_settings_white_24.xml deleted file mode 100644 index 85fd220a..00000000 --- a/app/src/main/res/drawable/ic_settings_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_share_24.xml b/app/src/main/res/drawable/ic_share_24.xml new file mode 100644 index 00000000..dfd6aa98 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_white_24.xml b/app/src/main/res/drawable/ic_share_white_24.xml deleted file mode 100644 index 5ad8bc86..00000000 --- a/app/src/main/res/drawable/ic_share_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_skin_32.xml b/app/src/main/res/drawable/ic_skin_32.xml new file mode 100644 index 00000000..d622fbf6 --- /dev/null +++ b/app/src/main/res/drawable/ic_skin_32.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_skin_white_32_skin.xml b/app/src/main/res/drawable/ic_skin_white_32_skin.xml deleted file mode 100644 index d300c75b..00000000 --- a/app/src/main/res/drawable/ic_skin_white_32_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_skip_next_24.xml b/app/src/main/res/drawable/ic_skip_next_24.xml new file mode 100644 index 00000000..6597bd79 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_next_white_24.xml b/app/src/main/res/drawable/ic_skip_next_white_24.xml deleted file mode 100644 index c6b3b823..00000000 --- a/app/src/main/res/drawable/ic_skip_next_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sort_24.xml b/app/src/main/res/drawable/ic_sort_24.xml new file mode 100644 index 00000000..77c6ced1 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_24.xml b/app/src/main/res/drawable/ic_star_24.xml new file mode 100644 index 00000000..4d372e30 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border_24.xml b/app/src/main/res/drawable/ic_star_border_24.xml new file mode 100644 index 00000000..16c4afb1 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_star_border_main_color_2_24_skin.xml deleted file mode 100644 index 38a585ad..00000000 --- a/app/src/main/res/drawable/ic_star_border_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_star_border_white_24.xml b/app/src/main/res/drawable/ic_star_border_white_24.xml deleted file mode 100644 index 88b3b285..00000000 --- a/app/src/main/res/drawable/ic_star_border_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_star_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_star_main_color_2_24_skin.xml deleted file mode 100644 index d5a2c9f6..00000000 --- a/app/src/main/res/drawable/ic_star_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_star_white_24_skin.xml b/app/src/main/res/drawable/ic_star_white_24_skin.xml deleted file mode 100644 index d3e0d7f7..00000000 --- a/app/src/main/res/drawable/ic_star_white_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_storage_24.xml b/app/src/main/res/drawable/ic_storage_24.xml new file mode 100644 index 00000000..5600778e --- /dev/null +++ b/app/src/main/res/drawable/ic_storage_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_storage_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_storage_main_color_2_24_skin.xml deleted file mode 100644 index ff1ef561..00000000 --- a/app/src/main/res/drawable/ic_storage_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sunny_24.xml b/app/src/main/res/drawable/ic_sunny_24.xml new file mode 100644 index 00000000..b6cbe193 --- /dev/null +++ b/app/src/main/res/drawable/ic_sunny_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sunny_main_color_24_skin.xml b/app/src/main/res/drawable/ic_sunny_main_color_24_skin.xml deleted file mode 100644 index eefb713a..00000000 --- a/app/src/main/res/drawable/ic_sunny_main_color_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sunny_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_sunny_main_color_2_24_skin.xml deleted file mode 100644 index caf7571f..00000000 --- a/app/src/main/res/drawable/ic_sunny_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sunny_white_24.xml b/app/src/main/res/drawable/ic_sunny_white_24.xml deleted file mode 100644 index 3251826a..00000000 --- a/app/src/main/res/drawable/ic_sunny_white_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_supervisor_24.xml b/app/src/main/res/drawable/ic_supervisor_24.xml new file mode 100644 index 00000000..39d12f05 --- /dev/null +++ b/app/src/main/res/drawable/ic_supervisor_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_swap_horiz_24.xml b/app/src/main/res/drawable/ic_swap_horiz_24.xml new file mode 100644 index 00000000..72a9a0f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_swap_horiz_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_swap_horiz_white_24_skin.xml b/app/src/main/res/drawable/ic_swap_horiz_white_24_skin.xml deleted file mode 100644 index d142a557..00000000 --- a/app/src/main/res/drawable/ic_swap_horiz_white_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_swap_vert_24.xml b/app/src/main/res/drawable/ic_swap_vert_24.xml new file mode 100644 index 00000000..f86ebe1f --- /dev/null +++ b/app/src/main/res/drawable/ic_swap_vert_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_swap_vert_white_24_skin.xml b/app/src/main/res/drawable/ic_swap_vert_white_24_skin.xml deleted file mode 100644 index 84f9f998..00000000 --- a/app/src/main/res/drawable/ic_swap_vert_white_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_turn_off_danmaku_64.xml b/app/src/main/res/drawable/ic_turn_off_danmaku_64.xml new file mode 100644 index 00000000..614518a0 --- /dev/null +++ b/app/src/main/res/drawable/ic_turn_off_danmaku_64.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_turn_off_danmu_main_color_2_64_skin.xml b/app/src/main/res/drawable/ic_turn_off_danmu_main_color_2_64_skin.xml deleted file mode 100644 index ab9d88ec..00000000 --- a/app/src/main/res/drawable/ic_turn_off_danmu_main_color_2_64_skin.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_turn_on_danmaku_64.xml b/app/src/main/res/drawable/ic_turn_on_danmaku_64.xml new file mode 100644 index 00000000..c9516ac2 --- /dev/null +++ b/app/src/main/res/drawable/ic_turn_on_danmaku_64.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_turn_on_danmu_main_color_2_64_skin.xml b/app/src/main/res/drawable/ic_turn_on_danmu_main_color_2_64_skin.xml deleted file mode 100644 index c07d383e..00000000 --- a/app/src/main/res/drawable/ic_turn_on_danmu_main_color_2_64_skin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_update_24.xml b/app/src/main/res/drawable/ic_update_24.xml new file mode 100644 index 00000000..c013f53a --- /dev/null +++ b/app/src/main/res/drawable/ic_update_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_update_main_color_2_24_skin.xml b/app/src/main/res/drawable/ic_update_main_color_2_24_skin.xml deleted file mode 100644 index 4211df7c..00000000 --- a/app/src/main/res/drawable/ic_update_main_color_2_24_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_vpn_key_24.xml b/app/src/main/res/drawable/ic_vpn_key_24.xml new file mode 100644 index 00000000..6c13327c --- /dev/null +++ b/app/src/main/res/drawable/ic_vpn_key_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_warning_24.xml b/app/src/main/res/drawable/ic_warning_24.xml new file mode 100644 index 00000000..54622c1d --- /dev/null +++ b/app/src/main/res/drawable/ic_warning_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_warning_2_24.xml b/app/src/main/res/drawable/ic_warning_2_24.xml new file mode 100644 index 00000000..b2337f45 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning_2_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_warning_main_color_3_24_skin.xml b/app/src/main/res/drawable/ic_warning_main_color_3_24_skin.xml deleted file mode 100644 index 1f5f2784..00000000 --- a/app/src/main/res/drawable/ic_warning_main_color_3_24_skin.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_wechat_34.xml b/app/src/main/res/drawable/ic_wechat_34.xml new file mode 100644 index 00000000..8930b99c --- /dev/null +++ b/app/src/main/res/drawable/ic_wechat_34.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_wechat_main_color_2_34_skin.xml b/app/src/main/res/drawable/ic_wechat_main_color_2_34_skin.xml deleted file mode 100644 index 25a4aade..00000000 --- a/app/src/main/res/drawable/ic_wechat_main_color_2_34_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_weibo_34.xml b/app/src/main/res/drawable/ic_weibo_34.xml new file mode 100644 index 00000000..125bd0db --- /dev/null +++ b/app/src/main/res/drawable/ic_weibo_34.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_weibo_main_color_2_34_skin.xml b/app/src/main/res/drawable/ic_weibo_main_color_2_34_skin.xml deleted file mode 100644 index fa286542..00000000 --- a/app/src/main/res/drawable/ic_weibo_main_color_2_34_skin.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_widget_everyday_anime_refresh.xml b/app/src/main/res/drawable/ic_widget_everyday_anime_refresh.xml index a9d56af1..383d92fe 100644 --- a/app/src/main/res/drawable/ic_widget_everyday_anime_refresh.xml +++ b/app/src/main/res/drawable/ic_widget_everyday_anime_refresh.xml @@ -1,9 +1,10 @@ diff --git a/app/src/main/res/drawable/layerlist_shortcuts_download_24.xml b/app/src/main/res/drawable/layerlist_shortcuts_download_24.xml index 6b4dfaf9..abdada2a 100644 --- a/app/src/main/res/drawable/layerlist_shortcuts_download_24.xml +++ b/app/src/main/res/drawable/layerlist_shortcuts_download_24.xml @@ -2,12 +2,12 @@ - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/layerlist_video_progress_bg_skin.xml b/app/src/main/res/drawable/layerlist_video_progress_bg_skin.xml deleted file mode 100644 index 2a46270a..00000000 --- a/app/src/main/res/drawable/layerlist_video_progress_bg_skin.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/seek_bar_progress_1.xml b/app/src/main/res/drawable/seek_bar_progress_1.xml new file mode 100644 index 00000000..51b8bafa --- /dev/null +++ b/app/src/main/res/drawable/seek_bar_progress_1.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/seek_bar_progress_1_skin.xml b/app/src/main/res/drawable/seek_bar_progress_1_skin.xml deleted file mode 100644 index d016d900..00000000 --- a/app/src/main/res/drawable/seek_bar_progress_1_skin.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/seek_bar_thumb_1_skin.xml b/app/src/main/res/drawable/seek_bar_thumb_1_skin.xml deleted file mode 100644 index d05eaebc..00000000 --- a/app/src/main/res/drawable/seek_bar_thumb_1_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/selector_everyday_anime_button_skin.xml b/app/src/main/res/drawable/selector_everyday_anime_button_skin.xml deleted file mode 100644 index 99591d48..00000000 --- a/app/src/main/res/drawable/selector_everyday_anime_button_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_home_button_skin.xml b/app/src/main/res/drawable/selector_home_button_skin.xml deleted file mode 100644 index a86fedc9..00000000 --- a/app/src/main/res/drawable/selector_home_button_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_horizontal_reverse_button.xml b/app/src/main/res/drawable/selector_horizontal_reverse_button.xml new file mode 100644 index 00000000..da8687aa --- /dev/null +++ b/app/src/main/res/drawable/selector_horizontal_reverse_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_horizontal_reverse_button_skin.xml b/app/src/main/res/drawable/selector_horizontal_reverse_button_skin.xml deleted file mode 100644 index 41021d29..00000000 --- a/app/src/main/res/drawable/selector_horizontal_reverse_button_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_more_button_skin.xml b/app/src/main/res/drawable/selector_more_button_skin.xml deleted file mode 100644 index 238a7c70..00000000 --- a/app/src/main/res/drawable/selector_more_button_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_no_reverse_button.xml b/app/src/main/res/drawable/selector_no_reverse_button.xml new file mode 100644 index 00000000..f7d67bf2 --- /dev/null +++ b/app/src/main/res/drawable/selector_no_reverse_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_no_reverse_button_skin.xml b/app/src/main/res/drawable/selector_no_reverse_button_skin.xml deleted file mode 100644 index ec8a2933..00000000 --- a/app/src/main/res/drawable/selector_no_reverse_button_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_spinner_1_skin.xml b/app/src/main/res/drawable/selector_spinner_1_skin.xml deleted file mode 100644 index 4917d330..00000000 --- a/app/src/main/res/drawable/selector_spinner_1_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_turn_on_off_danmaku.xml b/app/src/main/res/drawable/selector_turn_on_off_danmaku.xml new file mode 100644 index 00000000..0e635b57 --- /dev/null +++ b/app/src/main/res/drawable/selector_turn_on_off_danmaku.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_turn_on_off_danmu_skin.xml b/app/src/main/res/drawable/selector_turn_on_off_danmu_skin.xml deleted file mode 100644 index 748a82e8..00000000 --- a/app/src/main/res/drawable/selector_turn_on_off_danmu_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_vertical_reverse_button.xml b/app/src/main/res/drawable/selector_vertical_reverse_button.xml new file mode 100644 index 00000000..2e8df7a3 --- /dev/null +++ b/app/src/main/res/drawable/selector_vertical_reverse_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_vertical_reverse_button_skin.xml b/app/src/main/res/drawable/selector_vertical_reverse_button_skin.xml deleted file mode 100644 index b1cee284..00000000 --- a/app/src/main/res/drawable/selector_vertical_reverse_button_skin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_bottom_dialog_12.xml b/app/src/main/res/drawable/shape_circle_corner_bottom_dialog_12.xml new file mode 100644 index 00000000..031a5f77 --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_corner_bottom_dialog_12.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_bottom_dialog_white_12_skin.xml b/app/src/main/res/drawable/shape_circle_corner_bottom_dialog_white_12_skin.xml deleted file mode 100644 index 471b8442..00000000 --- a/app/src/main/res/drawable/shape_circle_corner_bottom_dialog_white_12_skin.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_edge_main_color_2_ripper_5_skin.xml b/app/src/main/res/drawable/shape_circle_corner_edge_main_color_2_ripper_5_skin.xml deleted file mode 100644 index c4b00603..00000000 --- a/app/src/main/res/drawable/shape_circle_corner_edge_main_color_2_ripper_5_skin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_edge_main_color_ripper_50_skin.xml b/app/src/main/res/drawable/shape_circle_corner_edge_main_color_ripper_50_skin.xml deleted file mode 100644 index 5ed0d48b..00000000 --- a/app/src/main/res/drawable/shape_circle_corner_edge_main_color_ripper_50_skin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_edge_primary_ripper_5.xml b/app/src/main/res/drawable/shape_circle_corner_edge_primary_ripper_5.xml new file mode 100644 index 00000000..0e700709 --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_corner_edge_primary_ripper_5.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_edge_white_ripper_5.xml b/app/src/main/res/drawable/shape_circle_corner_edge_white_ripper_5.xml new file mode 100644 index 00000000..b946ccb9 --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_corner_edge_white_ripper_5.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_edge_white_ripper_5_skin.xml b/app/src/main/res/drawable/shape_circle_corner_edge_white_ripper_5_skin.xml deleted file mode 100644 index 453aed0e..00000000 --- a/app/src/main/res/drawable/shape_circle_corner_edge_white_ripper_5_skin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_white_50_skin.xml b/app/src/main/res/drawable/shape_circle_corner_white_50_skin.xml deleted file mode 100644 index 9d0e1b4d..00000000 --- a/app/src/main/res/drawable/shape_circle_corner_white_50_skin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_corner_white_ripper_5.xml b/app/src/main/res/drawable/shape_circle_corner_white_ripper_5.xml index d382ddc0..db7a5dc7 100644 --- a/app/src/main/res/drawable/shape_circle_corner_white_ripper_5.xml +++ b/app/src/main/res/drawable/shape_circle_corner_white_ripper_5.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fill_circle_corner_50.xml b/app/src/main/res/drawable/shape_fill_circle_corner_50.xml new file mode 100644 index 00000000..971d3053 --- /dev/null +++ b/app/src/main/res/drawable/shape_fill_circle_corner_50.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fill_circle_corner_danmu_6.xml b/app/src/main/res/drawable/shape_fill_circle_corner_danmaku_6.xml similarity index 100% rename from app/src/main/res/drawable/shape_fill_circle_corner_danmu_6.xml rename to app/src/main/res/drawable/shape_fill_circle_corner_danmaku_6.xml diff --git a/app/src/main/res/drawable/shape_fill_circle_corner_main_color_2_50_skin.xml b/app/src/main/res/drawable/shape_fill_circle_corner_main_color_2_50_skin.xml deleted file mode 100644 index 37848c43..00000000 --- a/app/src/main/res/drawable/shape_fill_circle_corner_main_color_2_50_skin.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_widget_everyday_anime_body.xml b/app/src/main/res/drawable/shape_widget_everyday_anime_body.xml index b9bdab66..ceada166 100644 --- a/app/src/main/res/drawable/shape_widget_everyday_anime_body.xml +++ b/app/src/main/res/drawable/shape_widget_everyday_anime_body.xml @@ -3,6 +3,6 @@ android:shape="rectangle"> + android:bottomLeftRadius="16dp" + android:bottomRightRadius="16dp" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_widget_everyday_anime_header.xml b/app/src/main/res/drawable/shape_widget_everyday_anime_header.xml index 20988d15..5f862414 100644 --- a/app/src/main/res/drawable/shape_widget_everyday_anime_header.xml +++ b/app/src/main/res/drawable/shape_widget_everyday_anime_header.xml @@ -3,6 +3,6 @@ android:shape="rectangle"> + android:topLeftRadius="16dp" + android:topRightRadius="16dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_classify.xml b/app/src/main/res/layout-land/activity_classify.xml new file mode 100644 index 00000000..5e2548e2 --- /dev/null +++ b/app/src/main/res/layout-land/activity_classify.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_config_data_source.xml b/app/src/main/res/layout-land/activity_config_data_source.xml new file mode 100644 index 00000000..7b11f761 --- /dev/null +++ b/app/src/main/res/layout-land/activity_config_data_source.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 00000000..d3cd594d --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/activity_play.xml b/app/src/main/res/layout-sw600dp-land/activity_play.xml index 2049684e..d2860b8f 100644 --- a/app/src/main/res/layout-sw600dp-land/activity_play.xml +++ b/app/src/main/res/layout-sw600dp-land/activity_play.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white_skin" tools:context=".view.activity.PlayActivity"> - + android:layout_weight="1"> + + + + + + + - - - - + app:layout_constraintTop_toTopOf="parent" + app:tint="?attr/colorPrimary" + tools:src="@drawable/ic_star_border_24" /> - + android:layout_weight="1"> - - - - - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingTop="7dp" + app:layout_constraintTop_toBottomOf="@id/view_play_activity_line_1" /> + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-port/activity_play.xml b/app/src/main/res/layout-sw600dp-port/activity_play.xml index be1a2202..3be34b42 100644 --- a/app/src/main/res/layout-sw600dp-port/activity_play.xml +++ b/app/src/main/res/layout-sw600dp-port/activity_play.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white_skin" tools:context=".view.activity.PlayActivity"> @@ -25,144 +24,108 @@ android:layout_width="match_parent" android:layout_height="210dp" /> - - - + + - - - - - - - - + android:layout_gravity="center" + android:layout_marginStart="7dp" + android:layout_marginEnd="7dp" + android:background="?android:selectableItemBackground" + android:drawableStart="@drawable/ic_play_24" + android:gravity="center" + android:paddingHorizontal="10dp" + android:paddingVertical="7dp" + android:text="@string/play_video_now" + android:textColor="@android:color/white" + app:drawableTint="@android:color/white" /> + - - - + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + android:layout_height="match_parent"> - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:scaleType="fitXY" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml deleted file mode 100644 index dc2f112b..00000000 --- a/app/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_anime_detail.xml b/app/src/main/res/layout/activity_anime_detail.xml index 502bbe3a..c5548bb6 100644 --- a/app/src/main/res/layout/activity_anime_detail.xml +++ b/app/src/main/res/layout/activity_anime_detail.xml @@ -1,47 +1,44 @@ - - - - + android:layout_height="wrap_content" + app:buttonGravity="center_vertical" + app:layout_constraintTop_toTopOf="parent" + app:menu="@menu/menu_anime_detail_activity" + app:navigationIcon="@drawable/ic_arrow_back_24" + app:navigationIconTint="@android:color/white" + app:titleTextColor="@android:color/white" /> - - - + android:focusableInTouchMode="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/tb_anime_detail_activity"> - - \ No newline at end of file + android:layout_height="wrap_content" + android:clipToPadding="false" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_anime_download.xml b/app/src/main/res/layout/activity_anime_download.xml deleted file mode 100644 index a2df3c54..00000000 --- a/app/src/main/res/layout/activity_anime_download.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_backup_restore_container.xml b/app/src/main/res/layout/activity_backup_restore_container.xml new file mode 100644 index 00000000..7f7ebacb --- /dev/null +++ b/app/src/main/res/layout/activity_backup_restore_container.xml @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_classify.xml b/app/src/main/res/layout/activity_classify.xml index da399ccc..fe334878 100644 --- a/app/src/main/res/layout/activity_classify.xml +++ b/app/src/main/res/layout/activity_classify.xml @@ -1,47 +1,57 @@ - - - - + app:liftOnScroll="true" + app:liftOnScrollTargetViewId="@+id/srl_classify_activity"> - + - - + android:layout_height="wrap_content" + android:orientation="horizontal"> + + + + + + + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + android:layout_height="match_parent" + android:clipToPadding="false" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_config_data_source.xml b/app/src/main/res/layout/activity_config_data_source.xml new file mode 100644 index 00000000..ee0d3920 --- /dev/null +++ b/app/src/main/res/layout/activity_config_data_source.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_dlna.xml b/app/src/main/res/layout/activity_dlna.xml deleted file mode 100644 index 70e5ea83..00000000 --- a/app/src/main/res/layout/activity_dlna.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_dlna_control.xml b/app/src/main/res/layout/activity_dlna_control.xml index 9694f256..08991757 100644 --- a/app/src/main/res/layout/activity_dlna_control.xml +++ b/app/src/main/res/layout/activity_dlna_control.xml @@ -23,40 +23,65 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="70dp" android:background="?android:selectableItemBackground" - android:src="@drawable/ic_play_circle_white_24" /> + android:src="@drawable/ic_play_circle_24" + app:tint="@android:color/white" /> + android:src="@drawable/ic_power_24" + app:tint="@android:color/white" /> - + android:maxHeight="3dp" + android:minHeight="3dp" + android:paddingVertical="6dp" + android:progressDrawable="@drawable/video_seek_progress" /> - + + + + diff --git a/app/src/main/res/layout/activity_download_manager.xml b/app/src/main/res/layout/activity_download_manager.xml new file mode 100644 index 00000000..4350f4cf --- /dev/null +++ b/app/src/main/res/layout/activity_download_manager.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_favorite.xml b/app/src/main/res/layout/activity_favorite.xml deleted file mode 100644 index d4808be9..00000000 --- a/app/src/main/res/layout/activity_favorite.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml deleted file mode 100644 index 289b1278..00000000 --- a/app/src/main/res/layout/activity_history.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_license.xml b/app/src/main/res/layout/activity_license.xml deleted file mode 100644 index c30336a8..00000000 --- a/app/src/main/res/layout/activity_license.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 89a84609..656f3494 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,118 +1,22 @@ - - + app:layout_constraintBottom_toTopOf="@id/nv_main_activity" + app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + app:layout_constraintBottom_toBottomOf="parent" + app:menu="@menu/menu_navigation" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_month_anime.xml b/app/src/main/res/layout/activity_month_anime.xml index 78a59511..d08cd096 100644 --- a/app/src/main/res/layout/activity_month_anime.xml +++ b/app/src/main/res/layout/activity_month_anime.xml @@ -1,33 +1,45 @@ - - + + + + + android:layout_below="@id/tb_month_anime_activity" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + android:layout_height="match_parent" + android:clipToPadding="false" /> - \ No newline at end of file + android:layout="@layout/layout_image_text_tip_1" + app:layout_anchor="@id/srl_month_anime_activity" + app:layout_anchorGravity="center" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_notice.xml b/app/src/main/res/layout/activity_notice.xml deleted file mode 100644 index 96b1e303..00000000 --- a/app/src/main/res/layout/activity_notice.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_play.xml b/app/src/main/res/layout/activity_play.xml index be1a2202..95438088 100644 --- a/app/src/main/res/layout/activity_play.xml +++ b/app/src/main/res/layout/activity_play.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white_skin" tools:context=".view.activity.PlayActivity"> @@ -25,144 +24,107 @@ android:layout_width="match_parent" android:layout_height="210dp" /> - - - + + - - - - - - - - + android:layout_gravity="center" + android:layout_marginStart="7dp" + android:layout_marginEnd="7dp" + android:background="?android:selectableItemBackground" + android:drawableStart="@drawable/ic_play_24" + android:gravity="center" + android:paddingHorizontal="10dp" + android:paddingVertical="7dp" + android:text="@string/play_video_now" + android:textColor="@android:color/white" + app:drawableTint="@android:color/white" /> + - - - + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + android:layout_height="match_parent"> - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:scaleType="fitXY" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rank.xml b/app/src/main/res/layout/activity_rank.xml index 8cd2e4a4..8f307e2c 100644 --- a/app/src/main/res/layout/activity_rank.xml +++ b/app/src/main/res/layout/activity_rank.xml @@ -1,38 +1,51 @@ - - - - + app:liftOnScroll="true" + app:liftOnScrollTargetViewId="@+id/vp2_rank_activity"> + + + + + + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - \ No newline at end of file + android:layout="@layout/layout_image_text_tip_1" + app:layout_anchor="@id/vp2_rank_activity" + app:layout_anchorGravity="center" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index e75661a2..19ccda86 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -1,111 +1,73 @@ - - + android:layout_height="wrap_content" + app:liftOnScroll="true" + app:liftOnScrollTargetViewId="@+id/srl_search_activity"> - + android:paddingStart="0dp" + android:paddingEnd="6dp" + app:buttonGravity="center_vertical" + app:contentInsetEnd="0dp" + app:contentInsetStart="0dp" + app:menu="@menu/menu_search_activity"> - + + - - - - + + + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + android:scrollbars="vertical" /> - \ No newline at end of file + android:layout="@layout/layout_circle_progress_text_tip_1" + app:layout_anchor="@id/srl_search_activity" + app:layout_anchorGravity="center" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setting.xml b/app/src/main/res/layout/activity_setting.xml deleted file mode 100644 index 55ce366b..00000000 --- a/app/src/main/res/layout/activity_setting.xml +++ /dev/null @@ -1,416 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setting_container.xml b/app/src/main/res/layout/activity_setting_container.xml new file mode 100644 index 00000000..8d7c2481 --- /dev/null +++ b/app/src/main/res/layout/activity_setting_container.xml @@ -0,0 +1,6 @@ + + diff --git a/app/src/main/res/layout/activity_skin.xml b/app/src/main/res/layout/activity_skin.xml deleted file mode 100644 index e477b771..00000000 --- a/app/src/main/res/layout/activity_skin.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_web_view.xml b/app/src/main/res/layout/activity_web_view.xml deleted file mode 100644 index 7459e12d..00000000 --- a/app/src/main/res/layout/activity_web_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_bottom_sheet_1.xml b/app/src/main/res/layout/dialog_bottom_sheet_1.xml new file mode 100644 index 00000000..7bf32a74 --- /dev/null +++ b/app/src/main/res/layout/dialog_bottom_sheet_1.xml @@ -0,0 +1,43 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_bottom_sheet_2.xml b/app/src/main/res/layout/dialog_bottom_sheet_2.xml deleted file mode 100644 index 3704676f..00000000 --- a/app/src/main/res/layout/dialog_bottom_sheet_2.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/fragment_anime_show.xml b/app/src/main/res/layout/fragment_anime_show.xml index 1193aceb..ffe55114 100644 --- a/app/src/main/res/layout/fragment_anime_show.xml +++ b/app/src/main/res/layout/fragment_anime_show.xml @@ -1,6 +1,5 @@ + android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/fragment_danmaku_setting_dialog.xml b/app/src/main/res/layout/fragment_danmaku_setting_dialog.xml new file mode 100644 index 00000000..fa65be32 --- /dev/null +++ b/app/src/main/res/layout/fragment_danmaku_setting_dialog.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_data_source_market.xml b/app/src/main/res/layout/fragment_data_source_market.xml new file mode 100644 index 00000000..074a0088 --- /dev/null +++ b/app/src/main/res/layout/fragment_data_source_market.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_episode_dialog.xml b/app/src/main/res/layout/fragment_episode_dialog.xml new file mode 100644 index 00000000..285507fc --- /dev/null +++ b/app/src/main/res/layout/fragment_episode_dialog.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_everyday_anime.xml b/app/src/main/res/layout/fragment_everyday_anime.xml index fd0d02a7..3151b393 100644 --- a/app/src/main/res/layout/fragment_everyday_anime.xml +++ b/app/src/main/res/layout/fragment_everyday_anime.xml @@ -1,74 +1,54 @@ - - + android:layout_height="wrap_content" + app:liftOnScrollTargetViewId="@+id/srl_everyday_anime_fragment"> - + - - + app:tabGravity="center" + app:tabMode="scrollable" /> + - + android:layout_below="@id/tb_everyday_anime_fragment" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:srlEnableLoadMore="false"> - - - - - - - + android:clipToPadding="false" /> + - \ No newline at end of file + android:layout="@layout/layout_image_text_tip_1" + app:layout_anchor="@id/srl_everyday_anime_fragment" + app:layout_anchorGravity="center" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 7b018cec..986d7aa0 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -1,107 +1,66 @@ - - - - + android:layout_height="wrap_content" + app:liftOnScroll="true" + app:liftOnScrollTargetViewId="@+id/vp2_home_fragment"> - - - - - + android:clipToPadding="false" + app:buttonGravity="center_vertical" + app:layout_scrollFlags="scroll|enterAlways" + app:menu="@menu/menu_home_fragment" + app:navigationContentDescription="@string/home_fragment_menu_rank" + app:navigationIcon="@drawable/ic_bar_chart_24"> - - + + - + + + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - \ No newline at end of file + android:layout="@layout/layout_image_text_tip_1" + app:layout_anchor="@id/vp2_home_fragment" + app:layout_anchorGravity="center" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_local_data_source.xml b/app/src/main/res/layout/fragment_local_data_source.xml new file mode 100644 index 00000000..a8ba4eb0 --- /dev/null +++ b/app/src/main/res/layout/fragment_local_data_source.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_more.xml b/app/src/main/res/layout/fragment_more.xml index 01473247..152257c1 100644 --- a/app/src/main/res/layout/fragment_more.xml +++ b/app/src/main/res/layout/fragment_more.xml @@ -1,46 +1,30 @@ - - + android:layout_height="wrap_content"> - - - - + app:buttonGravity="center_vertical" + app:navigationIcon="@drawable/ic_beans_24" + app:title="@string/more" /> + - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingVertical="6dp" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + diff --git a/app/src/main/res/layout/fragment_more_dialog.xml b/app/src/main/res/layout/fragment_more_dialog.xml index 3fd223b2..779da0b8 100644 --- a/app/src/main/res/layout/fragment_more_dialog.xml +++ b/app/src/main/res/layout/fragment_more_dialog.xml @@ -3,29 +3,29 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/shape_circle_corner_bottom_dialog_white_12_skin" + android:background="@drawable/shape_circle_corner_bottom_dialog_12" android:gravity="center" android:orientation="vertical" android:paddingStart="13dp" android:paddingEnd="13dp"> - + android:textColor="?attr/colorOnSurface" /> - + android:textColor="?attr/colorOnSurface" + app:drawableTint="?attr/colorOnSurface" + app:drawableTopCompat="@drawable/ic_live_tv_24" /> - - + android:textColor="?attr/colorOnSurface" + app:drawableTint="?attr/colorOnSurface" + app:drawableTopCompat="@drawable/ic_play_circle_24" /> - - + android:textColor="?attr/colorOnSurface" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_rank.xml b/app/src/main/res/layout/fragment_rank.xml index fd5faa55..ca35e3e6 100644 --- a/app/src/main/res/layout/fragment_rank.xml +++ b/app/src/main/res/layout/fragment_rank.xml @@ -1,5 +1,4 @@ + android:descendantFocusability="blocksDescendants"> @@ -27,5 +25,4 @@ android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout="@layout/layout_image_text_tip_1" /> - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_send_danmaku_font_dialog.xml b/app/src/main/res/layout/fragment_send_danmaku_font_dialog.xml new file mode 100644 index 00000000..adfd1e9a --- /dev/null +++ b/app/src/main/res/layout/fragment_send_danmaku_font_dialog.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + +