diff --git "a/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md" "b/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md"
new file mode 100644
index 0000000..1078808
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md"
@@ -0,0 +1,23 @@
+---
+name: bug-反馈
+about: 描述你所遇到的bug
+title: 问题反馈
+labels: bug
+assignees: yaoxieyoulei
+
+---
+
+### 问题描述
+请提供一个清晰而简明的问题描述。
+
+### 复现步骤
+请提供复现该问题所需的具体步骤。
+
+### 预期行为
+请描述你期望的正确行为或结果。
+
+### 系统信息
+请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
+
+### 相关截图或日志
+如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。
diff --git "a/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.md" "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.md"
new file mode 100644
index 0000000..96905c5
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.md"
@@ -0,0 +1,20 @@
+---
+name: 功能请求
+about: 对于功能的一些建议
+title: ''
+labels: enhancement
+assignees: yaoxieyoulei
+
+---
+
+### 功能描述
+请提供对所请求功能的清晰描述。
+
+### 目标
+请描述你希望通过这个功能实现的目标。
+
+### 解决方案
+如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
+
+### 其他
+请提供已实现该功能或类似功能的应用
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..8b09373
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,41 @@
+name: Android CI
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Build with Gradle
+ # run: ./gradlew build
+ run: ./gradlew assembleDebug
+
+ - name: Set current date as env variable
+ run: |
+ echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
+
+ - name: Upload Release
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: nightly-tag-${{ env.date }}
+ name: release-${{ env.date }}
+ files: ./app/build/outputs/apk/debug/app-debug.apk
+ # files: ./app/build/outputs/apk/release/app-release-unsigned.apk
+ draft: false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..293344a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+/.idea/deploymentTargetDropDown.xml
+/.idea/git_toolbox_prj.xml
+/.idea/deploymentTargetSelector.xml
+/.idea/GrepConsole.xml
+/.idea/material_theme_project_new.xml
+/.idea/other.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+key.properties
+.kotlin/*
+app/debug/*
+*.apk
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..907f736
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+./deploymentTargetSelector.xml
+/git_toolbox_blame.xml
\ No newline at end of file
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..d0fb625
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+My TV
\ No newline at end of file
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
new file mode 100644
index 0000000..b35cab9
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,40 @@
+
+
:tv: Simple IPTV App for Android TV
+ ++ + + +
+ +---- diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..8583565 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,3 @@ +/build +keystore.jks +/release diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..1f734fa --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,123 @@ +import java.io.FileInputStream +import java.util.Properties + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) +} + +val keystorePropertiesFile = rootProject.file("key.properties") +val keystoreProperties = Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "me.lsong.mytv" + compileSdk = 34 + + defaultConfig { + applicationId = "me.lsong.mytv" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.4.4" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + + ndk { + abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86_64")) + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + signingConfigs { + create("release") { + storeFile = + file(System.getenv("KEYSTORE") ?: keystoreProperties["storeFile"] ?: "keystore.jks") + storePassword = System.getenv("KEYSTORE_PASSWORD") + ?: keystoreProperties.getProperty("storePassword") + keyAlias = System.getenv("KEY_ALIAS") ?: keystoreProperties.getProperty("keyAlias") + keyPassword = + System.getenv("KEY_PASSWORD") ?: keystoreProperties.getProperty("keyPassword") + } + } + buildTypes { + getByName("release") { + signingConfig = signingConfigs.getByName("release") + } + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.kotlinx.collections.immutable) + implementation(libs.androidx.material.icons.extended) + + // TV Compose + implementation(libs.androidx.tv.foundation) + implementation(libs.androidx.tv.material) + + // 播放器 + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.exoplayer.hls) + implementation(libs.androidx.media3.exoplayer.rtsp) + + // 序列化 + implementation(libs.kotlinx.serialization) + + // 网络请求 + implementation(libs.okhttp) + implementation(libs.androidasync) + + // 二维码 + implementation(libs.qrose) + implementation(libs.coil.compose) + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar")))) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/libs/lib-decoder-ffmpeg-release.aar b/app/libs/lib-decoder-ffmpeg-release.aar new file mode 100644 index 0000000..dcb9828 Binary files /dev/null and b/app/libs/lib-decoder-ffmpeg-release.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b69d34e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + +d?J(e,r,i,!0,!1,p):A(t,n,o,r,i,l,c,a,p)},H=(e,t,n,o,r,i,l,c,a)=>{let u=0;const d=t.length;let p=e.length-1,h=d-1;for(;u<=p&&u<=h;){const s=e[u],o=t[u]=a?rr(t[u]):or(t[u]);if(!Xo(s,o))break;y(s,o,n,null,r,i,l,c,a),u++}for(;u<=p&&u<=h;){const s=e[p],o=t[h]=a?rr(t[h]):or(t[h]);if(!Xo(s,o))break;y(s,o,n,null,r,i,l,c,a),p--,h--}if(u>p){if(u<=h){const e=h+1,s=e0;let f=!1,m=0,g=!1,v=!1,b=!1,_=!1,S=!1,x=!1;const C=[],k=e=>{u.length&&(d.push(Ql(ou(u),c)),u=[]),e&&d.push(e)},T=({key:e,value:n})=>{if(vc(e)){const r=e.content,l=i(r);if(!l||s&&!o||"onclick"===r.toLowerCase()||"onUpdate:modelValue"===r||w(r)||(_=!0),l&&w(r)&&(x=!0),l&&14===n.type&&(n=n.arguments[0]),20===n.type||(4===n.type||8===n.type)&&Sa(n,t)>0)return;"ref"===r?g=!0:"class"===r?v=!0:"style"===r?b=!0:"key"===r||C.includes(r)||C.push(r),!s||"class"!==r&&"style"!==r||C.includes(r)||C.push(r)}else S=!0};for(let i=0;it(e,n,void 0,r&&r[n])));else{const n=Object.keys(e);o=new Array(n.length);for(let s=0,i=n.length;sv(e)?e:null==e?"":p(e)||b(e)&&(e.toString===S||!g(e.toString))?JSON.stringify(e,se,2):String(e),e.toHandlerKey=F,e.toHandlers=function(e,t){const n={};for(const s in e)n[t&&/[A-Z]/.test(s)?`on:${s}`:F(s)]=e[s];return n},e.toRaw=Ct,e.toRef=function(e,t,n){return It(e)?e:g(e)?new Dt(e):b(e)&&arguments.length>1?Ut(e,t,n):Rt(e)},e.toRefs=function(e){const t=p(e)?new Array(e.length):{};for(const n in e)t[n]=Ut(e,n);return t},e.toValue=function(e){return g(e)?e():Ft(e)},e.transformVNodeArgs=function(e){},e.triggerRef=function(e){Nt(e,4)},e.unref=Ft,e.useAttrs=function(){return Os().attrs},e.useCssModule=function(e="$style"){return n},e.useCssVars=function(e){const t=pr();if(!t)return;const n=t.ut=(n=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach((e=>ci(e,n)))},s=()=>{const s=e(t.proxy);li(t.subTree,s),n(s)};vs((()=>{Mn(s);const e=new MutationObserver(s);e.observe(t.subTree.el.parentNode,{childList:!0}),Ss((()=>e.disconnect()))}))},e.useModel=function(e,t,s=n){const o=pr(),r=I(t),i=O(t),l=Bt(((n,l)=>{let c;return Pn((()=>{const n=e[t];M(c,n)&&(c=n,l())})),{get:()=>(n(),s.get?s.get(c):c),set(e){const n=o.vnode.props;n&&(t in n||r in n||i in n)&&(`onUpdate:${t}`in n||`onUpdate:${r}`in n||`onUpdate:${i}`in n)||!M(e,c)||(c=e,l()),o.emit(`update:${t}`,s.set?s.set(e):e)}}})),c="modelValue"===t?"modelModifiers":`${t}Modifiers`;return l[Symbol.iterator]=()=>{let t=0;return{next:()=>t<2?{value:t++?e[c]||{}:l,done:!1}:{done:!0}}},l},e.useSSRContext=()=>{},e.useSlots=function(){return Os().slots},e.useTransitionState=Kn,e.vModelCheckbox=Di,e.vModelDynamic=zi,e.vModelRadio=ji,e.vModelSelect=Hi,e.vModelText=Vi,e.vShow=oi,e.version=Rr,e.warn=Or,e.watch=Bn,e.watchEffect=function(e,t){return Vn(e,null,t)},e.watchPostEffect=Mn,e.watchSyncEffect=Pn,e.withAsyncContext=function(e){const t=pr();let n=e();return gr(),_(n)&&(n=n.catch((e=>{throw mr(t),e}))),[n,()=>mr(t)]},e.withCtx=gn,e.withDefaults=function(e,t){return null},e.withDirectives=function(e,t){if(null===hn)return e;const s=wr(hn)||hn.proxy,o=e.dirs||(e.dirs=[]);for(let r=0;r