├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── fr.yml └── workflows │ └── build.yml ├── .gitignore ├── HISTORY.md ├── Makefile ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lizongying │ │ └── mytv1 │ │ ├── BootReceiver.kt │ │ ├── ChannelFragment.kt │ │ ├── ConfirmationFragment.kt │ │ ├── ErrorFragment.kt │ │ ├── Ext.kt │ │ ├── GroupAdapter.kt │ │ ├── InfoFragment.kt │ │ ├── InitializerProvider.kt │ │ ├── ListAdapter.kt │ │ ├── LoadingFragment.kt │ │ ├── MainActivity.kt │ │ ├── MenuFragment.kt │ │ ├── ModalFragment.kt │ │ ├── MyTVApplication.kt │ │ ├── OnSharedPreferenceChangeListener.kt │ │ ├── PortUtil.kt │ │ ├── QrCodeUtil.kt │ │ ├── SP.kt │ │ ├── SettingFragment.kt │ │ ├── SimpleServer.kt │ │ ├── TimeFragment.kt │ │ ├── UpdateManager.kt │ │ ├── Utils.kt │ │ ├── WebFragment.kt │ │ ├── data │ │ ├── Global.kt │ │ ├── ReleaseResponse.kt │ │ ├── ReqSettings.kt │ │ ├── RespSettings.kt │ │ └── TV.kt │ │ ├── models │ │ ├── TVGroupModel.kt │ │ ├── TVList.kt │ │ ├── TVListModel.kt │ │ └── TVModel.kt │ │ └── requests │ │ ├── DnsCache.kt │ │ ├── HttpClient.kt │ │ └── Tls12SocketFactory.kt │ └── res │ ├── color │ ├── switch_thumb_color.xml │ └── switch_track_color.xml │ ├── drawable │ ├── appreciate.jpg │ ├── banner1.png │ ├── baseline_favorite_24.xml │ ├── baseline_favorite_border_24.xml │ ├── baseline_sentiment_dissatisfied_24.xml │ ├── custom_progress_drawable.xml │ ├── light_mode_24px.xml │ ├── logo1.png │ ├── rounded_dark_bottom.xml │ ├── rounded_dark_left.xml │ ├── rounded_dark_right.xml │ ├── rounded_light_bottom.xml │ ├── rounded_white_left.xml │ ├── rounded_white_right.xml │ ├── rounded_white_top.xml │ ├── volume_off_24px.xml │ └── volume_up_24px.xml │ ├── layout │ ├── activity_main.xml │ ├── channel.xml │ ├── error.xml │ ├── group_item.xml │ ├── info.xml │ ├── list_item.xml │ ├── loading.xml │ ├── menu.xml │ ├── modal.xml │ ├── player.xml │ ├── setting.xml │ ├── show.xml │ └── time.xml │ ├── raw │ ├── ahtv.js │ ├── ahtv1.js │ ├── ahtv2.js │ ├── channels.txt │ ├── gdtv.js │ ├── gua64min.js │ ├── index.html │ ├── jxtv1.js │ ├── nmgtv1.js │ ├── prev.js │ ├── sxrtv1.js │ └── xjtv1.js │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── file_paths.xml │ └── network.xml ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── history.sh ├── screenshots ├── appreciate.jpeg ├── img.jpg ├── img1.jpg └── zfb.jpg ├── settings.gradle.kts └── version.json /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 反馈 2 | description: 反馈一个 Bug 3 | labels: [ "bug" ] 4 | title: "[BUG] " 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: 检查清单 10 | description: 确保我们的错误报告表单适合您。 11 | options: 12 | - label: 之前没有人提交过类似或相同的 bug report。 13 | required: true 14 | - label: 我正在使用本软件的最新版本。 15 | required: true 16 | - type: dropdown 17 | id: version 18 | attributes: 19 | label: 我的电视·一 版本 20 | description: 请选择正在使用的版本 21 | options: 22 | - 最新 23 | - v1.1.3 24 | - v1.1.2 25 | - v1.1.1 26 | - v1.1.0 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: bug 31 | attributes: 32 | label: Bug 描述 33 | description: 请描述 bug 详情 34 | placeholder: | 35 | e.g. Crashed when generating snapshot. 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: expected 40 | attributes: 41 | label: 预期行为 42 | description: 你预期会发生什么? 43 | placeholder: | 44 | e.g. A New snapshot! 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: actual 49 | attributes: 50 | label: 实际行为 51 | description: 反而发生了什么? 52 | placeholder: | 53 | e.g. Crashed. 54 | validations: 55 | required: true 56 | - type: textarea 57 | id: steps 58 | attributes: 59 | label: 复现步骤 60 | description: 如何复现这个 bug。 61 | placeholder: | 62 | 1. Open the app 63 | 2. Crashed 64 | 65 | What an app. 66 | - type: input 67 | id: ui 68 | attributes: 69 | label: UI / OS 70 | description: 你的电视系统 UI 或 OS 或 品牌 71 | placeholder: TCL / XIAOMI / PHONE / etc. 72 | validations: 73 | required: true 74 | - type: input 75 | id: android 76 | attributes: 77 | label: Android 版本 78 | description: 你的 Android 版本 79 | placeholder: "12" 80 | validations: 81 | required: true 82 | - type: textarea 83 | id: additional 84 | attributes: 85 | label: 额外信息 86 | description: 任何你觉得值得说的。 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/fr.yml: -------------------------------------------------------------------------------- 1 | name: 功能(新频道)请求 2 | description: 提出一个建议 3 | labels: [ "enhancement" ] 4 | title: "[FR] " 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: 检查清单 10 | description: 确保我们的错误报告表单适合您。 11 | options: 12 | - label: 之前没有人提交过类似或相同的功能请求。 13 | required: true 14 | - label: 这个建议不会背离 我的电视·一 的初衷。 15 | required: true 16 | - type: textarea 17 | id: propose 18 | attributes: 19 | label: 改进目的 20 | description: 改进有什么用 21 | placeholder: | 22 | Show your idea here. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: solution 27 | attributes: 28 | label: 解决方案 29 | description: 你会怎么完成这个改进? 30 | placeholder: | 31 | How to do it on your opinion? Or left this blank 32 | - type: textarea 33 | id: addition 34 | attributes: 35 | label: 额外信息 36 | description: 任何你觉得值得说的。 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: set up JDK 21 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: '21' 22 | distribution: 'temurin' 23 | 24 | - name: Run build with Gradle wrapper 25 | run: ./gradlew assembleRelease 26 | 27 | - name: Sign app APK 28 | id: sign_app 29 | uses: r0adkll/sign-android-release@v1 30 | with: 31 | releaseDirectory: app/build/outputs/apk/release 32 | alias: ${{ secrets.ALIAS }} 33 | signingKeyBase64: ${{ secrets.KEYSTORE }} 34 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 35 | keyPassword: ${{ secrets.ALIAS_PASSWORD }} 36 | env: 37 | # override default build-tools version (29.0.3) -- optional 38 | BUILD_TOOLS_VERSION: "34.0.0" 39 | 40 | - name: Get History 41 | id: get_history 42 | run: | 43 | chmod +x history.sh 44 | output=$(./history.sh) 45 | echo "$output" > history.md 46 | 47 | - name: Create Release 48 | id: create_release 49 | uses: actions/create-release@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | tag_name: ${{ github.ref }} 54 | release_name: Release ${{ github.ref }} 55 | draft: false 56 | prerelease: false 57 | body_path: history.md 58 | 59 | - name: Set Asset Name 60 | id: set_asset_name 61 | run: | 62 | VERSION_WITHOUT_V=$(echo '${{ github.ref_name }}' | sed 's/^v//') 63 | echo "asset_name=my-tv-1_${VERSION_WITHOUT_V}.apk" >> $GITHUB_ENV 64 | 65 | - name: Upload Release Asset 66 | uses: actions/upload-release-asset@v1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: ${{ steps.sign_app.outputs.signedReleaseFile }} 72 | asset_name: ${{ env.asset_name }} 73 | asset_content_type: application/vnd.android.package-archive 74 | 75 | # - name: Gitee Create Release 76 | # run: | 77 | # latest_commit=$(git rev-parse HEAD) 78 | # history=$(cat history.md) 79 | # curl -v POST https://gitee.com/api/v5/repos/${{ github.repository }}/releases \ 80 | # -H "Content-Type: application/json" \ 81 | # -d '{ 82 | # "access_token": "${{ secrets.GITEE_ACCESS_TOKEN}}", 83 | # "tag_name": "${{ github.ref_name }}", 84 | # "name": "Release ${{ github.ref_name }}", 85 | # "body": "'"$history"'", 86 | # "prerelease": false, 87 | # "target_commitish": "'"$latest_commit"'" 88 | # }' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | files 18 | app/release 19 | .README.md 20 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 更新日誌 2 | 3 | ### v1.0.14 4 | 5 | * 優化安徽 6 | 7 | ### v1.0.13 8 | 9 | * 修復觸屏無法使用的問題 10 | * 增加調整音量和亮度 11 | 12 | ### v1.0.12 13 | 14 | * 調整頻道位置 15 | 16 | ### v1.0.11 17 | 18 | * 優化山西、內蒙 19 | 20 | ### v1.0.10 21 | 22 | * 解決部分頻道有浮層的問題 23 | * 時間顯示秒 24 | * 可配置緊湊的菜單 25 | 26 | ### v1.0.9 27 | 28 | * 增加頻道 29 | 30 | ### v1.0.8 31 | 32 | * 固定遠程配置端口為34568 33 | * 一些樣式優化 34 | 35 | ### v1.0.7 36 | 37 | * 優化頻道 38 | * 增加頻道 39 | 40 | ### v1.0.6 41 | 42 | * 配置地址兼容處理 43 | * 部分手機設備樣式兼容處理 44 | * 解决視頻源文件分組不連續的問題 45 | 46 | ### v1.0.5 47 | 48 | * 增加頻道 49 | * 修複部分電視設備無法播放的問題 50 | * 優化頻道列表樣式 51 | * 遥控器左鍵不再退出頻道列表 52 | 53 | ### v1.0.4 54 | 55 | * 增加頻道 56 | 57 | ### v1.0.3 58 | 59 | * 增加頻道 60 | * 優化樣式 61 | * 優化速度 62 | 63 | ### v1.0.0 64 | 65 | * 基本視頻播放 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: gen-version 2 | 3 | all: gen-version 4 | 5 | gen-version: 6 | git describe --tags --always 7 | git describe --tags --always | sed 's/v/ /g' | sed 's/\./ /g' | sed 's/-/ /g' | awk '{print ($$1*16777216)+($$2*65536)+($$3*256)+$$4}' 8 | 9 | #make gen v=1.0.8 10 | gen: 11 | echo $(v) | sed 's/v/ /g' | sed 's/\./ /g' | sed 's/-/ /g' | awk '{print "{\"version_code\": " ($$1*16777216)+($$2*65536)+($$3*256)+$$4 ", \"version_name\": \"" "v$(v)" "\"}"}' > version.json 12 | 13 | channels: 14 | gua64 -f -e files/channels.json -o app/src/main/res/raw/channels.txt 15 | 16 | channels2: 17 | python files/pretty.py && gua64 -f -e files/channels2.json -o app/src/main/res/raw/channels.txt 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 我的電視·一 2 | 3 | 電視視頻播放軟件,支持播放網頁視頻 4 | 5 | [my-tv-1](https://github.com/lizongying/my-tv-1) 6 | 7 | ## 使用 8 | 9 | * 遙控器左鍵/觸屏單擊打開視頻列表 10 | * 遙控器右鍵/觸屏雙擊打開配置 11 | * 遙控器返回鍵關閉視頻列表/配置 12 | * 在聚焦視頻標題的時候,右鍵收藏/取消收藏 13 | * 打開配置后,選擇遠程配置,掃描二維碼可以配置視頻源等。也可以直接遠程配置地址 http://0.0.0.0:34568 14 | * 如果視頻源地址已配置,並且打開了“應用啟動后更新視頻源”后,應用啟動后會自動更新視頻源 15 | * 默認遙控器下鍵/觸屏下滑切換到下一個視頻。換台反轉打開後,邏輯相反 16 | 17 | 注意: 18 | 19 | * 遇到問題可以先考慮重啟/恢復默認/清除數據/重新安裝等方式自助解決 20 | * 視頻源可以設置為本地文件,格式如:file:///mnt/sdcard/tmp/channels.json 21 | /channels.json 22 | 23 | 目前支持的配置格式: 24 | 25 | * json 26 | ```json 27 | [ 28 | { 29 | "group": "組名", 30 | "name": "標準標題", 31 | "title": "標題", 32 | "logo": "图标", 33 | "script": "腳本", 34 | "uris": [ 35 | "視頻地址" 36 | ], 37 | "headers": { 38 | "user-agent": "" 39 | } 40 | } 41 | ] 42 | ``` 43 | 44 | 推薦配合使用 [my-tv-server](https://github.com/lizongying/my-tv-server) 45 | 46 | 下載安裝 [releases](https://github.com/lizongying/my-tv-1/releases/) 47 | 48 | 更多下載地址 [my-tv-1](https://lyrics.run/my-tv-1.html) 49 | 50 | ![image](./screenshots/img.jpg) 51 | ![image](./screenshots/img1.jpg) 52 | 53 | ## 更新日誌 54 | 55 | [更新日誌](./HISTORY.md) 56 | 57 | ## 其他 58 | 59 | 建議通過ADB進行安裝: 60 | 61 | ```shell 62 | adb install my-tv-0.apk 63 | ``` 64 | 65 | 小米電視可以使用小米電視助手進行安裝 66 | 67 | ## TODO 68 | 69 | ## 讚賞 70 | 71 | ![image](./screenshots/appreciate.jpeg) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.BufferedReader 2 | 3 | plugins { 4 | id("com.android.application") 5 | id("org.jetbrains.kotlin.android") 6 | } 7 | 8 | android { 9 | namespace = "com.lizongying.mytv1" 10 | compileSdk = 35 11 | 12 | defaultConfig { 13 | applicationId = "com.lizongying.mytv1" 14 | minSdk = 21 15 | targetSdk = 35 16 | versionCode = getVersionCode() 17 | versionName = getVersionName() 18 | } 19 | 20 | buildFeatures { 21 | viewBinding = true 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = true 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_1_8 35 | targetCompatibility = JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = "1.8" 39 | } 40 | } 41 | 42 | fun getTag(): String { 43 | return try { 44 | val process = Runtime.getRuntime().exec("git describe --tags --always") 45 | process.waitFor() 46 | process.inputStream.bufferedReader().use(BufferedReader::readText).trim().removePrefix("v") 47 | } catch (_: Exception) { 48 | "" 49 | } 50 | } 51 | 52 | fun getVersionCode(): Int { 53 | return try { 54 | val arr = (getTag().replace(".", " ").replace("-", " ") + " 0").split(" ") 55 | arr[0].toInt() * 16777216 + arr[1].toInt() * 65536 + arr[2].toInt() * 256 + arr[3].toInt() 56 | } catch (_: Exception) { 57 | 1 58 | } 59 | } 60 | 61 | fun getVersionName(): String { 62 | return getTag().ifEmpty { 63 | "0.0.0-1" 64 | } 65 | } 66 | 67 | dependencies { 68 | implementation("androidx.appcompat:appcompat:1.7.0") 69 | implementation("androidx.core:core-ktx:1.15.0") 70 | implementation("androidx.constraintlayout:constraintlayout:2.2.1") 71 | implementation("androidx.recyclerview:recyclerview:1.4.0") 72 | implementation("com.github.bumptech.glide:glide:4.16.0") 73 | 74 | implementation("com.squareup.okhttp3:okhttp:4.12.0") 75 | implementation("com.google.code.gson:gson:2.11.0") 76 | 77 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") 78 | 79 | implementation("io.github.lizongying:gua64:1.4.5") 80 | 81 | implementation("org.nanohttpd:nanohttpd:2.3.1") 82 | 83 | implementation("com.google.zxing:core:3.5.3") 84 | 85 | implementation("androidx.webkit:webkit:1.12.1") 86 | } -------------------------------------------------------------------------------- /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 22 | 23 | -keep class com.lizongying.mytv1.data.** { 24 | ; 25 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 46 | 49 | 50 | 51 | 52 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | class BootReceiver : BroadcastReceiver() { 8 | 9 | override fun onReceive(context: Context, intent: Intent) { 10 | if (SP.bootStartup) { 11 | context.startActivity( 12 | Intent(context, MainActivity::class.java) 13 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 14 | ) 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/ChannelFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.os.Bundle 4 | import android.os.Handler 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.core.view.marginEnd 9 | import androidx.core.view.marginTop 10 | import androidx.fragment.app.Fragment 11 | import com.lizongying.mytv1.databinding.ChannelBinding 12 | import com.lizongying.mytv1.models.TVModel 13 | 14 | class ChannelFragment : Fragment() { 15 | private var _binding: ChannelBinding? = null 16 | private val binding get() = _binding!! 17 | 18 | private val handler = Handler() 19 | private val delay: Long = 3000 20 | private var channel = 0 21 | 22 | override fun onCreateView( 23 | inflater: LayoutInflater, container: ViewGroup?, 24 | savedInstanceState: Bundle? 25 | ): View { 26 | _binding = ChannelBinding.inflate(inflater, container, false) 27 | _binding!!.root.visibility = View.GONE 28 | 29 | val application = requireActivity().applicationContext as MyTVApplication 30 | 31 | binding.channel.layoutParams.width = application.px2Px(binding.channel.layoutParams.width) 32 | binding.channel.layoutParams.height = application.px2Px(binding.channel.layoutParams.height) 33 | 34 | val layoutParams = binding.channel.layoutParams as ViewGroup.MarginLayoutParams 35 | layoutParams.topMargin = application.px2Px(binding.channel.marginTop) 36 | layoutParams.marginEnd = application.px2Px(binding.channel.marginEnd) 37 | binding.channel.layoutParams = layoutParams 38 | 39 | binding.content.textSize = application.px2PxFont(binding.content.textSize) 40 | binding.time.textSize = application.px2PxFont(binding.time.textSize) 41 | 42 | binding.main.layoutParams.width = application.shouldWidthPx() 43 | binding.main.layoutParams.height = application.shouldHeightPx() 44 | 45 | return binding.root 46 | } 47 | 48 | fun show(tvViewModel: TVModel) { 49 | handler.removeCallbacks(hideRunnable) 50 | handler.removeCallbacks(playRunnable) 51 | binding.content.text = (tvViewModel.tv.id.plus(1)).toString() 52 | view?.visibility = View.VISIBLE 53 | handler.postDelayed(hideRunnable, delay) 54 | } 55 | 56 | fun show(channel: String) { 57 | if (binding.content.text.length > 1) { 58 | return 59 | } 60 | this.channel = "${binding.content.text}$channel".toInt() 61 | handler.removeCallbacks(hideRunnable) 62 | handler.removeCallbacks(playRunnable) 63 | if (binding.content.text == "") { 64 | binding.content.text = channel 65 | view?.visibility = View.VISIBLE 66 | handler.postDelayed(playRunnable, delay) 67 | } else { 68 | binding.content.text = "${binding.content.text}$channel" 69 | handler.postDelayed(playRunnable, 0) 70 | } 71 | } 72 | 73 | override fun onResume() { 74 | super.onResume() 75 | if (view?.visibility == View.VISIBLE) { 76 | handler.postDelayed(hideRunnable, delay) 77 | } 78 | } 79 | 80 | override fun onPause() { 81 | super.onPause() 82 | handler.removeCallbacks(hideRunnable) 83 | handler.removeCallbacks(playRunnable) 84 | } 85 | 86 | private val hideRunnable = Runnable { 87 | binding.content.text = "" 88 | view?.visibility = View.GONE 89 | } 90 | 91 | private val playRunnable = Runnable { 92 | (activity as MainActivity).play(channel - 1) 93 | handler.postDelayed(hideRunnable, delay) 94 | } 95 | 96 | override fun onDestroyView() { 97 | super.onDestroyView() 98 | _binding = null 99 | } 100 | 101 | companion object { 102 | private const val TAG = "ChannelFragment" 103 | } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/ConfirmationFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.app.AlertDialog 4 | import android.app.Dialog 5 | import android.os.Bundle 6 | import androidx.fragment.app.DialogFragment 7 | 8 | class ConfirmationFragment( 9 | private val listener: ConfirmationListener, 10 | private val message: String, 11 | private val update: Boolean 12 | ) : DialogFragment() { 13 | 14 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 15 | return activity?.let { 16 | val builder = AlertDialog.Builder(it) 17 | builder.setTitle(message) 18 | if (update) { 19 | builder.setMessage("确定更新吗?") 20 | .setPositiveButton( 21 | "确定" 22 | ) { _, _ -> 23 | listener.onConfirm() 24 | } 25 | .setNegativeButton( 26 | "取消" 27 | ) { _, _ -> 28 | listener.onCancel() 29 | } 30 | } else { 31 | builder.setMessage("") 32 | .setNegativeButton( 33 | "确定" 34 | ) { _, _ -> 35 | } 36 | } 37 | builder.create() 38 | } ?: throw IllegalStateException("Activity cannot be null") 39 | } 40 | 41 | interface ConfirmationListener { 42 | fun onConfirm() 43 | fun onCancel() 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/ErrorFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.core.view.marginTop 8 | import androidx.fragment.app.Fragment 9 | import com.lizongying.mytv1.databinding.ErrorBinding 10 | 11 | class ErrorFragment : Fragment() { 12 | private var _binding: ErrorBinding? = null 13 | private val binding get() = _binding!! 14 | 15 | override fun onCreateView( 16 | inflater: LayoutInflater, container: ViewGroup?, 17 | savedInstanceState: Bundle? 18 | ): View { 19 | _binding = ErrorBinding.inflate(inflater, container, false) 20 | 21 | val application = requireActivity().applicationContext as MyTVApplication 22 | 23 | binding.logo.layoutParams.width = application.px2Px(binding.logo.layoutParams.width) 24 | binding.logo.layoutParams.height = application.px2Px(binding.logo.layoutParams.height) 25 | 26 | val layoutParams = binding.msg.layoutParams as ViewGroup.MarginLayoutParams 27 | layoutParams.topMargin = application.px2Px(binding.msg.marginTop) 28 | binding.msg.layoutParams = layoutParams 29 | 30 | binding.msg.textSize = application.px2PxFont(binding.msg.textSize) 31 | 32 | (activity as MainActivity).ready(TAG) 33 | return binding.root 34 | } 35 | 36 | fun show(msg: String) { 37 | binding.msg.text = msg 38 | } 39 | 40 | override fun onDestroyView() { 41 | super.onDestroyView() 42 | _binding = null 43 | } 44 | 45 | companion object { 46 | private const val TAG = "ErrorFragment" 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/Ext.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.lizongying.mytv1 4 | 5 | import android.content.Context 6 | import android.content.pm.PackageInfo 7 | import android.content.pm.PackageManager 8 | import android.content.pm.Signature 9 | import android.content.pm.SigningInfo 10 | import android.os.Build 11 | import android.util.Log 12 | import android.widget.Toast 13 | import java.security.MessageDigest 14 | 15 | private const val TAG = "Extensions" 16 | 17 | private val Context.packageInfo: PackageInfo 18 | get() { 19 | val flag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 20 | PackageManager.GET_SIGNATURES 21 | } else { 22 | PackageManager.GET_SIGNING_CERTIFICATES 23 | } 24 | return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 25 | packageManager.getPackageInfo(packageName, flag) 26 | } else { 27 | packageManager.getPackageInfo( 28 | packageName, 29 | PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong()) 30 | ) 31 | } 32 | } 33 | 34 | /** 35 | * Return the version code of the app which is defined in build.gradle. 36 | * eg:100 37 | */ 38 | val Context.appVersionCode: Long 39 | get() { 40 | val packageInfo = this.packageInfo 41 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 42 | packageInfo.longVersionCode 43 | } else { 44 | packageInfo.versionCode.toLong() 45 | } 46 | } 47 | 48 | /** 49 | * Return the version name of the app which is defined in build.gradle. 50 | * eg:1.0.0 51 | */ 52 | val Context.appVersionName: String get() = packageInfo.versionName!! 53 | 54 | val Context.appSignature: String 55 | get() { 56 | val packageInfo = this.packageInfo 57 | var sign: Signature? = null 58 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 59 | val signatures: Array? = packageInfo.signatures 60 | if (signatures != null) { 61 | sign = signatures[0] 62 | } 63 | } else { 64 | val signingInfo: SigningInfo? = packageInfo.signingInfo 65 | if (signingInfo != null) { 66 | sign = signingInfo.apkContentsSigners[0] 67 | } 68 | } 69 | if (sign == null) { 70 | return "" 71 | } 72 | return hashSignature(sign) 73 | } 74 | 75 | private fun hashSignature(signature: Signature): String { 76 | return try { 77 | val md = MessageDigest.getInstance("MD5") 78 | md.update(signature.toByteArray()) 79 | val digest = md.digest() 80 | digest.let { it -> it.joinToString("") { "%02x".format(it) } } 81 | } catch (e: Exception) { 82 | Log.e(TAG, "Error hashing signature", e) 83 | "" 84 | } 85 | } 86 | 87 | fun String.showToast(duration: Int = Toast.LENGTH_SHORT) { 88 | MyTVApplication.getInstance().toast(this, duration) 89 | } 90 | 91 | fun Int.getString(): String { 92 | return MyTVApplication.getInstance().getString(this) 93 | } 94 | 95 | fun Int.showToast(duration: Int = Toast.LENGTH_SHORT) { 96 | this.getString().showToast(duration) 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/GroupAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.content.Context 4 | import android.view.KeyEvent 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.core.content.ContextCompat 9 | import androidx.core.view.marginBottom 10 | import androidx.core.view.marginStart 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.lizongying.mytv1.databinding.GroupItemBinding 14 | import com.lizongying.mytv1.models.TVGroupModel 15 | import com.lizongying.mytv1.models.TVListModel 16 | 17 | 18 | class GroupAdapter( 19 | private val context: Context, 20 | private val recyclerView: RecyclerView, 21 | private var tvGroupModel: TVGroupModel, 22 | ) : 23 | RecyclerView.Adapter() { 24 | 25 | private var listener: ItemListener? = null 26 | private var focused: View? = null 27 | private var defaultFocused = false 28 | private var defaultFocus: Int = -1 29 | 30 | var visiable = false 31 | 32 | val application = context.applicationContext as MyTVApplication 33 | 34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 35 | val inflater = LayoutInflater.from(context) 36 | val binding = GroupItemBinding.inflate(inflater, parent, false) 37 | 38 | val layoutParams = binding.title.layoutParams as ViewGroup.MarginLayoutParams 39 | layoutParams.marginStart = application.px2Px(binding.title.marginStart) 40 | layoutParams.bottomMargin = application.px2Px(binding.title.marginBottom) 41 | binding.title.layoutParams = layoutParams 42 | 43 | binding.title.textSize = application.px2PxFont(binding.title.textSize) 44 | 45 | binding.root.isFocusable = true 46 | binding.root.isFocusableInTouchMode = true 47 | return ViewHolder(context, binding) 48 | } 49 | 50 | fun focusable(able: Boolean) { 51 | recyclerView.isFocusable = able 52 | recyclerView.isFocusableInTouchMode = able 53 | if (able) { 54 | recyclerView.descendantFocusability = ViewGroup.FOCUS_BEFORE_DESCENDANTS 55 | } else { 56 | recyclerView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS 57 | } 58 | } 59 | 60 | fun clear() { 61 | focused?.clearFocus() 62 | recyclerView.invalidate() 63 | } 64 | 65 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 66 | val tvListModel = tvGroupModel.getTVListModel(position)!! 67 | val view = viewHolder.itemView 68 | view.tag = position 69 | 70 | if (!defaultFocused && position == defaultFocus) { 71 | view.requestFocus() 72 | defaultFocused = true 73 | } 74 | 75 | val onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> 76 | listener?.onItemFocusChange(tvListModel, hasFocus) 77 | 78 | if (hasFocus) { 79 | viewHolder.focus(true) 80 | focused = view 81 | if (visiable) { 82 | if (position != tvGroupModel.position.value) { 83 | tvGroupModel.setPosition(position) 84 | } 85 | } else { 86 | visiable = true 87 | } 88 | } else { 89 | viewHolder.focus(false) 90 | } 91 | } 92 | 93 | view.onFocusChangeListener = onFocusChangeListener 94 | 95 | view.setOnClickListener { _ -> 96 | listener?.onItemClicked(position) 97 | } 98 | 99 | view.setOnKeyListener { _, keyCode, event: KeyEvent? -> 100 | if (event?.action == KeyEvent.ACTION_DOWN) { 101 | if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) { 102 | val p = getItemCount() - 1 103 | 104 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 105 | p, 106 | 0 107 | ) 108 | 109 | recyclerView.postDelayed({ 110 | val v = recyclerView.findViewHolderForAdapterPosition(p) 111 | v?.itemView?.isSelected = true 112 | v?.itemView?.requestFocus() 113 | }, 0) 114 | } 115 | 116 | if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && position == getItemCount() - 1) { 117 | val p = 0 118 | 119 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 120 | p, 121 | 0 122 | ) 123 | 124 | recyclerView.postDelayed({ 125 | val v = recyclerView.findViewHolderForAdapterPosition(p) 126 | v?.itemView?.isSelected = true 127 | v?.itemView?.requestFocus() 128 | }, 0) 129 | } 130 | 131 | return@setOnKeyListener listener?.onKey(keyCode) ?: false 132 | } 133 | false 134 | } 135 | 136 | viewHolder.bindTitle(tvListModel.getName()) 137 | } 138 | 139 | override fun getItemCount() = tvGroupModel.size() 140 | 141 | class ViewHolder(private val context: Context, private val binding: GroupItemBinding) : 142 | RecyclerView.ViewHolder(binding.root) { 143 | fun bindTitle(text: String) { 144 | binding.title.text = text 145 | } 146 | 147 | fun focus(hasFocus: Boolean) { 148 | if (hasFocus) { 149 | binding.title.setTextColor(ContextCompat.getColor(context, R.color.white)) 150 | } else { 151 | binding.title.setTextColor( 152 | ContextCompat.getColor( 153 | context, 154 | R.color.description_blur 155 | ) 156 | ) 157 | } 158 | } 159 | } 160 | 161 | fun toPosition(position: Int) { 162 | recyclerView.post { 163 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 164 | position, 165 | 0 166 | ) 167 | 168 | recyclerView.postDelayed({ 169 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) 170 | viewHolder?.itemView?.isSelected = true 171 | viewHolder?.itemView?.requestFocus() 172 | }, 0) 173 | } 174 | } 175 | 176 | interface ItemListener { 177 | fun onItemFocusChange(tvListModel: TVListModel, hasFocus: Boolean) 178 | fun onItemClicked(position: Int) 179 | fun onKey(keyCode: Int): Boolean 180 | } 181 | 182 | fun setItemListener(listener: ItemListener) { 183 | this.listener = listener 184 | } 185 | 186 | fun update(tvGroupModel: TVGroupModel) { 187 | this.tvGroupModel = tvGroupModel 188 | recyclerView.post { 189 | notifyDataSetChanged() 190 | } 191 | } 192 | 193 | companion object { 194 | private const val TAG = "CategoryAdapter" 195 | } 196 | } 197 | 198 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/InfoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.drawable.BitmapDrawable 7 | import android.os.Bundle 8 | import android.os.Handler 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import androidx.core.content.ContextCompat 13 | import androidx.core.view.marginBottom 14 | import androidx.core.view.marginStart 15 | import androidx.core.view.marginTop 16 | import androidx.fragment.app.Fragment 17 | import com.bumptech.glide.Glide 18 | import com.lizongying.mytv1.databinding.InfoBinding 19 | import com.lizongying.mytv1.models.TVModel 20 | 21 | 22 | class InfoFragment : Fragment() { 23 | private var _binding: InfoBinding? = null 24 | private val binding get() = _binding!! 25 | 26 | private val handler = Handler() 27 | private val delay: Long = 3000 28 | 29 | override fun onCreateView( 30 | inflater: LayoutInflater, container: ViewGroup?, 31 | savedInstanceState: Bundle? 32 | ): View { 33 | _binding = InfoBinding.inflate(inflater, container, false) 34 | 35 | val application = requireActivity().applicationContext as MyTVApplication 36 | 37 | binding.info.layoutParams.width = application.px2Px(binding.info.layoutParams.width) 38 | binding.info.layoutParams.height = application.px2Px(binding.info.layoutParams.height) 39 | 40 | val layoutParams = binding.info.layoutParams as ViewGroup.MarginLayoutParams 41 | layoutParams.bottomMargin = application.px2Px(binding.info.marginBottom) 42 | binding.info.layoutParams = layoutParams 43 | 44 | binding.logo.layoutParams.width = application.px2Px(binding.logo.layoutParams.width) 45 | var padding = application.px2Px(binding.logo.paddingTop) 46 | binding.logo.setPadding(padding, padding, padding, padding) 47 | binding.main.layoutParams.width = application.px2Px(binding.main.layoutParams.width) 48 | padding = application.px2Px(binding.main.paddingTop) 49 | binding.main.setPadding(padding, padding, padding, padding) 50 | 51 | val layoutParamsMain = binding.main.layoutParams as ViewGroup.MarginLayoutParams 52 | layoutParamsMain.marginStart = application.px2Px(binding.main.marginStart) 53 | binding.main.layoutParams = layoutParamsMain 54 | 55 | val layoutParamsDesc = binding.desc.layoutParams as ViewGroup.MarginLayoutParams 56 | layoutParamsDesc.topMargin = application.px2Px(binding.desc.marginTop) 57 | binding.desc.layoutParams = layoutParamsDesc 58 | 59 | binding.title.textSize = application.px2PxFont(binding.title.textSize) 60 | binding.desc.textSize = application.px2PxFont(binding.desc.textSize) 61 | 62 | binding.container.layoutParams.width = application.shouldWidthPx() 63 | binding.container.layoutParams.height = application.shouldHeightPx() 64 | 65 | _binding!!.root.visibility = View.GONE 66 | return binding.root 67 | } 68 | 69 | fun show(tvViewModel: TVModel) { 70 | binding.title.text = "${tvViewModel.tv.group} ${tvViewModel.tv.title}" 71 | 72 | when (tvViewModel.tv.title) { 73 | else -> { 74 | if (tvViewModel.tv.logo.isNullOrBlank()) { 75 | val width = Utils.dpToPx(100) 76 | val height = Utils.dpToPx(60) 77 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 78 | val canvas = Canvas(bitmap) 79 | 80 | val paint = Paint().apply { 81 | color = ContextCompat.getColor(requireContext(), R.color.blur) 82 | textSize = 100f 83 | textAlign = Paint.Align.CENTER 84 | } 85 | val text = "${tvViewModel.tv.id + 1}" 86 | val x = width / 2f 87 | val y = height / 2f - (paint.descent() + paint.ascent()) / 2 88 | canvas.drawText(text, x, y, paint) 89 | 90 | Glide.with(this) 91 | .load(BitmapDrawable(context?.resources, bitmap)) 92 | // .centerInside() 93 | .into(binding.logo) 94 | } else { 95 | Glide.with(this) 96 | .load(tvViewModel.tv.logo) 97 | // .centerInside() 98 | .into(binding.logo) 99 | } 100 | } 101 | } 102 | 103 | handler.removeCallbacks(removeRunnable) 104 | view?.visibility = View.VISIBLE 105 | handler.postDelayed(removeRunnable, delay) 106 | } 107 | 108 | override fun onResume() { 109 | super.onResume() 110 | handler.postDelayed(removeRunnable, delay) 111 | } 112 | 113 | override fun onPause() { 114 | super.onPause() 115 | handler.removeCallbacks(removeRunnable) 116 | } 117 | 118 | private val removeRunnable = Runnable { 119 | view?.visibility = View.GONE 120 | } 121 | 122 | override fun onDestroyView() { 123 | super.onDestroyView() 124 | _binding = null 125 | } 126 | 127 | companion object { 128 | private const val TAG = "InfoFragment" 129 | } 130 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/InitializerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.content.ContentProvider 4 | import android.content.ContentValues 5 | import android.net.Uri 6 | import com.lizongying.mytv1.models.TVList 7 | 8 | internal class InitializerProvider : ContentProvider() { 9 | 10 | // Happens before Application#onCreate.It's fine to init something here 11 | override fun onCreate(): Boolean { 12 | SP.init(context!!) 13 | TVList.init(context!!) 14 | return true 15 | } 16 | 17 | override fun query( 18 | uri: Uri, 19 | projection: Array?, 20 | selection: String?, 21 | selectionArgs: Array?, 22 | sortOrder: String?, 23 | ) = unsupported() 24 | 25 | override fun getType(uri: Uri) = unsupported() 26 | 27 | override fun insert(uri: Uri, values: ContentValues?) = unsupported() 28 | 29 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) = 30 | unsupported() 31 | 32 | override fun update( 33 | uri: Uri, 34 | values: ContentValues?, 35 | selection: String?, 36 | selectionArgs: Array?, 37 | ) = unsupported() 38 | 39 | private fun unsupported(errorMessage: String? = null): Nothing = 40 | throw UnsupportedOperationException(errorMessage) 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/ListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.graphics.Color 7 | import android.graphics.Paint 8 | import android.graphics.drawable.BitmapDrawable 9 | import android.util.Log 10 | import android.view.KeyEvent 11 | import android.view.LayoutInflater 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.view.ViewGroup.FOCUS_BEFORE_DESCENDANTS 15 | import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS 16 | import androidx.constraintlayout.widget.ConstraintSet 17 | import androidx.core.content.ContextCompat 18 | import androidx.core.view.marginStart 19 | import androidx.core.view.setPadding 20 | import androidx.recyclerview.widget.LinearLayoutManager 21 | import androidx.recyclerview.widget.RecyclerView 22 | import com.bumptech.glide.Glide 23 | import com.lizongying.mytv1.databinding.ListItemBinding 24 | import com.lizongying.mytv1.models.TVListModel 25 | import com.lizongying.mytv1.models.TVModel 26 | 27 | 28 | class ListAdapter( 29 | private val context: Context, 30 | private val recyclerView: RecyclerView, 31 | var tvListModel: TVListModel, 32 | ) : 33 | RecyclerView.Adapter() { 34 | private var listener: ItemListener? = null 35 | private var focused: View? = null 36 | private var defaultFocused = false 37 | private var defaultFocus: Int = -1 38 | 39 | var visiable = false 40 | 41 | val application = context.applicationContext as MyTVApplication 42 | 43 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 44 | val inflater = LayoutInflater.from(context) 45 | val binding = ListItemBinding.inflate(inflater, parent, false) 46 | 47 | binding.icon.layoutParams.width = application.px2Px(binding.icon.layoutParams.width) 48 | binding.icon.layoutParams.height = application.px2Px(binding.icon.layoutParams.height) 49 | binding.icon.setPadding(application.px2Px(binding.icon.paddingTop)) 50 | 51 | binding.title.layoutParams.width = application.px2Px(binding.title.layoutParams.width) 52 | binding.title.layoutParams.height = application.px2Px(binding.title.layoutParams.height) 53 | binding.title.textSize = application.px2PxFont(binding.title.textSize) 54 | 55 | binding.heart.layoutParams.width = application.px2Px(binding.heart.layoutParams.width) 56 | binding.heart.layoutParams.height = application.px2Px(binding.heart.layoutParams.height) 57 | binding.heart.setPadding(application.px2Px(binding.heart.paddingTop)) 58 | 59 | return ViewHolder(context, binding) 60 | } 61 | 62 | fun focusable(able: Boolean) { 63 | recyclerView.isFocusable = able 64 | recyclerView.isFocusableInTouchMode = able 65 | if (able) { 66 | recyclerView.descendantFocusability = FOCUS_BEFORE_DESCENDANTS 67 | } else { 68 | recyclerView.descendantFocusability = FOCUS_BLOCK_DESCENDANTS 69 | } 70 | } 71 | 72 | fun update(tvListModel: TVListModel) { 73 | this.tvListModel = tvListModel 74 | recyclerView.post { 75 | notifyDataSetChanged() 76 | } 77 | } 78 | 79 | fun clear() { 80 | focused?.clearFocus() 81 | recyclerView.invalidate() 82 | } 83 | 84 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 85 | val tvModel = tvListModel.getTVModel(position)!! 86 | val view = viewHolder.itemView 87 | 88 | view.isFocusable = true 89 | view.isFocusableInTouchMode = true 90 | // view.alpha = 0.8F 91 | 92 | viewHolder.like(tvModel.like.value as Boolean) 93 | 94 | viewHolder.binding.heart.setOnClickListener { 95 | tvModel.setLike(!(tvModel.like.value as Boolean)) 96 | viewHolder.like(tvModel.like.value as Boolean) 97 | } 98 | 99 | if (!defaultFocused && position == defaultFocus) { 100 | view.requestFocus() 101 | defaultFocused = true 102 | } 103 | 104 | val onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> 105 | listener?.onItemFocusChange(tvModel, hasFocus) 106 | 107 | if (hasFocus) { 108 | viewHolder.focus(true) 109 | focused = view 110 | if (visiable) { 111 | if (position != tvListModel.position.value) { 112 | tvListModel.setPosition(position) 113 | } 114 | } else { 115 | visiable = true 116 | } 117 | } else { 118 | viewHolder.focus(false) 119 | } 120 | } 121 | 122 | view.onFocusChangeListener = onFocusChangeListener 123 | 124 | view.setOnClickListener { _ -> 125 | listener?.onItemClicked(tvModel) 126 | } 127 | 128 | view.setOnKeyListener { _, keyCode, event: KeyEvent? -> 129 | if (event?.action == KeyEvent.ACTION_DOWN) { 130 | if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) { 131 | val p = getItemCount() - 1 132 | 133 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 134 | p, 135 | 0 136 | ) 137 | 138 | recyclerView.postDelayed({ 139 | val v = recyclerView.findViewHolderForAdapterPosition(p) 140 | v?.itemView?.isSelected = true 141 | v?.itemView?.requestFocus() 142 | }, 0) 143 | } 144 | 145 | if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && position == getItemCount() - 1) { 146 | val p = 0 147 | 148 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 149 | p, 150 | 0 151 | ) 152 | 153 | recyclerView.postDelayed({ 154 | val v = recyclerView.findViewHolderForAdapterPosition(p) 155 | v?.itemView?.isSelected = true 156 | v?.itemView?.requestFocus() 157 | }, 0) 158 | } 159 | 160 | if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 161 | tvModel.setLike(!(tvModel.like.value as Boolean)) 162 | viewHolder.like(tvModel.like.value as Boolean) 163 | } 164 | 165 | return@setOnKeyListener listener?.onKey(this, keyCode) ?: false 166 | } 167 | false 168 | } 169 | 170 | viewHolder.bindTitle(tvModel.tv.title) 171 | 172 | viewHolder.bindImage(tvModel.tv.logo, tvModel.tv.id) 173 | } 174 | 175 | override fun getItemCount() = tvListModel.size() 176 | 177 | class ViewHolder(private val context: Context, val binding: ListItemBinding) : 178 | RecyclerView.ViewHolder(binding.root), OnSharedPreferenceChangeListener { 179 | 180 | init { 181 | SP.setOnSharedPreferenceChangeListener(this) 182 | } 183 | 184 | fun bindTitle(text: String) { 185 | binding.title.text = text 186 | } 187 | 188 | fun bindImage(url: String?, id: Int) { 189 | if (url.isNullOrBlank()) { 190 | val width = Utils.dpToPx(40) 191 | val height = Utils.dpToPx(40) 192 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 193 | val canvas = Canvas(bitmap) 194 | 195 | val paint = Paint().apply { 196 | color = Color.WHITE 197 | textSize = 32f 198 | textAlign = Paint.Align.CENTER 199 | } 200 | val text = String.format("%3d", id + 1) 201 | val x = width / 2f 202 | val y = height / 2f - (paint.descent() + paint.ascent()) / 2 203 | canvas.drawText(text, x, y, paint) 204 | Glide.with(context) 205 | .load(BitmapDrawable(context.resources, bitmap)) 206 | .centerInside() 207 | .into(binding.icon) 208 | // binding.imageView.setImageDrawable(null) 209 | } else { 210 | Glide.with(context) 211 | .load(url) 212 | .centerInside() 213 | // .error(BitmapDrawable(context.resources, bitmap)) 214 | .into(binding.icon) 215 | } 216 | } 217 | 218 | fun focus(hasFocus: Boolean) { 219 | if (hasFocus) { 220 | binding.title.setTextColor(ContextCompat.getColor(context, R.color.white)) 221 | binding.root.setBackgroundResource(R.color.focus) 222 | } else { 223 | binding.title.setTextColor(ContextCompat.getColor(context, R.color.title_blur)) 224 | binding.root.setBackgroundResource(R.color.blur) 225 | } 226 | } 227 | 228 | fun like(liked: Boolean) { 229 | if (liked) { 230 | binding.heart.setImageDrawable( 231 | ContextCompat.getDrawable( 232 | context, 233 | R.drawable.baseline_favorite_24 234 | ) 235 | ) 236 | } else { 237 | binding.heart.setImageDrawable( 238 | ContextCompat.getDrawable( 239 | context, 240 | R.drawable.baseline_favorite_border_24 241 | ) 242 | ) 243 | } 244 | } 245 | 246 | override fun onSharedPreferenceChanged(key: String) { 247 | Log.i(TAG, "$key changed") 248 | when (key) { 249 | SP.KEY_EPG -> { 250 | if (SP.epg.isNullOrEmpty()) { 251 | val constraintSet = ConstraintSet() 252 | constraintSet.clone(binding.root) 253 | 254 | constraintSet.connect(binding.title.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) 255 | constraintSet.connect(binding.heart.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) 256 | 257 | constraintSet.applyTo(binding.root) 258 | } else { 259 | val constraintSet = ConstraintSet() 260 | constraintSet.clone(binding.root) 261 | 262 | constraintSet.clear(binding.title.id, ConstraintSet.BOTTOM) 263 | constraintSet.clear(binding.heart.id, ConstraintSet.BOTTOM) 264 | 265 | constraintSet.applyTo(binding.root) 266 | } 267 | } 268 | } 269 | } 270 | } 271 | 272 | fun toPosition(position: Int) { 273 | recyclerView.post { 274 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 275 | position, 276 | 0 277 | ) 278 | 279 | recyclerView.postDelayed({ 280 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) 281 | viewHolder?.itemView?.isSelected = true 282 | viewHolder?.itemView?.requestFocus() 283 | }, 0) 284 | } 285 | } 286 | 287 | interface ItemListener { 288 | fun onItemFocusChange(tvModel: TVModel, hasFocus: Boolean) 289 | fun onItemClicked(tvModel: TVModel) 290 | fun onKey(listAdapter: ListAdapter, keyCode: Int): Boolean 291 | } 292 | 293 | fun setItemListener(listener: ItemListener) { 294 | this.listener = listener 295 | } 296 | 297 | companion object { 298 | private const val TAG = "ListAdapter" 299 | } 300 | } 301 | 302 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/LoadingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.lizongying.mytv1.databinding.LoadingBinding 9 | 10 | class LoadingFragment : Fragment() { 11 | private var _binding: LoadingBinding? = null 12 | private val binding get() = _binding!! 13 | 14 | override fun onCreateView( 15 | inflater: LayoutInflater, container: ViewGroup?, 16 | savedInstanceState: Bundle? 17 | ): View { 18 | _binding = LoadingBinding.inflate(inflater, container, false) 19 | 20 | val application = requireActivity().applicationContext as MyTVApplication 21 | 22 | binding.bar.layoutParams.width = application.px2Px(binding.bar.layoutParams.width) 23 | binding.bar.layoutParams.height = application.px2Px(binding.bar.layoutParams.height) 24 | 25 | (activity as MainActivity).ready(TAG) 26 | return binding.root 27 | } 28 | 29 | override fun onDestroyView() { 30 | super.onDestroyView() 31 | _binding = null 32 | } 33 | 34 | companion object { 35 | private const val TAG = "LoadingFragment" 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/MenuFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.KeyEvent 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.View.GONE 9 | import android.view.View.VISIBLE 10 | import android.view.ViewGroup 11 | import android.widget.Toast 12 | import androidx.core.view.isVisible 13 | import androidx.fragment.app.Fragment 14 | import androidx.recyclerview.widget.LinearLayoutManager 15 | import com.lizongying.mytv1.databinding.MenuBinding 16 | import com.lizongying.mytv1.models.TVList 17 | import com.lizongying.mytv1.models.TVListModel 18 | import com.lizongying.mytv1.models.TVModel 19 | 20 | class MenuFragment : Fragment(), GroupAdapter.ItemListener, ListAdapter.ItemListener { 21 | private var _binding: MenuBinding? = null 22 | private val binding get() = _binding!! 23 | 24 | private lateinit var groupAdapter: GroupAdapter 25 | private lateinit var listAdapter: ListAdapter 26 | 27 | private var groupWidth = 0 28 | private var listWidth = 0 29 | 30 | override fun onActivityCreated(savedInstanceState: Bundle?) { 31 | Log.i(TAG, "onCreate") 32 | super.onActivityCreated(savedInstanceState) 33 | } 34 | 35 | override fun onCreateView( 36 | inflater: LayoutInflater, container: ViewGroup?, 37 | savedInstanceState: Bundle? 38 | ): View { 39 | val context = requireContext() 40 | val application = context.applicationContext as MyTVApplication 41 | _binding = MenuBinding.inflate(inflater, container, false) 42 | 43 | groupAdapter = GroupAdapter( 44 | context, 45 | binding.group, 46 | TVList.groupModel, 47 | ) 48 | binding.group.adapter = groupAdapter 49 | binding.group.layoutManager = 50 | LinearLayoutManager(context) 51 | groupWidth = application.px2Px(binding.group.layoutParams.width) 52 | binding.group.layoutParams.width = if (SP.compactMenu) { 53 | groupWidth * 4 / 5 54 | } else { 55 | groupWidth 56 | } 57 | groupAdapter.setItemListener(this) 58 | 59 | var tvListModel = TVList.groupModel.getTVListModel(TVList.groupModel.position.value!!) 60 | if (tvListModel == null) { 61 | TVList.groupModel.setPosition(0) 62 | } 63 | 64 | tvListModel = TVList.groupModel.getTVListModel(TVList.groupModel.position.value!!) 65 | 66 | listAdapter = ListAdapter( 67 | requireContext(), 68 | binding.list, 69 | tvListModel!!, 70 | ) 71 | binding.list.adapter = listAdapter 72 | binding.list.layoutManager = 73 | LinearLayoutManager(context) 74 | listWidth = application.px2Px(binding.list.layoutParams.width) 75 | binding.list.layoutParams.width = if (SP.compactMenu) { 76 | listWidth * 4 / 5 77 | } else { 78 | listWidth 79 | } 80 | listAdapter.focusable(false) 81 | listAdapter.setItemListener(this) 82 | 83 | binding.menu.setOnClickListener { 84 | hideSelf() 85 | } 86 | 87 | return binding.root 88 | } 89 | 90 | fun update() { 91 | groupAdapter.update(TVList.groupModel) 92 | 93 | var tvListModel = TVList.groupModel.getTVListModel(TVList.groupModel.position.value!!) 94 | if (tvListModel == null) { 95 | TVList.groupModel.setPosition(0) 96 | } 97 | tvListModel = TVList.groupModel.getTVListModel(TVList.groupModel.position.value!!) 98 | 99 | if (tvListModel != null) { 100 | (binding.list.adapter as ListAdapter).update(tvListModel) 101 | } 102 | } 103 | 104 | fun updateList(position: Int) { 105 | TVList.groupModel.setPosition(position) 106 | SP.positionGroup = position 107 | val tvListModel = TVList.groupModel.getTVListModel() 108 | Log.i(TAG, "updateList tvListModel $position ${tvListModel?.size()}") 109 | if (tvListModel != null) { 110 | (binding.list.adapter as ListAdapter).update(tvListModel) 111 | } 112 | } 113 | 114 | private fun hideSelf() { 115 | requireActivity().supportFragmentManager.beginTransaction() 116 | .hide(this) 117 | .commit() 118 | } 119 | 120 | override fun onItemFocusChange(tvListModel: TVListModel, hasFocus: Boolean) { 121 | if (hasFocus) { 122 | (binding.list.adapter as ListAdapter).update(tvListModel) 123 | (activity as MainActivity).menuActive() 124 | } 125 | } 126 | 127 | override fun onItemClicked(position: Int) { 128 | } 129 | 130 | override fun onItemFocusChange(tvModel: TVModel, hasFocus: Boolean) { 131 | if (hasFocus) { 132 | (activity as MainActivity).menuActive() 133 | } 134 | } 135 | 136 | override fun onItemClicked(tvModel: TVModel) { 137 | TVList.setPosition(tvModel.tv.id) 138 | (activity as MainActivity).hideMenuFragment() 139 | } 140 | 141 | override fun onKey(keyCode: Int): Boolean { 142 | when (keyCode) { 143 | KeyEvent.KEYCODE_DPAD_RIGHT -> { 144 | if (listAdapter.itemCount == 0) { 145 | Toast.makeText(context, "暂无频道", Toast.LENGTH_LONG).show() 146 | return true 147 | } 148 | binding.group.visibility = GONE 149 | groupAdapter.focusable(false) 150 | listAdapter.focusable(true) 151 | listAdapter.toPosition(TVList.getTVModel()!!.listIndex) 152 | 153 | 154 | if (TVList.getTVModel()!!.groupIndex == TVList.groupModel.position.value!!) { 155 | Log.i( 156 | TAG, 157 | "list on show toPosition ${TVList.getTVModel()!!.tv.title} ${TVList.getTVModel()!!.listIndex}/${listAdapter.tvListModel.size()}" 158 | ) 159 | listAdapter.toPosition(TVList.getTVModel()!!.listIndex) 160 | } else { 161 | listAdapter.toPosition(0) 162 | } 163 | return true 164 | } 165 | 166 | KeyEvent.KEYCODE_DPAD_LEFT -> { 167 | // (activity as MainActivity).hideMenuFragment() 168 | return true 169 | } 170 | } 171 | return false 172 | } 173 | 174 | override fun onKey(listAdapter: ListAdapter, keyCode: Int): Boolean { 175 | when (keyCode) { 176 | KeyEvent.KEYCODE_DPAD_LEFT -> { 177 | binding.group.visibility = VISIBLE 178 | groupAdapter.focusable(true) 179 | listAdapter.focusable(false) 180 | listAdapter.clear() 181 | Log.i(TAG, "group toPosition on left") 182 | groupAdapter.toPosition(TVList.groupModel.position.value!!) 183 | return true 184 | } 185 | // KeyEvent.KEYCODE_DPAD_RIGHT -> { 186 | // binding.group.visibility = VISIBLE 187 | // groupAdapter.focusable(true) 188 | // listAdapter.focusable(false) 189 | // listAdapter.clear() 190 | // Log.i(TAG, "group toPosition on left") 191 | // groupAdapter.toPosition(TVList.groupModel.position.value!!) 192 | // return true 193 | // } 194 | } 195 | return false 196 | } 197 | 198 | override fun onHiddenChanged(hidden: Boolean) { 199 | super.onHiddenChanged(hidden) 200 | if (!hidden) { 201 | if (binding.list.isVisible) { 202 | // if (binding.group.isVisible) { 203 | // groupAdapter.focusable(true) 204 | // listAdapter.focusable(false) 205 | // } else { 206 | // groupAdapter.focusable(false) 207 | // listAdapter.focusable(true) 208 | // } 209 | 210 | val groupIndex = TVList.getTVModel()!!.groupIndex 211 | Log.i( 212 | TAG, 213 | "groupIndex $groupIndex ${TVList.groupModel.position.value!!}" 214 | ) 215 | 216 | if (groupIndex == TVList.groupModel.position.value!!) { 217 | if (listAdapter.tvListModel.getIndex() != TVList.getTVModel()!!.groupIndex) { 218 | updateList(groupIndex) 219 | } 220 | 221 | Log.i( 222 | TAG, 223 | "list on show toPosition ${TVList.getTVModel()!!.tv.title} ${TVList.getTVModel()!!.listIndex}/${listAdapter.tvListModel.size()}" 224 | ) 225 | listAdapter.toPosition(TVList.getTVModel()!!.listIndex) 226 | } else { 227 | listAdapter.toPosition(0) 228 | } 229 | } 230 | if (binding.group.isVisible) { 231 | // groupAdapter.focusable(true) 232 | // listAdapter.focusable(false) 233 | Log.i( 234 | TAG, 235 | "group on show toPosition ${TVList.groupModel.position.value!!}/${TVList.groupModel.size()}" 236 | ) 237 | groupAdapter.toPosition(TVList.groupModel.position.value!!) 238 | } 239 | (activity as MainActivity).menuActive() 240 | } else { 241 | view?.post { 242 | groupAdapter.visiable = false 243 | listAdapter.visiable = false 244 | } 245 | } 246 | } 247 | 248 | fun updateSize() { 249 | view?.post { 250 | binding.group.layoutParams.width = if (SP.compactMenu) { 251 | groupWidth * 4 / 5 252 | } else { 253 | groupWidth 254 | } 255 | 256 | binding.list.layoutParams.width = if (SP.compactMenu) { 257 | listWidth * 4 / 5 258 | } else { 259 | listWidth 260 | } 261 | } 262 | } 263 | 264 | override fun onResume() { 265 | super.onResume() 266 | // groupAdapter.toPosition(TVList.groupModel.position.value!!) 267 | } 268 | 269 | override fun onDestroyView() { 270 | super.onDestroyView() 271 | _binding = null 272 | } 273 | 274 | companion object { 275 | private const val TAG = "MenuFragment" 276 | } 277 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/ModalFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.os.Bundle 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.view.WindowManager 10 | import androidx.fragment.app.DialogFragment 11 | import com.bumptech.glide.Glide 12 | import com.lizongying.mytv1.Utils.getDateTimestamp 13 | import com.lizongying.mytv1.databinding.ModalBinding 14 | 15 | 16 | class ModalFragment : DialogFragment() { 17 | 18 | private var _binding: ModalBinding? = null 19 | private val binding get() = _binding!! 20 | 21 | private val handler = Handler(Looper.myLooper()!!) 22 | private val delayHideAppreciateModal = 10000L 23 | 24 | override fun onStart() { 25 | super.onStart() 26 | dialog?.window?.apply { 27 | addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 28 | decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 29 | } 30 | } 31 | 32 | override fun onCreateView( 33 | inflater: LayoutInflater, 34 | container: ViewGroup?, 35 | savedInstanceState: Bundle? 36 | ): View { 37 | _binding = ModalBinding.inflate(inflater, container, false) 38 | return binding.root 39 | } 40 | 41 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 42 | super.onViewCreated(view, savedInstanceState) 43 | 44 | val url = arguments?.getString(KEY_URL) 45 | if (!url.isNullOrEmpty()) { 46 | val size = Utils.dpToPx(200) 47 | val u = "$url?${getDateTimestamp().toString().reversed()}" 48 | val img = QrCodeUtil().createQRCodeBitmap(u, size, size) 49 | 50 | Glide.with(requireContext()) 51 | .load(img) 52 | .into(binding.modalImage) 53 | binding.modalText.text = u.removePrefix("http://") 54 | binding.modalText.visibility = View.VISIBLE 55 | // if (!isTV()) { 56 | // binding.modal.setOnClickListener { 57 | // try { 58 | // val mainActivity = (activity as MainActivity) 59 | // mainActivity.showWebViewPopup(u) 60 | // handler.postDelayed(hideAppreciateModal, 0) 61 | // } catch (e: Exception) { 62 | // Log.e(TAG, "onViewCreated", e) 63 | // } 64 | // } 65 | // } 66 | } else { 67 | Glide.with(requireContext()) 68 | .load(arguments?.getInt(KEY_DRAWABLE_ID)) 69 | .into(binding.modalImage) 70 | binding.modalText.visibility = View.GONE 71 | } 72 | 73 | handler.postDelayed(hideAppreciateModal, delayHideAppreciateModal) 74 | } 75 | 76 | private val hideAppreciateModal = Runnable { 77 | if (!this.isHidden) { 78 | this.dismiss() 79 | } 80 | } 81 | 82 | override fun onDestroyView() { 83 | super.onDestroyView() 84 | _binding = null 85 | handler.removeCallbacksAndMessages(null) 86 | } 87 | 88 | companion object { 89 | const val KEY_DRAWABLE_ID = "drawable_id" 90 | const val KEY_URL = "url" 91 | const val TAG = "ModalFragment" 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/MyTVApplication.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import android.os.Handler 7 | import android.os.Looper 8 | import android.util.DisplayMetrics 9 | import android.view.WindowManager 10 | import android.widget.Toast 11 | 12 | class MyTVApplication : Application() { 13 | 14 | companion object { 15 | private const val TAG = "MyTVApplication" 16 | private lateinit var instance: MyTVApplication 17 | 18 | fun getInstance(): MyTVApplication { 19 | return instance 20 | } 21 | } 22 | 23 | private lateinit var displayMetrics: DisplayMetrics 24 | private lateinit var realDisplayMetrics: DisplayMetrics 25 | 26 | private var width = 0 27 | private var height = 0 28 | private var shouldWidth = 0 29 | private var shouldHeight = 0 30 | private var ratio = 1.0 31 | private var density = 2.0f 32 | private var scale = 1.0f 33 | 34 | override fun onCreate() { 35 | super.onCreate() 36 | instance = this 37 | 38 | displayMetrics = DisplayMetrics() 39 | realDisplayMetrics = DisplayMetrics() 40 | val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager 41 | windowManager.defaultDisplay.getMetrics(displayMetrics) 42 | windowManager.defaultDisplay.getRealMetrics(realDisplayMetrics) 43 | 44 | if (realDisplayMetrics.heightPixels > realDisplayMetrics.widthPixels) { 45 | width = realDisplayMetrics.heightPixels 46 | height = realDisplayMetrics.widthPixels 47 | } else { 48 | width = realDisplayMetrics.widthPixels 49 | height = realDisplayMetrics.heightPixels 50 | } 51 | 52 | density = Resources.getSystem().displayMetrics.density 53 | scale = displayMetrics.scaledDensity 54 | 55 | if ((width.toDouble() / height) < (16.0 / 9.0)) { 56 | ratio = width * 2 / 1920.0 / density 57 | shouldWidth = width 58 | shouldHeight = (width * 9.0 / 16.0).toInt() 59 | } else { 60 | ratio = height * 2 / 1080.0 / density 61 | shouldHeight = height 62 | shouldWidth = (height * 16.0 / 9.0).toInt() 63 | } 64 | } 65 | 66 | fun getDisplayMetrics(): DisplayMetrics { 67 | return displayMetrics 68 | } 69 | 70 | fun toast(message: CharSequence = "", duration: Int = Toast.LENGTH_SHORT) { 71 | Handler(Looper.getMainLooper()).post { 72 | Toast.makeText(applicationContext, message, duration).show() 73 | } 74 | } 75 | 76 | fun shouldWidthPx(): Int { 77 | return shouldWidth 78 | } 79 | 80 | fun shouldHeightPx(): Int { 81 | return shouldHeight 82 | } 83 | 84 | fun dp2Px(dp: Int): Int { 85 | return (dp * ratio * density + 0.5f).toInt() 86 | } 87 | 88 | fun px2Px(px: Int): Int { 89 | return (px * ratio + 0.5f).toInt() 90 | } 91 | 92 | fun px2PxFont(px: Float): Float { 93 | return (px * ratio / scale).toFloat() 94 | } 95 | 96 | fun sp2Px(sp: Float): Float { 97 | return (sp * ratio * scale).toFloat() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/OnSharedPreferenceChangeListener.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | 4 | interface OnSharedPreferenceChangeListener { 5 | fun onSharedPreferenceChanged(key: String) 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/PortUtil.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import java.net.Inet4Address 4 | import java.net.NetworkInterface 5 | 6 | object PortUtil { 7 | 8 | fun lan(): String? { 9 | val networkInterfaces = NetworkInterface.getNetworkInterfaces() 10 | while (networkInterfaces.hasMoreElements()) { 11 | val inetAddresses = networkInterfaces.nextElement().inetAddresses 12 | while (inetAddresses.hasMoreElements()) { 13 | val inetAddress = inetAddresses.nextElement() 14 | if (inetAddress is Inet4Address) { 15 | if (inetAddress.hostAddress == "127.0.0.1") { 16 | continue 17 | } 18 | return inetAddress.hostAddress 19 | } 20 | } 21 | } 22 | return null 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/QrCodeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Color 5 | import androidx.annotation.ColorInt 6 | import com.google.zxing.BarcodeFormat 7 | import com.google.zxing.EncodeHintType 8 | import com.google.zxing.WriterException 9 | import com.google.zxing.qrcode.QRCodeWriter 10 | import java.util.Hashtable 11 | 12 | class QrCodeUtil { 13 | 14 | fun createQRCodeBitmap( 15 | content: String, 16 | width: Int, 17 | height: Int, 18 | characterSet: String = "UTF-8", 19 | errorCorrection: String = "L", 20 | margin: String = "1", 21 | @ColorInt colorBlack: Int = Color.BLACK, 22 | @ColorInt colorWhite: Int = Color.WHITE, 23 | ): Bitmap? { 24 | if (width < 0 || height < 0) { 25 | return null 26 | } 27 | try { 28 | val hints: Hashtable = Hashtable() 29 | if (characterSet.isNotEmpty()) { 30 | hints[EncodeHintType.CHARACTER_SET] = characterSet 31 | } 32 | if (errorCorrection.isNotEmpty()) { 33 | hints[EncodeHintType.ERROR_CORRECTION] = errorCorrection 34 | } 35 | if (margin.isNotEmpty()) { 36 | hints[EncodeHintType.MARGIN] = margin 37 | } 38 | val bitMatrix = 39 | QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints) 40 | 41 | val pixels = IntArray(width * height) 42 | for (y in 0 until height) { 43 | for (x in 0 until width) { 44 | if (bitMatrix[x, y]) { 45 | pixels[y * width + x] = colorBlack 46 | } else { 47 | pixels[y * width + x] = colorWhite 48 | } 49 | } 50 | } 51 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 52 | bitmap.setPixels(pixels, 0, width, 0, 0, width, height) 53 | return bitmap 54 | } catch (e: WriterException) { 55 | e.printStackTrace() 56 | } 57 | return null 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/SP.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | 7 | object SP { 8 | // If Change channel with up and down in reversed order or not 9 | private const val KEY_CHANNEL_REVERSAL = "channel_reversal" 10 | 11 | // If use channel num to select channel or not 12 | private const val KEY_CHANNEL_NUM = "channel_num" 13 | 14 | private const val KEY_TIME = "time" 15 | 16 | // If start app on device boot or not 17 | private const val KEY_BOOT_STARTUP = "boot_startup" 18 | 19 | // Position in list of the selected channel item 20 | private const val KEY_POSITION = "position" 21 | 22 | private const val KEY_POSITION_GROUP = "position_group" 23 | 24 | private const val KEY_POSITION_SUB = "position_sub" 25 | 26 | private const val KEY_REPEAT_INFO = "repeat_info" 27 | 28 | private const val KEY_CONFIG_URL = "config_url" 29 | 30 | private const val KEY_CONFIG_AUTO_LOAD = "config_auto_load" 31 | 32 | private const val KEY_CHANNEL = "channel" 33 | 34 | private const val KEY_LIKE = "like" 35 | 36 | private const val KEY_DISPLAY_SECONDS = "display_seconds" 37 | 38 | private const val KEY_COMPACT_MENU = "compact_menu" 39 | 40 | private const val KEY_STYLE = "style" 41 | 42 | const val KEY_EPG = "epg" 43 | 44 | const val DEFAULT_CHANNEL_REVERSAL = false 45 | const val DEFAULT_CHANNEL_NUM = false 46 | const val DEFAULT_TIME = true 47 | const val DEFAULT_BOOT_STARTUP = false 48 | const val DEFAULT_CONFIG_URL = "" 49 | const val DEFAULT_DISPLAY_SECONDS = true 50 | const val DEFAULT_COMPACT_MENU = true 51 | // transparent 52 | const val DEFAULT_STYLE = "" 53 | 54 | private lateinit var sp: SharedPreferences 55 | 56 | private var listener: OnSharedPreferenceChangeListener? = null 57 | 58 | /** 59 | * The method must be invoked as early as possible(At least before using the keys) 60 | */ 61 | fun init(context: Context) { 62 | sp = context.getSharedPreferences( 63 | context.resources.getString(R.string.app_name), 64 | Context.MODE_PRIVATE 65 | ) 66 | } 67 | 68 | fun setOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { 69 | this.listener = listener 70 | } 71 | 72 | var channelReversal: Boolean 73 | get() = sp.getBoolean(KEY_CHANNEL_REVERSAL, DEFAULT_CHANNEL_REVERSAL) 74 | set(value) = sp.edit().putBoolean(KEY_CHANNEL_REVERSAL, value).apply() 75 | 76 | var channelNum: Boolean 77 | get() = sp.getBoolean(KEY_CHANNEL_NUM, DEFAULT_CHANNEL_NUM) 78 | set(value) = sp.edit().putBoolean(KEY_CHANNEL_NUM, value).apply() 79 | 80 | var time: Boolean 81 | get() = sp.getBoolean(KEY_TIME, DEFAULT_TIME) 82 | set(value) = sp.edit().putBoolean(KEY_TIME, value).apply() 83 | 84 | var bootStartup: Boolean 85 | get() = sp.getBoolean(KEY_BOOT_STARTUP, DEFAULT_BOOT_STARTUP) 86 | set(value) = sp.edit().putBoolean(KEY_BOOT_STARTUP, value).apply() 87 | 88 | var position: Int 89 | get() = sp.getInt(KEY_POSITION, 0) 90 | set(value) = sp.edit().putInt(KEY_POSITION, value).apply() 91 | 92 | var positionGroup: Int 93 | get() = sp.getInt(KEY_POSITION_GROUP, 0) 94 | set(value) = sp.edit().putInt(KEY_POSITION_GROUP, value).apply() 95 | 96 | var positionSub: Int 97 | get() = sp.getInt(KEY_POSITION_SUB, 0) 98 | set(value) = sp.edit().putInt(KEY_POSITION_SUB, value).apply() 99 | 100 | var repeatInfo: Boolean 101 | get() = sp.getBoolean(KEY_REPEAT_INFO, true) 102 | set(value) = sp.edit().putBoolean(KEY_REPEAT_INFO, value).apply() 103 | 104 | var configUrl: String? 105 | get() = sp.getString(KEY_CONFIG_URL, DEFAULT_CONFIG_URL) 106 | set(value) = sp.edit().putString(KEY_CONFIG_URL, value).apply() 107 | 108 | var configAutoLoad: Boolean 109 | get() = sp.getBoolean(KEY_CONFIG_AUTO_LOAD, false) 110 | set(value) = sp.edit().putBoolean(KEY_CONFIG_AUTO_LOAD, value).apply() 111 | 112 | var channel: Int 113 | get() = sp.getInt(KEY_CHANNEL, 0) 114 | set(value) = sp.edit().putInt(KEY_CHANNEL, value).apply() 115 | 116 | fun getLike(id: Int): Boolean { 117 | val stringSet = sp.getStringSet(KEY_LIKE, emptySet()) 118 | return stringSet?.contains(id.toString()) ?: false 119 | } 120 | 121 | fun setLike(id: Int, liked: Boolean) { 122 | val stringSet = sp.getStringSet(KEY_LIKE, emptySet())?.toMutableSet() ?: mutableSetOf() 123 | if (liked) { 124 | stringSet.add(id.toString()) 125 | } else { 126 | stringSet.remove(id.toString()) 127 | } 128 | 129 | sp.edit().putStringSet(KEY_LIKE, stringSet).apply() 130 | } 131 | 132 | fun deleteLike() { 133 | sp.edit().remove(KEY_LIKE).apply() 134 | } 135 | 136 | var epg: String? 137 | get() = sp.getString(KEY_EPG, "") 138 | set(value) { 139 | if (value != this.epg) { 140 | sp.edit().putString(KEY_EPG, value).apply() 141 | listener?.onSharedPreferenceChanged(KEY_EPG) 142 | } 143 | } 144 | 145 | var displaySeconds: Boolean 146 | get() = sp.getBoolean(KEY_DISPLAY_SECONDS, DEFAULT_DISPLAY_SECONDS) 147 | set(value) = sp.edit().putBoolean(KEY_DISPLAY_SECONDS, value).apply() 148 | 149 | var compactMenu: Boolean 150 | get() = sp.getBoolean(KEY_COMPACT_MENU, DEFAULT_COMPACT_MENU) 151 | set(value) = sp.edit().putBoolean(KEY_COMPACT_MENU, value).apply() 152 | 153 | var style: String? 154 | get() = sp.getString(KEY_STYLE, DEFAULT_STYLE) 155 | set(value) = sp.edit().putString(KEY_STYLE, value).apply() 156 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/SimpleServer.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.os.Handler 7 | import android.os.Looper 8 | import android.util.Log 9 | import com.lizongying.mytv1.data.Global.gson 10 | import com.lizongying.mytv1.data.ReqSettings 11 | import com.lizongying.mytv1.data.RespSettings 12 | import com.lizongying.mytv1.models.TVList 13 | import com.lizongying.mytv1.models.TVList.CACHE_FILE_NAME 14 | import com.lizongying.mytv1.models.TVList.DEFAULT_CHANNELS_FILE 15 | import fi.iki.elonen.NanoHTTPD 16 | import java.io.File 17 | import java.io.IOException 18 | import java.nio.charset.StandardCharsets 19 | 20 | 21 | class SimpleServer(private val context: Context) : NanoHTTPD(PORT) { 22 | private val handler = Handler(Looper.getMainLooper()) 23 | 24 | init { 25 | try { 26 | start() 27 | } catch (e: IOException) { 28 | Log.e(TAG, "init", e) 29 | } 30 | } 31 | 32 | override fun serve(session: IHTTPSession): Response { 33 | return when (session.uri) { 34 | "/api/settings" -> handleSettings() 35 | "/api/default-channel" -> handleDefaultChannel(session) 36 | "/api/import-text" -> handleImportText(session) 37 | "/api/import-uri" -> handleImportUri(session) 38 | "/gua64min.js" -> handleStaticJs(session) 39 | else -> handleStaticContent(session) 40 | } 41 | } 42 | 43 | private fun handleSettings(): Response { 44 | val response: String 45 | try { 46 | val file = File(context.filesDir, CACHE_FILE_NAME) 47 | var str = if (file.exists()) { 48 | file.readText() 49 | } else { 50 | "" 51 | } 52 | if (str.isEmpty()) { 53 | str = context.resources.openRawResource(DEFAULT_CHANNELS_FILE).bufferedReader() 54 | .use { it.readText() } 55 | } 56 | 57 | val respSettings = RespSettings( 58 | channelUri = SP.configUrl ?: "", 59 | channelText = str, 60 | channelDefault = SP.channel, 61 | ) 62 | response = gson.toJson(respSettings) ?: "" 63 | } catch (e: Exception) { 64 | Log.e(TAG, "handleSettings", e) 65 | return newFixedLengthResponse( 66 | Response.Status.INTERNAL_ERROR, 67 | MIME_PLAINTEXT, 68 | e.message 69 | ) 70 | } 71 | return newFixedLengthResponse(Response.Status.OK, "application/json", response) 72 | } 73 | 74 | private fun handleDefaultChannel(session: IHTTPSession): Response { 75 | val response = "" 76 | try { 77 | readBody(session)?.let { 78 | handler.post { 79 | val req = gson.fromJson(it, ReqSettings::class.java) 80 | if (req.channel != null && req.channel > -1) { 81 | SP.channel = req.channel 82 | } else { 83 | } 84 | } 85 | } 86 | } catch (e: Exception) { 87 | Log.e(TAG, "handleDefaultChannel", e) 88 | return newFixedLengthResponse( 89 | Response.Status.INTERNAL_ERROR, 90 | MIME_PLAINTEXT, 91 | e.message 92 | ) 93 | } 94 | return newFixedLengthResponse(Response.Status.OK, "text/plain", response) 95 | } 96 | 97 | private fun handleImportText(session: IHTTPSession): Response { 98 | val response = "" 99 | try { 100 | readBody(session)?.let { 101 | handler.post { 102 | if (TVList.str2List(it)) { 103 | File(context.filesDir, TVList.CACHE_FILE_NAME).writeText(it) 104 | "频道导入成功".showToast() 105 | } else { 106 | "频道导入错误".showToast() 107 | } 108 | } 109 | } 110 | } catch (e: Exception) { 111 | Log.e(TAG, "handleImportText", e) 112 | return newFixedLengthResponse( 113 | Response.Status.INTERNAL_ERROR, 114 | MIME_PLAINTEXT, 115 | e.message 116 | ) 117 | } 118 | return newFixedLengthResponse(Response.Status.OK, "text/plain", response) 119 | } 120 | 121 | private fun handleImportUri(session: IHTTPSession): Response { 122 | val response = "" 123 | try { 124 | readBody(session)?.let { 125 | val req = gson.fromJson(it, ReqSettings::class.java) 126 | val uri = Uri.parse(req.uri) 127 | Log.i(TAG, "uri $uri") 128 | handler.post { 129 | TVList.parseUri(uri) 130 | } 131 | } 132 | } catch (e: Exception) { 133 | Log.e(TAG, "handleImportUri", e) 134 | return newFixedLengthResponse( 135 | Response.Status.INTERNAL_ERROR, 136 | MIME_PLAINTEXT, 137 | e.message 138 | ) 139 | } 140 | return newFixedLengthResponse(Response.Status.OK, "text/plain", response) 141 | } 142 | 143 | private fun readBody(session: IHTTPSession): String? { 144 | val map = HashMap() 145 | session.parseBody(map) 146 | return map["postData"] 147 | } 148 | 149 | private fun handleStaticJs(session: IHTTPSession): Response { 150 | val html = loadHtmlFromResource(R.raw.gua64min) 151 | return newFixedLengthResponse(Response.Status.OK, "text/javascript", html) 152 | } 153 | 154 | private fun handleStaticContent(session: IHTTPSession): Response { 155 | val html = loadHtmlFromResource(R.raw.index) 156 | return newFixedLengthResponse(Response.Status.OK, "text/html", html) 157 | } 158 | 159 | private fun loadHtmlFromResource(resourceId: Int): String { 160 | val inputStream = context.resources.openRawResource(resourceId) 161 | return inputStream.bufferedReader(StandardCharsets.UTF_8).use { it.readText() } 162 | } 163 | 164 | companion object { 165 | const val TAG = "SimpleServer" 166 | const val PORT = 34568 167 | } 168 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/TimeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.core.view.marginEnd 9 | import androidx.core.view.marginTop 10 | import androidx.fragment.app.Fragment 11 | import androidx.lifecycle.lifecycleScope 12 | import com.lizongying.mytv1.databinding.TimeBinding 13 | import com.lizongying.mytv1.models.TVList 14 | import kotlinx.coroutines.Job 15 | import kotlinx.coroutines.delay 16 | import kotlinx.coroutines.isActive 17 | import kotlinx.coroutines.launch 18 | 19 | class TimeFragment : Fragment() { 20 | private var _binding: TimeBinding? = null 21 | private val binding get() = _binding!! 22 | 23 | private val delay: Long = 1000 24 | 25 | private var job: Job? = null 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, container: ViewGroup?, 29 | savedInstanceState: Bundle? 30 | ): View { 31 | _binding = TimeBinding.inflate(inflater, container, false) 32 | 33 | val application = requireActivity().applicationContext as MyTVApplication 34 | 35 | binding.time.layoutParams.width = application.px2Px(binding.time.layoutParams.width) 36 | binding.time.layoutParams.height = application.px2Px(binding.time.layoutParams.height) 37 | 38 | val layoutParams = binding.time.layoutParams as ViewGroup.MarginLayoutParams 39 | layoutParams.topMargin = application.px2Px(binding.time.marginTop) 40 | layoutParams.marginEnd = application.px2Px(binding.time.marginEnd) 41 | binding.time.layoutParams = layoutParams 42 | 43 | binding.content.textSize = application.px2PxFont(binding.content.textSize) 44 | binding.channel.textSize = application.px2PxFont(binding.channel.textSize) 45 | 46 | binding.main.layoutParams.width = application.shouldWidthPx() 47 | binding.main.layoutParams.height = application.shouldHeightPx() 48 | 49 | return binding.root 50 | } 51 | 52 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 53 | super.onViewCreated(view, savedInstanceState) 54 | 55 | job = viewLifecycleOwner.lifecycleScope.launch { 56 | while (isActive) { 57 | binding.content.text = TVList.getTime() 58 | delay(delay) 59 | } 60 | } 61 | } 62 | 63 | override fun onHiddenChanged(hidden: Boolean) { 64 | super.onHiddenChanged(hidden) 65 | if (!hidden) { 66 | if (_binding == null) { 67 | Log.w(TAG, "_binding is null") 68 | return 69 | } 70 | 71 | job = viewLifecycleOwner.lifecycleScope.launch { 72 | while (isActive) { 73 | binding.content.text = TVList.getTime() 74 | delay(delay) 75 | } 76 | } 77 | } else { 78 | job?.cancel() 79 | job = null 80 | } 81 | } 82 | 83 | override fun onDestroyView() { 84 | super.onDestroyView() 85 | _binding = null 86 | job?.cancel() 87 | job = null 88 | } 89 | 90 | companion object { 91 | private const val TAG = "TimeFragment" 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/UpdateManager.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.app.DownloadManager 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.os.Environment 11 | import android.util.Log 12 | import androidx.core.app.NotificationCompat 13 | import androidx.core.content.FileProvider 14 | import androidx.fragment.app.FragmentActivity 15 | import com.lizongying.mytv1.data.Global.gson 16 | import com.lizongying.mytv1.data.ReleaseResponse 17 | import com.lizongying.mytv1.requests.HttpClient 18 | import com.lizongying.mytv1.requests.HttpClient.HOST 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.launch 22 | import kotlinx.coroutines.withContext 23 | import java.io.File 24 | 25 | 26 | class UpdateManager( 27 | private var context: Context, 28 | private var versionCode: Long 29 | ) : 30 | ConfirmationFragment.ConfirmationListener { 31 | 32 | private lateinit var notificationManager: NotificationManager 33 | private lateinit var notificationBuilder: NotificationCompat.Builder 34 | 35 | private var release: ReleaseResponse? = null 36 | 37 | private suspend fun getRelease(): ReleaseResponse? { 38 | return withContext(Dispatchers.IO) { 39 | try { 40 | val request = okhttp3.Request.Builder() 41 | .url(HOST + "main/version.json") 42 | .build() 43 | 44 | HttpClient.okHttpClient.newCall(request).execute().use { response -> 45 | if (!response.isSuccessful) return@withContext null 46 | 47 | response.body?.let { 48 | return@withContext gson.fromJson(it.string(), ReleaseResponse::class.java) 49 | } 50 | null 51 | } 52 | } catch (e: Exception) { 53 | Log.e(TAG, "getRelease", e) 54 | null 55 | } 56 | } 57 | } 58 | 59 | fun checkAndUpdate() { 60 | Log.i(TAG, "checkAndUpdate") 61 | CoroutineScope(Dispatchers.Main).launch { 62 | var text = "版本获取失败" 63 | var update = false 64 | try { 65 | release = getRelease() 66 | 67 | Log.i(TAG, "versionCode $versionCode ${release?.version_code}") 68 | if (release?.version_code != null) { 69 | if (release?.version_code!! >= versionCode) { 70 | text = "最新版本:${release?.version_name}" 71 | update = true 72 | } else { 73 | text = "已是最新版本,不需要更新" 74 | } 75 | } 76 | } catch (e: Exception) { 77 | Log.e(TAG, "Error occurred: ${e.message}", e) 78 | } 79 | updateUI(text, update) 80 | } 81 | } 82 | 83 | private fun updateUI(text: String, update: Boolean) { 84 | val dialog = ConfirmationFragment(this@UpdateManager, text, update) 85 | dialog.show((context as FragmentActivity).supportFragmentManager, TAG) 86 | } 87 | 88 | private fun startDownload(context: Context, release: ReleaseResponse) { 89 | val versionName = release.version_name 90 | val apkName = "my-tv-1" 91 | val apkFileName = apkName + "_${release.version_name}.apk" 92 | val downloadUrl = 93 | "${HttpClient.DOWNLOAD_HOST}${release.version_name}/$apkFileName" 94 | Log.i(TAG, "downloadUrl: $downloadUrl") 95 | 96 | CoroutineScope(Dispatchers.Main).launch { 97 | try { 98 | withContext(Dispatchers.IO) { 99 | downloadAndInstall(context, downloadUrl, apkFileName, versionName!!) 100 | } 101 | showNotification(context) 102 | } catch (e: Exception) { 103 | Log.i(TAG, "downloadAndInstallApk", e) 104 | } 105 | } 106 | } 107 | 108 | private fun showNotification(context: Context) { 109 | notificationManager = 110 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 111 | notificationBuilder = NotificationCompat.Builder(context, "download_channel").apply { 112 | setSmallIcon(android.R.drawable.stat_sys_download) 113 | setContentTitle("Downloading Update") 114 | setContentText("Download in progress") 115 | priority = NotificationCompat.PRIORITY_LOW 116 | setOngoing(true) 117 | setProgress(100, 0, false) 118 | } 119 | 120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 121 | val channel = NotificationChannel( 122 | "download_channel", 123 | "Download Updates", 124 | NotificationManager.IMPORTANCE_LOW 125 | ) 126 | notificationManager.createNotificationChannel(channel) 127 | } 128 | 129 | notificationManager.notify(1, notificationBuilder.build()) 130 | } 131 | 132 | private fun downloadAndInstall( 133 | context: Context, 134 | downloadUrl: String, 135 | apkFileName: String, 136 | versionName: String 137 | ) { 138 | context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.mkdirs() 139 | Log.i(TAG, "save dir ${Environment.DIRECTORY_DOWNLOADS}") 140 | val request = DownloadManager.Request(Uri.parse(downloadUrl)).apply { 141 | setTitle("${context.resources.getString(R.string.app_name)} $versionName Downloading") 142 | setDescription("Downloading the latest version of the app") 143 | setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, apkFileName) 144 | 145 | setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) 146 | // setAllowedOverRoaming(false) 147 | setMimeType("application/vnd.android.package-archive") 148 | } 149 | 150 | val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager 151 | val downloadId = manager.enqueue(request) 152 | 153 | var downloading = true 154 | while (downloading) { 155 | val query = DownloadManager.Query().setFilterById(downloadId) 156 | val cursor = manager.query(query) 157 | if (cursor.moveToFirst()) { 158 | val bytesDownloaded = 159 | cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) 160 | val bytesTotal = 161 | cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) 162 | 163 | if (bytesTotal > 0) { 164 | val progress = (bytesDownloaded * 100L / bytesTotal).toInt() 165 | notificationBuilder.setProgress(100, progress, false) 166 | notificationManager.notify(1, notificationBuilder.build()) 167 | } 168 | 169 | when (cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))) { 170 | DownloadManager.STATUS_SUCCESSFUL -> downloading = false 171 | DownloadManager.STATUS_FAILED -> { 172 | downloading = false 173 | throw Exception("Download failed") 174 | } 175 | } 176 | } 177 | cursor.close() 178 | } 179 | 180 | notificationBuilder.setContentText("Download complete") 181 | .setProgress(0, 0, false) 182 | .setOngoing(false) 183 | notificationManager.notify(1, notificationBuilder.build()) 184 | 185 | val apkFile = File( 186 | context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), 187 | apkFileName 188 | ) 189 | 190 | if (apkFile.exists()) { 191 | val apkUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 192 | FileProvider.getUriForFile(context, context.packageName + ".fileprovider", apkFile) 193 | .apply { 194 | Intent.FLAG_GRANT_READ_URI_PERMISSION 195 | } 196 | } else { 197 | Uri.parse("file://${apkFile.absolutePath}") 198 | // Uri.fromFile(apkFile) 199 | } 200 | // val apkUri = Uri.parse("file://$apkFile") 201 | Log.i(TAG, "apkUri $apkUri") 202 | val installIntent = Intent(Intent.ACTION_VIEW).apply { 203 | setDataAndType(apkUri, "application/vnd.android.package-archive") 204 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) 205 | } 206 | 207 | context.startActivity(installIntent) 208 | } else { 209 | Log.e(TAG, "APK file does not exist!") 210 | } 211 | } 212 | 213 | override fun onConfirm() { 214 | Log.i(TAG, "onConfirm $release") 215 | release?.let { startDownload(context, it) } 216 | } 217 | 218 | override fun onCancel() { 219 | } 220 | 221 | 222 | companion object { 223 | private const val TAG = "UpdateManager" 224 | } 225 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1 2 | 3 | import android.content.res.Resources 4 | import android.util.Log 5 | import android.util.TypedValue 6 | import com.lizongying.mytv1.requests.HttpClient 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.withContext 11 | import java.text.SimpleDateFormat 12 | import java.util.Date 13 | import java.util.Locale 14 | 15 | object Utils { 16 | const val TAG = "Utils" 17 | 18 | private var between: Long = 0 19 | 20 | fun getDateFormat(format: String): String { 21 | return SimpleDateFormat( 22 | format, 23 | Locale.CHINA 24 | ).format(Date(System.currentTimeMillis() - between)) 25 | } 26 | 27 | fun getDateTimestamp(): Long { 28 | return (System.currentTimeMillis() - between) / 1000 29 | } 30 | 31 | init { 32 | CoroutineScope(Dispatchers.IO).launch { 33 | try { 34 | val currentTimeMillis = getTimestampFromServer() 35 | Log.i(TAG, "currentTimeMillis $currentTimeMillis") 36 | if (currentTimeMillis > 0) { 37 | between = System.currentTimeMillis() - currentTimeMillis 38 | } 39 | } catch (e: Exception) { 40 | Log.e(TAG, "init", e) 41 | } 42 | } 43 | } 44 | 45 | private suspend fun getTimestampFromServer(): Long { 46 | return withContext(Dispatchers.IO) { 47 | try { 48 | val request = okhttp3.Request.Builder() 49 | .url("https://ip.ddnspod.com/timestamp") 50 | .build() 51 | 52 | HttpClient.okHttpClient.newCall(request).execute().use { response -> 53 | if (!response.isSuccessful) return@withContext 0 54 | response.body?.string()?.toLong() ?: 0 55 | } 56 | } catch (e: Exception) { 57 | Log.e(TAG, "getTimestampFromServer", e) 58 | 0 59 | } 60 | } 61 | } 62 | 63 | fun dpToPx(dp: Float): Int { 64 | return TypedValue.applyDimension( 65 | TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics 66 | ).toInt() 67 | } 68 | 69 | fun dpToPx(dp: Int): Int { 70 | return TypedValue.applyDimension( 71 | TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics 72 | ).toInt() 73 | } 74 | 75 | fun formatUrl(url: String): String { 76 | if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://") || url.startsWith( 77 | "socks://" 78 | ) || url.startsWith("socks5://") 79 | ) { 80 | return url 81 | } 82 | 83 | if (url.startsWith("//")) { 84 | return "http:$url" 85 | } 86 | 87 | return "http://$url" 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/data/Global.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.data 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | 6 | object Global { 7 | val gson = Gson() 8 | 9 | val typeTvList = object : TypeToken>() {}.type 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/data/ReleaseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.data 2 | 3 | 4 | data class ReleaseResponse( 5 | val version_code: Int?, 6 | val version_name: String?, 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/data/ReqSettings.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.data 2 | 3 | data class ReqSettings( 4 | var uri: String? = "", 5 | val channel: Int?, 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/data/RespSettings.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.data 2 | 3 | data class RespSettings( 4 | val channelUri: String, 5 | val channelText: String, 6 | val channelDefault: Int, 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/data/TV.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.data 2 | 3 | import java.io.Serializable 4 | 5 | data class TV( 6 | var id: Int = 0, 7 | var name: String = "", 8 | var title: String = "", 9 | var started: String? = null, 10 | var script: String? = null, 11 | var finished: String? = null, 12 | var logo: String = "", 13 | var uris: List, 14 | var headers: Map? = null, 15 | var group: String = "", 16 | var block: List, 17 | var selector: String? = null, 18 | var index: Int? = null, 19 | ) : Serializable { 20 | 21 | override fun toString(): String { 22 | return "TV{" + 23 | "id=" + id + 24 | ", name='" + name + '\'' + 25 | ", title='" + title + '\'' + 26 | ", started='" + started + '\'' + 27 | ", script='" + script + '\'' + 28 | ", finished='" + finished + '\'' + 29 | ", logo='" + logo + '\'' + 30 | ", uris='" + uris + '\'' + 31 | ", headers='" + headers + '\'' + 32 | ", group='" + group + '\'' + 33 | '}' 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/models/TVGroupModel.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.models 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.lizongying.mytv1.SP 7 | 8 | class TVGroupModel : ViewModel() { 9 | private val _tvGroupModel = MutableLiveData>() 10 | val tvGroupModel: LiveData> 11 | get() = _tvGroupModel 12 | 13 | private val _position = MutableLiveData() 14 | val position: LiveData 15 | get() = _position 16 | 17 | private val _change = MutableLiveData() 18 | val change: LiveData 19 | get() = _change 20 | 21 | fun setPosition(position: Int) { 22 | _position.value = position 23 | } 24 | 25 | fun setChange() { 26 | _change.value = true 27 | } 28 | 29 | fun setTVListModelList(tvListModelList: List) { 30 | _tvGroupModel.value = tvListModelList 31 | } 32 | 33 | fun addTVListModel(tvListModel: TVListModel) { 34 | if (_tvGroupModel.value == null) { 35 | _tvGroupModel.value = mutableListOf(tvListModel) 36 | return 37 | } 38 | 39 | val newList = _tvGroupModel.value!!.toMutableList() 40 | newList.add(tvListModel) 41 | _tvGroupModel.value = newList 42 | } 43 | 44 | fun clear() { 45 | _tvGroupModel.value = mutableListOf(getTVListModel(0)!!, getTVListModel(1)!!) 46 | setPosition(0) 47 | getTVListModel(1)?.clear() 48 | } 49 | 50 | fun getTVListModel(): TVListModel? { 51 | return getTVListModel(position.value as Int) 52 | } 53 | 54 | fun getTVListModel(idx: Int): TVListModel? { 55 | if (idx >= size()) { 56 | return null 57 | } 58 | return _tvGroupModel.value?.get(idx) 59 | } 60 | 61 | init { 62 | _position.value = SP.positionGroup 63 | } 64 | 65 | fun size(): Int { 66 | if (_tvGroupModel.value == null) { 67 | return 0 68 | } 69 | 70 | return _tvGroupModel.value!!.size 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/models/TVList.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.models 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.core.net.toFile 8 | import androidx.lifecycle.LiveData 9 | import androidx.lifecycle.MutableLiveData 10 | import com.lizongying.mytv1.R 11 | import com.lizongying.mytv1.SP 12 | import com.lizongying.mytv1.Utils.getDateFormat 13 | import com.lizongying.mytv1.data.Global.gson 14 | import com.lizongying.mytv1.data.Global.typeTvList 15 | import com.lizongying.mytv1.data.TV 16 | import com.lizongying.mytv1.showToast 17 | import io.github.lizongying.Gua 18 | import kotlinx.coroutines.CoroutineScope 19 | import kotlinx.coroutines.Dispatchers 20 | import kotlinx.coroutines.launch 21 | import kotlinx.coroutines.withContext 22 | import java.io.File 23 | 24 | object TVList { 25 | private const val TAG = "TVList" 26 | const val CACHE_FILE_NAME = "channels.txt" 27 | val DEFAULT_CHANNELS_FILE = R.raw.channels 28 | private lateinit var appDirectory: File 29 | private lateinit var serverUrl: String 30 | private lateinit var list: List 31 | var listModel: List = listOf() 32 | val groupModel = TVGroupModel() 33 | 34 | private var timeFormat = if (SP.displaySeconds) "HH:mm:ss" else "HH:mm" 35 | 36 | fun setDisplaySeconds(displaySeconds: Boolean) { 37 | timeFormat = if (displaySeconds) "HH:mm:ss" else "HH:mm" 38 | SP.displaySeconds = displaySeconds 39 | } 40 | 41 | fun getTime(): String { 42 | return getDateFormat(timeFormat) 43 | } 44 | 45 | private val _position = MutableLiveData() 46 | val position: LiveData 47 | get() = _position 48 | 49 | fun init(context: Context) { 50 | _position.value = 0 51 | 52 | groupModel.addTVListModel(TVListModel("我的收藏", 0)) 53 | groupModel.addTVListModel(TVListModel("全部频道", 1)) 54 | 55 | appDirectory = context.filesDir 56 | val file = File(appDirectory, CACHE_FILE_NAME) 57 | val str = if (file.exists()) { 58 | Log.i(TAG, "read $file") 59 | file.readText() 60 | } else { 61 | Log.i(TAG, "read resource") 62 | context.resources.openRawResource(DEFAULT_CHANNELS_FILE).bufferedReader(Charsets.UTF_8) 63 | .use { it.readText() } 64 | } 65 | 66 | try { 67 | str2List(str) 68 | } catch (e: Exception) { 69 | Log.e("", "error $e") 70 | file.deleteOnExit() 71 | Toast.makeText(context, "读取频道失败,请在菜单中进行设置", Toast.LENGTH_LONG).show() 72 | } 73 | 74 | if (SP.configAutoLoad && !SP.configUrl.isNullOrEmpty()) { 75 | SP.configUrl?.let { 76 | update(it) 77 | } 78 | } 79 | } 80 | 81 | private fun update() { 82 | CoroutineScope(Dispatchers.IO).launch { 83 | try { 84 | Log.i(TAG, "request $serverUrl") 85 | val client = okhttp3.OkHttpClient() 86 | val request = okhttp3.Request.Builder().url(serverUrl).build() 87 | val response = client.newCall(request).execute() 88 | 89 | if (response.isSuccessful) { 90 | val file = File(appDirectory, CACHE_FILE_NAME) 91 | if (!file.exists()) { 92 | file.createNewFile() 93 | } 94 | response.body?.let { 95 | val str = it.string() 96 | withContext(Dispatchers.Main) { 97 | if (str2List(str)) { 98 | file.writeText(str) 99 | SP.configUrl = serverUrl 100 | "频道导入成功".showToast() 101 | } else { 102 | "频道导入错误".showToast() 103 | } 104 | } 105 | } 106 | } else { 107 | Log.e("", "request status ${response.code}") 108 | "频道状态错误".showToast() 109 | } 110 | } catch (e: Exception) { 111 | Log.e("", "request error $e") 112 | "频道请求错误".showToast() 113 | } 114 | } 115 | } 116 | 117 | private fun update(serverUrl: String) { 118 | this.serverUrl = serverUrl 119 | update() 120 | } 121 | 122 | fun parseUri(uri: Uri) { 123 | if (uri.scheme == "file") { 124 | val file = uri.toFile() 125 | Log.i(TAG, "file $file") 126 | val str = if (file.exists()) { 127 | Log.i(TAG, "read $file") 128 | file.readText() 129 | } else { 130 | "文件不存在".showToast(Toast.LENGTH_LONG) 131 | return 132 | } 133 | 134 | try { 135 | if (str2List(str)) { 136 | SP.configUrl = uri.toString() 137 | "频道导入成功".showToast(Toast.LENGTH_LONG) 138 | } else { 139 | "频道导入失败".showToast(Toast.LENGTH_LONG) 140 | } 141 | } catch (e: Exception) { 142 | Log.e("", "error $e") 143 | file.deleteOnExit() 144 | "读取频道失败".showToast(Toast.LENGTH_LONG) 145 | } 146 | } else { 147 | update(uri.toString()) 148 | } 149 | } 150 | 151 | fun str2List(str: String): Boolean { 152 | var string = str 153 | val g = Gua() 154 | if (g.verify(str)) { 155 | string = g.decode(str) 156 | } 157 | if (string.isBlank()) { 158 | return false 159 | } 160 | when (string[0]) { 161 | '[' -> { 162 | try { 163 | list = gson.fromJson(string, typeTvList) 164 | Log.i(TAG, "导入频道 ${list.size}") 165 | } catch (e: Exception) { 166 | Log.i(TAG, "parse error $string") 167 | Log.i(TAG, e.message, e) 168 | return false 169 | } 170 | } 171 | } 172 | 173 | groupModel.clear() 174 | 175 | val map: MutableMap> = mutableMapOf() 176 | for (v in list) { 177 | if (v.group !in map) { 178 | map[v.group] = mutableListOf() 179 | } 180 | map[v.group]?.add(TVModel(v)) 181 | } 182 | 183 | val listModelNew: MutableList = mutableListOf() 184 | var groupIndex = 2 185 | var id = 0 186 | for ((k, v) in map) { 187 | val tvListModel = TVListModel(k, groupIndex) 188 | for ((listIndex, v1) in v.withIndex()) { 189 | v1.tv.id = id 190 | v1.groupIndex = groupIndex 191 | v1.listIndex = listIndex 192 | tvListModel.addTVModel(v1) 193 | listModelNew.add(v1) 194 | id++ 195 | } 196 | groupModel.addTVListModel(tvListModel) 197 | groupIndex++ 198 | } 199 | 200 | listModel = listModelNew 201 | 202 | // 全部频道 203 | groupModel.getTVListModel(1)?.setTVListModel(listModel) 204 | 205 | Log.i(TAG, "groupModel ${groupModel.size()}") 206 | groupModel.setChange() 207 | 208 | return true 209 | } 210 | 211 | fun getTVModel(): TVModel? { 212 | return getTVModel(position.value!!) 213 | } 214 | 215 | fun getTVModel(idx: Int): TVModel? { 216 | if (idx >= size()) { 217 | return null 218 | } 219 | return listModel[idx] 220 | } 221 | 222 | fun setPosition(position: Int): Boolean { 223 | Log.i(TAG, "setPosition $position/${size()}") 224 | if (position >= size()) { 225 | return false 226 | } 227 | 228 | if (_position.value != position) { 229 | _position.value = position 230 | } 231 | 232 | val tvModel = getTVModel(position) 233 | 234 | // set a new position or retry when position same 235 | tvModel!!.setReady() 236 | 237 | groupModel.setPosition(tvModel.groupIndex) 238 | 239 | SP.positionGroup = tvModel.groupIndex 240 | SP.position = position 241 | return true 242 | } 243 | 244 | fun size(): Int { 245 | return listModel.size 246 | } 247 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/models/TVListModel.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.models 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | 7 | class TVListModel(private val name: String, private val index: Int) : ViewModel() { 8 | fun getName(): String { 9 | return name 10 | } 11 | 12 | fun getIndex(): Int { 13 | return index 14 | } 15 | 16 | private val _tvListModel = MutableLiveData>() 17 | val tvListModel: LiveData> 18 | get() = _tvListModel 19 | 20 | private val _position = MutableLiveData() 21 | val position: LiveData 22 | get() = _position 23 | 24 | fun setPosition(position: Int) { 25 | _position.value = position 26 | } 27 | 28 | private val _change = MutableLiveData() 29 | val change: LiveData 30 | get() = _change 31 | 32 | fun setChange() { 33 | _change.value = true 34 | } 35 | 36 | fun setTVListModel(tvListModel: List) { 37 | _tvListModel.value = tvListModel 38 | } 39 | 40 | fun addTVModel(tvModel: TVModel) { 41 | if (_tvListModel.value == null) { 42 | _tvListModel.value = mutableListOf(tvModel) 43 | return 44 | } 45 | 46 | val newList = _tvListModel.value!!.toMutableList() 47 | newList.add(tvModel) 48 | _tvListModel.value = newList 49 | } 50 | 51 | fun removeTVModel(id: Int) { 52 | if (_tvListModel.value == null) { 53 | return 54 | } 55 | val newList = _tvListModel.value!!.toMutableList() 56 | val iterator = newList.iterator() 57 | while (iterator.hasNext()) { 58 | if (iterator.next().tv.id == id) { 59 | iterator.remove() 60 | } 61 | } 62 | _tvListModel.value = newList 63 | } 64 | 65 | fun replaceTVModel(tvModel: TVModel) { 66 | if (_tvListModel.value == null) { 67 | _tvListModel.value = mutableListOf(tvModel) 68 | return 69 | } 70 | 71 | val newList = _tvListModel.value!!.toMutableList() 72 | var exists = false 73 | val iterator = newList.iterator() 74 | while (iterator.hasNext()) { 75 | if (iterator.next().tv.id == tvModel.tv.id) { 76 | exists = true 77 | } 78 | } 79 | if (!exists) { 80 | newList.add(tvModel) 81 | _tvListModel.value = newList 82 | } 83 | } 84 | 85 | fun clear() { 86 | _tvListModel.value = mutableListOf() 87 | setPosition(0) 88 | } 89 | 90 | fun getTVModel(): TVModel? { 91 | return getTVModel(position.value as Int) 92 | } 93 | 94 | fun getTVModel(idx: Int): TVModel? { 95 | return _tvListModel.value?.get(idx) 96 | } 97 | 98 | init { 99 | _position.value = 0 100 | } 101 | 102 | fun size(): Int { 103 | if (_tvListModel.value == null) { 104 | return 0 105 | } 106 | 107 | return _tvListModel.value!!.size 108 | } 109 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/models/TVModel.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.models 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.lizongying.mytv1.SP 7 | import com.lizongying.mytv1.data.TV 8 | 9 | class TVModel(var tv: TV) : ViewModel() { 10 | private val _position = MutableLiveData() 11 | val position: LiveData 12 | get() = _position 13 | 14 | var retryTimes = 0 15 | var retryMaxTimes = 8 16 | 17 | var groupIndex = 0 18 | var listIndex = 0 19 | 20 | private val _errInfo = MutableLiveData() 21 | val errInfo: LiveData 22 | get() = _errInfo 23 | 24 | fun setErrInfo(info: String) { 25 | _errInfo.value = info 26 | } 27 | 28 | private val _videoUrl = MutableLiveData() 29 | val videoUrl: LiveData 30 | get() = _videoUrl 31 | 32 | fun setVideoUrl(url: String) { 33 | _videoUrl.value = url 34 | } 35 | 36 | private fun getVideoUrl(): String? { 37 | if (_videoIndex.value == null || tv.uris.isEmpty()) { 38 | return null 39 | } 40 | 41 | if (videoIndex.value!! >= tv.uris.size) { 42 | return null 43 | } 44 | 45 | return tv.uris[_videoIndex.value!!] 46 | } 47 | 48 | private val _like = MutableLiveData() 49 | val like: LiveData 50 | get() = _like 51 | 52 | fun setLike(liked: Boolean) { 53 | _like.value = liked 54 | } 55 | 56 | private val _ready = MutableLiveData() 57 | val ready: LiveData 58 | get() = _ready 59 | 60 | fun setReady() { 61 | _ready.value = true 62 | } 63 | 64 | private val _videoIndex = MutableLiveData() 65 | private val videoIndex: LiveData 66 | get() = _videoIndex 67 | 68 | init { 69 | _position.value = 0 70 | _videoIndex.value = 0 71 | _like.value = SP.getLike(tv.id) 72 | _videoUrl.value = getVideoUrl() 73 | } 74 | 75 | fun update(t: TV) { 76 | tv = t 77 | } 78 | 79 | companion object { 80 | private const val TAG = "TVModel" 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/requests/DnsCache.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.requests 2 | 3 | import okhttp3.Dns 4 | import java.net.InetAddress 5 | import java.util.concurrent.ConcurrentHashMap 6 | 7 | class DnsCache : Dns { 8 | private val dnsCache: MutableMap> = ConcurrentHashMap() 9 | 10 | override fun lookup(hostname: String): List { 11 | dnsCache[hostname]?.let { 12 | return it 13 | } 14 | 15 | val addresses = InetAddress.getAllByName(hostname).toList() 16 | 17 | if (addresses.isNotEmpty()) { 18 | dnsCache[hostname] = addresses 19 | } 20 | 21 | return addresses 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/requests/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.requests 2 | 3 | 4 | import android.os.Build 5 | import android.util.Log 6 | import okhttp3.ConnectionSpec 7 | import okhttp3.OkHttpClient 8 | import okhttp3.TlsVersion 9 | import java.security.KeyStore 10 | import javax.net.ssl.SSLContext 11 | import javax.net.ssl.TrustManagerFactory 12 | import javax.net.ssl.X509TrustManager 13 | 14 | 15 | object HttpClient { 16 | const val TAG = "HttpClient" 17 | const val HOST = "https://www.gitlink.org.cn/lizongying/my-tv-1/raw/" 18 | const val DOWNLOAD_HOST = "https://www.gitlink.org.cn/lizongying/my-tv-1/releases/download/" 19 | 20 | val okHttpClient: OkHttpClient by lazy { 21 | getUnsafeOkHttpClient() 22 | } 23 | 24 | private fun OkHttpClient.Builder.enableTls12OnPreLollipop() { 25 | if (Build.VERSION.SDK_INT < 22) { 26 | try { 27 | val sslContext = SSLContext.getInstance("TLSv1.2") 28 | sslContext.init(null, null, java.security.SecureRandom()) 29 | 30 | val trustManagerFactory = TrustManagerFactory.getInstance( 31 | TrustManagerFactory.getDefaultAlgorithm() 32 | ) 33 | trustManagerFactory.init(null as KeyStore?) 34 | val trustManagers = trustManagerFactory.trustManagers 35 | val trustManager = trustManagers[0] as X509TrustManager 36 | 37 | sslSocketFactory(Tls12SocketFactory(sslContext.socketFactory), trustManager) 38 | connectionSpecs( 39 | listOf( 40 | ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) 41 | .tlsVersions(TlsVersion.TLS_1_2) 42 | .build(), 43 | ConnectionSpec.COMPATIBLE_TLS, 44 | ConnectionSpec.CLEARTEXT 45 | ) 46 | ) 47 | } catch (e: Exception) { 48 | Log.e(TAG, "enableTls12OnPreLollipop", e) 49 | } 50 | } 51 | } 52 | 53 | private fun getUnsafeOkHttpClient(): OkHttpClient { 54 | try { 55 | val trustManager = 56 | object : X509TrustManager { 57 | override fun checkClientTrusted( 58 | chain: Array?, 59 | authType: String? 60 | ) { 61 | } 62 | 63 | override fun checkServerTrusted( 64 | chain: Array?, 65 | authType: String? 66 | ) { 67 | } 68 | 69 | override fun getAcceptedIssuers(): Array { 70 | return emptyArray() 71 | } 72 | } 73 | 74 | val sslContext = SSLContext.getInstance("TLS") 75 | sslContext.init(null, arrayOf(trustManager), java.security.SecureRandom()) 76 | 77 | return OkHttpClient.Builder() 78 | .sslSocketFactory(sslContext.socketFactory, trustManager) 79 | .hostnameVerifier { _, _ -> true } 80 | .dns(DnsCache()) 81 | .apply { enableTls12OnPreLollipop() } 82 | .build() 83 | } catch (e: Exception) { 84 | throw RuntimeException(e) 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv1/requests/Tls12SocketFactory.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv1.requests 2 | 3 | import java.io.IOException 4 | import java.net.InetAddress 5 | import java.net.Socket 6 | import java.net.UnknownHostException 7 | import javax.net.ssl.SSLSocket 8 | import javax.net.ssl.SSLSocketFactory 9 | 10 | 11 | /** 12 | * Enables TLS v1.2 when creating SSLSockets. 13 | * 14 | * 15 | * For some reason, android supports TLS v1.2 from API 16, but enables it by 16 | * default only from API 20. 17 | * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html 18 | * @see SSLSocketFactory 19 | */ 20 | class Tls12SocketFactory(val delegate: SSLSocketFactory) : SSLSocketFactory() { 21 | override fun getDefaultCipherSuites(): Array { 22 | return delegate.defaultCipherSuites 23 | } 24 | 25 | override fun getSupportedCipherSuites(): Array { 26 | return delegate.supportedCipherSuites 27 | } 28 | 29 | @Throws(IOException::class) 30 | override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { 31 | return patch(delegate.createSocket(s, host, port, autoClose)) 32 | } 33 | 34 | @Throws(IOException::class, UnknownHostException::class) 35 | override fun createSocket(host: String, port: Int): Socket { 36 | return patch(delegate.createSocket(host, port)) 37 | } 38 | 39 | @Throws(IOException::class, UnknownHostException::class) 40 | override fun createSocket( 41 | host: String, 42 | port: Int, 43 | localHost: InetAddress, 44 | localPort: Int 45 | ): Socket { 46 | return patch(delegate.createSocket(host, port, localHost, localPort)) 47 | } 48 | 49 | @Throws(IOException::class) 50 | override fun createSocket(host: InetAddress, port: Int): Socket { 51 | return patch(delegate.createSocket(host, port)) 52 | } 53 | 54 | @Throws(IOException::class) 55 | override fun createSocket( 56 | address: InetAddress, 57 | port: Int, 58 | localAddress: InetAddress, 59 | localPort: Int 60 | ): Socket { 61 | return patch(delegate.createSocket(address, port, localAddress, localPort)) 62 | } 63 | 64 | private fun patch(s: Socket): Socket { 65 | if (s is SSLSocket) { 66 | s.enabledProtocols = TLS_V12_ONLY 67 | } 68 | return s 69 | } 70 | 71 | companion object { 72 | private val TLS_V12_ONLY = arrayOf("TLSv1.2") 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/res/color/switch_thumb_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/switch_track_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/appreciate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/app/src/main/res/drawable/appreciate.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/banner1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/app/src/main/res/drawable/banner1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_favorite_border_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_sentiment_dissatisfied_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/custom_progress_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/light_mode_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/app/src/main/res/drawable/logo1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_dark_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_dark_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_dark_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_light_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_white_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_white_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_white_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/volume_off_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/volume_up_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 22 | 32 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/error.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 21 | 22 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/group_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 21 | 22 | 30 | 39 | 40 | 48 | 49 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 35 | 36 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 17 | 18 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/modal.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/player.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 31 | 32 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 17 | 27 | 32 | 40 | 49 | 50 | 51 | 61 | 62 | 67 | 76 | 85 | 95 | 96 | 97 | 103 | 113 | 123 | 133 | 134 | 135 | 146 | 157 | 168 | 179 | 190 | 201 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /app/src/main/res/layout/show.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/time.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 22 | 32 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/raw/ahtv.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const body = document.querySelector('body'); 3 | body.style.position = 'fixed'; 4 | body.style.left = '100vw'; 5 | body.style.backgroundColor = '#000'; 6 | 7 | let count = 0; 8 | const interval = setInterval(() => { 9 | const video = body.querySelector('video'); 10 | if (video !== null) { 11 | video.attributes.autoplay = 'true'; 12 | video.attributes.muted = 'false'; 13 | video.attributes.controls = 'false'; 14 | video.style.objectFit = 'contain'; 15 | video.style.position = 'fixed'; 16 | video.style.width = "100vw"; 17 | video.style.height = "100vh"; 18 | video.style.top = '0'; 19 | video.style.left = '0'; 20 | video.style.zIndex = '9999'; 21 | 22 | const images = body.querySelectorAll('img'); 23 | for (let i = 0; i < images.length; i++) { 24 | images[i].style.display = 'none'; 25 | } 26 | 27 | clearInterval(interval); 28 | setTimeout(function () { 29 | console.log('success'); 30 | }, 0) 31 | } 32 | 33 | count++; 34 | if (count > 6 * 1000) { 35 | clearInterval(interval); 36 | console.log('timeout'); 37 | } 38 | }, 10); 39 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/ahtv1.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const body = document.body; 3 | body.style.position = 'fixed'; 4 | body.style.left = '100vw'; 5 | body.style.backgroundColor = '#000'; 6 | 7 | let timeout = 0; 8 | 9 | const videoStyle = (video) => { 10 | video.attributes.autoplay = 'true'; 11 | video.attributes.muted = 'false'; 12 | video.attributes.controls = 'false'; 13 | video.style.objectFit = 'contain'; 14 | video.style.position = 'fixed'; 15 | video.style.width = "100vw"; 16 | video.style.height = "100vh"; 17 | video.style.top = '0'; 18 | video.style.left = '0'; 19 | video.style.zIndex = '9999'; 20 | video.style.transform = 'translate(0, 0)'; 21 | }; 22 | 23 | const success = (video) => { 24 | videoStyle(video); 25 | 26 | setTimeout(() => { 27 | videoStyle(video); 28 | }, 10); 29 | 30 | setTimeout(() => { 31 | videoStyle(video); 32 | }, 100); 33 | 34 | setTimeout(() => { 35 | videoStyle(video); 36 | }, 1000); 37 | 38 | console.log('success'); 39 | if (timeout > 0) { 40 | clearInterval(timeout); 41 | } 42 | }; 43 | 44 | const observerVideo = (box) => { 45 | const video = box.querySelector('video'); 46 | if (video !== null) { 47 | success(video); 48 | return null 49 | } else { 50 | const observer = new MutationObserver((_) => { 51 | const video = box.querySelector('video'); 52 | if (video !== null) { 53 | if (observer !== null) { 54 | observer.disconnect(); 55 | } 56 | success(video); 57 | } 58 | }); 59 | 60 | observer.observe(body, { 61 | childList: true, 62 | subtree: true 63 | }); 64 | return observer 65 | } 66 | } 67 | 68 | const observer = observerVideo(body); 69 | 70 | timeout = setTimeout(() => { 71 | if (observer !== null) { 72 | observer.disconnect(); 73 | } 74 | console.log('timeout'); 75 | }, 10000); 76 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/ahtv2.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | document.querySelector('meta[name="viewport"]').content = "width=device-width, initial-scale=1.0" 3 | 4 | const body = document.body; 5 | body.style.position = 'fixed'; 6 | body.style.left = '100vw'; 7 | body.style.backgroundColor = '#000'; 8 | 9 | let timeout = 0; 10 | 11 | const videoStyle = (video) => { 12 | video.attributes.autoplay = 'true'; 13 | video.attributes.muted = 'false'; 14 | video.attributes.controls = 'false'; 15 | video.style.objectFit = 'contain'; 16 | video.style.position = 'fixed'; 17 | video.style.width = "100vw"; 18 | video.style.height = "100vh"; 19 | video.style.top = '0'; 20 | video.style.left = '0'; 21 | video.style.zIndex = '9999'; 22 | video.style.transform = 'translate(0, 0)'; 23 | }; 24 | 25 | const success = (video) => { 26 | if (video.readyState >= 3) { 27 | video.play(); 28 | } else { 29 | video.addEventListener('canplaythrough', () => { 30 | video.play(); 31 | }); 32 | video.addEventListener('canplaythrough', () => { 33 | video.play(); 34 | }, {once: true}); 35 | } 36 | 37 | videoStyle(video); 38 | 39 | setTimeout(() => { 40 | videoStyle(video); 41 | }, 10); 42 | 43 | setTimeout(() => { 44 | videoStyle(video); 45 | }, 100); 46 | 47 | setTimeout(() => { 48 | videoStyle(video); 49 | }, 1000); 50 | 51 | console.log('success'); 52 | if (timeout > 0) { 53 | clearInterval(timeout); 54 | } 55 | }; 56 | 57 | const observerVideo = (box) => { 58 | const video = box.querySelector('video'); 59 | if (video !== null) { 60 | success(video); 61 | return null 62 | } else { 63 | const observer = new MutationObserver((_) => { 64 | const video = box.querySelector('video'); 65 | if (video !== null) { 66 | if (observer !== null) { 67 | observer.disconnect(); 68 | } 69 | success(video); 70 | } 71 | }); 72 | 73 | observer.observe(body, { 74 | childList: true, 75 | subtree: true 76 | }); 77 | return observer 78 | } 79 | } 80 | 81 | const observer = observerVideo(body); 82 | 83 | timeout = setTimeout(() => { 84 | if (observer !== null) { 85 | observer.disconnect(); 86 | } 87 | console.log('timeout'); 88 | }, 10000); 89 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/gdtv.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | return new Promise(function (resolve, reject) { 3 | const divElement = document.createElement('div'); 4 | divElement.style.position = 'fixed'; 5 | divElement.style.top = '0'; 6 | divElement.style.left = '0'; 7 | divElement.style.width = '100%'; 8 | divElement.style.height = '100%'; 9 | divElement.style.backgroundColor = '#000'; 10 | divElement.style.zIndex = '9998'; 11 | document.body.appendChild(divElement); 12 | 13 | let count = 0; 14 | const interval = setInterval(() => { 15 | const video = document.querySelector('video'); 16 | if (video !== null) { 17 | video.attributes.autoplay = 'true'; 18 | video.attributes.muted = 'false'; 19 | video.attributes.controls = 'false'; 20 | video.style.objectFit = 'contain'; 21 | video.style.position = 'fixed'; 22 | video.style.width = "100vw"; 23 | video.style.height = "100vh"; 24 | video.style.top = '0'; 25 | video.style.left = '0'; 26 | video.style.zIndex = '9999'; 27 | 28 | const images = document.querySelectorAll('img'); 29 | for(let i = 0; i < images.length; i++) { 30 | images[i].style.display = 'none'; 31 | } 32 | clearInterval(interval); 33 | setTimeout(function () { 34 | console.log('success'); 35 | }, 0) 36 | } 37 | count ++; 38 | if (count > 6 * 1000) { 39 | clearInterval(interval); 40 | console.log('timeout'); 41 | } 42 | }, 10); 43 | }); 44 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/gua64min.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/lizongying/js-gua64 3 | */ 4 | const gua="䷁䷖䷇䷓䷏䷢䷬䷋"+"䷎䷳䷦䷴䷽䷷䷞䷠"+"䷆䷃䷜䷺䷧䷿䷮䷅"+"䷭䷑䷯䷸䷟䷱䷛䷫"+"䷗䷚䷂䷩䷲䷔䷐䷘"+"䷣䷕䷾䷤䷶䷝䷰䷌"+"䷒䷨䷻䷼䷵䷥䷹䷉"+"䷊䷙䷄䷈䷡䷍䷪䷀";const encode=str=>{const bytes=(new TextEncoder).encode(str);const encoded=[];const len=bytes.length;for(let i=0;i>2]);encoded.push(gua[(bytes[i]&3)<<4|bytes[i+1]>>4]);encoded.push(gua[(bytes[i+1]&15)<<2|bytes[i+2]>>6]);encoded.push(gua[bytes[i+2]&63]);continue}if(i+3===len+1){encoded.push(gua[bytes[i]>>2]);encoded.push(gua[(bytes[i]&3)<<4|bytes[i+1]>>4]);encoded.push(gua[(bytes[i+1]&15)<<2]);encoded.push("〇");continue}if(i+3===len+2){encoded.push(gua[bytes[i]>>2]);encoded.push(gua[(bytes[i]&3)<<4]);encoded.push("〇");encoded.push("〇")}}return encoded.join("")};const decode=str=>{const gua64dict={};for(let i=0;i>4&3);if(b[i+2]!==255){encoded.push((b[i+1]&15)<<4|b[i+2]>>2&15)}if(b[i+3]!==255){encoded.push((b[i+2]&3)<<6|b[i+3]&63)}}return new TextDecoder("utf-8").decode(new Uint8Array(encoded))};const verify=str=>{const array=gua.split("");array.push("〇");return[...str].every((char=>array.includes(char)))};export{encode,decode,verify}; -------------------------------------------------------------------------------- /app/src/main/res/raw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 我的電視·一 9 | 73 | 74 | 75 |

我的電視·一

76 |

視頻源可以设置為地址/文本/文件其中之一

77 |
78 | 79 |
80 | 81 |
82 | 83 | 84 |
85 |
86 | 87 |
88 | 89 |
90 | 91 |
92 | 93 |
94 | 95 | 96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 | 104 |
105 | 106 |
107 |
108 | 109 |
110 | 111 |
112 | 113 |
114 | 115 | 116 |
117 |
118 | 119 |
120 | 121 | 212 | -------------------------------------------------------------------------------- /app/src/main/res/raw/jxtv1.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const body = document.body; 3 | body.style.position = 'fixed'; 4 | body.style.left = '100%'; 5 | body.style.backgroundColor = '#000'; 6 | 7 | let timeout = 0; 8 | 9 | const success = (video) => { 10 | video.attributes.autoplay = 'true'; 11 | video.attributes.muted = 'false'; 12 | video.attributes.controls = 'false'; 13 | video.style.objectFit = 'contain'; 14 | video.style.position = 'fixed'; 15 | video.style.width = "100vw"; 16 | video.style.height = "100vh"; 17 | video.style.top = '0'; 18 | video.style.left = '0'; 19 | video.style.zIndex = '9999'; 20 | 21 | console.log('success'); 22 | if (timeout > 0) { 23 | clearInterval(timeout); 24 | } 25 | }; 26 | 27 | const observerSelector = (selector, index) => { 28 | let items = body.querySelectorAll(selector); 29 | if (items.length > index) { 30 | items[index].click(); 31 | return null 32 | } else { 33 | const observer = new MutationObserver((_) => { 34 | items = body.querySelectorAll(selector); 35 | if (items.length > index) { 36 | if (observer !== null) { 37 | observer.disconnect(); 38 | } 39 | items[index].click(); 40 | } 41 | }); 42 | 43 | observer.observe(body, { 44 | childList: true, 45 | subtree: true, 46 | attributes: false, 47 | characterData: false 48 | }); 49 | return observer 50 | } 51 | }; 52 | 53 | const observerVideo = (box) => { 54 | let video = box.querySelector('video'); 55 | if (video !== null) { 56 | success(video); 57 | return null 58 | } else { 59 | const observer = new MutationObserver((_) => { 60 | video = box.querySelector('video'); 61 | if (video !== null) { 62 | if (observer !== null) { 63 | observer.disconnect(); 64 | } 65 | 66 | setTimeout(() => { 67 | const arr = document.URL.split('#') 68 | observerSelector('.item', parseInt(arr[arr.length - 1])); 69 | success(box.querySelector('video')); 70 | }, 0); 71 | } 72 | }); 73 | 74 | observer.observe(box, { 75 | childList: true, 76 | subtree: true 77 | }); 78 | return observer 79 | } 80 | }; 81 | 82 | const observer = observerVideo(body); 83 | 84 | timeout = setTimeout(() => { 85 | if (observer !== null) { 86 | observer.disconnect(); 87 | } 88 | console.log('timeout'); 89 | }, 10000); 90 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/nmgtv1.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const body = document.body; 3 | body.style.position = 'fixed'; 4 | body.style.left = '100%'; 5 | body.style.backgroundColor = '#000'; 6 | 7 | let timeout = 0; 8 | 9 | const success = (video) => { 10 | video.attributes.autoplay = 'true'; 11 | video.attributes.muted = 'false'; 12 | video.attributes.controls = 'false'; 13 | video.style.objectFit = 'contain'; 14 | video.style.position = 'fixed'; 15 | video.style.width = "100vw"; 16 | video.style.height = "100vh"; 17 | video.style.top = '0'; 18 | video.style.left = '0'; 19 | video.style.zIndex = '9999'; 20 | video.style.transform = 'translate(0, 0)'; 21 | 22 | console.log('success'); 23 | if (timeout > 0) { 24 | clearInterval(timeout); 25 | } 26 | }; 27 | 28 | const observerSelector = (selector, index) => { 29 | let items = body.querySelectorAll(selector); 30 | if (items.length > index) { 31 | items[index].click(); 32 | return null 33 | } else { 34 | const observer = new MutationObserver((_) => { 35 | items = body.querySelectorAll(selector); 36 | if (items.length > index) { 37 | if (observer !== null) { 38 | observer.disconnect(); 39 | } 40 | items[index].click(); 41 | } 42 | }); 43 | 44 | observer.observe(body, { 45 | childList: true, 46 | subtree: true, 47 | attributes: false, 48 | characterData: false 49 | }); 50 | return observer 51 | } 52 | }; 53 | 54 | const observerVideo = (box) => { 55 | let video = box.querySelector('video'); 56 | if (video !== null) { 57 | success(video); 58 | return null 59 | } else { 60 | const observer = new MutationObserver((_) => { 61 | video = box.querySelector('video'); 62 | if (video !== null) { 63 | if (observer !== null) { 64 | observer.disconnect(); 65 | } 66 | 67 | setTimeout(() => { 68 | const arr = document.URL.split('#') 69 | observerSelector('.c-label', parseInt(arr[arr.length - 1])); 70 | success(box.querySelector('video')); 71 | }, 0); 72 | } 73 | }); 74 | 75 | observer.observe(box, { 76 | childList: true, 77 | subtree: true 78 | }); 79 | return observer 80 | } 81 | }; 82 | 83 | const observer = observerVideo(body); 84 | 85 | timeout = setTimeout(() => { 86 | if (observer !== null) { 87 | observer.disconnect(); 88 | } 89 | console.log('timeout'); 90 | }, 10000); 91 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/prev.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | console.log(Date()); 3 | const channel = '{channel}'; 4 | const host = window.location.host; 5 | switch (host) { 6 | case 'tv.cctv.com': { 7 | localStorage.setItem('cctv_live_resolution', '720'); 8 | } 9 | } 10 | const originalSend = XMLHttpRequest.prototype.send; 11 | const originalOpen = XMLHttpRequest.prototype.open; 12 | 13 | XMLHttpRequest.prototype.open = function(method, url, async, user, password) { 14 | this._url = url; 15 | return originalOpen.apply(this, arguments); 16 | }; 17 | 18 | XMLHttpRequest.prototype.send = function(body) { 19 | this.addEventListener('load', function() { 20 | console.log('URL:', this._url); 21 | let response; 22 | 23 | if (this.responseType === '' || this.responseType === 'text') { 24 | response = this.responseText; 25 | } else { 26 | response = JSON.stringify(this.response); 27 | } 28 | console.log(`channel {channel}, Response:`, response); 29 | }); 30 | return originalSend.apply(this, arguments); 31 | }; 32 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/sxrtv1.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const body = document.body; 3 | body.style.position = 'fixed'; 4 | body.style.left = '100vw'; 5 | body.style.backgroundColor = '#000'; 6 | 7 | const arr = document.URL.split('#'); 8 | const items = body.querySelectorAll('[onclick="liveList.selectChannel(this)"]'); 9 | items[parseInt(arr[arr.length - 1])].click(); 10 | 11 | let timeout = 0; 12 | 13 | const success = (video) => { 14 | video.attributes.autoplay = 'true'; 15 | video.attributes.muted = 'false'; 16 | video.attributes.controls = 'false'; 17 | video.style.objectFit = 'contain'; 18 | video.style.position = 'fixed'; 19 | video.style.width = "100vw"; 20 | video.style.height = "100vh"; 21 | video.style.top = '0'; 22 | video.style.left = '0'; 23 | video.style.zIndex = '9999'; 24 | 25 | console.log('success'); 26 | if (timeout > 0) { 27 | clearInterval(timeout); 28 | } 29 | }; 30 | 31 | const observerVideo = (box) => { 32 | const video = box.querySelector('video'); 33 | if (video !== null) { 34 | success(video); 35 | return null 36 | } else { 37 | const observer = new MutationObserver((_) => { 38 | const video = box.querySelector('video'); 39 | if (video !== null) { 40 | if (observer !== null) { 41 | observer.disconnect(); 42 | } 43 | success(video); 44 | } 45 | }); 46 | 47 | observer.observe(box, { 48 | childList: true, 49 | subtree: true 50 | }); 51 | return observer 52 | } 53 | }; 54 | 55 | const observerBox = () => { 56 | const box = body.querySelector('#showplayerbox'); 57 | if (box !== null) { 58 | return observerVideo(box.shadowRoot); 59 | } else { 60 | const observer = new MutationObserver((_) => { 61 | const box = body.querySelector('#showplayerbox'); 62 | if (box !== null) { 63 | if (observer !== null) { 64 | observer.disconnect(); 65 | } 66 | return observerVideo(box.shadowRoot); 67 | } 68 | }); 69 | 70 | observer.observe(body, { 71 | childList: true, 72 | subtree: true 73 | }); 74 | return observer 75 | } 76 | } 77 | 78 | const observer = observerBox() 79 | 80 | timeout = setTimeout(() => { 81 | if (observer !== null) { 82 | observer.disconnect(); 83 | } 84 | console.log('timeout'); 85 | }, 10000); 86 | })() -------------------------------------------------------------------------------- /app/src/main/res/raw/xjtv1.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const body = document.body; 3 | body.style.position = 'fixed'; 4 | body.style.left = '100vw'; 5 | body.style.backgroundColor = '#000'; 6 | 7 | let timeout = 0; 8 | let selector = '{selector}'; 9 | let index = {index}; 10 | 11 | const videoStyle = (video) => { 12 | video.attributes.autoplay = 'true'; 13 | video.attributes.muted = 'false'; 14 | video.attributes.controls = 'false'; 15 | video.style.objectFit = 'contain'; 16 | video.style.position = 'fixed'; 17 | video.style.width = "100vw"; 18 | video.style.height = "100vh"; 19 | video.style.top = '0'; 20 | video.style.left = '0'; 21 | video.style.zIndex = '9999'; 22 | video.style.transform = 'translate(0, 0)'; 23 | }; 24 | 25 | const success = (video) => { 26 | videoStyle(video); 27 | 28 | setTimeout(() => { 29 | videoStyle(video); 30 | }, 10); 31 | 32 | setTimeout(() => { 33 | videoStyle(video); 34 | }, 100); 35 | 36 | setTimeout(() => { 37 | videoStyle(video); 38 | }, 1000); 39 | 40 | console.log('success'); 41 | if (timeout > 0) { 42 | clearInterval(timeout); 43 | } 44 | }; 45 | 46 | const observerSelector = () => { 47 | let items = body.querySelectorAll(selector); 48 | if (items.length > index) { 49 | items[index].click(); 50 | return null; 51 | } else { 52 | const observer = new MutationObserver((_) => { 53 | items = body.querySelectorAll(selector); 54 | if (items.length > index) { 55 | if (observer !== null) { 56 | observer.disconnect(); 57 | } 58 | items[index].click(); 59 | return null; 60 | } 61 | }); 62 | 63 | observer.observe(body, { 64 | childList: true, 65 | subtree: true, 66 | attributes: false, 67 | characterData: false 68 | }); 69 | return observer 70 | } 71 | }; 72 | 73 | const observerVideo = (box) => { 74 | const video = box.querySelector('video'); 75 | if (video !== null) { 76 | if (index !== 0) { 77 | setTimeout(() => { 78 | observerSelector(); 79 | success(box.querySelector('video')); 80 | }, 0); 81 | } else { 82 | success(video); 83 | } 84 | return null 85 | } else { 86 | const observer = new MutationObserver((_) => { 87 | const video = box.querySelector('video'); 88 | if (video !== null) { 89 | if (observer !== null) { 90 | observer.disconnect(); 91 | } 92 | 93 | if (index !== 0) { 94 | setTimeout(() => { 95 | observerSelector(); 96 | success(box.querySelector('video')); 97 | }, 0); 98 | } else { 99 | success(video); 100 | } 101 | } 102 | }); 103 | 104 | observer.observe(box, { 105 | childList: true, 106 | subtree: true 107 | }); 108 | return observer 109 | } 110 | }; 111 | 112 | const observer = observerVideo(body); 113 | 114 | timeout = setTimeout(() => { 115 | if (observer !== null) { 116 | observer.disconnect(); 117 | } 118 | 119 | const video = body.querySelector('video'); 120 | if (video !== null) { 121 | success(video); 122 | } else { 123 | console.log('timeout'); 124 | } 125 | }, 10000); 126 | })() -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #0096a6 3 | #FF263238 4 | #000 5 | #FFF 6 | #F00 7 | #FFEEEEEE 8 | #B3EEEEEE 9 | #400096A6 10 | #B3EEEEEE 11 | #0096A6 12 | #FFEEEEEE 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 我的電視·一 3 | 換台反轉 4 | 換台時顯示頻道號 5 | 更新應用 6 | 開機自啟 7 | 讚賞作者 8 | 配置地址 9 | 默認頻道 10 | 顯示時間 11 | 應用啟動後更新視頻源 12 | 退出應用 13 | 默認頻道號 14 | 恢復默認 15 | 遠程配置 16 | 再按一次退出 17 | 緊湊的菜單 18 | 時間顯示秒 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.7.3" apply false 4 | id("org.jetbrains.kotlin.android") version "1.9.10" apply false 5 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 14 14:50:34 HKT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | in_changelog=false 4 | 5 | while IFS= read -r line; do 6 | if [[ "$line" == "## "* ]]; then 7 | continue 8 | fi 9 | 10 | if [[ $in_changelog == false ]] && [[ "$line" == "### "* ]]; then 11 | in_changelog=true 12 | continue 13 | fi 14 | 15 | if [[ $in_changelog == true ]] && [[ "$line" == "### "* ]]; then 16 | break 17 | fi 18 | 19 | echo "$line" 20 | done < HISTORY.md 21 | -------------------------------------------------------------------------------- /screenshots/appreciate.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/screenshots/appreciate.jpeg -------------------------------------------------------------------------------- /screenshots/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/screenshots/img.jpg -------------------------------------------------------------------------------- /screenshots/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/screenshots/img1.jpg -------------------------------------------------------------------------------- /screenshots/zfb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-1/f15d7290e1c9798cc54a9724627ab56b86cb27f6/screenshots/zfb.jpg -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "My TV 1" 17 | include(":app") 18 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | {"version_code": 16780800, "version_name": "v1.0.14"} 2 | --------------------------------------------------------------------------------