├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── a.png │ │ │ │ ├── b.png │ │ │ │ ├── tfl.png │ │ │ │ ├── ic_bilibili.png │ │ │ │ ├── ic_bilimiao.png │ │ │ │ ├── ic_bilibilihd.png │ │ │ │ ├── ic_bilibilitv.png │ │ │ │ ├── ic_bilibili_blue.png │ │ │ │ ├── ic_movie_pay_area_limit.png │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_shizuku.png │ │ │ │ ├── ic_app_icon.webp │ │ │ │ ├── ic_app_icon_round.webp │ │ │ │ └── ic_app_icon_foreground.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_shizuku.png │ │ │ │ ├── ic_app_icon.webp │ │ │ │ ├── ic_app_icon_round.webp │ │ │ │ └── ic_app_icon_foreground.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_shizuku.png │ │ │ │ ├── ic_app_icon.webp │ │ │ │ ├── ic_app_icon_round.webp │ │ │ │ └── ic_app_icon_foreground.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_shizuku.png │ │ │ │ ├── ic_app_icon.webp │ │ │ │ ├── ic_app_icon_round.webp │ │ │ │ └── ic_app_icon_foreground.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_shizuku.png │ │ │ │ ├── ic_app_icon.webp │ │ │ │ ├── ic_app_icon_round.webp │ │ │ │ └── ic_app_icon_foreground.webp │ │ │ ├── xml │ │ │ │ ├── filepaths.xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ ├── values │ │ │ │ ├── ic_app_icon_background.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_app_icon.xml │ │ │ │ └── ic_app_icon_round.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── ic_app_icon-playstore.png │ │ ├── java │ │ │ └── cn │ │ │ │ └── a10miaomiao │ │ │ │ └── bilidown │ │ │ │ ├── common │ │ │ │ ├── ConstantUtil.kt │ │ │ │ ├── UrlUtil.kt │ │ │ │ ├── CommandUtil.kt │ │ │ │ ├── datastore │ │ │ │ │ ├── DataStoreKeys.kt │ │ │ │ │ └── datastore.kt │ │ │ │ ├── file │ │ │ │ │ ├── MiaoFile.kt │ │ │ │ │ ├── MiaoJavaFile.kt │ │ │ │ │ └── MiaoDocumentFile.kt │ │ │ │ ├── MiaoLog.kt │ │ │ │ ├── CompositionLocal.kt │ │ │ │ ├── viewmodel │ │ │ │ │ └── viewmodel.kt │ │ │ │ ├── BiliDownOutFile.kt │ │ │ │ ├── scrollable │ │ │ │ │ └── ScaffoldScrollableState.kt │ │ │ │ ├── BiliDownUtils.kt │ │ │ │ ├── lifecycle │ │ │ │ │ └── ComposeLifecycle.kt │ │ │ │ ├── permission │ │ │ │ │ └── StoragePermission.kt │ │ │ │ ├── molecule │ │ │ │ │ └── molecule.kt │ │ │ │ └── BiliDownFile.kt │ │ │ │ ├── entity │ │ │ │ ├── BiliAppInfo.kt │ │ │ │ ├── VideoOutInfo.kt │ │ │ │ ├── BiliDownloadEntryAndPathInfo.kt │ │ │ │ ├── DownloadInfo.kt │ │ │ │ └── BiliDownloadEntryInfo.kt │ │ │ │ ├── ui │ │ │ │ ├── theme │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Type.kt │ │ │ │ │ └── Theme.kt │ │ │ │ ├── animation │ │ │ │ │ ├── Constants.kt │ │ │ │ │ ├── MaterialFade.kt │ │ │ │ │ ├── MaterialFadeThrough.kt │ │ │ │ │ └── MaterialSharedAxis.kt │ │ │ │ ├── page │ │ │ │ │ ├── AboutPage.kt │ │ │ │ │ ├── ListPage.kt │ │ │ │ │ └── MorePage.kt │ │ │ │ ├── components │ │ │ │ │ ├── miao │ │ │ │ │ │ ├── MiaoBottomNavigation.kt │ │ │ │ │ │ ├── MiaoSwitch.kt │ │ │ │ │ │ ├── MiaoBottomNavigationItem.kt │ │ │ │ │ │ └── MiaoTabs.kt │ │ │ │ │ ├── SwipeToRefresh.kt │ │ │ │ │ ├── PermissionDialog.kt │ │ │ │ │ ├── OutFolderDialog.kt │ │ │ │ │ ├── SettingItem.kt │ │ │ │ │ ├── DownloadListItem.kt │ │ │ │ │ ├── ShizukuHelpDialog.kt │ │ │ │ │ ├── FileNameInputDialog.kt │ │ │ │ │ ├── DownloadDetailItem.kt │ │ │ │ │ └── RecordItem.kt │ │ │ │ └── BiliDownScreen.kt │ │ │ │ ├── BiliDownApp.kt │ │ │ │ ├── shizuku │ │ │ │ ├── CompositionLocal.kt │ │ │ │ ├── service │ │ │ │ │ └── ShizukuRxFFmpegSubscriber.kt │ │ │ │ ├── util │ │ │ │ │ └── RemoteServiceUtil.kt │ │ │ │ └── permission │ │ │ │ │ └── ShizukuPermission.kt │ │ │ │ ├── state │ │ │ │ ├── AppState.kt │ │ │ │ └── TaskStatus.kt │ │ │ │ ├── db │ │ │ │ ├── dao │ │ │ │ │ └── OutRecord.kt │ │ │ │ ├── model │ │ │ │ │ └── OutRecordDao.kt │ │ │ │ └── AppDatabase.kt │ │ │ │ ├── service │ │ │ │ ├── MyRxFFmpegSubscriber.kt │ │ │ │ └── MyProgressCallback.kt │ │ │ │ ├── LogViewerActivity.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── AppCrashHandler.kt │ │ ├── aidl │ │ │ └── cn │ │ │ │ └── a10miaomiao │ │ │ │ └── bilidown │ │ │ │ ├── entity │ │ │ │ ├── VideoOutInfo.aidl │ │ │ │ └── BiliDownloadEntryAndPathInfo.aidl │ │ │ │ ├── callback │ │ │ │ └── ProgressCallback.aidl │ │ │ │ └── shizuku │ │ │ │ └── IUserService.aidl │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── cn │ │ │ └── a10miaomiao │ │ │ └── bilidown │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── cn │ │ └── a10miaomiao │ │ └── bilidown │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── fastlane └── metadata │ └── android │ ├── zh-CN │ ├── title.txt │ ├── short_description.txt │ ├── changelogs │ │ └── 1004.txt │ ├── full_description.txt │ └── images │ │ ├── icon.png │ │ ├── featureGraphic.png │ │ └── phoneScreenshots │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ └── 3.jpg │ └── en-US │ ├── title.txt │ ├── short_description.txt │ ├── images │ ├── icon.png │ ├── featureGraphic.png │ └── phoneScreenshots │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ └── 3.jpg │ └── full_description.txt ├── .idea ├── .gitignore ├── compiler.xml ├── vcs.xml ├── misc.xml ├── gradle.xml └── inspectionProfiles │ └── Project_Default.xml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── settings.gradle ├── README.md ├── gradle.properties ├── .github └── workflows │ └── android.yml ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/title.txt: -------------------------------------------------------------------------------- 1 | 哔哩缓存导出 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | BiliDownOut -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/short_description.txt: -------------------------------------------------------------------------------- 1 | 导出安卓版B站App缓存的视频 -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/1004.txt: -------------------------------------------------------------------------------- 1 | 增加 任务队列功能 2 | 增加 已导出列表页,与进度页分离 3 | 增加 预测性返回手势支持 4 | 修复 若干BUG -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Export videos downloaded from the Android version of Bilibili App -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/full_description.txt: -------------------------------------------------------------------------------- 1 | 哔哩缓存导出是一个用于导出哔哩哔哩APP离线缓存视频的工具。 2 | 3 | 主要功能: 4 | 导出哔哩哔哩APP离线缓存视频 -------------------------------------------------------------------------------- /app/src/main/res/drawable/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/a.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/b.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/tfl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/tfl.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_app_icon-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/ic_app_icon-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bilibili.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/ic_bilibili.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bilimiao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/ic_bilimiao.png -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/ConstantUtil.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | object ConstantUtil { 4 | 5 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bilibilihd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/ic_bilibilihd.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bilibilitv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/ic_bilibilitv.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_shizuku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-hdpi/ic_shizuku.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_shizuku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-mdpi/ic_shizuku.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_shizuku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xhdpi/ic_shizuku.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bilibili_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/ic_bilibili_blue.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-hdpi/ic_app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-mdpi/ic_app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xhdpi/ic_app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_shizuku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxhdpi/ic_shizuku.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_shizuku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_shizuku.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxhdpi/ic_app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_app_icon.webp -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/zh-CN/images/icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-hdpi/ic_app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-mdpi/ic_app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_movie_pay_area_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/drawable/ic_movie_pay_area_limit.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xhdpi/ic_app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxhdpi/ic_app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/aidl/cn/a10miaomiao/bilidown/entity/VideoOutInfo.aidl: -------------------------------------------------------------------------------- 1 | // VideoOutInfo.aidl 2 | package cn.a10miaomiao.bilidown.entity; 3 | 4 | 5 | parcelable VideoOutInfo; -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_app_icon_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-hdpi/ic_app_icon_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_app_icon_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-mdpi/ic_app_icon_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_app_icon_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xhdpi/ic_app_icon_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_app_icon_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxhdpi/ic_app_icon_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_app_icon_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_foreground.webp -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/zh-CN/images/featureGraphic.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_app_icon_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10miaomiao/bili-down-out/HEAD/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | BiliDownOut is a tool for exporting downloaded videos from BiliDownOut App. 2 | 3 | Main Features: 4 | Exporting videos downloaded from Bilibili App -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /app/release 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | .idea/ 12 | app/signing.properties 13 | -------------------------------------------------------------------------------- /app/src/main/aidl/cn/a10miaomiao/bilidown/entity/BiliDownloadEntryAndPathInfo.aidl: -------------------------------------------------------------------------------- 1 | // BiliDownloadEntryAndPathInfo.aidl 2 | package cn.a10miaomiao.bilidown.entity; 3 | 4 | 5 | parcelable BiliDownloadEntryAndPathInfo; -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BiliDownOut 3 | 4 | Log Viewer 5 | Copy log 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 哔哩缓存导出 4 | 5 | 程序日志 6 | 复制日志 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 01 23:17:07 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/UrlUtil.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | object UrlUtil { 4 | 5 | fun autoHttps(url: String) =if ("://" in url) { 6 | url.replace("http://","https://") 7 | } else { 8 | "https:$url" 9 | } 10 | 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_app_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/CommandUtil.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | import java.io.File 4 | 5 | object CommandUtil { 6 | 7 | fun filePath(file: File): String { 8 | val path = file.absolutePath.replace("\"", "\\\"") 9 | return "\"${path}\"" 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/entity/BiliAppInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.entity 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | data class BiliAppInfo( 6 | val name: String, 7 | val packageName: String, 8 | @DrawableRes 9 | val icon: Int, 10 | val isInstall: Boolean = false, 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/entity/VideoOutInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.entity 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class VideoOutInfo( 8 | val entryDirPath: String, 9 | val outFilePath: String, 10 | val name: String, 11 | val cover: String, 12 | ) : Parcelable 13 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/entity/BiliDownloadEntryAndPathInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.entity 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class BiliDownloadEntryAndPathInfo( 8 | val pageDirPath: String, 9 | val entryDirPath: String, 10 | val entry: BiliDownloadEntryInfo, 11 | ): Parcelable 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_app_icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/animation/Constants.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilimiao.compose.animation 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.dp 5 | 6 | internal const val DefaultMotionDuration: Int = 300 7 | internal const val DefaultFadeInDuration: Int = 150 8 | internal const val DefaultFadeOutDuration: Int = 75 9 | internal val DefaultSlideDistance: Dp = 30.dp -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/page/AboutPage.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.page 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.NavHostController 6 | 7 | @OptIn(ExperimentalMaterial3Api::class) 8 | @Composable 9 | fun AboutPage( 10 | navController: NavHostController, 11 | ) { 12 | 13 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/datastore/DataStoreKeys.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.datastore 2 | 3 | import androidx.datastore.preferences.core.booleanPreferencesKey 4 | import androidx.datastore.preferences.core.stringSetPreferencesKey 5 | 6 | object DataStoreKeys { 7 | 8 | val appPackageNameSet = stringSetPreferencesKey("app_package_name_set") 9 | 10 | val enabledShizuku = booleanPreferencesKey("enabled_shizuku") 11 | } -------------------------------------------------------------------------------- /app/src/test/java/cn/a10miaomiao/bilidown/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/aidl/cn/a10miaomiao/bilidown/callback/ProgressCallback.aidl: -------------------------------------------------------------------------------- 1 | // ProgressCallback.aidl 2 | package cn.a10miaomiao.bilidown.callback; 3 | 4 | import cn.a10miaomiao.bilidown.entity.VideoOutInfo; 5 | 6 | interface ProgressCallback { 7 | void onStart(in VideoOutInfo info); 8 | void onFinish(in VideoOutInfo info); 9 | void onCancel(in VideoOutInfo info); 10 | void onProgress(in VideoOutInfo info, int progress, long progressTime); 11 | void onError(in VideoOutInfo info, String message); 12 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url 'https://www.jitpack.io' } 14 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 15 | } 16 | } 17 | rootProject.name = "BiliDown" 18 | include ':app' 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/BiliDownApp.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown 2 | 3 | import android.app.Application 4 | import cn.a10miaomiao.bilidown.db.AppDatabase 5 | import cn.a10miaomiao.bilidown.state.AppState 6 | 7 | class BiliDownApp: Application() { 8 | 9 | val state = AppState() 10 | 11 | lateinit var database: AppDatabase 12 | private set 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | AppCrashHandler.getInstance(this) 17 | state.init(this) 18 | database = AppDatabase.initialize(this) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/file/MiaoFile.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.file 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Environment 9 | import android.provider.DocumentsContract 10 | import androidx.documentfile.provider.DocumentFile 11 | import java.io.File 12 | 13 | interface MiaoFile { 14 | val path: String 15 | val name: String 16 | val isDirectory: Boolean 17 | fun exists(): Boolean 18 | fun listFiles(): List 19 | fun canRead(): Boolean 20 | fun readText(): String 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/shizuku/CompositionLocal.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.shizuku 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ProvidableCompositionLocal 5 | import androidx.compose.runtime.staticCompositionLocalOf 6 | import cn.a10miaomiao.bilidown.shizuku.permission.ShizukuPermission 7 | 8 | private fun noLocalProvidedFor(name: String): Nothing { 9 | error("CompositionLocal $name not present") 10 | } 11 | 12 | internal val LocalShizukuPermission: ProvidableCompositionLocal = staticCompositionLocalOf { 13 | noLocalProvidedFor("LocalContext") 14 | } 15 | 16 | @Composable 17 | fun localShizukuPermission() = LocalShizukuPermission.current -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/MiaoLog.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | typealias AndroidLog = android.util.Log 4 | 5 | object MiaoLog { 6 | 7 | private val currentLevel: Int = AndroidLog.INFO 8 | private fun String.simpleName() = substring(lastIndexOf('.') + 1, indexOf("$")) 9 | 10 | fun info(msg: () -> String) { 11 | AndroidLog.i("MiaoLog:" + msg::class.java.name.simpleName(), msg()) 12 | } 13 | 14 | fun debug(msg: () -> String) { 15 | AndroidLog.d("MiaoLog:" + msg::class.java.name.simpleName(), msg()) 16 | } 17 | 18 | fun error(msg: () -> String) { 19 | AndroidLog.e("MiaoLog:" + msg::class.java.name.simpleName(), msg()) 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/aidl/cn/a10miaomiao/bilidown/shizuku/IUserService.aidl: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.shizuku; 2 | 3 | import cn.a10miaomiao.bilidown.entity.BiliDownloadEntryAndPathInfo; 4 | import cn.a10miaomiao.bilidown.callback.ProgressCallback; 5 | 6 | interface IUserService { 7 | 8 | void destroy() = 16777114; // Destroy method defined by Shizuku server 9 | 10 | void exit() = 1; // Exit method defined by user 11 | 12 | String doSomething() = 2; 13 | 14 | List readDownloadList(String path) = 3; 15 | 16 | List readDownloadDirectory(String path) = 4; 17 | 18 | String exportBiliVideo(String entryDirPath, String outFilePath, ProgressCallback callback) = 5; 19 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/CompositionLocal.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | import android.view.ViewGroup 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.ProvidableCompositionLocal 6 | import androidx.compose.runtime.staticCompositionLocalOf 7 | import cn.a10miaomiao.bilidown.common.permission.StoragePermission 8 | 9 | private fun noLocalProvidedFor(name: String): Nothing { 10 | error("CompositionLocal $name not present") 11 | } 12 | 13 | internal val LocalStoragePermission: ProvidableCompositionLocal = staticCompositionLocalOf { 14 | noLocalProvidedFor("LocalContext") 15 | } 16 | 17 | @Composable 18 | fun localStoragePermission() = LocalStoragePermission.current -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/androidTest/java/cn/a10miaomiao/bilidown/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("cn.a10miaomiao.bilidown", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/viewmodel/viewmodel.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.viewmodel 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | 7 | fun newViewModelFactory(initializer: (() -> T)): ViewModelProvider.Factory { 8 | return object : ViewModelProvider.Factory { 9 | override fun create(modelClass: Class): R { 10 | return initializer.invoke() as R 11 | } 12 | } 13 | } 14 | 15 | @Suppress("MissingJvmstatic") 16 | @Composable 17 | inline fun viewModel( 18 | noinline initializer: (() -> VM) 19 | ): VM { 20 | return androidx.lifecycle.viewmodel.compose.viewModel( 21 | factory = newViewModelFactory(initializer) 22 | ) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/miao/MiaoBottomNavigation.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components.miao 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.RowScope 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material3.ColorScheme 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun MiaoBottomNavigation( 16 | modifier: Modifier = Modifier, 17 | content: @Composable RowScope.() -> Unit, 18 | ) { 19 | Row( 20 | modifier = modifier 21 | .background(MaterialTheme.colorScheme.surfaceVariant) 22 | .height(60.dp), 23 | content = { content() } 24 | ) 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/file/MiaoJavaFile.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.file 2 | 3 | import java.io.File 4 | 5 | class MiaoJavaFile( 6 | private val file: File 7 | ) : MiaoFile { 8 | 9 | constructor( 10 | pathName: String 11 | ) : this(File(pathName)) 12 | 13 | override val path: String 14 | get() = file.path 15 | 16 | override val isDirectory: Boolean 17 | get() = file.isDirectory 18 | 19 | override val name: String 20 | get() = file.name 21 | 22 | override fun exists(): Boolean { 23 | return file.exists() 24 | } 25 | 26 | override fun listFiles(): List { 27 | return file.listFiles().map { 28 | MiaoJavaFile(it) 29 | } 30 | } 31 | 32 | override fun canRead(): Boolean { 33 | return file.canRead() 34 | } 35 | 36 | override fun readText(): String { 37 | return file.readText() 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/state/AppState.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.state 2 | 3 | import android.content.Context 4 | import cn.a10miaomiao.bilidown.shizuku.permission.ShizukuPermission 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.StateFlow 7 | 8 | class AppState { 9 | 10 | private val _taskStatus = MutableStateFlow(TaskStatus.InIdle) 11 | val taskStatus: StateFlow = _taskStatus 12 | 13 | private val _shizukuState = MutableStateFlow( 14 | ShizukuPermission.ShizukuPermissionState() 15 | ) 16 | val shizukuState: StateFlow = _shizukuState 17 | 18 | fun init(context: Context) { 19 | 20 | } 21 | 22 | fun putTaskStatus(taskStatus: TaskStatus) { 23 | _taskStatus.value = taskStatus 24 | } 25 | 26 | fun putShizukuState(state: ShizukuPermission.ShizukuPermissionState) { 27 | _shizukuState.value = state 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/db/dao/OutRecord.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.db.dao 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "out_record") 8 | data class OutRecord ( 9 | @PrimaryKey(autoGenerate = true) val id: Long? = null, 10 | @ColumnInfo(name = "input_path") val entryDirPath: String, 11 | @ColumnInfo(name = "out_file_path") val outFilePath: String, 12 | @ColumnInfo val title: String, 13 | @ColumnInfo val cover: String, 14 | @ColumnInfo val status: Int, 15 | @ColumnInfo val type: Int, 16 | @ColumnInfo val message: String? = null, 17 | @ColumnInfo(name = "create_time") val createTime: Long, 18 | @ColumnInfo(name = "update_time") val updateTime: Long, 19 | ) { 20 | companion object { 21 | const val STATUS_WAIT = 0 22 | const val STATUS_SUCCESS = 1 23 | const val STATUS_FAIL = 2 24 | const val STATUS_IN_PROGRESS = 10 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/entity/DownloadInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.entity 2 | 3 | enum class DownloadType { 4 | VIDEO, 5 | BANGUMI 6 | } 7 | data class DownloadInfo( 8 | val dir_path: String, 9 | val media_type: Int, 10 | val has_dash_audio: Boolean, 11 | var is_completed: Boolean, 12 | val total_bytes: Long, 13 | val downloaded_bytes: Long, 14 | val title: String, 15 | val cover: String, 16 | val id: Long, 17 | val cid: Long, 18 | val type: DownloadType, 19 | val items: MutableList, 20 | // val owner_id: Long, 21 | ) 22 | 23 | data class DownloadItemInfo( 24 | val dir_path: String, 25 | val media_type: Int, 26 | val has_dash_audio: Boolean, 27 | val is_completed: Boolean, 28 | val total_bytes: Long, 29 | val downloaded_bytes: Long, 30 | val title: String, 31 | val cover: String, 32 | val id: Long, 33 | val type: DownloadType, 34 | val index_title: String, 35 | val cid: Long, 36 | val epid: Long, 37 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/db/model/OutRecordDao.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.db.model 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import androidx.room.Update 8 | import cn.a10miaomiao.bilidown.db.dao.OutRecord 9 | 10 | @Dao 11 | interface OutRecordDao { 12 | @Query("SELECT * FROM out_record order by update_time desc") 13 | suspend fun getAll(): List 14 | 15 | @Query("SELECT * FROM out_record WHERE status=:status order by update_time desc") 16 | suspend fun getAllByStatus(status: Int): List 17 | 18 | @Query("SELECT * FROM out_record WHERE input_path IN (:entryDirPaths)") 19 | suspend fun getAllByEntryDirPaths(entryDirPaths: Array): List 20 | 21 | @Query("SELECT * FROM out_record WHERE input_path=:path") 22 | suspend fun findByPath(path: String): OutRecord? 23 | 24 | @Insert 25 | suspend fun insertAll(vararg task: OutRecord) 26 | 27 | @Delete 28 | suspend fun delete(user: OutRecord) 29 | 30 | @Update 31 | suspend fun update(user: OutRecord) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/BiliDownOutFile.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Environment 6 | import android.util.Log 7 | import java.io.File 8 | 9 | class BiliDownOutFile( 10 | name: String, 11 | ) { 12 | 13 | companion object { 14 | const val DIR_NAME = "BiliDownOut" 15 | val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) 16 | 17 | fun getOutFolderUri(): Uri { 18 | return Uri.parse("content://com.android.externalstorage.documents/document/primary:Download%2f${DIR_NAME}") 19 | } 20 | 21 | fun getOutFolderPath(): String { 22 | return downloadDir.path + File.separator + DIR_NAME 23 | } 24 | } 25 | 26 | private val outDir = File(downloadDir, DIR_NAME) 27 | 28 | init { 29 | if (!outDir.exists()){ 30 | outDir.mkdir() 31 | } 32 | } 33 | 34 | val file = File(outDir, name) 35 | 36 | val path get() = file.path 37 | val name get() = file.name 38 | 39 | fun exists() = file.exists() 40 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/datastore/datastore.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.datastore 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.datastore.core.DataStore 7 | import androidx.datastore.preferences.core.Preferences 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | 12 | private const val DATA_STORE_KEY = "BiliDown_DataStore" 13 | 14 | val Context.dataStore: DataStore by preferencesDataStore(DATA_STORE_KEY) 15 | 16 | @Composable 17 | fun rememberDataStorePreferencesFlow( 18 | context: Context, 19 | key: Preferences.Key 20 | ) : Flow { 21 | return remember { 22 | context.dataStore.data.map { 23 | it[key] 24 | } 25 | } 26 | } 27 | 28 | @Composable 29 | fun rememberDataStorePreferencesFlow( 30 | context: Context, 31 | key: Preferences.Key, 32 | initial: T, 33 | ) : Flow { 34 | return remember { 35 | context.dataStore.data.map { 36 | it[key] ?: initial 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 |
7 | 8 | # BiliDownOut(哔哩缓存导出) 9 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/10miaomiao/bili-down-out)](https://github.com/10miaomiao/bili-down-out/releases) ![GitHub All Releases](https://img.shields.io/github/downloads/10miaomiao/bili-down-out/total) ![GitHub stars](https://img.shields.io/github/stars/10miaomiao/bili-down-out?style=flat) ![GitHub forks](https://img.shields.io/github/forks/10miaomiao/bili-down-out) 10 | 11 |
12 | 13 | 14 | ### 关于本项目 15 | BiliDownOut(哔哩缓存导出)是一个用于导出哔哩哔哩APP离线缓存视频的工具。 16 | 17 | 高版本安卓(13+)需要使用[Shizuku](https://shizuku.rikka.app/)授权访问Andoird/data文件夹。 18 | 19 | 主要功能: 20 | - 导出哔哩哔哩APP离线缓存视频 21 | - 无其它功能 22 | 23 | ### 声明 24 | 此项目 (BiliDownOut) 是个人为了兴趣而开发, 仅用于学习和测试。 25 | 26 | ### 下载 27 | 1. 从[GithubRelease](https://github.com/10miaomiao/bili-down-out/releases)下载 28 | 2. 从[GiteeRelease](https://gitee.com/10miaomiao/bili-down-out/releases)下载 29 | 30 | [下载应用请到 F-Droid](https://f-droid.org/packages/cn.a10miaomiao.bilidown) 33 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/db/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.db 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import androidx.room.migration.Migration 8 | import androidx.sqlite.db.SupportSQLiteDatabase 9 | import cn.a10miaomiao.bilidown.db.dao.OutRecord 10 | import cn.a10miaomiao.bilidown.db.model.OutRecordDao 11 | 12 | @Database( 13 | entities = [ OutRecord::class,], 14 | version = 2 15 | ) 16 | abstract class AppDatabase : RoomDatabase() { 17 | 18 | abstract fun outRecordDao(): OutRecordDao 19 | 20 | companion object { 21 | 22 | fun initialize(context: Context): AppDatabase { 23 | return Room.databaseBuilder( 24 | context, 25 | AppDatabase::class.java, 26 | "bili-down-out" 27 | ).apply { 28 | fallbackToDestructiveMigration() 29 | addMigrations( 30 | MIGRATION_1_2 31 | ) 32 | }.build() 33 | } 34 | 35 | private val MIGRATION_1_2 = object : Migration(1, 2) { 36 | override fun migrate(database: SupportSQLiteDatabase) { 37 | database.execSQL("ALTER TABLE out_record ADD COLUMN `message` TEXT") 38 | } 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/scrollable/ScaffoldScrollableState.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.scrollable 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.ui.geometry.Offset 6 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 7 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource 8 | 9 | @Stable 10 | class ScaffoldScrollableState { 11 | 12 | private val _showBottomBar = mutableStateOf(true) 13 | val showBottomBar get() = _showBottomBar.value 14 | 15 | fun slideDown() { 16 | _showBottomBar.value = false 17 | } 18 | 19 | fun slideUp() { 20 | _showBottomBar.value = true 21 | } 22 | } 23 | 24 | class ScaffoldNestedScrollConnection( 25 | val state: ScaffoldScrollableState, 26 | ) : NestedScrollConnection { 27 | override fun onPreScroll( 28 | available: Offset, 29 | source: NestedScrollSource 30 | ): Offset { 31 | return Offset.Zero 32 | } 33 | 34 | override fun onPostScroll( 35 | consumed: Offset, 36 | available: Offset, 37 | source: NestedScrollSource 38 | ): Offset { 39 | if (consumed.y > 0) { 40 | state.slideUp() 41 | } else if (consumed.y < 0) { 42 | state.slideDown() 43 | } 44 | return Offset.Zero 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/BiliDownScreen.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.filled.List 5 | import androidx.compose.material.icons.filled.* 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | 8 | sealed class BiliDownScreen( 9 | val route: String, 10 | val name: String, 11 | val icon: ImageVector = Icons.Filled.Favorite, 12 | ) { 13 | object List : BiliDownScreen("list", "列表", Icons.Filled.Home) 14 | object More : BiliDownScreen("more", "设置", Icons.Filled.Settings) 15 | object Progress : BiliDownScreen("progress", "进度", Icons.Filled.DateRange) 16 | object OutList: BiliDownScreen("out_list", "已导出", Icons.Filled.CheckCircle) 17 | object Detail : BiliDownScreen("detail", "详情") 18 | object AddApp : BiliDownScreen("add_app", "添加APP信息") 19 | object About : BiliDownScreen("about", "关于") 20 | 21 | companion object { 22 | private val routeToNameMap = mapOf( 23 | "list" to "哔哩缓存导出", 24 | "progress" to "当前进度", 25 | "out_list" to "导出记录", 26 | "more" to "设置", 27 | "add_app" to "添加APP", 28 | "about" to "关于", 29 | "detail" to "哔哩缓存详情", 30 | ) 31 | 32 | fun getRouteName(route: String?): String { 33 | val key = route?.split("?")?.get(0) ?: "" 34 | return routeToNameMap[key] ?: "BiliDownOut" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/service/MyRxFFmpegSubscriber.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.service 2 | 3 | import android.util.Log 4 | import cn.a10miaomiao.bilidown.common.MiaoLog 5 | import cn.a10miaomiao.bilidown.state.AppState 6 | import cn.a10miaomiao.bilidown.state.TaskStatus 7 | import io.microshow.rxffmpeg.RxFFmpegSubscriber 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import java.io.File 10 | 11 | open class MyRxFFmpegSubscriber( 12 | private val appState: AppState, 13 | // private val tempPath: String, 14 | ) : RxFFmpegSubscriber() { 15 | private val TAG = "MyRxFFmpegSubscriber" 16 | 17 | override fun onFinish() { 18 | appState.putTaskStatus(TaskStatus.InIdle) 19 | } 20 | 21 | override fun onProgress(progress: Int, progressTime: Long) { 22 | val taskStatus = appState.taskStatus.value 23 | if (taskStatus is TaskStatus.InProgress) { 24 | appState.putTaskStatus( 25 | taskStatus.copy( 26 | progress = progress.toFloat() / 100f 27 | ) 28 | ) 29 | } 30 | Log.d(TAG, "onProgress$progress $progressTime") 31 | } 32 | 33 | override fun onCancel() { 34 | appState.putTaskStatus(TaskStatus.InIdle) 35 | } 36 | 37 | override fun onError(message: String) { 38 | MiaoLog.info { message } 39 | appState.putTaskStatus(TaskStatus.Error( 40 | appState.taskStatus.value, 41 | message 42 | )) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/SwipeToRefresh.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material.ExperimentalMaterialApi 7 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 8 | import androidx.compose.material.pullrefresh.pullRefresh 9 | import androidx.compose.material.pullrefresh.rememberPullRefreshState 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | 15 | @OptIn(ExperimentalMaterialApi::class) 16 | @Composable 17 | fun SwipeToRefresh( 18 | modifier: Modifier = Modifier, 19 | refreshing: Boolean, 20 | onRefresh: () -> Unit, 21 | content: @Composable BoxScope.() -> Unit 22 | ) { 23 | val state = rememberPullRefreshState( 24 | refreshing = refreshing, 25 | onRefresh = onRefresh, 26 | ) 27 | Box(modifier = modifier 28 | .fillMaxSize() 29 | .pullRefresh(state) 30 | ){ 31 | content() 32 | PullRefreshIndicator( 33 | refreshing, 34 | state, 35 | Modifier.align(Alignment.TopCenter), 36 | contentColor = MaterialTheme.colorScheme.primary, 37 | backgroundColor = MaterialTheme.colorScheme.surface, 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.defaults.buildfeatures.buildconfig=true 25 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Generate signing.properties 24 | run: | 25 | rm -rf 10miaomiao.jks 26 | ${{ secrets.BASH_DOWNLOAD_JKS }} 27 | echo "KEYSTORE_FILE = ${{github.workspace}}/10miaomiao.jks" > app/signing.properties 28 | echo "KEYSTORE_PASSWORD = ${{ secrets.KEYSTORE_PASSWORD }}" >> app/signing.properties 29 | echo "KEY_ALIAS = ${{ secrets.KEY_ALIAS }}" >> app/signing.properties 30 | echo "KEY_PASSWORD = ${{ secrets.KEY_PASSWORD }}" >> app/signing.properties 31 | - name: Grant execute permission for gradlew 32 | run: chmod +x gradlew 33 | - name: Build with Gradle 34 | run: ./gradlew assembleRelease 35 | 36 | - name: Upload binaries to release 37 | uses: svenstaro/upload-release-action@v2 38 | with: 39 | repo_token: ${{ secrets.GITHUB_TOKEN }} 40 | file: app/build/outputs/apk/release/*.apk 41 | tag: ${{ github.ref }} 42 | release_name: ${{ github.ref }} 43 | overwrite: true 44 | file_glob: true 45 | body: "" -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/animation/MaterialFade.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilimiao.compose.animation 2 | 3 | import androidx.compose.animation.EnterTransition 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.animation.core.FastOutSlowInEasing 6 | import androidx.compose.animation.core.LinearEasing 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.animation.fadeIn 9 | import androidx.compose.animation.fadeOut 10 | import androidx.compose.animation.scaleIn 11 | 12 | private const val DefaultFadeEndThresholdEnter = 0.3f 13 | 14 | private val Int.ForFade: Int 15 | get() = (this * DefaultFadeEndThresholdEnter).toInt() 16 | 17 | /** 18 | * [materialFadeIn] allows to switch a layout with a fade-in animation. 19 | */ 20 | fun materialFadeIn( 21 | durationMillis: Int = DefaultFadeInDuration, 22 | ): EnterTransition = fadeIn( 23 | animationSpec = tween( 24 | durationMillis = durationMillis.ForFade, 25 | easing = LinearEasing, 26 | ), 27 | ) + scaleIn( 28 | animationSpec = tween( 29 | durationMillis = durationMillis, 30 | easing = FastOutSlowInEasing, 31 | ), 32 | initialScale = 0.8f, 33 | ) 34 | 35 | /** 36 | * [materialFadeOut] allows to switch a layout with a fade-out animation. 37 | */ 38 | fun materialFadeOut( 39 | durationMillis: Int = DefaultFadeOutDuration, 40 | ): ExitTransition = fadeOut( 41 | animationSpec = tween( 42 | durationMillis = durationMillis, 43 | easing = LinearEasing, 44 | ), 45 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/state/TaskStatus.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.state 2 | 3 | sealed class TaskStatus { 4 | open val entryDirPath: String = "" 5 | open val name: String = "" 6 | open val cover: String = "" 7 | open val progress = 0f 8 | 9 | object InIdle : TaskStatus() 10 | 11 | data class CopyingToTemp( 12 | override val entryDirPath: String, 13 | override val name: String, 14 | override val cover: String, 15 | override val progress: Float, 16 | // val videoFile: MiaoDocumentFile, 17 | // val audioFile: MiaoDocumentFile, 18 | ) : TaskStatus() 19 | 20 | data class Copying( 21 | override val entryDirPath: String, 22 | override val name: String, 23 | override val cover: String, 24 | override val progress: Float, 25 | ) : TaskStatus() 26 | 27 | data class InProgress( 28 | override val entryDirPath: String, 29 | override val name: String, 30 | override val cover: String, 31 | override val progress: Float, 32 | ) : TaskStatus() 33 | 34 | data class Error( 35 | override val entryDirPath: String, 36 | override val name: String, 37 | override val cover: String, 38 | override val progress: Float, 39 | val message: String, 40 | ) : TaskStatus() { 41 | constructor( 42 | status: TaskStatus, 43 | message: String 44 | ) : this( 45 | status.entryDirPath, 46 | status.name, 47 | status.cover, 48 | status.progress, 49 | message 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/PermissionDialog.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.provider.Settings 6 | import androidx.compose.material3.AlertDialog 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.platform.LocalContext 11 | import androidx.compose.ui.platform.LocalLifecycleOwner 12 | import cn.a10miaomiao.bilidown.ui.page.AddAppPageAction 13 | 14 | 15 | @Composable 16 | fun PermissionDialog( 17 | showPermissionDialog: Boolean, 18 | isGranted: Boolean, 19 | onDismiss: () -> Unit 20 | ) { 21 | val context = LocalContext.current 22 | val toPermissionSettingPage = remember { 23 | { 24 | val intent = Intent() 25 | intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS 26 | intent.data = Uri.parse("package:" + context.packageName) 27 | context.startActivity(intent) 28 | } 29 | } 30 | 31 | if (showPermissionDialog && !isGranted) { 32 | AlertDialog( 33 | onDismissRequest = onDismiss, 34 | title = { Text(text = "请授予存储权限") }, 35 | text = { 36 | Text(text = "需要访问Android/data文件夹,读取哔哩哔哩APP缓存文件") 37 | }, 38 | confirmButton = { 39 | TextButton( 40 | onClick = toPermissionSettingPage, 41 | ) { 42 | Text("去设置") 43 | } 44 | }, 45 | dismissButton = { 46 | TextButton( 47 | onClick = onDismiss, 48 | ) { 49 | Text("取消") 50 | } 51 | } 52 | ) 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/BiliDownUtils.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import android.net.Uri 8 | import android.os.Build 9 | import androidx.core.app.ActivityCompat.requestPermissions 10 | import androidx.core.content.ContextCompat 11 | import cn.a10miaomiao.bilidown.R 12 | import cn.a10miaomiao.bilidown.entity.BiliAppInfo 13 | import java.io.File 14 | 15 | 16 | object BiliDownUtils { 17 | 18 | val biliAppList = listOf( 19 | BiliAppInfo( 20 | "哔哩哔哩", 21 | "tv.danmaku.bili", 22 | icon = R.drawable.ic_bilibili 23 | ), 24 | BiliAppInfo( 25 | "哔哩哔哩(概念版)", 26 | "com.bilibili.app.blue", 27 | icon = R.drawable.ic_bilibili_blue 28 | ), 29 | BiliAppInfo( 30 | "哔哩哔哩(谷歌版)", 31 | "com.bilibili.app.in", 32 | icon = R.drawable.ic_bilibili 33 | ), 34 | BiliAppInfo( 35 | "bilimiao", 36 | "com.a10miaomiao.bilimiao", 37 | icon = R.drawable.ic_bilimiao 38 | ), 39 | // BiliAppInfo( 40 | // "bilimiao-dev", 41 | // "cn.a10miaomiao.bilimiao.dev", 42 | // icon = R.drawable.ic_bilimiao 43 | // ), 44 | ) 45 | 46 | fun checkSelfPermission( 47 | context: Context, 48 | packageName: String, 49 | ): Boolean { 50 | if (Build.VERSION.SDK_INT < 23) { // 5.0 51 | return true 52 | } 53 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { // 6.0-10.0 54 | val f = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) 55 | return f == PackageManager.PERMISSION_GRANTED 56 | } 57 | return false 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/miao/MiaoSwitch.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components.miao 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.animateDpAsState 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.offset 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.shape.CircleShape 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.alpha 17 | import androidx.compose.ui.unit.dp 18 | 19 | @Composable 20 | fun MiaoSwitch( 21 | modifier: Modifier = Modifier, 22 | activated: Boolean, 23 | enable: Boolean = true, 24 | onClick: (() -> Unit)? = null, 25 | ) { 26 | Surface( 27 | modifier = modifier 28 | .size(56.dp, 28.dp) 29 | .alpha(if (enable) 1f else 0.5f), 30 | shape = CircleShape, 31 | color = animateColorAsState( 32 | if (activated) MaterialTheme.colorScheme.primary 33 | else MaterialTheme.colorScheme.outline 34 | ).value 35 | ) { 36 | Box( 37 | modifier = Modifier.fillMaxSize() 38 | then if (onClick != null) Modifier.clickable { onClick() } else Modifier 39 | ) { 40 | Surface( 41 | modifier = Modifier 42 | .size(20.dp) 43 | .align(Alignment.CenterStart) 44 | .offset(x = animateDpAsState(if (activated) 32.dp else 4.dp).value), 45 | shape = CircleShape, 46 | color = animateColorAsState( 47 | if (activated) MaterialTheme.colorScheme.primary 48 | else MaterialTheme.colorScheme.primary 49 | ).value 50 | ) {} 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.ViewCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun BiliDownTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | MaterialTheme( 56 | colorScheme = colorScheme, 57 | typography = Typography, 58 | content = content 59 | ) 60 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/service/MyProgressCallback.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.service 2 | 3 | import android.util.Log 4 | import cn.a10miaomiao.bilidown.callback.ProgressCallback 5 | import cn.a10miaomiao.bilidown.common.MiaoLog 6 | import cn.a10miaomiao.bilidown.entity.VideoOutInfo 7 | import cn.a10miaomiao.bilidown.state.AppState 8 | import cn.a10miaomiao.bilidown.state.TaskStatus 9 | 10 | class MyProgressCallback( 11 | val service: BiliDownService, 12 | val appState: AppState, 13 | ) : ProgressCallback.Stub() { 14 | override fun onStart(info: VideoOutInfo?) { 15 | MiaoLog.debug { "onStart(${info})" } 16 | appState.putTaskStatus( 17 | TaskStatus.InProgress( 18 | name = info?.name ?: "unknown name", 19 | entryDirPath = info?.entryDirPath ?: "unknown path", 20 | cover = info?.cover ?: "", 21 | progress = 0f 22 | ) 23 | ) 24 | } 25 | 26 | override fun onFinish(info: VideoOutInfo?) { 27 | MiaoLog.debug { "onFinish(${info})" } 28 | appState.putTaskStatus(TaskStatus.InIdle) 29 | service.tryAddOutRecord( 30 | entryDirPath = info?.entryDirPath ?: "unknown path", 31 | outFilePath = info?.outFilePath ?: "unknown path", 32 | title = info?.name ?: "unknown name", 33 | cover = info?.cover ?: "", 34 | ) 35 | } 36 | 37 | override fun onCancel(info: VideoOutInfo?) { 38 | MiaoLog.debug { "onCancel(${info})" } 39 | appState.putTaskStatus(TaskStatus.InIdle) 40 | } 41 | 42 | override fun onProgress(info: VideoOutInfo?, progress: Int, progressTime: Long) { 43 | MiaoLog.debug { "onProgress(_, $progress, $progressTime)" } 44 | val taskStatus = appState.taskStatus.value 45 | if (taskStatus is TaskStatus.InProgress) { 46 | appState.putTaskStatus( 47 | taskStatus.copy( 48 | progress = progress.toFloat() / 100f 49 | ) 50 | ) 51 | } 52 | } 53 | 54 | override fun onError(info: VideoOutInfo?, message: String?) { 55 | MiaoLog.debug { "onError(${info}, $message)" } 56 | appState.putTaskStatus(TaskStatus.Error( 57 | appState.taskStatus.value, 58 | message ?: "unknown error", 59 | )) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/lifecycle/ComposeLifecycle.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.lifecycle 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.platform.LocalLifecycleOwner 7 | import androidx.lifecycle.DefaultLifecycleObserver 8 | import androidx.lifecycle.LifecycleOwner 9 | 10 | 11 | private typealias LifecycleEvent = () -> Unit 12 | 13 | class ComposeLifecycle( 14 | private val createEvent: LifecycleEvent? = null, 15 | private val startEvent: LifecycleEvent? = null, 16 | private val resumeEvent: LifecycleEvent? = null, 17 | private val pauseEvent: LifecycleEvent? = null, 18 | private val stopEvent: LifecycleEvent? = null, 19 | private val destroyEvent: LifecycleEvent? = null, 20 | ) : DefaultLifecycleObserver { 21 | 22 | override fun onCreate(owner: LifecycleOwner) { 23 | createEvent?.invoke() 24 | } 25 | 26 | override fun onStart(owner: LifecycleOwner) { 27 | startEvent?.invoke() 28 | } 29 | 30 | override fun onResume(owner: LifecycleOwner) { 31 | resumeEvent?.invoke() 32 | } 33 | 34 | override fun onPause(owner: LifecycleOwner) { 35 | pauseEvent?.invoke() 36 | } 37 | 38 | override fun onStop(owner: LifecycleOwner) { 39 | stopEvent?.invoke() 40 | } 41 | 42 | override fun onDestroy(owner: LifecycleOwner) { 43 | destroyEvent?.invoke() 44 | } 45 | 46 | } 47 | 48 | @Composable 49 | fun LaunchedLifecycleObserver( 50 | onCreate: LifecycleEvent? = null, 51 | onStart: LifecycleEvent? = null, 52 | onResume: LifecycleEvent? = null, 53 | onPause: LifecycleEvent? = null, 54 | onStop: LifecycleEvent? = null, 55 | onDestroy: LifecycleEvent? = null, 56 | ) { 57 | val composeLifecycle = remember( 58 | // onCreate, onStart, onResume, onPause, onStop, onDestroy 59 | ) { 60 | ComposeLifecycle( 61 | onCreate, 62 | onStart, 63 | onResume, 64 | onPause, 65 | onStop, 66 | onDestroy 67 | ) 68 | } 69 | val lifecycle = LocalLifecycleOwner.current.lifecycle 70 | DisposableEffect(lifecycle) { 71 | lifecycle.addObserver(composeLifecycle) 72 | onDispose { 73 | lifecycle.removeObserver(composeLifecycle) 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/miao/MiaoBottomNavigationItem.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components.miao 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.vector.ImageVector 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun RowScope.MiaoBottomNavigationItem( 19 | // icon: @Composable () -> Unit, 20 | // label: @Composable () -> Unit, 21 | icon: ImageVector, 22 | label: String, 23 | selected: Boolean, 24 | onClick: () -> Unit, 25 | ) { 26 | Box( 27 | modifier = Modifier.fillMaxHeight() 28 | .weight(1f), 29 | contentAlignment = Alignment.Center, 30 | ) { 31 | Surface( 32 | modifier = Modifier.clickable(onClick = onClick), 33 | shape = RoundedCornerShape(8.dp), 34 | color = if (selected) { 35 | MaterialTheme.colorScheme.primary 36 | } else { 37 | Color.Transparent 38 | } 39 | ) { 40 | Row( 41 | verticalAlignment = Alignment.CenterVertically, 42 | horizontalArrangement = Arrangement.Center, 43 | modifier = Modifier.padding(12.dp, 8.dp) 44 | ) { 45 | Icon( 46 | icon, 47 | modifier = Modifier.size(20.dp), 48 | contentDescription = label, 49 | tint = if (selected) { 50 | Color.White 51 | } else { 52 | Color.LightGray 53 | } 54 | ) 55 | Spacer(modifier = Modifier.width(8.dp)) 56 | Text( 57 | label, 58 | style = MaterialTheme.typography.labelSmall, 59 | color = if (selected) { 60 | Color.White 61 | } else { 62 | Color.Gray 63 | } 64 | ) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/animation/MaterialFadeThrough.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilimiao.compose.animation 2 | 3 | 4 | import androidx.compose.animation.ContentTransform 5 | import androidx.compose.animation.EnterTransition 6 | import androidx.compose.animation.ExitTransition 7 | import androidx.compose.animation.core.FastOutLinearInEasing 8 | import androidx.compose.animation.core.LinearOutSlowInEasing 9 | import androidx.compose.animation.core.tween 10 | import androidx.compose.animation.fadeIn 11 | import androidx.compose.animation.fadeOut 12 | import androidx.compose.animation.scaleIn 13 | import androidx.compose.animation.togetherWith 14 | 15 | private const val ProgressThreshold = 0.35f 16 | 17 | private val Int.ForOutgoing: Int 18 | get() = (this * ProgressThreshold).toInt() 19 | 20 | private val Int.ForIncoming: Int 21 | get() = this - this.ForOutgoing 22 | 23 | /** 24 | * [materialFadeThrough] allows to switch a layout with a fade through animation. 25 | * 26 | * @param durationMillis the duration of transition. 27 | */ 28 | fun materialFadeThrough( 29 | durationMillis: Int = DefaultMotionDuration, 30 | ): ContentTransform = materialFadeThroughIn( 31 | durationMillis = durationMillis, 32 | ) togetherWith materialFadeThroughOut( 33 | durationMillis = durationMillis, 34 | ) 35 | 36 | /** 37 | * [materialFadeThroughIn] allows to switch a layout with fade through enter transition. 38 | * 39 | * @param initialScale the starting scale of the enter transition. 40 | * @param durationMillis the duration of the enter transition. 41 | */ 42 | fun materialFadeThroughIn( 43 | initialScale: Float = 0.92f, 44 | durationMillis: Int = DefaultMotionDuration, 45 | ): EnterTransition = fadeIn( 46 | animationSpec = tween( 47 | durationMillis = durationMillis.ForIncoming, 48 | delayMillis = durationMillis.ForOutgoing, 49 | easing = LinearOutSlowInEasing, 50 | ), 51 | ) + scaleIn( 52 | animationSpec = tween( 53 | durationMillis = durationMillis.ForIncoming, 54 | delayMillis = durationMillis.ForOutgoing, 55 | easing = LinearOutSlowInEasing, 56 | ), 57 | initialScale = initialScale, 58 | ) 59 | 60 | /** 61 | * [materialFadeThroughOut] allows to switch a layout with fade through exit transition. 62 | * 63 | * @param durationMillis the duration of the exit transition. 64 | */ 65 | fun materialFadeThroughOut( 66 | durationMillis: Int = DefaultMotionDuration, 67 | ): ExitTransition = fadeOut( 68 | animationSpec = tween( 69 | durationMillis = durationMillis.ForOutgoing, 70 | delayMillis = 0, 71 | easing = FastOutLinearInEasing, 72 | ), 73 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/shizuku/service/ShizukuRxFFmpegSubscriber.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.shizuku.service 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Build 5 | import cn.a10miaomiao.bilidown.callback.ProgressCallback 6 | import cn.a10miaomiao.bilidown.common.CommandUtil 7 | import cn.a10miaomiao.bilidown.entity.VideoOutInfo 8 | import io.microshow.rxffmpeg.RxFFmpegSubscriber 9 | import java.io.File 10 | import java.io.IOException 11 | import java.nio.file.Files 12 | import java.nio.file.Paths 13 | import java.nio.file.attribute.PosixFilePermission 14 | 15 | 16 | class ShizukuRxFFmpegSubscriber( 17 | val videoOutInfo: VideoOutInfo, 18 | val callback: ProgressCallback? 19 | ) : RxFFmpegSubscriber() { 20 | override fun onError(message: String?) { 21 | callback?.onError(videoOutInfo, message) 22 | } 23 | 24 | override fun onFinish() { 25 | callback?.onFinish(videoOutInfo) 26 | val outFile = File(videoOutInfo.outFilePath) 27 | val ffTxtFile = File(outFile.parent, ".ff.txt") 28 | if (ffTxtFile.exists()) { 29 | ffTxtFile.delete() 30 | } 31 | if (outFile.exists()) { 32 | // 设置读写权限 33 | changeFolderPermission(outFile) 34 | } 35 | } 36 | 37 | override fun onProgress(progress: Int, progressTime: Long) { 38 | callback?.onProgress(videoOutInfo, progress, progressTime) 39 | } 40 | 41 | override fun onCancel() { 42 | callback?.onCancel(videoOutInfo) 43 | } 44 | 45 | private fun changeFolderPermission(file: File) { 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 47 | try { 48 | val perms: MutableSet = HashSet() 49 | perms.add(PosixFilePermission.OWNER_READ) 50 | perms.add(PosixFilePermission.OWNER_WRITE) 51 | perms.add(PosixFilePermission.OWNER_EXECUTE) 52 | perms.add(PosixFilePermission.GROUP_READ) 53 | perms.add(PosixFilePermission.GROUP_WRITE) 54 | perms.add(PosixFilePermission.GROUP_EXECUTE) 55 | val path = Paths.get(file.absolutePath) 56 | Files.setPosixFilePermissions(path, perms) 57 | } catch (e: IOException) { 58 | e.printStackTrace() 59 | } 60 | } else { 61 | chmodFile(file) 62 | } 63 | } 64 | 65 | private fun chmodFile(destFile: File) { 66 | try { 67 | val command = "chmod 777 ${CommandUtil.filePath(destFile)}" 68 | val runtime = Runtime.getRuntime() 69 | runtime.exec(command) 70 | } catch (e: IOException) { 71 | e.printStackTrace() 72 | } 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/LogViewerActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.widget.Toast 8 | import androidx.activity.ComponentActivity 9 | import androidx.activity.compose.setContent 10 | import androidx.activity.enableEdgeToEdge 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.rememberScrollState 15 | import androidx.compose.foundation.text.selection.SelectionContainer 16 | import androidx.compose.foundation.verticalScroll 17 | import androidx.compose.material3.* 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import cn.a10miaomiao.bilidown.ui.theme.BiliDownTheme 23 | 24 | class LogViewerActivity : ComponentActivity() { 25 | 26 | private val mLogSummary by lazy { 27 | intent.getStringExtra("log_summary") ?: "null" 28 | } 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | enableEdgeToEdge() 33 | setContent { 34 | BiliDownTheme { 35 | LogViewerView() 36 | } 37 | } 38 | } 39 | 40 | private fun copyLogText() { 41 | val logSummary = mLogSummary 42 | val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 43 | val clip = ClipData.newPlainText("log", logSummary) 44 | clipboard.setPrimaryClip(clip) 45 | Toast.makeText(this, "复制成功(●'◡'●)", Toast.LENGTH_SHORT).show() 46 | } 47 | 48 | @OptIn(ExperimentalMaterial3Api::class) 49 | @Composable 50 | fun LogViewerView() { 51 | val scrollState = rememberScrollState() 52 | 53 | Scaffold( 54 | topBar = { 55 | TopAppBar( 56 | title = { 57 | Text(text = stringResource(id = R.string.log_viewer)) 58 | }, 59 | actions = { 60 | TextButton(onClick = ::copyLogText) { 61 | Text( 62 | text = stringResource(id = R.string.copy_log) 63 | ) 64 | } 65 | } 66 | ) 67 | }, 68 | content = { innerPadding -> 69 | Column( 70 | modifier = Modifier 71 | .fillMaxSize() 72 | .verticalScroll(scrollState) 73 | .padding(innerPadding) 74 | .padding(10.dp) 75 | ) { 76 | SelectionContainer { 77 | Text( 78 | text = mLogSummary 79 | ) 80 | } 81 | } 82 | } 83 | ) 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/permission/StoragePermission.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.permission 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.os.Environment 10 | import android.provider.Settings 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.collectAsState 13 | import androidx.core.app.ActivityCompat 14 | import androidx.core.content.ContextCompat 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | 17 | 18 | class StoragePermission( 19 | val activity: Activity, 20 | ) { 21 | 22 | private var resultCallBack: (() -> Unit)? = null 23 | 24 | private val state = MutableStateFlow(StoragePermissionState( 25 | isGranted = checkSelfPermission(), 26 | isExternalStorage = isExternalStorageManager() 27 | )) 28 | 29 | init { 30 | 31 | } 32 | 33 | @Composable 34 | fun collectState(): StoragePermissionState { 35 | return state.collectAsState().value 36 | } 37 | 38 | fun requestPermissionsResult() { 39 | resultCallBack?.invoke() 40 | state.value = StoragePermissionState( 41 | isGranted = checkSelfPermission(), 42 | isExternalStorage = isExternalStorageManager() 43 | ) 44 | } 45 | 46 | fun checkSelfPermission(): Boolean { 47 | if (Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { // 5.0或安卓10以上 48 | return true 49 | } 50 | val permission1 = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) 51 | val permission2 = ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) 52 | return permission1 == PackageManager.PERMISSION_GRANTED && permission2 == PackageManager.PERMISSION_GRANTED 53 | } 54 | 55 | fun isExternalStorageManager(): Boolean { 56 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { // 10以上 57 | return Environment.isExternalStorageManager() 58 | } 59 | return true 60 | } 61 | 62 | fun requestPermissions( 63 | callback: () -> Unit 64 | ): Boolean { 65 | resultCallBack = callback 66 | if (!checkSelfPermission()) { 67 | //2、申请权限: 参数二:权限的数组;参数三:请求码 68 | ActivityCompat.requestPermissions( 69 | activity, 70 | arrayOf( 71 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 72 | Manifest.permission.READ_EXTERNAL_STORAGE, 73 | ), 74 | 1 75 | ) 76 | return false //没有权限 77 | } else if (!isExternalStorageManager()){ 78 | val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) 79 | intent.data = Uri.parse("package:${activity.packageName}") 80 | activity.startActivityForResult(intent, 1) 81 | return false //没有权限 82 | } 83 | return true 84 | } 85 | 86 | data class StoragePermissionState( 87 | val isGranted: Boolean, 88 | val isExternalStorage: Boolean, 89 | ) 90 | 91 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/miao/MiaoTabs.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components.miao 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyRow 5 | import androidx.compose.foundation.lazy.itemsIndexed 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.focus.FocusRequester 14 | import androidx.compose.ui.focus.onFocusChanged 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | 18 | //@Composable 19 | //fun MiaoTabs( 20 | // 21 | //) { 22 | // LazyRow( 23 | // contentPadding = PaddingValues(start = 12.dp, end = 12.dp), 24 | // horizontalArrangement = Arrangement.spacedBy(36.dp) 25 | // ) { 26 | // itemsIndexed(HomeAreaViewModel.CATEGORY) { index, item -> 27 | // val focusRequester = remember { FocusRequester() } 28 | // val isFocused = remember { mutableStateOf(false) } 29 | // 30 | // if (hasFocus() && cb() == index) { 31 | // SideEffect { 32 | // focusRequester.requestFocus() 33 | // } 34 | // } 35 | // Column( 36 | // horizontalAlignment = Alignment.CenterHorizontally, 37 | // modifier = Modifier.onFocusChanged { 38 | // isFocused.value = it.isFocused 39 | // if (isFocused.value) { 40 | // change(index) 41 | // } 42 | // if (it.isFocused) { 43 | // println("home tab isFocused") 44 | // } 45 | // }.focusRequester(focusRequester).focusProperties { 46 | // if (index == 0) { 47 | // left = otherRequester 48 | // } 49 | // }.focusTarget().noRippleClickable { 50 | // focusRequester.requestFocus() 51 | // }, 52 | // ) { 53 | // Text( 54 | // text = item.name.orEmpty(), style = MaterialTheme.typography.h3, color = if (isFocused.value) { 55 | // ColorResource.acRed 56 | // } else if (cb() == index) { 57 | // ColorResource.acRed30 58 | // } else { 59 | // Color.Black 60 | // } 61 | // ) 62 | // Spacer(modifier = Modifier.height(3.dp)) 63 | // Box( 64 | // modifier = Modifier.height(3.dp).width(22.dp).background( 65 | // if (isFocused.value) { 66 | // ColorResource.acRed 67 | // } else if (cb() == index) { 68 | // ColorResource.acRed30 69 | // } else { 70 | // Color.Transparent 71 | // } 72 | // ) 73 | // ) 74 | // } 75 | // } 76 | // } 77 | //} -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/entity/BiliDownloadEntryInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.entity 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | @Parcelize 9 | data class BiliDownloadEntryInfo( 10 | val media_type: Int = 1, 11 | val has_dash_audio: Boolean = false, 12 | var is_completed: Boolean, 13 | var total_bytes: Long, 14 | var downloaded_bytes: Long, 15 | val title: String, 16 | val type_tag: String? = null, 17 | val cover: String, 18 | val video_quality: Int? = null, 19 | val prefered_video_quality: Int, 20 | val quality_pithy_description: String = "", 21 | val guessed_total_bytes: Int, 22 | var total_time_milli: Long, 23 | val danmaku_count: Int, 24 | val time_update_stamp: Long = 0L, 25 | val time_create_stamp: Long = 0L, 26 | val can_play_in_advance: Boolean = false, 27 | var interrupt_transform_temp_file: Boolean = false, 28 | val avid: Long? = null, 29 | val spid: Long? = null, 30 | val bvid: String? = null, 31 | val owner_id: Long? = null, 32 | var page_data: PageInfo? = null, 33 | val season_id: String? = null, 34 | val source: SourceInfo? = null, 35 | var ep: EpInfo? = null, 36 | ): Parcelable { 37 | 38 | val key: Long 39 | get() { 40 | return source?.cid ?: page_data?.cid ?: avid!! 41 | } 42 | 43 | val name: String 44 | get() { 45 | val e = ep 46 | if (e != null) { 47 | return title + e.index_title 48 | } 49 | val p = page_data 50 | if (p != null) { 51 | return title + p.part 52 | } 53 | return title 54 | } 55 | 56 | val videoDirName: String 57 | get() = type_tag ?: video_quality.toString() 58 | 59 | // 视频分P信息 60 | @Serializable 61 | @Parcelize 62 | data class PageInfo( 63 | val cid: Long, 64 | val page: Int, 65 | val from: String? = null, 66 | val part: String? = null, 67 | val vid: String? = null, 68 | val has_alias: Boolean, 69 | val tid: Int, 70 | val width: Int = 0, 71 | val height: Int = 0, 72 | val rotate: Int = 0, 73 | val download_title: String? = null, 74 | val download_subtitle: String? = null 75 | ): Parcelable 76 | 77 | // 番剧源信息 78 | @Serializable 79 | @Parcelize 80 | data class SourceInfo( 81 | val av_id: Long, 82 | val cid: Long, 83 | // val website: String, 84 | ): Parcelable 85 | 86 | // 番剧剧集信息 87 | @Serializable 88 | @Parcelize 89 | data class EpInfo( 90 | val av_id: Long, 91 | val page: Int, 92 | val danmaku: Long, 93 | val cover: String, 94 | val episode_id: Long, 95 | val index: String, 96 | val index_title: String, 97 | val from: String, 98 | val season_type: Int, 99 | val width: Int, 100 | val height: Int, 101 | val rotate: Int, 102 | val link: String = "", 103 | val bvid: String = "", 104 | val sort_index: Int = 0, 105 | ): Parcelable 106 | 107 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/OutFolderDialog.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.os.Build 8 | import android.provider.DocumentsContract 9 | import android.widget.Toast 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.material3.AlertDialog 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TextButton 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.platform.LocalContext 17 | import cn.a10miaomiao.bilidown.common.BiliDownOutFile 18 | 19 | @Composable 20 | fun OutFolderDialog( 21 | showOutFolderDialog: Boolean, 22 | onDismiss: () -> Unit 23 | ) { 24 | val context = LocalContext.current 25 | 26 | if (showOutFolderDialog) { 27 | AlertDialog( 28 | onDismissRequest = onDismiss, 29 | title = { Text(text = "导出文件夹") }, 30 | text = { 31 | Column() { 32 | Text(text = "视频输出文件夹为:${BiliDownOutFile.getOutFolderPath()}") 33 | Text(text = "打开方式:[文件夹管理器]->内部储存空间->Download->${BiliDownOutFile.DIR_NAME}") 34 | Text(text = "或 [文件夹管理器]->下载->${BiliDownOutFile.DIR_NAME}") 35 | } 36 | }, 37 | confirmButton = { 38 | Row() { 39 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ 40 | TextButton( 41 | onClick = { 42 | val uri = BiliDownOutFile.getOutFolderUri() 43 | val intent = Intent(Intent.ACTION_VIEW) 44 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 45 | intent.setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR) 46 | context.startActivity(intent) 47 | onDismiss() 48 | }, 49 | ) { 50 | Text("尝试打开") 51 | } 52 | } 53 | TextButton( 54 | onClick = { 55 | val path = BiliDownOutFile.getOutFolderPath() 56 | val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 57 | clipboardManager.setPrimaryClip(ClipData.newPlainText("", path)) 58 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2){ 59 | Toast.makeText(context, "已复制路径到剪切板", Toast.LENGTH_SHORT).show() 60 | } 61 | onDismiss() 62 | }, 63 | ) { 64 | Text("复制路径") 65 | } 66 | } 67 | }, 68 | dismissButton = { 69 | TextButton( 70 | onClick = onDismiss, 71 | ) { 72 | Text("取消") 73 | } 74 | } 75 | ) 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 45 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 74 | 77 | 78 | 79 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/SettingItem.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.Divider 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Surface 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.alpha 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.painter.Painter 21 | import androidx.compose.ui.graphics.vector.ImageVector 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | 25 | @Composable 26 | fun SettingItem( 27 | modifier: Modifier = Modifier, 28 | enabled: Boolean = true, 29 | title: String, 30 | desc: String? = null, 31 | icon: ImageVector? = null, 32 | iconPainter: Painter? = null, 33 | separatedActions: Boolean = false, 34 | action: (@Composable () -> Unit)? = null, 35 | onClick: () -> Unit, 36 | ) { 37 | Surface( 38 | modifier = modifier 39 | .clickable(enabled = enabled) { onClick() } 40 | .alpha(if (enabled) 1f else 0.5f), 41 | color = Color.Unspecified 42 | ) { 43 | Row( 44 | modifier = Modifier 45 | .fillMaxWidth() 46 | .padding(24.dp, 16.dp, 16.dp, 16.dp), 47 | verticalAlignment = Alignment.CenterVertically 48 | ) { 49 | if (icon != null) { 50 | Icon( 51 | modifier = Modifier.padding(end = 24.dp), 52 | imageVector = icon, 53 | contentDescription = title, 54 | tint = MaterialTheme.colorScheme.onSurfaceVariant, 55 | ) 56 | } else { 57 | iconPainter?.let { 58 | Icon( 59 | modifier = Modifier 60 | .padding(end = 24.dp) 61 | .size(24.dp), 62 | painter = it, 63 | contentDescription = title, 64 | tint = MaterialTheme.colorScheme.onSurfaceVariant, 65 | ) 66 | } 67 | } 68 | Column(modifier = Modifier.weight(1f)) { 69 | Text( 70 | text = title, 71 | maxLines = if (desc == null) 2 else 1, 72 | style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp) 73 | ) 74 | desc?.let { 75 | Text( 76 | text = it, 77 | color = MaterialTheme.colorScheme.onSurfaceVariant, 78 | maxLines = 1, 79 | style = MaterialTheme.typography.bodyMedium, 80 | ) 81 | } 82 | } 83 | action?.let { 84 | if (separatedActions) { 85 | Divider( 86 | modifier = Modifier 87 | .padding(start = 16.dp) 88 | .size(1.dp, 32.dp), 89 | ) 90 | } 91 | Box(Modifier.padding(start = 16.dp)) { 92 | it() 93 | } 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/DownloadListItem.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.layout.ContentScale 14 | import androidx.compose.ui.text.style.TextOverflow 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import cn.a10miaomiao.bilidown.common.UrlUtil 18 | import cn.a10miaomiao.bilidown.entity.DownloadInfo 19 | import cn.a10miaomiao.bilidown.entity.DownloadType 20 | import coil.compose.AsyncImage 21 | 22 | @Composable 23 | fun DownloadListItem( 24 | item: DownloadInfo, 25 | onClick: () -> Unit 26 | ) { 27 | Box( 28 | modifier = Modifier.padding(5.dp), 29 | ) { 30 | Surface( 31 | modifier = Modifier.fillMaxWidth(), 32 | shape = RoundedCornerShape(10.dp), 33 | color = MaterialTheme.colorScheme.secondaryContainer 34 | ) { 35 | Column() { 36 | Row( 37 | modifier = Modifier 38 | .clickable(onClick = onClick) 39 | .padding(10.dp) 40 | .fillMaxWidth(), 41 | verticalAlignment = Alignment.CenterVertically, 42 | ) { 43 | AsyncImage( 44 | model = UrlUtil.autoHttps(item.cover) + "@672w_378h_1c_", 45 | contentDescription = item.title, 46 | contentScale = ContentScale.Crop, 47 | modifier = Modifier 48 | .size(width = 120.dp, height = 80.dp) 49 | .clip(RoundedCornerShape(5.dp)) 50 | ) 51 | 52 | Column( 53 | modifier = Modifier 54 | .weight(1f) 55 | .height(80.dp) 56 | .padding(horizontal = 10.dp), 57 | ) { 58 | Text( 59 | text = item.title, 60 | maxLines = 2, 61 | modifier = Modifier.weight(1f), 62 | overflow = TextOverflow.Ellipsis, 63 | ) 64 | val status = if (item.is_completed) { 65 | "已完成下载" 66 | } else { 67 | "暂停中" 68 | } 69 | Text( 70 | text = "${item.items.size}个视频 • $status", 71 | maxLines = 1, 72 | color = MaterialTheme.colorScheme.outline, 73 | overflow = TextOverflow.Ellipsis, 74 | ) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | @Preview 83 | @Composable 84 | fun DownloadListItemPreview() { 85 | DownloadListItem( 86 | DownloadInfo("", 1, 87 | has_dash_audio = true, 88 | is_completed = true, 89 | total_bytes = 0L, 90 | downloaded_bytes = 0L, 91 | title = "标题", 92 | cover = "", 93 | id = 0L, 94 | cid = 0L, 95 | type = DownloadType.VIDEO, 96 | items = mutableListOf() 97 | ), 98 | {} 99 | ) 100 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/ShizukuHelpDialog.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.widget.Toast 6 | import androidx.compose.material3.AlertDialog 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.platform.LocalContext 11 | import androidx.core.content.ContextCompat.startActivity 12 | import rikka.shizuku.ShizukuProvider.MANAGER_APPLICATION_ID 13 | 14 | 15 | @Composable 16 | fun ShizukuHelpDialog( 17 | action: ShizukuHelpDialogAction, 18 | onDismiss: () -> Unit, 19 | ) { 20 | val context = LocalContext.current 21 | var message = "" 22 | var confirmButton = "确认" 23 | 24 | when (action) { 25 | ShizukuHelpDialogAction.None -> Unit 26 | ShizukuHelpDialogAction.InstallShizuku -> { 27 | message = "Shizuku未安装,请先安装Shizuku!" 28 | confirmButton = "去下载" 29 | } 30 | ShizukuHelpDialogAction.ShizukuVersionLow -> { 31 | message = "Shizuku未安装,请先升级Shizuku到最新版本!" 32 | confirmButton = "去更新" 33 | } 34 | ShizukuHelpDialogAction.RunShizuku -> { 35 | message = "Shizuku未启动,请先启动Shizuku!" 36 | confirmButton = "去运行" 37 | } 38 | ShizukuHelpDialogAction.RequestPermission -> { 39 | message = "Shizuku授权失败,请手动打开授权管理器进行授权!" 40 | confirmButton = "去设置" 41 | } 42 | } 43 | 44 | fun onConfirm() { 45 | when (action) { 46 | ShizukuHelpDialogAction.None -> Unit 47 | ShizukuHelpDialogAction.InstallShizuku, 48 | ShizukuHelpDialogAction.ShizukuVersionLow -> { 49 | try { 50 | val uri = Uri.parse("https://shizuku.rikka.app/") 51 | val intent = Intent(Intent.ACTION_VIEW, uri) 52 | context.startActivity(intent) 53 | } catch (e: Exception) { 54 | Toast.makeText(context, "打开失败:https://shizuku.rikka.app/", Toast.LENGTH_LONG) 55 | .show() 56 | e.printStackTrace() 57 | } 58 | } 59 | ShizukuHelpDialogAction.RunShizuku, 60 | ShizukuHelpDialogAction.RequestPermission-> { 61 | try { 62 | val packageManager = context.packageManager 63 | val intent = packageManager.getLaunchIntentForPackage(MANAGER_APPLICATION_ID) 64 | if (intent == null) { 65 | Toast.makeText(context, "未找到Shizuku", Toast.LENGTH_LONG) 66 | .show() 67 | } else { 68 | context.startActivity(intent) 69 | } 70 | } catch (e: Exception) { 71 | Toast.makeText(context, "Shizuku启动失败", Toast.LENGTH_LONG) 72 | .show() 73 | e.printStackTrace() 74 | } 75 | } 76 | } 77 | onDismiss() 78 | } 79 | 80 | if (action != ShizukuHelpDialogAction.None) { 81 | AlertDialog( 82 | onDismissRequest = onDismiss, 83 | title = { Text(text = "提示") }, 84 | text = { Text(text = message) }, 85 | confirmButton = { 86 | TextButton( 87 | onClick = ::onConfirm, 88 | ) { 89 | Text(confirmButton) 90 | } 91 | }, 92 | dismissButton = { 93 | TextButton( 94 | onClick = onDismiss, 95 | ) { 96 | Text("取消") 97 | } 98 | } 99 | ) 100 | } 101 | } 102 | 103 | enum class ShizukuHelpDialogAction { 104 | None, 105 | InstallShizuku, 106 | ShizukuVersionLow, 107 | RunShizuku, 108 | RequestPermission, 109 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.enableEdgeToEdge 9 | import androidx.compose.runtime.CompositionLocalProvider 10 | import androidx.core.view.WindowCompat 11 | import androidx.lifecycle.lifecycleScope 12 | import cn.a10miaomiao.bilidown.common.LocalStoragePermission 13 | import cn.a10miaomiao.bilidown.common.MiaoLog 14 | import cn.a10miaomiao.bilidown.common.permission.StoragePermission 15 | import cn.a10miaomiao.bilidown.service.BiliDownService 16 | import cn.a10miaomiao.bilidown.ui.MainComposeApp 17 | import cn.a10miaomiao.bilidown.shizuku.IUserService 18 | import cn.a10miaomiao.bilidown.shizuku.LocalShizukuPermission 19 | import cn.a10miaomiao.bilidown.shizuku.permission.ShizukuPermission 20 | import io.microshow.rxffmpeg.RxFFmpegInvoke 21 | import kotlinx.coroutines.launch 22 | import moe.tlaster.precompose.PreComposeApp 23 | import rikka.shizuku.Shizuku 24 | 25 | 26 | class MainActivity : ComponentActivity(), Shizuku.OnRequestPermissionResultListener { 27 | 28 | private lateinit var storagePermission: StoragePermission 29 | private lateinit var shizukuPermission: ShizukuPermission 30 | private lateinit var biliDownService: BiliDownService 31 | private var userService: IUserService? = null 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | enableEdgeToEdge() 36 | RxFFmpegInvoke.getInstance().setDebug(true); 37 | storagePermission = StoragePermission(this) 38 | shizukuPermission = ShizukuPermission(this) 39 | setContent { 40 | PreComposeApp { 41 | CompositionLocalProvider( 42 | LocalStoragePermission provides storagePermission, 43 | LocalShizukuPermission provides shizukuPermission, 44 | ) { 45 | MainComposeApp() 46 | } 47 | } 48 | } 49 | lifecycleScope.launch { 50 | biliDownService = BiliDownService.getService(this@MainActivity) 51 | } 52 | shizukuPermission.onCreate() 53 | // bindUserService() 54 | } 55 | 56 | override fun onResume() { 57 | super.onResume() 58 | shizukuPermission.syncShizukuState(this) 59 | } 60 | 61 | override fun onDestroy() { 62 | super.onDestroy() 63 | shizukuPermission.onDestroy() 64 | 65 | // unbindUserService() 66 | } 67 | 68 | override fun onRequestPermissionsResult( 69 | requestCode: Int, 70 | permissions: Array, 71 | grantResults: IntArray 72 | ) { 73 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 74 | if (requestCode == 1) { 75 | storagePermission.requestPermissionsResult() 76 | } 77 | } 78 | 79 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 80 | super.onActivityResult(requestCode, resultCode, data) 81 | MiaoLog.debug { "onActivityResult" + requestCode } 82 | if (requestCode == 1) { 83 | storagePermission.requestPermissionsResult() 84 | } 85 | var uri = data?.data ?: return 86 | if (requestCode == 2) { 87 | if (resultCode == Activity.RESULT_OK) { 88 | try { 89 | val flags = 90 | Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION 91 | contentResolver.takePersistableUriPermission(uri, flags) 92 | } catch (e: Exception) { 93 | e.printStackTrace() 94 | } 95 | MiaoLog.debug { uri.toString() } 96 | } 97 | } 98 | } 99 | 100 | override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { 101 | 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/shizuku/util/RemoteServiceUtil.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.shizuku.util 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.ServiceConnection 6 | import android.os.IBinder 7 | import android.os.RemoteException 8 | import android.util.Log 9 | import cn.a10miaomiao.bilidown.BuildConfig 10 | import cn.a10miaomiao.bilidown.common.MiaoLog 11 | import cn.a10miaomiao.bilidown.service.BiliDownService 12 | import cn.a10miaomiao.bilidown.shizuku.IUserService 13 | import cn.a10miaomiao.bilidown.shizuku.service.UserService 14 | import kotlinx.coroutines.TimeoutCancellationException 15 | import kotlinx.coroutines.channels.Channel 16 | import kotlinx.coroutines.delay 17 | import kotlinx.coroutines.runBlocking 18 | import kotlinx.coroutines.withTimeout 19 | import rikka.shizuku.Shizuku 20 | import kotlin.jvm.Throws 21 | 22 | object RemoteServiceUtil { 23 | 24 | private var mChannel = Channel() 25 | private var mUserService: IUserService? = null 26 | 27 | @Throws(TimeoutCancellationException::class) 28 | suspend fun getUserService(): IUserService{ 29 | MiaoLog.debug { "getUserService" } 30 | mUserService?.let { return it } 31 | bindUserService() 32 | return withTimeout(10000) { 33 | mChannel.receive() 34 | } 35 | } 36 | 37 | 38 | private val userServiceConnection: ServiceConnection = object : ServiceConnection { 39 | override fun onServiceConnected(componentName: ComponentName, binder: IBinder?) { 40 | MiaoLog.debug { "onServiceConnected" } 41 | val res = StringBuilder() 42 | res.append("onServiceConnected: ").append(componentName.className).append('\n') 43 | if (binder != null && binder.pingBinder()) { 44 | val service = IUserService.Stub.asInterface(binder) 45 | try { 46 | res.append(service.doSomething()) 47 | } catch (e: RemoteException) { 48 | e.printStackTrace() 49 | res.append(Log.getStackTraceString(e)) 50 | } 51 | service.doSomething() 52 | mUserService = service 53 | mChannel.trySend(service) 54 | } else { 55 | res.append("invalid binder for ").append(componentName).append(" received") 56 | } 57 | MiaoLog.debug { res.toString().trim { it <= ' ' } } 58 | } 59 | 60 | override fun onServiceDisconnected(componentName: ComponentName) { 61 | MiaoLog.debug { "onServiceDisconnected" } 62 | mUserService = null 63 | } 64 | } 65 | 66 | private val userServiceArgs = Shizuku.UserServiceArgs( 67 | ComponentName( 68 | BuildConfig.APPLICATION_ID, 69 | UserService::class.java.name 70 | ) 71 | ) 72 | .daemon(false) 73 | .processNameSuffix("service") 74 | .debuggable(BuildConfig.DEBUG) 75 | .version(BuildConfig.VERSION_CODE) 76 | 77 | private fun bindUserService() { 78 | MiaoLog.debug { "bindUserService" } 79 | val res = java.lang.StringBuilder() 80 | try { 81 | if (Shizuku.getVersion() < 10) { 82 | res.append("requires Shizuku API 10") 83 | } else { 84 | Shizuku.bindUserService(userServiceArgs, userServiceConnection) 85 | } 86 | } catch (tr: Throwable) { 87 | tr.printStackTrace() 88 | res.append(tr.toString()) 89 | } 90 | MiaoLog.debug { res.toString().trim { it <= ' ' } } 91 | } 92 | 93 | private fun unbindUserService() { 94 | MiaoLog.debug { "unbindUserService" } 95 | val res = java.lang.StringBuilder() 96 | try { 97 | if (Shizuku.getVersion() < 10) { 98 | res.append("requires Shizuku API 10") 99 | } else { 100 | Shizuku.unbindUserService(userServiceArgs, userServiceConnection, true) 101 | } 102 | } catch (tr: Throwable) { 103 | tr.printStackTrace() 104 | res.append(tr.toString()) 105 | } 106 | MiaoLog.debug { res.toString().trim { it <= ' ' } } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/AppCrashHandler.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown 2 | 3 | 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.pm.ApplicationInfo 7 | import android.content.pm.PackageManager 8 | import android.os.Build 9 | import android.os.Looper 10 | import android.widget.Toast 11 | import androidx.core.content.pm.PackageInfoCompat 12 | import cn.a10miaomiao.bilidown.common.MiaoLog 13 | import java.text.SimpleDateFormat 14 | import java.util.* 15 | import kotlin.system.exitProcess 16 | 17 | 18 | /** 19 | * 参考:https://www.jianshu.com/p/0ea4615674f0 20 | */ 21 | class AppCrashHandler private constructor( 22 | private val context: Context 23 | ) : Thread.UncaughtExceptionHandler { 24 | // 用于格式化日期 25 | private val mDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) 26 | 27 | // 单例模式 28 | companion object { 29 | private var instance: AppCrashHandler? = null 30 | fun getInstance(context: Context): AppCrashHandler? { 31 | if (instance == null) { 32 | synchronized(AppCrashHandler::class) { 33 | instance = AppCrashHandler(context) 34 | } 35 | } 36 | return instance 37 | } 38 | } 39 | 40 | /** 41 | * 构造初始化 42 | */ 43 | init { 44 | // 设置当前类为应用默认处理器 45 | Thread.setDefaultUncaughtExceptionHandler(this) 46 | } 47 | 48 | /** 49 | * 当 UncaughtException 发生时转入该函数 50 | * @param t 51 | * @param e 52 | */ 53 | override fun uncaughtException(t: Thread, e: Throwable) { 54 | handleException(e) 55 | try { 56 | Thread.sleep(2000) 57 | } catch (e: Exception) { 58 | } 59 | exitProcess(0) 60 | } 61 | 62 | /** 63 | * 自定义错误处理 64 | * @param e 65 | */ 66 | private fun handleException(e: Throwable) { 67 | MiaoLog.error { e.message.toString() } 68 | e.printStackTrace() 69 | Thread { 70 | Looper.prepare() 71 | Toast.makeText(context, "程序崩溃了喵>﹏<", Toast.LENGTH_SHORT).show() 72 | Looper.loop() 73 | }.start() 74 | 75 | // 跳转到日志显示界面 76 | val intent = Intent(context, LogViewerActivity::class.java) 77 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 78 | intent.putExtra("log_summary", getLogSummary(e)) 79 | context.startActivity(intent) 80 | } 81 | 82 | /** 83 | * 收集参数信息 84 | * @param info 85 | */ 86 | private fun putInfoToMap(info: MutableMap) { 87 | info["设备型号"] = Build.MODEL 88 | info["设备品牌"] = Build.BOARD 89 | info["硬件名称"] = Build.HARDWARE 90 | info["硬件制造商"] = Build.MANUFACTURER 91 | info["系统版本"] = Build.VERSION.RELEASE 92 | info["系统版本号"] = "${Build.VERSION.SDK_INT}" 93 | 94 | val pm = context.packageManager 95 | val pi = pm.getPackageInfo(context.packageName, PackageManager.GET_ACTIVITIES) 96 | if (pi != null) { 97 | info["应用版本"] = pi.versionName ?: "unknown" 98 | info["应用版本号"] = "${PackageInfoCompat.getLongVersionCode(pi)}" 99 | } 100 | } 101 | 102 | /** 103 | * 获取日志头信息 104 | * @return StringBuffer 105 | */ 106 | private fun getLogHeader(): StringBuffer { 107 | val info = LinkedHashMap() 108 | val sb = StringBuffer() 109 | sb.append(">>>>时间: ${mDateFormat.format(Date())}\n") 110 | putInfoToMap(info) 111 | info.entries.forEach { 112 | sb.append("${it.key}: ${it.value}\n") 113 | } 114 | return sb 115 | } 116 | 117 | /** 118 | * 获取日志概要 119 | * @param e 120 | * @return 日志概要 121 | */ 122 | private fun getLogSummary(e: Throwable): String { 123 | val sb = getLogHeader().append("\n") 124 | sb.append("异常类: ${e.javaClass}\n") 125 | sb.append("异常信息: ${e.message}\n\n") 126 | for (i in e.stackTrace.indices) { 127 | sb.append("****堆栈追踪 ${i + 1}\n") 128 | sb.append("类名: ${e.stackTrace[i].className}\n") 129 | sb.append("方法: ${e.stackTrace[i].methodName}\n") 130 | sb.append("文件: ${e.stackTrace[i].fileName}\n") 131 | sb.append("行数: ${e.stackTrace[i].lineNumber}\n\n") 132 | } 133 | return sb.toString().trim() 134 | } 135 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/FileNameInputDialog.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.focus.FocusRequester 11 | import androidx.compose.ui.focus.focusRequester 12 | import androidx.compose.ui.text.TextRange 13 | import androidx.compose.ui.text.input.ImeAction 14 | import androidx.compose.ui.text.input.TextFieldValue 15 | import cn.a10miaomiao.bilidown.common.BiliDownOutFile 16 | import kotlinx.coroutines.launch 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun FileNameInputDialog( 21 | showInputDialog: Boolean, 22 | fileName: String, 23 | confirmText: String, 24 | onDismiss: () -> Unit, 25 | onConfirm: (outFile: BiliDownOutFile) -> Unit, 26 | ) { 27 | var errorText by remember() { 28 | mutableStateOf("") 29 | } 30 | var value by remember(fileName) { 31 | mutableStateOf(TextFieldValue(text = fileName, selection = TextRange(fileName.length))) 32 | } 33 | val focusRequester = remember { FocusRequester() } 34 | LaunchedEffect(showInputDialog) { 35 | if (showInputDialog) { 36 | launch { 37 | focusRequester.requestFocus() 38 | } 39 | } 40 | } 41 | 42 | fun handleConfirm() { 43 | val name = value.text + ".mp4" 44 | if (name.isBlank()) { 45 | errorText = "文件名不能为空" 46 | } else if (name.indexOf(' ') > 0) { 47 | errorText = "文件名不能含有格" 48 | } else { 49 | val outFile = BiliDownOutFile(name) 50 | if (outFile.exists()) { 51 | errorText = "文件已存在" 52 | } else { 53 | onConfirm(outFile) 54 | } 55 | } 56 | } 57 | 58 | fun handleClearSpace() { 59 | val text = value.text.replace(" ", "") 60 | value = TextFieldValue( 61 | text = text, 62 | selection = TextRange(text.length) 63 | ) 64 | } 65 | 66 | if (showInputDialog) { 67 | AlertDialog( 68 | onDismissRequest = onDismiss, 69 | title = { Text(text = "输入文件名:") }, 70 | text = { 71 | TextField( 72 | label = { 73 | Text(text = "文件名") 74 | }, 75 | trailingIcon = { 76 | Text(text = ".mp4") 77 | }, 78 | supportingText = { 79 | Text(text = errorText) 80 | }, 81 | isError = errorText.isNotBlank(), 82 | value = value, 83 | onValueChange = { 84 | value = it 85 | errorText = "" 86 | }, 87 | modifier = Modifier 88 | .fillMaxWidth() 89 | .focusRequester(focusRequester), 90 | singleLine = true, 91 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), 92 | keyboardActions = KeyboardActions( 93 | onDone = { handleConfirm() } 94 | ), 95 | ) 96 | }, 97 | confirmButton = { 98 | TextButton( 99 | onClick = ::handleConfirm, 100 | ) { 101 | Text(confirmText) 102 | } 103 | }, 104 | dismissButton = { 105 | Row() { 106 | if (" " in value.text) { 107 | TextButton( 108 | onClick = ::handleClearSpace, 109 | ) { 110 | Text("清除空格") 111 | } 112 | } 113 | TextButton( 114 | onClick = onDismiss, 115 | ) { 116 | Text("取消") 117 | } 118 | } 119 | } 120 | ) 121 | } 122 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/DownloadDetailItem.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.MoreVert 8 | import androidx.compose.material.icons.filled.PlayArrow 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.layout.ContentScale 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.text.style.TextOverflow 17 | import androidx.compose.ui.unit.dp 18 | import cn.a10miaomiao.bilidown.common.UrlUtil 19 | import cn.a10miaomiao.bilidown.entity.DownloadItemInfo 20 | import coil.compose.AsyncImage 21 | import kotlinx.coroutines.flow.MutableStateFlow 22 | 23 | @Composable 24 | fun DownloadDetailItem( 25 | item: DownloadItemInfo, 26 | isOut: Boolean, 27 | onClick: () -> Unit, 28 | onStartClick: () -> Unit, 29 | onPauseClick: (taskId: Long) -> Unit, 30 | onExportClick: () -> Unit, 31 | ) { 32 | var expandedMoreMenu by remember { mutableStateOf(false) } 33 | 34 | Box( 35 | modifier = Modifier.padding(5.dp), 36 | ) { 37 | Surface( 38 | modifier = Modifier.fillMaxWidth() 39 | .clickable(onClick = onClick), 40 | shape = RoundedCornerShape(10.dp), 41 | color = MaterialTheme.colorScheme.secondaryContainer 42 | ) { 43 | Column() { 44 | Row( 45 | modifier = Modifier.padding(5.dp), 46 | verticalAlignment = Alignment.CenterVertically, 47 | ) { 48 | AsyncImage( 49 | model = UrlUtil.autoHttps(item.cover) + "@672w_378h_1c_", 50 | contentDescription = item.title, 51 | contentScale = ContentScale.Crop, 52 | modifier = Modifier 53 | .size(width = 60.dp, height = 40.dp) 54 | .clip(RoundedCornerShape(5.dp)) 55 | ) 56 | Column( 57 | modifier = Modifier 58 | .weight(1f) 59 | .padding(start = 5.dp) 60 | ) { 61 | Text( 62 | text = item.title, 63 | maxLines = 1, 64 | overflow = TextOverflow.Ellipsis, 65 | ) 66 | val status = if (isOut) { 67 | "已导出" 68 | } else if (item.is_completed) { 69 | "已完成下载" 70 | } else { 71 | "暂停中" 72 | } 73 | Text( 74 | text = status, 75 | color = MaterialTheme.colorScheme.outline, 76 | maxLines = 1, 77 | overflow = TextOverflow.Ellipsis, 78 | ) 79 | } 80 | Box() { 81 | IconButton( 82 | onClick = { expandedMoreMenu = true } 83 | ) { 84 | Icon(Icons.Filled.MoreVert, null) 85 | } 86 | DropdownMenu( 87 | expanded = expandedMoreMenu, 88 | onDismissRequest = { expandedMoreMenu = false }, 89 | ) { 90 | DropdownMenuItem( 91 | onClick = { 92 | expandedMoreMenu = false 93 | onExportClick() 94 | }, 95 | text = { 96 | Text(text = "导出视频") 97 | } 98 | ) 99 | } 100 | } 101 | 102 | } 103 | } 104 | 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/page/ListPage.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.page 2 | 3 | import android.content.Context 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.pager.HorizontalPager 8 | import androidx.compose.foundation.pager.rememberPagerState 9 | import androidx.compose.material3.* 10 | import androidx.compose.material3.PrimaryTabRow 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.unit.dp 16 | import androidx.navigation.NavHostController 17 | import cn.a10miaomiao.bilidown.common.BiliDownUtils 18 | import cn.a10miaomiao.bilidown.common.datastore.DataStoreKeys 19 | import cn.a10miaomiao.bilidown.common.datastore.rememberDataStorePreferencesFlow 20 | import cn.a10miaomiao.bilidown.common.molecule.collectAction 21 | import cn.a10miaomiao.bilidown.common.molecule.rememberPresenter 22 | import cn.a10miaomiao.bilidown.entity.BiliAppInfo 23 | import cn.a10miaomiao.bilidown.ui.BiliDownScreen 24 | import kotlinx.coroutines.flow.Flow 25 | import kotlinx.coroutines.launch 26 | 27 | 28 | data class ListPageState( 29 | val appList: List, 30 | ) 31 | 32 | sealed class ListPageAction { 33 | object Add : ListPageAction() 34 | } 35 | 36 | @Composable 37 | fun ListPagePresenter( 38 | context: Context, 39 | action: Flow, 40 | ): ListPageState { 41 | var appList by remember { mutableStateOf>(emptyList()) } 42 | val selectedAppPackageNameSet by rememberDataStorePreferencesFlow( 43 | context = context, 44 | key = DataStoreKeys.appPackageNameSet, 45 | initial = emptySet(), 46 | ).collectAsState(emptySet()) 47 | LaunchedEffect(selectedAppPackageNameSet) { 48 | appList = BiliDownUtils.biliAppList.filter { 49 | selectedAppPackageNameSet.contains(it.packageName) 50 | } 51 | } 52 | action.collectAction { 53 | when (it) { 54 | ListPageAction.Add -> { 55 | } 56 | } 57 | } 58 | return ListPageState( 59 | appList, 60 | ) 61 | } 62 | 63 | @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) 64 | @Composable 65 | fun ListPage( 66 | navController: NavHostController, 67 | ) { 68 | val context = LocalContext.current 69 | val scope = rememberCoroutineScope() 70 | val (state, channel) = rememberPresenter { 71 | ListPagePresenter(context, it) 72 | } 73 | if (state.appList.isEmpty()) { 74 | Box( 75 | modifier = Modifier.fillMaxSize(), 76 | contentAlignment = Alignment.Center, 77 | ) { 78 | Column( 79 | horizontalAlignment = Alignment.CenterHorizontally 80 | ) { 81 | // 空布局 82 | Text(text = "还未添加APP信息") 83 | Spacer(modifier = Modifier.height(20.dp)) 84 | Button( 85 | onClick = { 86 | navController.navigate(BiliDownScreen.AddApp.route) 87 | } 88 | ) { 89 | Text(text = "添加") 90 | } 91 | } 92 | } 93 | } else { 94 | val pagerState = rememberPagerState(pageCount = { state.appList.size }) 95 | Column( 96 | modifier = Modifier.fillMaxSize() 97 | ) { 98 | PrimaryTabRow( 99 | modifier = Modifier.fillMaxWidth() 100 | .background(MaterialTheme.colorScheme.background), 101 | contentColor = MaterialTheme.colorScheme.onBackground, 102 | selectedTabIndex = pagerState.currentPage, 103 | ) { 104 | state.appList.forEachIndexed { index, app -> 105 | Tab( 106 | text = { Text(text = app.name) }, 107 | selected = pagerState.currentPage == index, 108 | onClick = { 109 | scope.launch { 110 | pagerState.animateScrollToPage(index) 111 | } 112 | }, 113 | ) 114 | } 115 | } 116 | HorizontalPager( 117 | modifier = Modifier 118 | .fillMaxWidth() 119 | .weight(1f), 120 | state = pagerState, 121 | ) { page -> 122 | DownloadListPage( 123 | navController = navController, 124 | packageName = state.appList[page].packageName, 125 | ) 126 | } 127 | } 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/molecule/molecule.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.molecule 2 | 3 | 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.runtime.MonotonicFrameClock 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.remember 10 | import app.cash.molecule.RecompositionMode 11 | import app.cash.molecule.launchMolecule 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.cancel 15 | import kotlinx.coroutines.channels.Channel 16 | import kotlinx.coroutines.flow.Flow 17 | import kotlinx.coroutines.flow.StateFlow 18 | import kotlinx.coroutines.flow.consumeAsFlow 19 | import moe.tlaster.precompose.viewmodel.ViewModel 20 | import moe.tlaster.precompose.viewmodel.viewModel 21 | import kotlin.coroutines.CoroutineContext 22 | 23 | internal fun providePlatformDispatcher(): CoroutineContext = Dispatchers.Main 24 | 25 | private class PresenterViewModel( 26 | body: @Composable () -> T, 27 | ) : ViewModel() { 28 | private val dispatcher = providePlatformDispatcher() 29 | private val mode = if (dispatcher[MonotonicFrameClock] == null) { 30 | RecompositionMode.Immediate 31 | } else { 32 | RecompositionMode.ContextClock 33 | } 34 | private val scope = CoroutineScope(dispatcher) 35 | val state = scope.launchMolecule(mode, body = body) 36 | 37 | override fun onCleared() { 38 | scope.cancel() 39 | } 40 | } 41 | 42 | @Composable 43 | private fun rememberPresenterState( 44 | keys: List, 45 | body: @Composable () -> T, 46 | ): StateFlow { 47 | @Suppress("UNCHECKED_CAST") 48 | val viewModel = viewModel( 49 | modelClass = PresenterViewModel::class, 50 | keys = keys, 51 | creator = { PresenterViewModel(body) } 52 | ) as PresenterViewModel 53 | return viewModel.state 54 | } 55 | 56 | private class ActionViewModel : ViewModel() { 57 | val channel = Channel() 58 | val pair = channel to channel.consumeAsFlow() 59 | override fun onCleared() { 60 | channel.close() 61 | } 62 | } 63 | 64 | @Composable 65 | private fun rememberAction( 66 | keys: List, 67 | ): Pair, Flow> { 68 | @Suppress("UNCHECKED_CAST") 69 | val viewModel = viewModel( 70 | modelClass = ActionViewModel::class, 71 | keys = keys, 72 | creator = { ActionViewModel() } 73 | ) as ActionViewModel 74 | return viewModel.pair 75 | } 76 | 77 | /** 78 | * Return pair of State and Action Channel, use it in your Compose UI 79 | * The molecule scope and the Action Channel will be managed by the [ViewModel], so it has the same lifecycle as the [ViewModel] 80 | * 81 | * @param keys The keys to use to identify the Presenter 82 | * @param body The body of the molecule presenter, the flow parameter is the flow of the action channel 83 | * @return Pair of State and Action channel 84 | */ 85 | @Composable 86 | fun rememberPresenter( 87 | keys: List, 88 | body: @Composable (flow: Flow) -> T 89 | ): Pair> { 90 | val (channel, action) = rememberAction(keys = keys) 91 | val presenter = rememberPresenterState(keys = keys) { body(action) } 92 | val state by presenter.collectAsState() 93 | return state to channel 94 | } 95 | 96 | /** 97 | * Return pair of State and Action Channel, use it in your Compose UI 98 | * The molecule scope and the Action Channel will be managed by the [ViewModel], so it has the same lifecycle as the [ViewModel] 99 | * 100 | * @param body The body of the molecule presenter, the flow parameter is the flow of the action channel 101 | * @return Pair of State and Action channel 102 | */ 103 | @Composable 104 | inline fun rememberPresenter( 105 | crossinline body: @Composable (flow: Flow) -> T 106 | ): Pair> { 107 | return rememberPresenter(keys = listOf(T::class, E::class)) { 108 | body.invoke(it) 109 | } 110 | } 111 | 112 | /** 113 | * Return pair of State and Action channel, use it in your Presenter, not Compose UI 114 | * 115 | * @param body The body of the molecule presenter, the flow parameter is the flow of the action channel 116 | * @return Pair of State and Action channel 117 | */ 118 | @Composable 119 | fun rememberNestedPresenter( 120 | body: @Composable (flow: Flow) -> T 121 | ): Pair> { 122 | val channel = remember { Channel() } 123 | val flow = remember { channel.consumeAsFlow() } 124 | val presenter = body(flow) 125 | return presenter to channel 126 | } 127 | 128 | /** 129 | * Helper function to collect the action channel in your Presenter 130 | * 131 | * @param body Your action handler 132 | */ 133 | @Composable 134 | fun Flow.collectAction( 135 | body: suspend (T) -> Unit, 136 | ) { 137 | LaunchedEffect(Unit) { 138 | collect { 139 | body(it) 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlinx-serialization' 5 | id 'kotlin-parcelize' 6 | id 'com.google.devtools.ksp' 7 | } 8 | 9 | android { 10 | namespace 'cn.a10miaomiao.bilidown' 11 | compileSdk 35 12 | 13 | boolean isReleaseTask = gradle.startParameter.taskNames.any { it.contains("Release") } 14 | 15 | defaultConfig { 16 | applicationId "cn.a10miaomiao.bilidown" 17 | minSdk 21 18 | targetSdk 35 19 | versionCode 100 20 | versionName "1.0" 21 | 22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 23 | vectorDrawables { 24 | useSupportLibrary true 25 | } 26 | 27 | } 28 | 29 | File signingFile = file("signing.properties") 30 | if (signingFile.exists()) { 31 | Properties props = new Properties() 32 | props.load(new FileInputStream(signingFile)) 33 | signingConfigs { 34 | release { 35 | keyAlias props['KEY_ALIAS'] 36 | keyPassword props['KEY_PASSWORD'] 37 | storeFile file(props['KEYSTORE_FILE']) 38 | storePassword props['KEYSTORE_PASSWORD'] 39 | } 40 | debug { 41 | keyAlias props['KEY_ALIAS'] 42 | keyPassword props['KEY_PASSWORD'] 43 | storeFile file(props['KEYSTORE_FILE']) 44 | storePassword props['KEYSTORE_PASSWORD'] 45 | } 46 | } 47 | } 48 | 49 | buildTypes { 50 | release { 51 | minifyEnabled false 52 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 53 | if (signingConfigs.hasProperty('release')) { 54 | signingConfig signingConfigs.release 55 | } 56 | } 57 | } 58 | compileOptions { 59 | sourceCompatibility JavaVersion.VERSION_17 60 | targetCompatibility JavaVersion.VERSION_17 61 | } 62 | kotlinOptions { 63 | jvmTarget = '17' 64 | } 65 | buildFeatures { 66 | compose true 67 | aidl true 68 | } 69 | composeOptions { 70 | kotlinCompilerExtensionVersion "1.5.10" 71 | } 72 | packagingOptions { 73 | resources { 74 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 75 | } 76 | } 77 | 78 | splits { 79 | if (!isReleaseTask) { 80 | android.defaultConfig.ndk { abiFilters "arm64-v8a", "x86_64" } 81 | } 82 | abi { 83 | reset() 84 | enable isReleaseTask 85 | include "armeabi-v7a","arm64-v8a","x86","x86_64" 86 | universalApk false 87 | } 88 | } 89 | 90 | final abiCodes = [ 91 | "x86": 1, 92 | "x86_64": 2, 93 | "armeabi-v7a": 3, 94 | "arm64-v8a": 4, 95 | ] 96 | applicationVariants.all { variant -> 97 | variant.outputs.each { output -> 98 | final abiName = com.android.build.OutputFile.ABI 99 | final abiVersionCode = abiCodes.get(output.getFilter(abiName)) 100 | if (abiVersionCode != null) { 101 | output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode 102 | } 103 | } 104 | } 105 | 106 | dependenciesInfo { 107 | // Disables dependency metadata when building APKs. 108 | includeInApk false 109 | // Disables dependency metadata when building Android App Bundles. 110 | includeInBundle false 111 | } 112 | } 113 | 114 | dependencies { 115 | implementation(libs.androidx.ktx) 116 | implementation(libs.androidx.lifecycle.rt.ktx) 117 | implementation(libs.androidx.activity.compose) 118 | implementation(libs.androidx.documentfile) 119 | implementation(libs.androidx.datastoree.preferences) 120 | 121 | implementation(platform(libs.compose.bom)) 122 | implementation(libs.compose.ui.core) 123 | implementation(libs.compose.ui.tooling.preview) 124 | implementation(libs.compose.material.core) 125 | implementation(libs.compose.material3.core) 126 | implementation(libs.compose.material3.window.size) 127 | implementation(libs.androidx.navigation.compose) { 128 | exclude group: 'androidx.compose.ui', module: 'ui-test-android' 129 | } 130 | 131 | implementation(libs.coil.compose) 132 | implementation(libs.precompose.core) 133 | implementation(libs.precompose.viewmodel) 134 | implementation(libs.molecule.rt) 135 | 136 | implementation(libs.room.rt) 137 | implementation(libs.room.ktx) 138 | annotationProcessor(libs.room.compiler) 139 | ksp(libs.room.compiler) 140 | 141 | implementation(libs.kotlinx.serialization.json) 142 | implementation(libs.rxffmpeg) 143 | implementation(libs.shizuku.api) 144 | implementation(libs.shizuku.provider) 145 | 146 | testImplementation(libs.junit) 147 | androidTestImplementation(libs.androidx.test.junit) 148 | // androidTestImplementation(libs.androidx.test.espresso) 149 | androidTestImplementation(libs.compose.test.junit4) 150 | 151 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/BiliDownFile.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Environment 8 | import android.provider.DocumentsContract 9 | import android.util.Log 10 | import cn.a10miaomiao.bilidown.common.file.MiaoDocumentFile 11 | import cn.a10miaomiao.bilidown.common.file.MiaoFile 12 | import cn.a10miaomiao.bilidown.common.file.MiaoJavaFile 13 | import cn.a10miaomiao.bilidown.entity.BiliDownloadEntryAndPathInfo 14 | import cn.a10miaomiao.bilidown.entity.BiliDownloadEntryInfo 15 | import cn.a10miaomiao.bilidown.shizuku.util.RemoteServiceUtil 16 | import kotlinx.coroutines.TimeoutCancellationException 17 | import kotlinx.serialization.json.Json 18 | import java.io.File 19 | import kotlin.jvm.Throws 20 | 21 | 22 | class BiliDownFile( 23 | val context: Context, 24 | val packageName: String, 25 | val enabledShizuku: Boolean, 26 | ) { 27 | 28 | private val TAG = "BiliDownFile" 29 | private val externalStorage = Environment.getExternalStorageDirectory() 30 | private val DIR_DOWNLOAD = "download" 31 | var path = "" 32 | var list = emptyList() 33 | 34 | fun canRead(): Boolean { 35 | if (enabledShizuku) { 36 | return true 37 | } 38 | val downloadDir = createMiaoFile(DIR_DOWNLOAD) 39 | return downloadDir.canRead() 40 | } 41 | 42 | @Throws(TimeoutCancellationException::class) 43 | suspend fun readDownloadList(): List { 44 | val downloadDir = createMiaoFile(DIR_DOWNLOAD) 45 | val list = mutableListOf() 46 | MiaoLog.debug { enabledShizuku.toString() } 47 | if (enabledShizuku) { 48 | MiaoLog.debug { downloadDir.path } 49 | val userService = RemoteServiceUtil.getUserService() 50 | list.addAll(userService.readDownloadList(downloadDir.path)) 51 | } else { 52 | downloadDir.listFiles() 53 | .filter { it.isDirectory } 54 | .forEach { 55 | Log.d(TAG, it.path) 56 | list.addAll(readDownloadDirectory(it)) 57 | } 58 | } 59 | return list.reversed() 60 | } 61 | 62 | suspend fun readDownloadDirectory(dir: MiaoFile): List { 63 | if (enabledShizuku) { 64 | val userService = RemoteServiceUtil.getUserService() 65 | return userService.readDownloadDirectory(dir.path) 66 | } 67 | if (!dir.exists() || !dir.isDirectory) { 68 | return emptyList() 69 | } 70 | return dir.listFiles() 71 | .filter { pageDir -> pageDir.isDirectory } 72 | .map { 73 | val entryFile = if (it is MiaoDocumentFile) { 74 | MiaoDocumentFile(context, it.documentFile, "/entry.json") 75 | } else { 76 | MiaoJavaFile(it.path + "/entry.json") 77 | } 78 | Pair(it, entryFile) 79 | } 80 | .filter { it.second.exists() } 81 | .map { 82 | val (entryDir, entryFile) = it 83 | val entryJson = entryFile.readText() 84 | val json = Json { ignoreUnknownKeys = true } 85 | val entry = json.decodeFromString(entryJson) 86 | BiliDownloadEntryAndPathInfo( 87 | entry = entry, 88 | entryDirPath = entryDir.path, 89 | pageDirPath = dir.path 90 | // entryDirPath = it.parent, 91 | // pageDirPath = it.parentFile.parent 92 | ) 93 | } 94 | } 95 | 96 | //获取指定目录的权限 97 | fun startFor(REQUEST_CODE_FOR_DIR: Int) { 98 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 99 | MiaoDocumentFile.requestFolderPermission( 100 | context as Activity, 101 | REQUEST_CODE_FOR_DIR, 102 | getDocumentFileId() 103 | ) 104 | } 105 | } 106 | 107 | private fun createMiaoFile( 108 | dirName: String, 109 | ): MiaoFile { 110 | if (!enabledShizuku && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 10以上 111 | return MiaoDocumentFile( 112 | context, 113 | getDocumentFileId(), 114 | File.separator + dirName 115 | ) 116 | } 117 | var file = File(getExternalDir(), dirName) 118 | if (!enabledShizuku && !file.exists()) { 119 | file.mkdir() 120 | } 121 | return MiaoJavaFile(file) 122 | } 123 | 124 | private fun getExternalDir(): String { 125 | var externalStorage = Environment.getExternalStorageDirectory() 126 | var path = externalStorage.absolutePath + "/Android/data/" + packageName 127 | return path 128 | } 129 | 130 | private fun getDocumentFileId(): String { 131 | var path = "primary:Android/data/$packageName" 132 | return path 133 | } 134 | 135 | 136 | 137 | 138 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "1.9.22" 3 | 4 | androidx-core = "1.13.1" 5 | androidx-activity = "1.8.2" 6 | compose = "1.7.0" 7 | compose-bom = "2024.09.00" 8 | navigation = "2.8.2" 9 | lifecycle = "2.8.3" 10 | documentfile="1.0.1" 11 | datastore = "1.0.0" 12 | room = "2.6.1" 13 | 14 | coil = "2.6.0" 15 | qrose = "1.0.0-beta3" # QrCode for compose https://github.com/alexzhirkevich/qrose 16 | okhttp = "4.12.0" 17 | gson = "2.10.1" 18 | grpc = "1.62.2" 19 | kotlin-coroutine-android = "1.8.0" 20 | androidx-browser = "1.8.0" 21 | protoc = "3.12.0" 22 | serialization = "1.6.2" 23 | precompose = "1.6.0-rc02" 24 | shizuku = "13.1.5" 25 | rxffmpeg = "4.9.0" 26 | molecule = "1.4.1" 27 | 28 | # test 29 | junit = "4.+" 30 | androidx-test-junit = "1.1.5" 31 | androidx-test-espresso = "3.5.1" 32 | 33 | # plugins 34 | android-plugin = "8.3.0" 35 | protobuf-plugin = "0.9.4" 36 | ksp-plugin = "1.9.22-1.0.17" 37 | 38 | [libraries] 39 | # androidx 40 | androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } 41 | androidx-activity-core = { group = "androidx.activity", name = "activity", version.ref = "androidx-activity" } 42 | androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidx-activity" } 43 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" } 44 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } 45 | androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref="documentfile" } 46 | androidx-datastoree-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref="datastore" } 47 | androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidx-browser" } 48 | androidx-lifecycle-rt-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } 49 | androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } 50 | 51 | # compose 52 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } 53 | compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } 54 | compose-ui-core = { group = "androidx.compose.ui", name = "ui" } 55 | compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 56 | compose-material-core = { group = "androidx.compose.material", name = "material" } 57 | compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } 58 | compose-material3-core = { group = "androidx.compose.material3", name = "material3-android" } 59 | compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" } 60 | 61 | 62 | # 3rd party compose 63 | precompose-core = { group = "moe.tlaster", name = "precompose", version.ref = "precompose" } 64 | precompose-viewmodel = { group = "moe.tlaster", name = "precompose-viewmodel", version.ref = "precompose" } 65 | molecule-rt = { group = "app.cash.molecule", name = "molecule-runtime", version.ref = "molecule"} 66 | coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } 67 | coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } 68 | qrose = { group = "io.github.alexzhirkevich", name = "qrose", version.ref = "qrose" } 69 | 70 | # room 71 | room-rt = { group = "androidx.room", name = "room-runtime", version.ref = "room" } 72 | room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } 73 | room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } 74 | 75 | 76 | # serde 77 | gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } 78 | okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } 79 | grpc-protobuf-lite = { group = "io.grpc", name = "grpc-protobuf-lite", version.ref = "grpc" } 80 | grpc-stub = { group = "io.grpc", name = "grpc-stub", version.ref = "grpc" } 81 | protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protoc" } 82 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref="serialization" } 83 | rxffmpeg = { group = "com.github.microshow", name = "RxFFmpeg", version.ref = "rxffmpeg" } 84 | shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" } 85 | shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" } 86 | 87 | # androidx 88 | kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlin-coroutine-android" } 89 | 90 | # test 91 | junit = { group = "junit", name = "junit", version.ref = "junit" } 92 | androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } 93 | androidx-test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-test-espresso" } 94 | compose-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose" } 95 | 96 | [plugins] 97 | android-application = { id = "com.android.application", version.ref = "android-plugin" } 98 | android-library = { id = "com.android.library", version.ref = "android-plugin" } 99 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 100 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-plugin" } 101 | protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" } 102 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } 103 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/page/MorePage.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.page 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.compose.foundation.gestures.rememberScrollableState 7 | import androidx.compose.foundation.gestures.scrollable 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.verticalScroll 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.outlined.Info 14 | import androidx.compose.material3.Switch 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.LaunchedEffect 17 | import androidx.compose.runtime.collectAsState 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.res.painterResource 25 | import androidx.compose.ui.unit.dp 26 | import androidx.navigation.NavHostController 27 | import cn.a10miaomiao.bilidown.R 28 | import cn.a10miaomiao.bilidown.common.BiliDownUtils 29 | import cn.a10miaomiao.bilidown.common.datastore.DataStoreKeys 30 | import cn.a10miaomiao.bilidown.common.datastore.rememberDataStorePreferencesFlow 31 | import cn.a10miaomiao.bilidown.common.molecule.collectAction 32 | import cn.a10miaomiao.bilidown.common.molecule.rememberPresenter 33 | import cn.a10miaomiao.bilidown.entity.BiliAppInfo 34 | import cn.a10miaomiao.bilidown.shizuku.localShizukuPermission 35 | import cn.a10miaomiao.bilidown.shizuku.permission.ShizukuPermission 36 | import cn.a10miaomiao.bilidown.ui.BiliDownScreen 37 | import cn.a10miaomiao.bilidown.ui.components.SettingItem 38 | import cn.a10miaomiao.bilidown.ui.components.ShizukuHelpDialog 39 | import cn.a10miaomiao.bilidown.ui.components.ShizukuHelpDialogAction 40 | import kotlinx.coroutines.flow.Flow 41 | 42 | data class MorePageState( 43 | val versionName: String, 44 | ) 45 | 46 | sealed class MorePageAction { 47 | object About : MorePageAction() 48 | } 49 | 50 | @Composable 51 | fun MorePagePresenter( 52 | context: Context, 53 | action: Flow, 54 | ): MorePageState { 55 | var versionName by remember { mutableStateOf("v-") } 56 | LaunchedEffect(Unit) { 57 | val manager = context.packageManager 58 | val info = manager.getPackageInfo(context.packageName, 0) 59 | versionName = info.versionName.toString() 60 | } 61 | action.collectAction { 62 | when (it) { 63 | MorePageAction.About -> { 64 | } 65 | } 66 | } 67 | return MorePageState( 68 | versionName, 69 | ) 70 | } 71 | 72 | @Composable 73 | fun MorePage( 74 | navController: NavHostController, 75 | ) { 76 | val context = LocalContext.current 77 | val shizukuPermission = localShizukuPermission() 78 | val shizukuPermissionState by shizukuPermission.collectState() 79 | val (state, channel) = rememberPresenter { 80 | MorePagePresenter(context, it) 81 | } 82 | var dialogAction by remember { 83 | mutableStateOf(ShizukuHelpDialogAction.None) 84 | } 85 | 86 | fun changeShizukuEnabled(enabled: Boolean) { 87 | if (enabled) { 88 | if (!shizukuPermissionState.isInstalled) { 89 | dialogAction = ShizukuHelpDialogAction.InstallShizuku 90 | } else if (shizukuPermissionState.isPreV11) { 91 | dialogAction = ShizukuHelpDialogAction.ShizukuVersionLow 92 | } else if (!shizukuPermissionState.isRunning) { 93 | dialogAction = ShizukuHelpDialogAction.RunShizuku 94 | } else if (!shizukuPermissionState.isGranted) { 95 | if (!shizukuPermission.requestPermission()) { 96 | dialogAction = ShizukuHelpDialogAction.RequestPermission 97 | } 98 | } else { 99 | shizukuPermission.setEnabled(true) 100 | } 101 | } else { 102 | shizukuPermission.setEnabled(false) 103 | } 104 | } 105 | 106 | ShizukuHelpDialog( 107 | action = dialogAction, 108 | onDismiss = { dialogAction = ShizukuHelpDialogAction.None } 109 | ) 110 | val scrollableState = rememberScrollState() 111 | Column( 112 | modifier = Modifier.verticalScroll(scrollableState) 113 | .padding(bottom = 80.dp) 114 | ) { 115 | val isAboveN = ShizukuPermission.isAboveN 116 | SettingItem( 117 | title = "Shizuku", 118 | desc = if (isAboveN) { 119 | shizukuPermissionState.statusText 120 | } else { 121 | "需Android7.0+" 122 | }, 123 | iconPainter = painterResource(R.mipmap.ic_shizuku), 124 | enabled = isAboveN, 125 | action = { 126 | Switch( 127 | checked = shizukuPermissionState.isEnabled, 128 | onCheckedChange = ::changeShizukuEnabled, 129 | enabled = isAboveN, 130 | ) 131 | } 132 | ) { 133 | changeShizukuEnabled(!shizukuPermissionState.isEnabled) 134 | } 135 | 136 | SettingItem( 137 | title = "关于", 138 | desc = "当前版本:${state.versionName}", 139 | icon = Icons.Outlined.Info, 140 | ) { 141 | val intent = Intent(Intent.ACTION_VIEW) 142 | intent.data = Uri.parse("https://github.com/10miaomiao/bili-down-out") 143 | context.startActivity(intent) 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/components/RecordItem.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.filled.MoreVert 14 | import androidx.compose.material3.DropdownMenu 15 | import androidx.compose.material3.DropdownMenuItem 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.IconButton 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Surface 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.layout.ContentScale 31 | import androidx.compose.ui.text.style.TextOverflow 32 | import androidx.compose.ui.unit.dp 33 | import cn.a10miaomiao.bilidown.common.UrlUtil 34 | import cn.a10miaomiao.bilidown.db.dao.OutRecord 35 | import coil.compose.AsyncImage 36 | 37 | @Composable 38 | fun RecordItem( 39 | title: String, 40 | cover: String, 41 | status: Int, 42 | onClick: () -> Unit, 43 | onDeleteClick: (isDeleteFile: Boolean) -> Unit, 44 | ) { 45 | var expandedMoreMenu by remember { mutableStateOf(false) } 46 | 47 | Box( 48 | modifier = Modifier.padding(5.dp), 49 | ) { 50 | Surface( 51 | modifier = Modifier.fillMaxWidth(), 52 | shape = RoundedCornerShape(10.dp), 53 | color = MaterialTheme.colorScheme.secondaryContainer 54 | ) { 55 | Column() { 56 | Row( 57 | modifier = Modifier 58 | .clickable(onClick = onClick) 59 | .padding(10.dp) 60 | .fillMaxWidth(), 61 | verticalAlignment = Alignment.CenterVertically, 62 | ) { 63 | AsyncImage( 64 | model = UrlUtil.autoHttps(cover) + "@672w_378h_1c_", 65 | contentDescription = title, 66 | contentScale = ContentScale.Crop, 67 | modifier = Modifier 68 | .size(width = 120.dp, height = 80.dp) 69 | .clip(RoundedCornerShape(5.dp)) 70 | ) 71 | 72 | Column( 73 | modifier = Modifier 74 | .weight(1f) 75 | .height(80.dp) 76 | .padding(horizontal = 10.dp), 77 | ) { 78 | Text( 79 | text = title, 80 | maxLines = 2, 81 | modifier = Modifier.weight(1f), 82 | overflow = TextOverflow.Ellipsis, 83 | ) 84 | Row( 85 | modifier = Modifier.fillMaxWidth(), 86 | verticalAlignment = Alignment.CenterVertically, 87 | ) { 88 | val statusText = when (status) { 89 | OutRecord.STATUS_WAIT -> "队列中" 90 | OutRecord.STATUS_SUCCESS -> "已导出" 91 | OutRecord.STATUS_FAIL -> "导出出现异常" 92 | -1 -> "导出文件已被删除" 93 | else -> "未导出" 94 | } 95 | Text( 96 | modifier = Modifier.weight(1f), 97 | text = statusText, 98 | // maxLines = 1, 99 | color = if (status >= 0) { 100 | MaterialTheme.colorScheme.outline 101 | } else { 102 | Color.Red 103 | }, 104 | overflow = TextOverflow.Ellipsis, 105 | ) 106 | Box() { 107 | IconButton( 108 | onClick = { expandedMoreMenu = true } 109 | ) { 110 | Icon(Icons.Filled.MoreVert, null) 111 | } 112 | val menus = remember>(status) { 113 | if (status == OutRecord.STATUS_SUCCESS) { 114 | listOf("删除记录", "删除记录及文件") 115 | } else { 116 | listOf("移除任务") 117 | } 118 | } 119 | DropdownMenu( 120 | expanded = expandedMoreMenu, 121 | onDismissRequest = { expandedMoreMenu = false }, 122 | ) { 123 | menus.forEachIndexed { index, text -> 124 | DropdownMenuItem( 125 | onClick = { 126 | expandedMoreMenu = false 127 | onDeleteClick(index == 1) 128 | }, 129 | text = { 130 | Text(text = text) 131 | } 132 | ) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/common/file/MiaoDocumentFile.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.common.file 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Environment 9 | import android.provider.DocumentsContract 10 | import androidx.annotation.RequiresApi 11 | import androidx.documentfile.provider.DocumentFile 12 | import java.io.BufferedReader 13 | import java.io.File 14 | import java.io.FileOutputStream 15 | import java.io.InputStreamReader 16 | 17 | class MiaoDocumentFile( 18 | val context: Context, 19 | val documentFile: DocumentFile, 20 | ): MiaoFile { 21 | 22 | companion object { 23 | const val DOC_AUTHORITY = "com.android.externalstorage.documents" 24 | val externalStorage = Environment.getExternalStorageDirectory() 25 | 26 | @JvmStatic 27 | fun getFolderUri(id: String, tree: Boolean): Uri { 28 | var rootId: String = id 29 | // var path = "" 30 | // if (id.startsWith("primary:Android/data/")) { 31 | // val i = id.indexOf("/", 21) 32 | // rootId = id.substring(0, i) 33 | // path = id.replace(":", "%3A").replace("/", "%2F") 34 | // } 35 | // Log.d("getFolderUri", rootId + path) 36 | val uri = if (tree){ 37 | DocumentsContract.buildTreeDocumentUri(DOC_AUTHORITY, rootId) 38 | } else { 39 | DocumentsContract.buildDocumentUri(DOC_AUTHORITY, rootId) 40 | } 41 | return uri 42 | // Log.d("getFolderUri", uri.toString()) 43 | // return Uri.parse(uri.toString() + "/document/" + path) 44 | } 45 | 46 | @JvmStatic 47 | @RequiresApi(Build.VERSION_CODES.O) 48 | fun requestFolderPermission(activity: Activity, requestCode: Int, id: String) { 49 | val i = getUriOpenIntent(getFolderUri(id, false)) 50 | 51 | val flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or 52 | Intent.FLAG_GRANT_READ_URI_PERMISSION or 53 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 54 | i.addFlags(flags) 55 | 56 | activity.startActivityForResult(i, requestCode) 57 | } 58 | 59 | @JvmStatic 60 | @RequiresApi(Build.VERSION_CODES.O) 61 | fun getUriOpenIntent(uri: Uri): Intent { 62 | return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 63 | .putExtra("android.provider.extra.SHOW_ADVANCED", true) 64 | .putExtra("android.content.extra.SHOW_ADVANCED", true) 65 | .putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri) 66 | } 67 | 68 | @JvmStatic 69 | fun checkFolderPermission(context: Context, id: String): Boolean { 70 | return if (atLeastR()) { 71 | val treeUri: Uri = getFolderUri(id, true) 72 | //Log.e(TAG, "treeUri:" + treeUri) 73 | isInPersistedUriPermissions(context, treeUri) 74 | } else { 75 | true 76 | } 77 | } 78 | 79 | @JvmStatic 80 | @RequiresApi(Build.VERSION_CODES.KITKAT) 81 | fun isInPersistedUriPermissions(context: Context, uri: Uri): Boolean { 82 | val pList = context.contentResolver.persistedUriPermissions 83 | //Log.e(TAG, "pList:" + pList.size) 84 | for (uriPermission in pList) { 85 | //Log.e(TAG, "uriPermission:$uriPermission") 86 | if (uriPermission.uri == uri && (uriPermission.isReadPermission || uriPermission.isWritePermission)) { 87 | return true 88 | } else { 89 | //Log.e(TAG, "up:" + uriPermission.uri) 90 | } 91 | } 92 | return false 93 | } 94 | 95 | fun atLeastTiramisu(): Boolean { 96 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU 97 | } 98 | 99 | fun atLeastR(): Boolean { 100 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R 101 | } 102 | } 103 | 104 | constructor( 105 | context: Context, 106 | id: String, 107 | path: String, 108 | ): this( 109 | context, 110 | DocumentFile.fromTreeUri( 111 | context, getFolderUri(id, true) 112 | )!!.let { 113 | DocumentFile.fromTreeUri( 114 | context, 115 | Uri.parse(it.uri.toString() + path.replace("/", "%2F")) 116 | )!! 117 | } 118 | ) 119 | 120 | constructor( 121 | context: Context, 122 | documentFile: DocumentFile, 123 | path: String, 124 | ): this( 125 | context, 126 | DocumentFile.fromTreeUri( 127 | context, 128 | Uri.parse(documentFile.uri.toString() + path.replace("/", "%2F")) 129 | )!! 130 | ) 131 | 132 | fun open() { 133 | context.contentResolver.openOutputStream(documentFile!!.uri) 134 | } 135 | 136 | override val path: String 137 | get() = documentFile.uri.toString() 138 | 139 | override val isDirectory: Boolean 140 | get() = documentFile.isDirectory 141 | 142 | override val name: String 143 | get() = documentFile.name ?: "" 144 | 145 | override fun exists(): Boolean { 146 | return documentFile.exists() 147 | } 148 | 149 | override fun listFiles(): List { 150 | return documentFile.listFiles().map { 151 | MiaoDocumentFile(context, it) 152 | } 153 | } 154 | 155 | override fun canRead(): Boolean { 156 | return documentFile.canRead() 157 | } 158 | 159 | override fun readText(): String { 160 | // try { 161 | val fileUri = documentFile.uri 162 | //DocumentFile输入流 163 | 164 | val inputStream = context.contentResolver.openInputStream(fileUri) 165 | val text = BufferedReader(InputStreamReader(inputStream)).useLines { lines -> 166 | val results = StringBuilder() 167 | lines.forEach { results.append(it) } 168 | results.toString() 169 | } 170 | return text 171 | // } catch (e: Exception) { 172 | // e.printStackTrace() 173 | // } 174 | } 175 | 176 | suspend fun copyToTemp( 177 | tempFile: File 178 | ) { 179 | val fileUri = documentFile.uri 180 | val fileInputStream = context.contentResolver.openInputStream(fileUri)!! 181 | val fileOutputStream = FileOutputStream(tempFile) 182 | val buffer = ByteArray(1024) 183 | var byteRead: Int 184 | while (-1 != fileInputStream.read(buffer).also { byteRead = it }) { 185 | fileOutputStream.write(buffer, 0, byteRead) 186 | } 187 | fileInputStream.close() 188 | fileOutputStream.flush() 189 | fileOutputStream.close() 190 | } 191 | 192 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/shizuku/permission/ShizukuPermission.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilidown.shizuku.permission 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.view.View 8 | import androidx.activity.ComponentActivity 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.State 12 | import androidx.compose.runtime.collectAsState 13 | import androidx.datastore.preferences.core.edit 14 | import androidx.fragment.app.FragmentActivity 15 | import androidx.lifecycle.coroutineScope 16 | import androidx.lifecycle.lifecycleScope 17 | import cn.a10miaomiao.bilidown.BiliDownApp 18 | import cn.a10miaomiao.bilidown.MainActivity 19 | import cn.a10miaomiao.bilidown.common.MiaoLog 20 | import cn.a10miaomiao.bilidown.common.datastore.DataStoreKeys 21 | import cn.a10miaomiao.bilidown.common.datastore.dataStore 22 | import cn.a10miaomiao.bilidown.common.permission.StoragePermission 23 | import kotlinx.coroutines.flow.MutableStateFlow 24 | import kotlinx.coroutines.flow.collect 25 | import kotlinx.coroutines.flow.collectLatest 26 | import kotlinx.coroutines.flow.map 27 | import kotlinx.coroutines.launch 28 | import rikka.shizuku.Shizuku 29 | import rikka.shizuku.ShizukuProvider.MANAGER_APPLICATION_ID 30 | 31 | class ShizukuPermission( 32 | val activity: ComponentActivity, 33 | ): Shizuku.OnRequestPermissionResultListener 34 | , Shizuku.OnBinderReceivedListener 35 | , Shizuku.OnBinderDeadListener { 36 | 37 | companion object { 38 | val isAboveN get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N 39 | const val SHIZUKU_PERMISSION_REQUEST_CODE = 413 40 | } 41 | 42 | private var resultCallBack: (() -> Unit)? = null 43 | 44 | private val appState = (activity.application as BiliDownApp).state 45 | 46 | @Composable 47 | fun collectState(): State { 48 | return appState.shizukuState.collectAsState() 49 | } 50 | 51 | init { 52 | Shizuku.addBinderReceivedListenerSticky(this) 53 | Shizuku.addBinderDeadListener(this) 54 | Shizuku.addRequestPermissionResultListener(this) 55 | } 56 | 57 | fun onCreate() { 58 | activity.lifecycleScope.launch { 59 | activity.dataStore.data.map { 60 | it[DataStoreKeys.enabledShizuku] 61 | }.collect { 62 | if (it == true) { 63 | syncShizukuState(activity) 64 | } else { 65 | val state = appState.shizukuState.value 66 | appState.putShizukuState( 67 | state.copy( 68 | isEnabled = false, 69 | ) 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | 76 | fun onDestroy() { 77 | Shizuku.removeBinderReceivedListener(this) 78 | Shizuku.removeBinderDeadListener(this) 79 | Shizuku.removeRequestPermissionResultListener(this) 80 | } 81 | 82 | override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { 83 | when (requestCode) { 84 | SHIZUKU_PERMISSION_REQUEST_CODE -> { 85 | val state = appState.shizukuState.value 86 | appState.putShizukuState( 87 | state.copy( 88 | isGranted = grantResult == PackageManager.PERMISSION_DENIED 89 | ) 90 | ) 91 | } 92 | } 93 | } 94 | 95 | override fun onBinderReceived() { 96 | val state = appState.shizukuState.value 97 | appState.putShizukuState( 98 | state.copy( 99 | isRunning = true 100 | ) 101 | ) 102 | } 103 | 104 | override fun onBinderDead() { 105 | val state = appState.shizukuState.value 106 | if (Shizuku.isPreV11()) { 107 | appState.putShizukuState( 108 | state.copy( 109 | isPreV11 = true, 110 | isRunning = false, 111 | ) 112 | ) 113 | } else { 114 | appState.putShizukuState( 115 | state.copy( 116 | isRunning = true, 117 | ) 118 | ) 119 | } 120 | } 121 | 122 | fun requestPermission(): Boolean { 123 | if (Shizuku.shouldShowRequestPermissionRationale()) { 124 | return false 125 | } else { 126 | if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_DENIED) { 127 | Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) 128 | } 129 | return true 130 | } 131 | } 132 | 133 | fun checkSelfPermission(): Boolean { 134 | return Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED 135 | } 136 | 137 | fun syncShizukuState(context: Context) { 138 | if (!isAboveN) { 139 | return 140 | } 141 | if (isRunning()) { 142 | val isGranted = checkSelfPermission() 143 | val state = appState.shizukuState.value 144 | val newState = ShizukuPermissionState( 145 | isInstalled = true, 146 | isPreV11 = Shizuku.isPreV11(), 147 | isRunning = true, 148 | isGranted = isGranted, 149 | isEnabled = isGranted && state.isEnabled, 150 | ) 151 | appState.putShizukuState(newState) 152 | activity.lifecycleScope.launch { 153 | activity.dataStore.edit { 154 | val state = appState.shizukuState.value 155 | appState.putShizukuState( 156 | state.copy( 157 | isEnabled = isGranted && (it[DataStoreKeys.enabledShizuku] ?: false), 158 | ) 159 | ) 160 | } 161 | } 162 | } else { 163 | appState.putShizukuState( 164 | ShizukuPermissionState( 165 | isInstalled = isInstalled(context), 166 | ) 167 | ) 168 | } 169 | } 170 | 171 | fun isInstalled(context: Context): Boolean { 172 | return runCatching { 173 | val packageManager = context.packageManager 174 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 175 | packageManager.getPackageInfo( 176 | MANAGER_APPLICATION_ID, 177 | PackageManager.PackageInfoFlags.of(0) 178 | ) 179 | } else { 180 | packageManager.getPackageInfo(MANAGER_APPLICATION_ID, 0) 181 | } 182 | }.isSuccess 183 | } 184 | 185 | fun isRunning(): Boolean { 186 | return try { 187 | Shizuku.getUid() != -1 188 | } catch (e: Exception) { 189 | false 190 | } 191 | } 192 | 193 | fun setEnabled(enabled: Boolean) { 194 | ShizukuPermissionState( 195 | isEnabled = enabled 196 | ) 197 | activity.lifecycleScope.launch { 198 | activity.dataStore.edit { 199 | it[DataStoreKeys.enabledShizuku] = enabled 200 | } 201 | } 202 | } 203 | 204 | data class ShizukuPermissionState( 205 | val isInstalled: Boolean = false, 206 | val isPreV11: Boolean = false, 207 | val isRunning: Boolean = false, 208 | val isGranted: Boolean = false, 209 | val isEnabled: Boolean = false, 210 | ) { 211 | val statusText: String get() { 212 | return if (!isInstalled) { 213 | "Shizuku未安装" 214 | } else if (isPreV11) { 215 | "Shizuku版本过低" 216 | } else if (!isRunning){ 217 | "Shizuku未在运行" 218 | } else if (!isGranted){ 219 | "Shizuku未授权" 220 | } else if (isEnabled){ 221 | "已启用" 222 | } else { 223 | "未启用" 224 | } 225 | } 226 | } 227 | 228 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/a10miaomiao/bilidown/ui/animation/MaterialSharedAxis.kt: -------------------------------------------------------------------------------- 1 | package cn.a10miaomiao.bilimiao.compose.animation 2 | 3 | 4 | import androidx.compose.animation.ContentTransform 5 | import androidx.compose.animation.EnterTransition 6 | import androidx.compose.animation.ExitTransition 7 | import androidx.compose.animation.core.FastOutLinearInEasing 8 | import androidx.compose.animation.core.FastOutSlowInEasing 9 | import androidx.compose.animation.core.LinearOutSlowInEasing 10 | import androidx.compose.animation.core.tween 11 | import androidx.compose.animation.fadeIn 12 | import androidx.compose.animation.fadeOut 13 | import androidx.compose.animation.scaleIn 14 | import androidx.compose.animation.scaleOut 15 | import androidx.compose.animation.slideInHorizontally 16 | import androidx.compose.animation.slideInVertically 17 | import androidx.compose.animation.slideOutHorizontally 18 | import androidx.compose.animation.slideOutVertically 19 | import androidx.compose.animation.togetherWith 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.platform.LocalDensity 23 | import androidx.compose.ui.unit.Dp 24 | 25 | /** 26 | * Returns the provided [Dp] as an [Int] value by the [LocalDensity]. 27 | * 28 | * @param slideDistance Value to the slide distance dimension, 30dp by default. 29 | */ 30 | @Composable 31 | fun rememberSlideDistance( 32 | slideDistance: Dp = DefaultSlideDistance, 33 | ): Int { 34 | val density = LocalDensity.current 35 | return remember(density, slideDistance) { 36 | with(density) { slideDistance.roundToPx() } 37 | } 38 | } 39 | 40 | private const val ProgressThreshold = 0.35f 41 | 42 | private val Int.ForOutgoing: Int 43 | get() = (this * ProgressThreshold).toInt() 44 | 45 | private val Int.ForIncoming: Int 46 | get() = this - this.ForOutgoing 47 | 48 | /** 49 | * [materialSharedAxisX] allows to switch a layout with shared X-axis transition. 50 | * 51 | * @param forward whether the direction of the animation is forward. 52 | * @param slideDistance the slide distance of transition. 53 | * @param durationMillis the duration of transition. 54 | */ 55 | fun materialSharedAxisX( 56 | forward: Boolean, 57 | slideDistance: Int, 58 | durationMillis: Int = DefaultMotionDuration, 59 | ): ContentTransform = materialSharedAxisXIn( 60 | forward = forward, 61 | slideDistance = slideDistance, 62 | durationMillis = durationMillis, 63 | ) togetherWith materialSharedAxisXOut( 64 | forward = forward, 65 | slideDistance = slideDistance, 66 | durationMillis = durationMillis, 67 | ) 68 | 69 | /** 70 | * [materialSharedAxisXIn] allows to switch a layout with shared X-axis enter transition. 71 | * 72 | * @param forward whether the direction of the animation is forward. 73 | * @param slideDistance the slide distance of the enter transition. 74 | * @param durationMillis the duration of the enter transition. 75 | */ 76 | fun materialSharedAxisXIn( 77 | forward: Boolean, 78 | slideDistance: Int, 79 | durationMillis: Int = DefaultMotionDuration, 80 | ): EnterTransition = slideInHorizontally( 81 | animationSpec = tween( 82 | durationMillis = durationMillis, 83 | easing = FastOutSlowInEasing, 84 | ), 85 | initialOffsetX = { 86 | if (forward) slideDistance else -slideDistance 87 | }, 88 | ) + fadeIn( 89 | animationSpec = tween( 90 | durationMillis = durationMillis.ForIncoming, 91 | delayMillis = durationMillis.ForOutgoing, 92 | easing = LinearOutSlowInEasing, 93 | ), 94 | ) 95 | 96 | /** 97 | * [materialSharedAxisXOut] allows to switch a layout with shared X-axis exit transition. 98 | * 99 | * @param forward whether the direction of the animation is forward. 100 | * @param slideDistance the slide distance of the exit transition. 101 | * @param durationMillis the duration of the exit transition. 102 | */ 103 | fun materialSharedAxisXOut( 104 | forward: Boolean, 105 | slideDistance: Int, 106 | durationMillis: Int = DefaultMotionDuration, 107 | ): ExitTransition = slideOutHorizontally( 108 | animationSpec = tween( 109 | durationMillis = durationMillis, 110 | easing = FastOutSlowInEasing, 111 | ), 112 | targetOffsetX = { 113 | if (forward) -slideDistance else slideDistance 114 | }, 115 | ) + fadeOut( 116 | animationSpec = tween( 117 | durationMillis = durationMillis.ForOutgoing, 118 | delayMillis = 0, 119 | easing = FastOutLinearInEasing, 120 | ), 121 | ) 122 | 123 | /** 124 | * [materialSharedAxisY] allows to switch a layout with shared Y-axis transition. 125 | * 126 | * @param forward whether the direction of the animation is forward. 127 | * @param slideDistance the slide distance of transition. 128 | * @param durationMillis the duration of transition. 129 | */ 130 | fun materialSharedAxisY( 131 | forward: Boolean, 132 | slideDistance: Int, 133 | durationMillis: Int = DefaultMotionDuration, 134 | ): ContentTransform = materialSharedAxisYIn( 135 | forward = forward, 136 | slideDistance = slideDistance, 137 | durationMillis = durationMillis, 138 | ) togetherWith materialSharedAxisYOut( 139 | forward = forward, 140 | slideDistance = slideDistance, 141 | durationMillis = durationMillis, 142 | ) 143 | 144 | /** 145 | * [materialSharedAxisYIn] allows to switch a layout with shared Y-axis enter transition. 146 | * 147 | * @param forward whether the direction of the animation is forward. 148 | * @param slideDistance the slide distance of the enter transition. 149 | * @param durationMillis the duration of the enter transition. 150 | */ 151 | fun materialSharedAxisYIn( 152 | forward: Boolean, 153 | slideDistance: Int, 154 | durationMillis: Int = DefaultMotionDuration, 155 | ): EnterTransition = slideInVertically( 156 | animationSpec = tween( 157 | durationMillis = durationMillis, 158 | easing = FastOutSlowInEasing, 159 | ), 160 | initialOffsetY = { 161 | if (forward) slideDistance else -slideDistance 162 | }, 163 | ) + fadeIn( 164 | animationSpec = tween( 165 | durationMillis = durationMillis.ForIncoming, 166 | delayMillis = durationMillis.ForOutgoing, 167 | easing = LinearOutSlowInEasing, 168 | ), 169 | ) 170 | 171 | /** 172 | * [materialSharedAxisYOut] allows to switch a layout with shared Y-axis exit transition. 173 | * 174 | * @param forward whether the direction of the animation is forward. 175 | * @param slideDistance the slide distance of the exit transition. 176 | * @param durationMillis the duration of the exit transition. 177 | */ 178 | fun materialSharedAxisYOut( 179 | forward: Boolean, 180 | slideDistance: Int, 181 | durationMillis: Int = DefaultMotionDuration, 182 | ): ExitTransition = slideOutVertically( 183 | animationSpec = tween( 184 | durationMillis = durationMillis, 185 | easing = FastOutSlowInEasing, 186 | ), 187 | targetOffsetY = { 188 | if (forward) -slideDistance else slideDistance 189 | }, 190 | ) + fadeOut( 191 | animationSpec = tween( 192 | durationMillis = durationMillis.ForOutgoing, 193 | delayMillis = 0, 194 | easing = FastOutLinearInEasing, 195 | ), 196 | ) 197 | 198 | /** 199 | * [materialSharedAxisZ] allows to switch a layout with shared Z-axis transition. 200 | * 201 | * @param forward whether the direction of the animation is forward. 202 | * @param durationMillis the duration of transition. 203 | */ 204 | fun materialSharedAxisZ( 205 | forward: Boolean, 206 | durationMillis: Int = DefaultMotionDuration, 207 | ): ContentTransform = materialSharedAxisZIn( 208 | forward = forward, 209 | durationMillis = durationMillis, 210 | ) togetherWith materialSharedAxisZOut( 211 | forward = forward, 212 | durationMillis = durationMillis, 213 | ) 214 | 215 | /** 216 | * [materialSharedAxisZIn] allows to switch a layout with shared Z-axis enter transition. 217 | * 218 | * @param forward whether the direction of the animation is forward. 219 | * @param durationMillis the duration of the enter transition. 220 | */ 221 | fun materialSharedAxisZIn( 222 | forward: Boolean, 223 | durationMillis: Int = DefaultMotionDuration, 224 | ): EnterTransition = fadeIn( 225 | animationSpec = tween( 226 | durationMillis = durationMillis.ForIncoming, 227 | delayMillis = durationMillis.ForOutgoing, 228 | easing = LinearOutSlowInEasing, 229 | ), 230 | ) + scaleIn( 231 | animationSpec = tween( 232 | durationMillis = durationMillis, 233 | easing = FastOutSlowInEasing, 234 | ), 235 | initialScale = if (forward) 0.8f else 1.1f, 236 | ) 237 | 238 | /** 239 | * [materialSharedAxisZOut] allows to switch a layout with shared Z-axis exit transition. 240 | * 241 | * @param forward whether the direction of the animation is forward. 242 | * @param durationMillis the duration of the exit transition. 243 | */ 244 | fun materialSharedAxisZOut( 245 | forward: Boolean, 246 | durationMillis: Int = DefaultMotionDuration, 247 | ): ExitTransition = fadeOut( 248 | animationSpec = tween( 249 | durationMillis = durationMillis.ForOutgoing, 250 | delayMillis = 0, 251 | easing = FastOutLinearInEasing, 252 | ), 253 | ) + scaleOut( 254 | animationSpec = tween( 255 | durationMillis = durationMillis, 256 | easing = FastOutSlowInEasing, 257 | ), 258 | targetScale = if (forward) 1.1f else 0.8f, 259 | ) --------------------------------------------------------------------------------