diff --git a/simple_live_tv_app/android/app/.gitignore b/simple_live_tv_app/android/app/.gitignore new file mode 100644 index 00000000..918640ed --- /dev/null +++ b/simple_live_tv_app/android/app/.gitignore @@ -0,0 +1 @@ +/proguardMapping.txt diff --git a/simple_live_tv_app/android/app/build.gradle b/simple_live_tv_app/android/app/build.gradle index e81ecc3c..2adcc978 100644 --- a/simple_live_tv_app/android/app/build.gradle +++ b/simple_live_tv_app/android/app/build.gradle @@ -91,4 +91,27 @@ flutter { source '../..' } -dependencies {} +dependencies { + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + // exoplayer + implementation "androidx.media3:media3-exoplayer:1.5.1" + implementation "androidx.media3:media3-exoplayer-hls:1.5.1" + implementation "androidx.media3:media3-exoplayer-dash:1.5.1" + implementation 'androidx.media3:media3-datasource-okhttp:1.5.1' + implementation "androidx.media3:media3-ui:1.5.1" + // ijkplayer + implementation (name: 'ijk-2024.9.7.1', ext: 'aar') + // okhttp + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' + // 弹窗使用 + implementation 'com.owen:tv-recyclerview:3.0.0' + // Gson + implementation 'com.google.code.gson:gson:2.10.1' + // lombok + annotationProcessor 'org.projectlombok:lombok:1.18.30' + // 弹幕库 + implementation 'com.kuaishou:akdanmaku:1.0.3' +} diff --git a/simple_live_tv_app/android/app/libs/ijk-2024.9.7.1.aar b/simple_live_tv_app/android/app/libs/ijk-2024.9.7.1.aar new file mode 100644 index 00000000..a956a4c5 Binary files /dev/null and b/simple_live_tv_app/android/app/libs/ijk-2024.9.7.1.aar differ diff --git a/simple_live_tv_app/android/app/proguard-rules.pro b/simple_live_tv_app/android/app/proguard-rules.pro new file mode 100644 index 00000000..a3a83269 --- /dev/null +++ b/simple_live_tv_app/android/app/proguard-rules.pro @@ -0,0 +1,248 @@ +############################################# +# +# 对于一些基本指令的添加 +# +############################################# +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontskipnonpubliclibraryclassmembers +-dontpreverify +-verbose +-printmapping proguardMapping.txt +-optimizations !code/simplification/cast,!field/*,!class/merging/* +-keepattributes *Annotation*,InnerClasses +-keepattributes EnclosingMethod, InnerClasses +-keepattributes *Annotation* +-keepattributes Signature +-keepattributes LineNumberTable +-renamesourcefileattribute SourceFile + +# 重新包装所有重命名的包并放在给定的单一包中 +-flattenpackagehierarchy androidx.base + +# 将包里的类混淆成n个再重新打包到一个统一的package中 会覆盖flattenpackagehierarchy选项 +-repackageclasses androidx.base + +# 把混淆类中的方法名也混淆了 +-useuniqueclassmembernames +############################################# +# +# Android开发中一些需要保留的公共部分 +# +############################################# + +# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 +# 因为这些子类都有可能被外部调用 +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class * extends android.view.View + +-keep class com.google.android.material.** { *; } +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** +-keep class androidx.** { *; } +-keep interface androidx.** { *; } +#-keep public class * extends androidx.** + +# 保留R下面的资源 +-keep class **.R$* {*;} + +# 保留本地native方法不被混淆 +-keepclasseswithmembernames class * { + native ; +} + +# 保留在Activity中的方法参数是view的方法, +# 这样以来我们在layout中写的onClick就不会被影响 +-keepclassmembers class * extends android.app.Activity{ + public void *(android.view.View); +} + +# 保留枚举类不被混淆 +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# 保留我们自定义控件(继承自View)不被混淆 +-keep public class * extends android.view.View{ + *** get*(); + void set*(***); + public (android.content.Context); + public (android.content.Context, android.util.AttributeSet); + public (android.content.Context, android.util.AttributeSet, int); +} + +-keep public class * extends androidx.recyclerview.widget.RecyclerView$LayoutManager{ + *** get*(); + void set*(***); + public (android.content.Context); + public (android.content.Context, android.util.AttributeSet); + public (android.content.Context, android.util.AttributeSet, int); +} + +# 保留Parcelable序列化类不被混淆 +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +# 保留Serializable序列化的类不被混淆 +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆 +-keepclassmembers class * { + void *(**On*Event); + void *(**On*Listener); +} +#xwalk +-keep class org.xwalk.core.** { *; } +-keep class org.crosswalk.engine.** { *; } +-keep class org.chromium.** { *; } +-dontwarn android.view.** +-dontwarn android.media.** +-dontwarn org.chromium.** +#okhttp +-dontwarn okhttp3.** +-keep class okhttp3.**{*;} +#okio +-dontwarn okio.** +-keep class okio.**{*;} + +#gson +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# IjkPlayer +-keep class tv.danmaku.ijk.** { *; } +-dontwarn tv.danmaku.ijk.** + +# ExoPlayer +-keep class com.google.androidx.media3.exoplayer.** { *; } +-dontwarn com.google.androidx.media3.exoplayer.** +-keep class androidx.media3.exoplayer.** { *; } +-dontwarn androidx.media3.exoplayer.** + +# lombok +-keep class lombok.** {*;} +-dontwarn lombok.** + +# 实体类 +-keep class com.xycz.simple_live_tv.**{*;} + +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken + +#弹幕库 +-dontwarn com.badlogic.gdx.backends.android.AndroidFragmentApplication +-dontwarn com.badlogic.gdx.utils.GdxBuild +-dontwarn com.badlogic.gdx.jnigen.BuildTarget* +-dontwarn com.badlogic.gdx.graphics.g2d.freetype.FreetypeBuild + +# Required if using Gdx-Controllers extension +-keep class com.badlogic.gdx.controllers.android.AndroidControllers +-keep class com.kuaishou.akdanmaku.** {*;} + +# from app -> build -> outputs -> mapping -> your_app_name -> missing_rules.txt +# Please add these rules to your existing keep rules in order to suppress warnings. +# This is generated automatically by the Android Gradle plugin. +-dontwarn com.android.org.conscrypt.SSLParametersImpl +-dontwarn com.ctc.wstx.stax.WstxInputFactory +-dontwarn com.ctc.wstx.stax.WstxOutputFactory +-dontwarn java.awt.Color +-dontwarn java.awt.Font +-dontwarn java.beans.BeanInfo +-dontwarn java.beans.IntrospectionException +-dontwarn java.beans.Introspector +-dontwarn java.beans.PropertyDescriptor +-dontwarn java.beans.PropertyEditor +-dontwarn javax.activation.ActivationDataFlavor +-dontwarn javax.swing.plaf.FontUIResource +-dontwarn javax.xml.bind.DatatypeConverter +-dontwarn net.sf.cglib.proxy.Callback +-dontwarn net.sf.cglib.proxy.CallbackFilter +-dontwarn net.sf.cglib.proxy.Enhancer +-dontwarn net.sf.cglib.proxy.Factory +-dontwarn net.sf.cglib.proxy.NoOp +-dontwarn net.sf.cglib.proxy.Proxy +-dontwarn nu.xom.Attribute +-dontwarn nu.xom.Builder +-dontwarn nu.xom.Document +-dontwarn nu.xom.Element +-dontwarn nu.xom.Elements +-dontwarn nu.xom.Node +-dontwarn nu.xom.ParentNode +-dontwarn nu.xom.ParsingException +-dontwarn nu.xom.Text +-dontwarn nu.xom.ValidityException +-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl +-dontwarn org.codehaus.jettison.AbstractXMLStreamWriter +-dontwarn org.codehaus.jettison.mapped.Configuration +-dontwarn org.codehaus.jettison.mapped.MappedNamespaceConvention +-dontwarn org.codehaus.jettison.mapped.MappedXMLInputFactory +-dontwarn org.codehaus.jettison.mapped.MappedXMLOutputFactory +-dontwarn org.dom4j.Attribute +-dontwarn org.dom4j.Branch +-dontwarn org.dom4j.Document +-dontwarn org.dom4j.DocumentException +-dontwarn org.dom4j.DocumentFactory +-dontwarn org.dom4j.Element +-dontwarn org.dom4j.io.OutputFormat +-dontwarn org.dom4j.io.SAXReader +-dontwarn org.dom4j.io.XMLWriter +-dontwarn org.dom4j.tree.DefaultElement +-dontwarn org.jdom.Attribute +-dontwarn org.jdom.Content +-dontwarn org.jdom.DefaultJDOMFactory +-dontwarn org.jdom.Document +-dontwarn org.jdom.Element +-dontwarn org.jdom.JDOMException +-dontwarn org.jdom.JDOMFactory +-dontwarn org.jdom.Text +-dontwarn org.jdom.input.SAXBuilder +-dontwarn org.jdom2.Attribute +-dontwarn org.jdom2.Content +-dontwarn org.jdom2.DefaultJDOMFactory +-dontwarn org.jdom2.Document +-dontwarn org.jdom2.Element +-dontwarn org.jdom2.JDOMException +-dontwarn org.jdom2.JDOMFactory +-dontwarn org.jdom2.Text +-dontwarn org.jdom2.input.SAXBuilder +-dontwarn org.joda.time.DateTime +-dontwarn org.joda.time.DateTimeZone +-dontwarn org.joda.time.format.DateTimeFormatter +-dontwarn org.joda.time.format.ISODateTimeFormat +-dontwarn org.kxml2.io.KXmlParser +-dontwarn org.xmlpull.mxp1.MXParser + +-dontwarn kotlin.jvm.internal.SourceDebugExtension \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/AndroidManifest.xml b/simple_live_tv_app/android/app/src/main/AndroidManifest.xml index 54151819..9825c3b3 100644 --- a/simple_live_tv_app/android/app/src/main/AndroidManifest.xml +++ b/simple_live_tv_app/android/app/src/main/AndroidManifest.xml @@ -1,17 +1,18 @@ - - - - - + + + + + android:resource="@style/NormalTheme" /> + - - + + + + + + + + + diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/LiveApplication.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/LiveApplication.java new file mode 100644 index 00000000..304cc539 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/LiveApplication.java @@ -0,0 +1,68 @@ +package com.xycz.simple_live_tv; + +import android.app.Application; + +import androidx.ijk.IJK; +import androidx.ijk.enums.Display; + +import tv.danmaku.ijk.media.player.IjkMediaPlayer; + +/** + * Created by wangyan on 2024/12/21 + */ +public class LiveApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + initIjkConfig(); + } + + private void initIjkConfig() { + IJK ijk = IJK.config(); + // 设置默认显示方式 + ijk.display(Display.AUTO); + // 设置默认显示比例 + ijk.ratio(16,9); + // 使用硬解码器解码 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1); + // 自动旋转视频画面 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1); + // 处理分辨率变化 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1); + // 设置最大缓冲区大小(默认是0,表示无限制) + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", 1024*1024*5); + // 设置最小缓冲帧数 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 60); + // 设置最大缓存时长 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 5000); + // 设置启动时的探测时间(毫秒) + ijk.option(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 400); + // 设置分析最大时长(毫秒) + ijk.option(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100); + // 强制刷新数据包 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1L); + // 禁用数据包缓冲 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0L); + // 设置帧率为30 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fps", 120); + // 设置超时时间 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 10000); + // 启用无限缓冲模式 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "infbuf", 0); + // 启用帧丢弃 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1); + // 跳过环路过滤器(Loop Filter),提高解码性能 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "skip_loop_filter", 48); + // 禁用 HTTP 资源范围检测 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "http-detect-range-support", 1); + // 启用精确的 seek(定位) + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1); + // 清除DNS缓存(为了提高域名解析的效率) + ijk.option(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1); + // 自动重新连接 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1); + // 调用prepareAsync()方法后是否自动开始播放 + ijk.option(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/ExoLiveActivity.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/ExoLiveActivity.java new file mode 100644 index 00000000..a0e84fa0 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/ExoLiveActivity.java @@ -0,0 +1,152 @@ +package com.xycz.simple_live_tv.activity; + +import android.os.Build; +import android.widget.RelativeLayout; + +import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.Player; +import androidx.media3.common.VideoSize; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.datasource.HttpDataSource; +import androidx.media3.datasource.okhttp.OkHttpDataSource; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.core.BaseActivity; +import com.xycz.simple_live_tv.core.FlutterManager; +import com.xycz.simple_live_tv.core.LogUtils; +import com.xycz.simple_live_tv.core.OkHttpManager; +import com.xycz.simple_live_tv.player.ExoPlayerView; + +public class ExoLiveActivity extends BaseActivity implements Player.Listener { + + private static final String TAG = "ExoLiveActivity"; + private ExoPlayer exoPlayer; + private ExoPlayerView playerView; + + @Override + protected void initViews() { + super.initViews(); + playerView = new ExoPlayerView(this); + playerView.setUseController(false); + playerView.setId(R.id.player_view); + playerView.setFocusable(false); + playerView.setClickable(false); + player = playerView; + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); + playerLayout.addView(playerView, 0, layoutParams); + } + + @Override + protected void onStart() { + super.onStart(); + if (Build.VERSION.SDK_INT >= 24) { + prepareToPlay(); + } + } + + @Override + protected void onResume() { + super.onResume(); + playerView.onResume(); + if (Build.VERSION.SDK_INT < 24) { + prepareToPlay(); + } + } + + @Override + protected void onPause() { + super.onPause(); + playerView.onPause(); + if (Build.VERSION.SDK_INT < 24) { + exoPlayer.release(); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (Build.VERSION.SDK_INT >= 24) { + exoPlayer.release(); + } + } + + @Override + protected void initExoPlayer() { + OkHttpDataSource.Factory okHttpDataSource = new OkHttpDataSource.Factory(OkHttpManager.getInstance().getOkHttpClient()); + DefaultDataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this, okHttpDataSource); + exoPlayer = new ExoPlayer.Builder(this) + .setMediaSourceFactory(new DefaultMediaSourceFactory(this).setDataSourceFactory(dataSourceFactory)) + .build(); + // 关联ExoPlayer与PlayerView + playerView.setPlayer(exoPlayer); + + // 添加事件监听器 + exoPlayer.addListener(this); + + // 自动开始播放 + exoPlayer.setPlayWhenReady(true); + + // 注册播放停止函数 + FlutterManager.getInstance().registerMethod("stopPlay"); + } + + @Override + public void prepareToPlay() { + super.prepareToPlay(); + String videoUrl = ""; + if (liveModel != null && !liveModel.isPlayEmpty()) { + videoUrl = liveModel.getLine(); + } + MediaItem mediaItem = MediaItem.fromUri(videoUrl); + exoPlayer.setMediaItem(mediaItem); + // 准备播放 + exoPlayer.prepare(); + exoPlayer.play(); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + this.isPlaying = isPlaying; + if (!isPlaying) { + // 播放结束后的处理,比如自动播放下一集(如果有)等 + FlutterManager.getInstance().invokerFlutterMethod("mediaEnd", null); + } + } + + @Override + public void onPlayerError(PlaybackException error) { + // 详细的错误处理,根据不同错误类型提示用户或者记录日志等 + String errorMessage = error.getMessage(); + Throwable cause = error.getCause(); + if (cause instanceof HttpDataSource.HttpDataSourceException) { + // An HTTP error occurred. + HttpDataSource.HttpDataSourceException httpError = (HttpDataSource.HttpDataSourceException) cause; + // It's possible to find out more about the error both by casting and by querying + // the cause. + if (httpError instanceof HttpDataSource.InvalidResponseCodeException) { + // Cast to InvalidResponseCodeException and retrieve the response code, message + // and headers. + HttpDataSource.InvalidResponseCodeException invalidResponseCodeException = (HttpDataSource.InvalidResponseCodeException)httpError; + errorMessage = invalidResponseCodeException.getMessage(); + if (invalidResponseCodeException.responseCode == 302) { + return; + } + } else { + // Try calling httpError.getCause() to retrieve the underlying cause, although + // note that it may be null. + errorMessage = httpError.getCause() == null ? "" : httpError.getCause().getMessage(); + } + } + LogUtils.e(TAG, errorMessage, error); + FlutterManager.getInstance().invokerFlutterMethod("mediaError", errorMessage); + } + + @Override + public void onVideoSizeChanged(VideoSize videoSize) { + LogUtils.i(TAG, videoSize.width + "x" + videoSize.height); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/FlutterActivity.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/FlutterActivity.java new file mode 100644 index 00000000..b0d6d695 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/FlutterActivity.java @@ -0,0 +1,170 @@ +package com.xycz.simple_live_tv.activity; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; + +import com.xycz.simple_live_tv.core.LogUtils; +import com.xycz.simple_live_tv.core.MessageManager; +import com.xycz.simple_live_tv.core.FlutterManager; +import com.xycz.simple_live_tv.core.MethodCallModel; +import com.xycz.simple_live_tv.core.OkHttpManager; +import com.xycz.simple_live_tv.player.NativePlayerView; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Request; +import okhttp3.Response; + +import static com.xycz.simple_live_tv.core.MessageManager.FLUTTER_TO_JAVA_CMD; + +public class FlutterActivity extends io.flutter.embedding.android.FlutterActivity implements Handler.Callback { + + private static final String TAG = "FlutterActivity"; + private static final String TEST_APK_URL = "http://192.168.100.1:12321/test.apk"; + private File tempApk; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + tempApk = new File(getExternalCacheDir(), "test.apk"); + MessageManager.getInstance().registerCallback(this); + } + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + FlutterManager.getInstance().initFlutter(flutterEngine); + FlutterManager.getInstance().registerMethod("openLivePage"); + FlutterManager.getInstance().registerMethod("checkTestUpdate"); + flutterEngine.getPlatformViewsController().getRegistry().registerViewFactory("nativeVideoView", new PlatformViewFactory(null) { + @NonNull + @Override + public PlatformView create(Context context, int viewId, @Nullable Object args) { + return new NativePlayerView(context); + } + }); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + MessageManager.getInstance().unRegisterCallback(this); + } + + @Override + public boolean handleMessage(@NonNull Message message) { + if (message.what != FLUTTER_TO_JAVA_CMD) { + return false; + } + + MethodCallModel model = (MethodCallModel)message.obj; + if (message.arg1 == "openLivePage".hashCode()) { + Integer playerMode = model.getMethodCall().arguments(); + Intent intent; + if (playerMode == null || playerMode == 0) { + intent = new Intent(this, IjkLiveActivity.class); + } else { + intent = new Intent(this, ExoLiveActivity.class); + } + startActivity(intent); + } else if (message.arg1 == "checkTestUpdate".hashCode()) { + downloadApk(); + } else { + return false; + } + + return true; + } + + private void installAPK() { + if (!tempApk.exists()) { + LogUtils.w("Test", "tempApk not exist: " + tempApk.getAbsolutePath()); + return; + } + + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 安装完成后打开新版本 + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 给目标应用一个临时授权 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 判断版本大于等于7.0 + // 如果SDK版本>=24,即:Build.VERSION.SDK_INT >= 24,使用FileProvider兼容安装apk + String packageName = this.getApplicationContext().getPackageName(); + String authority = packageName + ".fileprovider"; + Uri apkUri = FileProvider.getUriForFile(this, authority, tempApk); + intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); + } else { + intent.setDataAndType(Uri.fromFile(tempApk), "application/vnd.android.package-archive"); + } + + this.startActivity(intent); + } catch (Exception e) { + LogUtils.e(TAG, "install failed", e); + } + } + + private void downloadApk() { + if (tempApk.exists() && tempApk.delete()) { + LogUtils.w(TAG, "Delete exist apk!"); + } + + Request request = new Request.Builder() + .get() + .url(FlutterActivity.TEST_APK_URL) + .build(); + Call call = OkHttpManager.getInstance().getOkHttpClient().newCall(request); + call.enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + LogUtils.e(TAG, "http error", e); + if (tempApk.exists() && tempApk.delete()) { + LogUtils.e(TAG, "delete apk", e); + } + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + if (!response.isSuccessful()) { + return; + } + + InputStream inputStream = Objects.requireNonNull(response.body()).byteStream(); + FileOutputStream fileOutputStream = new FileOutputStream(tempApk); + try { + byte[] buffer = new byte[2048]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + fileOutputStream.write(buffer, 0, len); + } + fileOutputStream.flush(); + installAPK(); + } catch (IOException e) { + LogUtils.e(TAG, "download error", e); + } finally { + inputStream.close(); + fileOutputStream.close(); + response.close(); + } + } + }); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/IjkLiveActivity.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/IjkLiveActivity.java new file mode 100644 index 00000000..877441ab --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/activity/IjkLiveActivity.java @@ -0,0 +1,118 @@ +package com.xycz.simple_live_tv.activity; + +import android.net.Uri; +import android.text.TextUtils; +import android.view.View; +import android.widget.RelativeLayout; +import androidx.ijk.widget.VideoHolder; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.core.BaseActivity; +import com.xycz.simple_live_tv.core.FlutterManager; +import com.xycz.simple_live_tv.core.LogUtils; +import com.xycz.simple_live_tv.core.MessageManager; +import com.xycz.simple_live_tv.player.IjkPlayerView; +import com.xycz.simple_live_tv.player.VideoPlayerListener; +import tv.danmaku.ijk.media.player.IMediaPlayer; + +public class IjkLiveActivity extends BaseActivity implements VideoPlayerListener { + + private IjkPlayerView playerView; + + @Override + protected void initViews() { + super.initViews(); + playerView = new IjkPlayerView(this); + playerView.setId(R.id.player_view); + playerView.setFocusable(false); + playerView.setClickable(false); + playerView.getVideoHolder().setControlGroupVisibility(false); + playerView.setLiveSource(true); + player = playerView; + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); + playerLayout.addView(playerView, 0, layoutParams); + } + + @Override + protected void onStop() { + super.onStop(); + playerView.stop(); + playerView.release(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + playerView.destroy(); + // 移除进度更新任务 + MessageManager.getInstance().unRegisterCallback(this); + FlutterManager.getInstance().invokerFlutterMethod("onDestroy", null); + } + + @Override + protected void initExoPlayer() { + // 修改默认的显示方式 + // playerView.setDisplay(Display.AUTO); + // 修改模式显示比例,注意:比例修改只适用Display.RATIO_WIDTH和Display.RATIO_HEIGHT + // playerView.setRatio(Display.RATIO_WIDTH,16,9); + // 视频控制ViewHolder + VideoHolder holder = playerView.getVideoHolder(); + holder.getControlGroup().setVisibility(View.GONE); + playerView.setOnIJKVideoListener(this); + // 自定义全屏还是小屏幕显示,不设置就采用默认的逻辑; + playerView.setOnVideoSwitchScreenListener(orientation -> { + //TODO: 自定显示方式 + }); + + // 注册播放停止函数 + FlutterManager.getInstance().registerMethod("stopPlay"); + } + + @Override + public void prepareToPlay() { + super.prepareToPlay(); + + // 播放视频 + String videoUrl = ""; + if (liveModel != null && !liveModel.isPlayEmpty()) { + videoUrl = liveModel.getLine(); + } + + // 是否是直播源 + playerView.setLiveSource(true); + // 开始播放 + String source = playerView.getDataSource(); + if (TextUtils.isEmpty(source)) { + playerView.setDataSource(Uri.parse(videoUrl), liveModel.getHeaderMap()); + playerView.start(); + } else { + playerView.reset(); + playerView.setDataSource(Uri.parse(videoUrl), liveModel.getHeaderMap()); + playerView.prepareAsync(); + } + } + + @Override + public void onVideoPrepared(IMediaPlayer var1) { + isPlaying = true; + } + + @Override + public void onVideoCompletion(IMediaPlayer var1) { + FlutterManager.getInstance().invokerFlutterMethod("mediaEnd", null); + isPlaying = false; + } + + @Override + public void onVideoError(IMediaPlayer mp, int what, int extra) { + LogUtils.w("Test", "what = " + what + " extra = " + extra); + FlutterManager.getInstance().invokerFlutterMethod("mediaError", extra); + isPlaying = false; + } + + @Override + public void onVideoSizeChanged(IMediaPlayer mp, int width, int height, int sar_num, int sar_den) { + LogUtils.w("Test", "onVideoSizeChanged=> " + width + "x" + height); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/adapter/SelectAdapter.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/adapter/SelectAdapter.java new file mode 100644 index 00000000..2256eeb5 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/adapter/SelectAdapter.java @@ -0,0 +1,96 @@ +package com.xycz.simple_live_tv.adapter; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import com.xycz.simple_live_tv.R; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +public class SelectAdapter extends ListAdapter { + + private boolean muteCheck = false; + + public static class SelectViewHolder extends RecyclerView.ViewHolder { + public SelectViewHolder(@NonNull View itemView) { + super(itemView); + } + } + + public interface ISelectDialog { + void click(T key, String value); + } + + private final LinkedHashMap dataMap = new LinkedHashMap<>(); + private final List keyList = new ArrayList<>(); + + private T select = null; + + private ISelectDialog dialogInterface = null; + + public SelectAdapter(ISelectDialog dialogInterface, DiffUtil.ItemCallback diffCallback) { + this(dialogInterface, diffCallback, false); + } + + public SelectAdapter(ISelectDialog dialogInterface, DiffUtil.ItemCallback diffCallback, boolean muteCheck) { + super(diffCallback); + this.dialogInterface = dialogInterface; + this.muteCheck = muteCheck; + } + + public void setData(LinkedHashMap newData, T defaultSelect) { + dataMap.clear(); + dataMap.putAll(newData); + keyList.clear(); + keyList.addAll(newData.keySet()); + select = defaultSelect; + notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + return dataMap.size(); + } + + + @NonNull + @Override + public SelectViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new SelectViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_dialog_select, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull SelectAdapter.SelectViewHolder holder, @SuppressLint("RecyclerView") int position) { + T selectKey = keyList.get(position); + final String value = dataMap.get(selectKey); + String showValue = value; + if (!muteCheck && selectKey == select) { + showValue = "√ " + showValue; + } + + ((TextView) holder.itemView.findViewById(R.id.tvName)).setText(showValue); + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!muteCheck && selectKey == select) { + return; + } + + notifyItemChanged(position); + select = selectKey; + notifyItemChanged(position); + dialogInterface.click(selectKey, value); + } + }); + } +} \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/BaseActivity.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/BaseActivity.java new file mode 100644 index 00000000..a6e202c9 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/BaseActivity.java @@ -0,0 +1,525 @@ +package com.xycz.simple_live_tv.core; + +import android.animation.ObjectAnimator; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.adapter.SelectAdapter; +import com.xycz.simple_live_tv.core.setting.ButtonDelegate; +import com.xycz.simple_live_tv.core.setting.LineDelegate; +import com.xycz.simple_live_tv.core.setting.SelectDelegate; +import com.xycz.simple_live_tv.danmaku.DanmakuRender; +import com.xycz.simple_live_tv.model.LiveModel; +import com.xycz.simple_live_tv.multitype.MultiTypeAdapter; +import com.google.gson.Gson; +import com.kuaishou.akdanmaku.DanmakuConfig; +import com.kuaishou.akdanmaku.data.DanmakuItem; +import com.kuaishou.akdanmaku.data.DanmakuItemData; +import com.kuaishou.akdanmaku.data.DataSource; +import com.kuaishou.akdanmaku.ui.DanmakuPlayer; +import com.kuaishou.akdanmaku.ui.DanmakuView; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +import static com.xycz.simple_live_tv.core.MessageManager.FLUTTER_TO_JAVA_CMD; + +/** + * Created by wangyan on 2024/12/19 + */ +public abstract class BaseActivity extends AppCompatActivity implements View.OnClickListener, Handler.Callback, Runnable { + + protected LiveModel liveModel; + protected IVideoPlayer player; + + protected RelativeLayout playerLayout; + protected TextView liveTitle; + protected View topControlLayout; + protected RecyclerView settingRecycler; + protected final MultiTypeAdapter multiTypeAdapter = new MultiTypeAdapter(); + + protected long lastRequestFocusTime; + protected boolean isPlaying = false; + protected boolean danmakuSwitch = true; + protected volatile boolean isShowControl = true; + protected final Gson gson = new Gson(); + protected final Handler handler = new Handler(Looper.getMainLooper()); + protected DanmakuPlayer danmakuPlayer; + protected DataSource dataSource; + + // 弹幕文本大小 + protected int danmakuTextSize = 40; + // 弹幕描边宽度 + protected int danmakuStrokeWidth = 0; + // 弹幕透明度 (1-10代表10%到100%) + protected int danmakuOpacity = 10; + // 弹幕是否播放 + protected boolean danmakuPlaying = true; + // 弹幕显示方式 + protected boolean danmakuRollStyle = true; + // 弹幕绘制类 + protected DanmakuRender danmakuRender = new DanmakuRender(); + // 双击退出页面 + protected long lastBackTime = 0; + + protected abstract void initExoPlayer(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_live); + // 先初始化数据 + initData(); + + // 初始化视图组件 + initViews(); + + // 初始化ExoPlayer及相关配置 + initExoPlayer(); + + // 设置播放控制按钮点击事件 + setButtonClickListeners(); + } + + @Override + protected void onResume() { + super.onResume(); + FlutterManager.getInstance().invokerFlutterMethod("onResume", null); + settingRecycler.post(this); + } + + @Override + protected void onPause() { + super.onPause(); + danmakuPlayer.pause(); + FlutterManager.getInstance().invokerFlutterMethod("onPause", null); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // 移除进度更新任务 + MessageManager.getInstance().unRegisterCallback(this); + FlutterManager.getInstance().invokerFlutterMethod("onDestroy", null); + handler.removeCallbacksAndMessages(null); + if (danmakuPlayer != null) { + danmakuPlayer.release(); + danmakuPlayer = null; + } + } + + @Override + public void onBackPressed() { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastBackTime < 3000) { + super.onBackPressed(); + } else { + Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show(); + lastBackTime = currentTime; + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + lastRequestFocusTime = System.currentTimeMillis(); + return super.onTouchEvent(event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event == null) { + return super.dispatchKeyEvent(null); + } + + int keyCode = event.getKeyCode(); + int action = event.getAction(); + lastRequestFocusTime = System.currentTimeMillis(); + if (action == KeyEvent.ACTION_UP) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (isShowControl) { + hideControlView(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + if (isShowControl) { + break; + } + + if (danmakuPlaying) { + danmakuPlaying = false; + danmakuPlayer.pause(); + } else { + danmakuPlaying = true; + danmakuPlayer.start(danmakuPlayer.getConfig()); + } + return true; + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_LEFT: + showControlView(); + return true; + } + } + + return super.dispatchKeyEvent(event); + } + + @CallSuper + protected void initViews() { + hideSystemUI(false); + playerLayout = findViewById(R.id.player_layout); + liveTitle = findViewById(R.id.tv_video_title); + topControlLayout = findViewById(R.id.top_control_bar); + settingRecycler = findViewById(R.id.setting_recyclerview); + multiTypeAdapter.register(SelectDelegate.SelectModel.class, new SelectDelegate()); + multiTypeAdapter.register(Boolean.class, new LineDelegate()); + multiTypeAdapter.register(ButtonDelegate.ButtonModel.class, new ButtonDelegate()); + this.settingRecycler.setLayoutManager(new LinearLayoutManager(this)); + this.settingRecycler.setAdapter(multiTypeAdapter); + danmakuTextSize = getResources().getDimensionPixelSize(R.dimen.ds40); + DanmakuView danmakuView = findViewById(R.id.container); + danmakuView.setLayerType(View.LAYER_TYPE_NONE, null); + dataSource = new DataSource(); + danmakuPlayer = new DanmakuPlayer(danmakuRender, dataSource); + danmakuPlayer.bindView(danmakuView); + } + + @CallSuper + protected void initData() { + MessageManager.getInstance().registerCallback(this); + FlutterManager.getInstance().registerMethod("parseLiveUrl"); + FlutterManager.getInstance().registerMethod("stopPlay"); + FlutterManager.getInstance().registerMethod("danmaku"); + FlutterManager.getInstance().invokerFlutterMethod("onCreate", null); + } + + @CallSuper + public void prepareToPlay() { + if (liveModel == null) { + return; + } + + this.multiTypeAdapter.setItems(buildSettingData()); + this.multiTypeAdapter.notifyDataSetChanged(); + if (liveModel.getRoomTitle() != null) { + liveTitle.setText(liveModel.getRoomTitle()); + } else { + liveTitle.setText(liveModel.getName()); + } + + DanmakuConfig danmakuConfig = danmakuPlayer.getConfig() == null ? + new DanmakuConfig() : danmakuPlayer.getConfig(); + danmakuConfig.setAllowOverlap(false); + danmakuConfig.setVisibility(true); + danmakuConfig.setBold(true); + danmakuConfig.setDurationMs(2000); + danmakuConfig.setRollingDurationMs(4000); + danmakuPlayer.start(danmakuConfig); + } + + @CallSuper + protected void setButtonClickListeners() { + findViewById(R.id.back_layout).setOnClickListener(this); + findViewById(R.id.more_layout).setOnClickListener(this); + findViewById(R.id.container).setOnClickListener(this); + } + + @Override + public boolean handleMessage(@NonNull Message message) { + if (message.what == FLUTTER_TO_JAVA_CMD) { + MethodCallModel model = (MethodCallModel)message.obj; + if (message.arg1 == "stopPlay".hashCode()) { + player.stop(); + } else if (message.arg1 == "parseLiveUrl".hashCode()) { + parseLiveUrl(model.getMethodCall(), model.getResult()); + } else if (message.arg1 == "danmaku".hashCode()) { + if (!danmakuSwitch) { + return true; + } + + String info = model.getMethodCall().argument("message"); + String color = model.getMethodCall().argument("color"); + if (info == null) { + return true; + } + + int style = danmakuRollStyle ? + DanmakuItemData.DANMAKU_MODE_ROLLING : + (System.currentTimeMillis() % 2 == 0 ? + DanmakuItemData.DANMAKU_MODE_CENTER_TOP : + DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM); + // 发送弹幕 + DanmakuItemData data = new DanmakuItemData( + danmakuPlayer.getCurrentTimeMs(), danmakuPlayer.getCurrentTimeMs(), info, + style, danmakuTextSize, getColor(color), + 1, DanmakuItemData.DANMAKU_STYLE_NONE, 1, + 100L, DanmakuItemData.MERGED_TYPE_NORMAL); + DanmakuItem danmakuItem = danmakuPlayer.obtainItem(data); + danmakuRender.setStrokeWidth(danmakuStrokeWidth); + danmakuPlayer.send(danmakuItem); + } else { + return false; + } + + return true; + } else { + return false; + } + } + + @Override + public void onClick(View view) { + int viewId = view.getId(); + if (viewId == R.id.back_layout) { + onBackPressed(); + } else if (viewId == R.id.more_layout) { + // 这里可以弹出更多功能菜单,比如画质切换等功能 + Toast.makeText(this, "更多功能待完善", Toast.LENGTH_SHORT).show(); + } else if (viewId == R.id.container) { + if (!isShowControl) { + showControlView(); + } else { + hideControlView(); + } + } + } + + @Override + public void run() { + if (System.currentTimeMillis() - lastRequestFocusTime > 3000) { + hideControlView(); + } else { + handler.postDelayed(this, 3000); + } + } + + private void hideSystemUI(boolean shownavbar) { + int uiVisibility = getWindow().getDecorView().getSystemUiVisibility(); + uiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + uiVisibility |= View.SYSTEM_UI_FLAG_LOW_PROFILE; + uiVisibility |= View.SYSTEM_UI_FLAG_FULLSCREEN; + uiVisibility |= View.SYSTEM_UI_FLAG_IMMERSIVE; + uiVisibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + if (!shownavbar) { + uiVisibility |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + uiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + } + getWindow().getDecorView().setSystemUiVisibility(uiVisibility); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + protected void parseLiveUrl(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + liveModel = gson.fromJson((String) call.arguments, LiveModel.class); + OkHttpManager.getInstance().resetRequestHeader(liveModel.getHeaderMap()); + if (liveModel.getPlayUrls() != null && !liveModel.getPlayUrls().isEmpty()) { + result.success(true); + prepareToPlay(); + return; + } + + result.success(false); + } + + protected void showControlView() { + if (isShowControl) { + return; + } + + isShowControl = true; + ObjectAnimator topAnimator = ObjectAnimator.ofFloat(topControlLayout, "translationY", -topControlLayout.getHeight(), 0); + topAnimator.setDuration(400); // 动画时长 + topAnimator.start(); + + ObjectAnimator recyclerViewAnimator = ObjectAnimator.ofFloat(settingRecycler, "translationX", settingRecycler.getWidth(), 0); + recyclerViewAnimator.setDuration(400); // 动画时长 + recyclerViewAnimator.start(); + + handler.removeCallbacks(this); + handler.postDelayed(this, 3000); + } + + protected void hideControlView() { + if (!isShowControl) { + return; + } + + isShowControl = false; + ObjectAnimator topAnimator = ObjectAnimator.ofFloat(topControlLayout, "translationY", 0, -topControlLayout.getHeight()); + topAnimator.setDuration(400); // 动画时长 + topAnimator.start(); + + ObjectAnimator recyclerViewAnimator = ObjectAnimator.ofFloat(settingRecycler, "translationX", 0, settingRecycler.getWidth()); + recyclerViewAnimator.setDuration(400); // 动画时长 + recyclerViewAnimator.start(); + } + + private List buildSettingData() { + ButtonDelegate.ButtonModel settingTitle = new ButtonDelegate.ButtonModel("设置", new View.OnClickListener() { + @Override + public void onClick(View v) { + FlutterManager.getInstance().invokerFlutterMethod("refresh", null); + } + }); + + LinkedHashMap followMap = new LinkedHashMap<>(); + followMap.put(true, "是"); + followMap.put(false, "否"); + SelectDelegate.SelectModel followSetting = new SelectDelegate.SelectModel("关注用户", followMap, liveModel.isFollowed(), new SelectAdapter.ISelectDialog() { + @Override + public void click(Boolean key, String value) { + boolean result = key; + if (result == liveModel.isFollowed()) { + return; + } + + FlutterManager.getInstance().invokerFlutterMethod("followUser", null, new FlutterManager.Result() { + @Override + public void success(@Nullable Object result) { + if (result == null) { + return; + } + + boolean followed = (boolean) result; + liveModel.setFollowed(followed); + Toast.makeText(BaseActivity.this, followed ? "关注成功!" : "取消关注成功!", Toast.LENGTH_SHORT).show(); + } + }); + } + }); + + SelectDelegate.SelectModel clarityAndLine = new SelectDelegate.SelectModel("清晰度与线路", null, null, null); + LinkedHashMap clarityMap = new LinkedHashMap<>(); + for (int index = 0; index < liveModel.getQualites().size(); index++) { + String quality = liveModel.getQualites().get(index); + clarityMap.put(index, quality); + } + SelectDelegate.SelectModel claritySelect = new SelectDelegate.SelectModel(getResources().getString(R.string.clarity), clarityMap, liveModel.getCurrentQuality(), new SelectAdapter.ISelectDialog() { + @Override + public void click(Integer key, String value) { + liveModel.setCurrentQuality(key); + FlutterManager.getInstance().invokerFlutterMethod("changeQuality", key); + } + }); + + LinkedHashMap lineMap = new LinkedHashMap<>(); + for (int index = 0; index < liveModel.getPlayUrls().size(); index++) { + lineMap.put(index, "线路" + (index + 1)); + } + + SelectDelegate.SelectModel lineSelect = new SelectDelegate.SelectModel<>("线路", lineMap, liveModel.getCurrentLineIndex(), new SelectAdapter.ISelectDialog() { + @Override + public void click(Integer key, String value) { + liveModel.setCurrentLineIndex(key); + FlutterManager.getInstance().invokerFlutterMethod("changeLine", key); + } + }); + + SelectDelegate.SelectModel danmaku = new SelectDelegate.SelectModel("弹幕", null, null, null); + LinkedHashMap danmakuMap = new LinkedHashMap<>(); + danmakuMap.put(true, "开"); + danmakuMap.put(false, "关"); + SelectDelegate.SelectModel danmakuStatus = new SelectDelegate.SelectModel<>("弹幕开关", danmakuMap, danmakuSwitch, new SelectAdapter.ISelectDialog() { + @Override + public void click(Boolean key, String value) { + danmakuSwitch = key; + } + }); + + LinkedHashMap danmakuSizeMap = new LinkedHashMap<>(); + danmakuSizeMap.put(24, "24"); + danmakuSizeMap.put(32, "32"); + danmakuSizeMap.put(40, "40"); + danmakuSizeMap.put(48, "48"); + danmakuSizeMap.put(56, "56"); + danmakuSizeMap.put(64, "64"); + danmakuSizeMap.put(72, "72"); + SelectDelegate.SelectModel danmakuSizeModel = new SelectDelegate.SelectModel<>("弹幕大小", danmakuSizeMap, danmakuTextSize, new SelectAdapter.ISelectDialog() { + @Override + public void click(Integer key, String value) { + danmakuTextSize = key; + } + }); + + LinkedHashMap opacityMap = new LinkedHashMap<>(); + opacityMap.put(10, "10%"); + opacityMap.put(20, "20%"); + opacityMap.put(30, "30%"); + opacityMap.put(40, "40%"); + opacityMap.put(50, "50%"); + opacityMap.put(60, "60%"); + opacityMap.put(70, "70%"); + opacityMap.put(80, "80%"); + opacityMap.put(90, "90%"); + opacityMap.put(100, "100%"); + SelectDelegate.SelectModel danmakuOpacitySetting = new SelectDelegate.SelectModel<>("不透明度", opacityMap, danmakuOpacity, new SelectAdapter.ISelectDialog() { + @Override + public void click(Integer key, String value) { + danmakuOpacity = key; + } + }); + + LinkedHashMap strokeMap = new LinkedHashMap<>(); + strokeMap.put(0, "0"); + strokeMap.put(2, "2"); + strokeMap.put(4, "4"); + strokeMap.put(6, "6"); + strokeMap.put(8, "8"); + strokeMap.put(10, "10"); + strokeMap.put(12, "12"); + strokeMap.put(14, "14"); + strokeMap.put(16, "16"); + SelectDelegate.SelectModel danmakuStroke = new SelectDelegate.SelectModel<>("描边宽度", strokeMap, danmakuStrokeWidth, new SelectAdapter.ISelectDialog() { + @Override + public void click(Integer key, String value) { + danmakuStrokeWidth = key; + } + }); + + LinkedHashMap danmakuStyleMap = new LinkedHashMap<>(Map.of(false, "固定", true, "滚动")); + danmakuStyleMap.put(true, "滚动"); + danmakuStyleMap.put(false, "固定"); + SelectDelegate.SelectModel danmakuStyleSetting = new SelectDelegate.SelectModel<>("显示方式", danmakuStyleMap, danmakuRollStyle, new SelectAdapter.ISelectDialog() { + @Override + public void click(Boolean key, String value) { + danmakuRollStyle = key; + } + }); + + return List.of(settingTitle, true, followSetting, true, clarityAndLine, claritySelect, lineSelect, true, danmaku, danmakuStatus, danmakuSizeModel, danmakuOpacitySetting, danmakuStroke, danmakuStyleSetting); + } + + private int getColor(String colorStr) { + int color = 0xFFFFFFFF; + try { + color = Color.parseColor(colorStr); + } catch (Exception ignore) {} + + int opacity = (int) (0xFF * (danmakuOpacity * 1.0f / 10)) << 24 | 0x00FFFFFF; + color = color & opacity; + + return color; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/FlutterManager.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/FlutterManager.java new file mode 100644 index 00000000..455afcd0 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/FlutterManager.java @@ -0,0 +1,85 @@ +package com.xycz.simple_live_tv.core; + +import android.os.Message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +/** + * Created by wangyan on 2024/12/20 + */ +public class FlutterManager implements MethodChannel.MethodCallHandler { + private static volatile FlutterManager manager; + + private FlutterEngine flutterEngine; + private static final String CHANNEL_NAME = "samples.flutter.jumpto.android"; + private MethodChannel channel; + private final Map methodMap = new HashMap(); + + private FlutterManager() { + } + + public static FlutterManager getInstance() { + if (manager == null) { + synchronized (FlutterManager.class) { + if (manager == null) { + manager = new FlutterManager(); + } + } + } + + return manager; + } + + public void initFlutter(FlutterEngine flutterEngine) { + this.flutterEngine = flutterEngine; + this.channel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger() + , CHANNEL_NAME); + this.channel.setMethodCallHandler(this); + } + + public void registerMethod(@NonNull String methodName) { + methodMap.put(methodName, methodName.hashCode()); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + LogUtils.w("Test", "onMethodCall=>" + call.method + " args: " + call.arguments); + + Integer methodCode = methodMap.get(call.method); + if (methodCode == null) { + return; + } + + Message message = Message.obtain(); + message.what = MessageManager.FLUTTER_TO_JAVA_CMD; + message.arg1 = methodCode; + message.obj = new MethodCallModel(call, result); + MessageManager.getInstance().sendMessage(message); + } + + public void invokerFlutterMethod(String methodName, Object arguments) { + LogUtils.w("Test", "invokerFlutterMethod=>" + methodName + " args=>" + arguments); + this.channel.invokeMethod(methodName, arguments); + } + + public void invokerFlutterMethod(String methodName, Object arguments, MethodChannel.Result result) { + LogUtils.w("Test", "invokerFlutterMethod=>" + methodName); + this.channel.invokeMethod(methodName, arguments, result); + } + + public interface Result extends MethodChannel.Result { + @Override + default void error(@NonNull String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {} + + @Override + default void notImplemented() {} + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/IVideoPlayer.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/IVideoPlayer.java new file mode 100644 index 00000000..a022db9d --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/IVideoPlayer.java @@ -0,0 +1,15 @@ +package com.xycz.simple_live_tv.core; + +/** + * Created by wangyan on 2024/12/22 + */ +public interface IVideoPlayer { + + void prepare(); + + void start(); + + void stop(); + + void release(); +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/LogUtils.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/LogUtils.java new file mode 100644 index 00000000..0c452546 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/LogUtils.java @@ -0,0 +1,85 @@ +package com.xycz.simple_live_tv.core; + +import android.util.Log; + +/** + * Created by wangyan on 2020-07-24 + */ +public class LogUtils { + private static boolean isShowLog = true; + private static final String DEFAULT_TAG = "Live"; + + public static void i(String tag, String message) { + if (isShowLog) { + Log.i(DEFAULT_TAG, tag + " [" + message + "]"); + } + } + + public static void i(String message) { + if (isShowLog) { + Log.i(DEFAULT_TAG, message); + } + } + + public static void d(String tag, String message) { + if (isShowLog) { + Log.w(DEFAULT_TAG, tag + " [" + message + "]"); + } + } + + public static void d(String message) { + if (isShowLog) { + Log.w(DEFAULT_TAG, message); + } + } + + public static void w(String tag, String message) { + if (isShowLog) { + Log.w(DEFAULT_TAG, tag + " [" + message + "]"); + } + } + + public static void w(String message) { + if (isShowLog) { + Log.w(DEFAULT_TAG, message); + } + } + + public static void e(String tag, String message, Throwable e) { + if (isShowLog) { + Log.e(DEFAULT_TAG, tag + " [" + message + "]", e); + } + } + + public static void e(String tag, Throwable e) { + if (isShowLog) { + Log.e(DEFAULT_TAG, tag + " [" + Log.getStackTraceString(e) + "]"); + } + } + + public static void setDebug(boolean isDebug) { + isShowLog = isDebug; + } + + public static boolean isDebug() { + return isShowLog; + } + + public static void showLog(String tag, String msg) throws StringIndexOutOfBoundsException { + msg = msg.trim(); + int var2 = 0; + short var3 = 4000; + + while(var2 < msg.length()) { + String var4; + if (msg.length() <= var2 + var3) { + var4 = msg.substring(var2); + } else { + var4 = msg.substring(var2, var2 + var3); + } + + var2 += var3; + Log.i(tag, var4.trim()); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/MessageManager.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/MessageManager.java new file mode 100644 index 00000000..e04c0a5c --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/MessageManager.java @@ -0,0 +1,81 @@ +package com.xycz.simple_live_tv.core; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * 消息中心,订阅消息包括与flutter之间的交互 + * Created by wangyan on 2024/12/20 + */ +public class MessageManager implements Handler.Callback { + + private static volatile MessageManager manager; + + private final Handler handler = new Handler(Looper.getMainLooper(), this); + private final List registerCallbacks = new ArrayList<>(); + private final Map registerCallbackMap = new HashMap<>(); + + // 默认信号,仅传递数据 + public static final int DEFAULT_CMD = 1000; + // 传递flutter回调java函数的消息 + public static final int FLUTTER_TO_JAVA_CMD = 1001; + + private MessageManager() { + } + + public static MessageManager getInstance() { + if (manager == null) { + synchronized (MessageManager.class) { + if (manager == null) { + manager = new MessageManager(); + } + } + } + + return manager; + } + + @Override + public boolean handleMessage(@NonNull Message message) { + for (Handler.Callback callback : registerCallbacks) { + if (callback.handleMessage(message)) { + return true; + } + } + + return false; + } + + public void sendMessage(Message message) { + this.handler.sendMessage(message); + } + + public void registerCallback(Handler.Callback callback) { + this.registerCallbacks.add(callback); + } + + public void registerCallback(String tag, Handler.Callback callback) { + this.registerCallbacks.add(callback); + this.registerCallbackMap.put(tag, callback); + } + + public void unRegisterCallback(Handler.Callback callback) { + this.registerCallbacks.remove(callback); + } + + public void unRegisterCallback(String tag) { + Handler.Callback callback = this.registerCallbackMap.get(tag); + if (callback != null) { + this.registerCallbacks.remove(callback); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/MethodCallModel.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/MethodCallModel.java new file mode 100644 index 00000000..3f4066f3 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/MethodCallModel.java @@ -0,0 +1,26 @@ +package com.xycz.simple_live_tv.core; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +/** + * Created by wangyan on 2024/12/20 + */ +public class MethodCallModel { + + private final MethodCall methodCall; + private final MethodChannel.Result result; + + public MethodCallModel(MethodCall methodCall, MethodChannel.Result result) { + this.methodCall = methodCall; + this.result = result; + } + + public MethodCall getMethodCall() { + return methodCall; + } + + public MethodChannel.Result getResult() { + return result; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/OkHttpManager.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/OkHttpManager.java new file mode 100644 index 00000000..9af4db78 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/OkHttpManager.java @@ -0,0 +1,98 @@ +package com.xycz.simple_live_tv.core; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.util.Map; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.logging.HttpLoggingInterceptor; + +/** + * Created by wangyan on 2024/12/21 + */ +public class OkHttpManager implements Interceptor { + + private static volatile OkHttpManager manager; + + private final OkHttpClient okHttpClient; + private Map headerMap; + + private OkHttpManager() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { + @Override + public void log(@NonNull String s) { + LogUtils.w("OkHttp", s); + } + }); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); + builder.addInterceptor(loggingInterceptor); + builder.addInterceptor(this) + .followRedirects(false); + okHttpClient = builder.build(); + } + + public static OkHttpManager getInstance() { + if (manager == null) { + synchronized (OkHttpManager.class) { + if (manager == null) { + manager = new OkHttpManager(); + } + } + } + + return manager; + } + + public void resetRequestHeader(Map headerMap) { + this.headerMap = headerMap; + } + + @NonNull + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Request.Builder builder = request.newBuilder(); + if (headerMap != null && !headerMap.isEmpty()) { + for (String key: headerMap.keySet()) { + String value = headerMap.get(key); + if (value != null) { + builder.addHeader(key, value); + } + } + } + + Response response = chain.proceed(builder.build()); + if (response.code() != 302) { + return response; + } + + // 获取重定向的目标URL + String newUrl = response.header("Location"); + if (newUrl == null || newUrl.isEmpty()) { + return response; + } + + HttpUrl httpUrl = HttpUrl.parse(newUrl); + if (httpUrl == null) { + return response; + } + + if (httpUrl.scheme().equals("https") && httpUrl.host().contains("_")) { + httpUrl = httpUrl.newBuilder().scheme("http").build(); + } + + builder.url(httpUrl); + response.close(); + return chain.proceed(builder.build()); + } + + public OkHttpClient getOkHttpClient() { + return okHttpClient; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/ButtonDelegate.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/ButtonDelegate.java new file mode 100644 index 00000000..86278419 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/ButtonDelegate.java @@ -0,0 +1,59 @@ +package com.xycz.simple_live_tv.core.setting; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.multitype.ItemViewBinder; + +/** + * Created by wangyan on 2024/12/26 + */ +public class ButtonDelegate extends ItemViewBinder { + + @NonNull + @Override + protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + View view = inflater.inflate(R.layout.item_button, parent, false); + return new ViewHolder(view); + } + + @Override + protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull ButtonModel item) { + holder.setData(item); + } + + public static class ButtonModel { + private final String title; + + private final View.OnClickListener clickListener; + + public ButtonModel(String title, View.OnClickListener clickListener) { + this.title = title; + this.clickListener = clickListener; + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + private final ImageView button; + private final TextView title; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + this.button = itemView.findViewById(R.id.button_content); + this.title = itemView.findViewById(R.id.item_title); + } + + public void setData(ButtonModel buttonModel) { + this.title.setText(buttonModel.title); + this.button.setOnClickListener(buttonModel.clickListener); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/LineDelegate.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/LineDelegate.java new file mode 100644 index 00000000..2bbd3c03 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/LineDelegate.java @@ -0,0 +1,36 @@ +package com.xycz.simple_live_tv.core.setting; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.multitype.ItemViewBinder; + +/** + * Created by wangyan on 2024/12/26 + */ +public class LineDelegate extends ItemViewBinder { + + @NonNull + @Override + protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + View view = inflater.inflate(R.layout.item_line, parent, false); + return new ViewHolder(view); + } + + @Override + protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Boolean item) { + + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(@NonNull View itemView) { + super(itemView); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/SelectDelegate.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/SelectDelegate.java new file mode 100644 index 00000000..50d89166 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/core/setting/SelectDelegate.java @@ -0,0 +1,173 @@ +package com.xycz.simple_live_tv.core.setting; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.adapter.SelectAdapter; +import com.xycz.simple_live_tv.multitype.ItemViewBinder; +import com.xycz.simple_live_tv.widgets.SelectDialog; + +import java.util.LinkedHashMap; + +/** + * Created by wangyan on 2024/12/26 + */ +public class SelectDelegate extends ItemViewBinder, SelectDelegate.ViewHolder> { + + @NonNull + @Override + protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + View view = inflater.inflate(R.layout.item_select, parent, false); + return new SelectDelegate.ViewHolder(view); + } + + @Override + protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull SelectModel item) { + holder.setData(item); + } + + public static class SelectModel { + private final String selectTitle; + + private final LinkedHashMap selectMap; + + private T selectValue; + + private final SelectAdapter.ISelectDialog selectCallback; + + public SelectModel(String selectTitle, LinkedHashMap selectMap, T selectValue, SelectAdapter.ISelectDialog dialogInterface) { + this.selectTitle = selectTitle; + this.selectMap = selectMap; + this.selectCallback = dialogInterface; + this.selectValue = selectValue; + } + + public String getSelectTitle() { + return selectTitle; + } + + public LinkedHashMap getSelectMap() { + return selectMap; + } + + public T getSelectValue() { + return selectValue; + } + + public void setSelectValue(T selectValue) { + this.selectValue = selectValue; + } + + public SelectAdapter.ISelectDialog getSelectCallback() { + return selectCallback; + } + + public String getCurrent() { + if (selectMap == null || selectValue == null) { + return null; + } + + return selectMap.get(selectValue); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + private final TextView title; + private final TextView selectValue; + + private final TextView leftArrow; + private final TextView rightArrow; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + this.title = itemView.findViewById(R.id.select_title); + this.selectValue = itemView.findViewById(R.id.select_value); + this.leftArrow = itemView.findViewById(R.id.left_arrow); + this.rightArrow = itemView.findViewById(R.id.right_arrow); + } + + public void setData(SelectModel selectModel) { + this.title.setText(selectModel.getSelectTitle()); + String showValue = selectModel.getCurrent(); + if (showValue == null) { + this.selectValue.setVisibility(View.GONE); + this.leftArrow.setVisibility(View.GONE); + this.rightArrow.setVisibility(View.GONE); + this.itemView.setBackgroundResource(R.color.transparent); + this.itemView.setFocusable(false); + this.itemView.setClickable(false); + return; + } + + this.itemView.setFocusable(true); + this.itemView.setClickable(true); + this.selectValue.setVisibility(View.VISIBLE); + this.leftArrow.setVisibility(View.VISIBLE); + this.rightArrow.setVisibility(View.VISIBLE); + this.selectValue.setText(showValue); + this.itemView.setBackgroundResource(R.drawable.shape_model_focus); + this.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (selectModel.getSelectMap().size() == 2) { + T currentKey = selectModel.getSelectValue(); + LinkedHashMap valueMap = selectModel.getSelectMap(); + for (T key : valueMap.keySet()) { + if (!key.equals(currentKey)) { + currentKey = key; + break; + } + } + + String showValue = valueMap.get(currentKey); + selectModel.setSelectValue(currentKey); + selectValue.setText(showValue); + if (selectModel.getSelectCallback() != null) { + selectModel.getSelectCallback().click(currentKey, showValue); + } + + return; + } + + SelectDialog dialog = new SelectDialog<>(itemView.getContext()); + dialog.setTip(selectModel.getSelectTitle()); + dialog.setAdapter(null, new SelectAdapter.ISelectDialog() { + @Override + public void click(T key, String value) { + dialog.cancel(); + if (selectModel.getSelectCallback() != null) { + selectModel.getSelectCallback().click(key, value); + } + selectValue.setText(value == null ? "" : value); + selectModel.setSelectValue(key); + } + }, itemDiff, selectModel.getSelectMap(), selectModel.getSelectValue()); + dialog.show(); + } + }); + } + + public DiffUtil.ItemCallback itemDiff = new DiffUtil.ItemCallback() { + + @Override + public boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem) { + return oldItem.equals(newItem); + } + + @SuppressLint("DiffUtilEquals") + @Override + public boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem) { + return oldItem.equals(newItem); + } + }; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/danmaku/DanmakuRender.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/danmaku/DanmakuRender.java new file mode 100644 index 00000000..d327793e --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/danmaku/DanmakuRender.java @@ -0,0 +1,91 @@ +package com.xycz.simple_live_tv.danmaku; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.text.TextPaint; + +import androidx.annotation.NonNull; + +import com.kuaishou.akdanmaku.DanmakuConfig; +import com.kuaishou.akdanmaku.data.DanmakuItem; +import com.kuaishou.akdanmaku.data.DanmakuItemData; +import com.kuaishou.akdanmaku.render.DanmakuRenderer; +import com.kuaishou.akdanmaku.ui.DanmakuDisplayer; +import com.kuaishou.akdanmaku.utils.Size; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by wangyan on 2024/12/28 + */ +public class DanmakuRender implements DanmakuRenderer { + + private static final int DEFAULT_DARK_COLOR = Color.argb(255, 34, 34, 34); + private static final int CANVAS_PADDING = 6; + private final Map textHeightCache = new HashMap<>(); + + private final TextPaint textPaint = new TextPaint(); + + private final TextPaint strokePaint = new TextPaint(); + + public DanmakuRender() { + strokePaint.setStyle(Paint.Style.STROKE); + } + + public void setStrokeWidth(int width) { + strokePaint.setStrokeWidth(width); + } + + @Override + public void draw(@NonNull DanmakuItem danmakuItem, @NonNull Canvas canvas, @NonNull DanmakuDisplayer danmakuDisplayer, @NonNull DanmakuConfig danmakuConfig) { + updatePaint(danmakuItem, danmakuDisplayer, danmakuConfig); + DanmakuItemData danmakuItemData = danmakuItem.getData(); + float x = CANVAS_PADDING * 0.5f; + float y = CANVAS_PADDING * 0.5f - textPaint.ascent(); + if (strokePaint.getStrokeWidth() > 0) { + canvas.drawText(danmakuItemData.getContent(), x, y, strokePaint); + } + + canvas.drawText(danmakuItemData.getContent(), x, y, textPaint); + } + + @Override + public void updatePaint(@NonNull DanmakuItem danmakuItem, @NonNull DanmakuDisplayer danmakuDisplayer, @NonNull DanmakuConfig danmakuConfig) { + DanmakuItemData danmakuItemData = danmakuItem.getData(); + // update textPaint + float textSize = danmakuItemData.getTextSize() * 2.0f; + textPaint.setTextSize(textSize * danmakuConfig.getTextSizeScale()); + textPaint.setTypeface(danmakuConfig.getBold() ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); + textPaint.setColor(danmakuItemData.getTextColor()); + // update strokePaint + strokePaint.setTextSize(textPaint.getTextSize()); + strokePaint.setTypeface(textPaint.getTypeface()); + strokePaint.setColor(textPaint.getColor() == DEFAULT_DARK_COLOR ? Color.WHITE : Color.BLACK); + } + + @NonNull + @Override + public Size measure(@NonNull DanmakuItem danmakuItem, @NonNull DanmakuDisplayer danmakuDisplayer, @NonNull DanmakuConfig danmakuConfig) { + updatePaint(danmakuItem, danmakuDisplayer, danmakuConfig); + DanmakuItemData danmakuItemData = danmakuItem.getData(); + float textWidth = textPaint.measureText(danmakuItemData.getContent()); + float textHeight = getCacheHeight(textPaint); + return new Size((int)textWidth + CANVAS_PADDING, (int)textHeight + CANVAS_PADDING); + } + + private float getCacheHeight(Paint paint) { + float textSize = paint.getTextSize(); + Float textHeight = textHeightCache.get(textSize); + if (textHeight != null) { + return textHeight; + } + + Paint.FontMetrics fontMetrics = paint.getFontMetrics(); + textHeight = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading; + textHeightCache.put(textSize, textHeight); + return textHeight; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/flutter/FlutterViewEngine.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/flutter/FlutterViewEngine.java new file mode 100644 index 00000000..0bd69265 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/flutter/FlutterViewEngine.java @@ -0,0 +1,124 @@ +package com.xycz.simple_live_tv.flutter; + +import android.app.Activity; + +import androidx.activity.ComponentActivity; +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; + +import io.flutter.embedding.android.ExclusiveAppComponent; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.platform.PlatformPlugin; + +/** + * Created by wangyan on 2024/12/30 + */ +public class FlutterViewEngine implements LifecycleObserver, ExclusiveAppComponent { + + private FlutterView flutterView; + private ComponentActivity activity; + private PlatformPlugin platformPlugin; + private final FlutterEngine engine; + + public FlutterViewEngine(FlutterEngine engine) { + this.engine = engine; + } + + private void hookActivityAndView() { + if (activity == null) { + return; + } + + if (flutterView == null) { + return; + } + + platformPlugin = new PlatformPlugin(activity, engine.getPlatformChannel()); + engine.getActivityControlSurface().attachToActivity(this, activity.getLifecycle()); + flutterView.attachToFlutterEngine(engine); + activity.getLifecycle().addObserver(this); + } + + private void unHookActivityAndView() { + if (activity != null) { + activity.getLifecycle().removeObserver(this); + } + + engine.getActivityControlSurface().detachFromActivity(); + if (platformPlugin != null) { + platformPlugin.destroy(); + platformPlugin = null; + } + + engine.getLifecycleChannel().appIsDetached(); + if (flutterView != null) { + flutterView.detachFromFlutterEngine(); + } + } + + public void attachToActivity(ComponentActivity activity) { + this.activity = activity; + if (flutterView != null) { + hookActivityAndView(); + } + } + + public void detachActivity() { + if (flutterView != null) { + unHookActivityAndView(); + } + + activity = null; + } + + public void attachFlutterView(FlutterView flutterView) { + this.flutterView = flutterView; + if (activity != null) { + hookActivityAndView(); + } + } + + public void detachFlutterView() { + unHookActivityAndView(); + flutterView = null; + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + private void resumeActivity() { + if (activity != null) { + engine.getLifecycleChannel().appIsResumed(); + } + + if (platformPlugin != null) { + platformPlugin.updateSystemUiOverlays(); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + private void pauseActivity() { + if (activity != null) { + engine.getLifecycleChannel().appIsInactive(); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + private void stopActivity() { + if (activity != null) { + engine.getLifecycleChannel().appIsPaused(); + } + } + + @Override + public void detachFromFlutterEngine() { + + } + + @NonNull + @Override + public Activity getAppComponent() { + return activity; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/model/LiveModel.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/model/LiveModel.java new file mode 100644 index 00000000..284cae06 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/model/LiveModel.java @@ -0,0 +1,171 @@ +package com.xycz.simple_live_tv.model; + +import androidx.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map; + +/** + * Created by wangyan on 2024/12/20 + */ +public class LiveModel { + + private String id; + private String roomId; + private String name; + private String logo; + private Integer index; + // 是否已经关注 + private boolean followed; + // 当前播放线路 + private Integer currentLineIndex; + // 播放线路地址 + @SerializedName("liveUrl") + private List playUrls; + // 当前清晰度index + private Integer currentQuality; + // 清晰度 + @SerializedName("qualites") + private List qualites; + @SerializedName("headers") + private Map headerMap; + @SerializedName("roomTitle") + private String roomTitle; + + private static final String kBiliBili = "bilibili"; + private static final String kDouyu = "douyu"; + private static final String kHuya = "huya"; + private static final String kDouyin = "douyin"; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRoomId() { + return roomId; + } + + public void setRoomId(String roomId) { + this.roomId = roomId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLogo() { + return logo; + } + + public void setLogo(String logo) { + this.logo = logo; + } + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public boolean isFollowed() { + return followed; + } + + public void setFollowed(boolean followed) { + this.followed = followed; + } + + public Integer getCurrentLineIndex() { + return currentLineIndex; + } + + public void setCurrentLineIndex(Integer currentLineIndex) { + this.currentLineIndex = currentLineIndex; + } + + public List getPlayUrls() { + return playUrls; + } + + public void setPlayUrls(List playUrls) { + this.playUrls = playUrls; + } + + public Integer getCurrentQuality() { + return currentQuality; + } + + public void setCurrentQuality(Integer currentQuality) { + this.currentQuality = currentQuality; + } + + public List getQualites() { + return qualites; + } + + public void setQualites(List qualites) { + this.qualites = qualites; + } + + public Map getHeaderMap() { + return headerMap; + } + + public void setHeaderMap(Map headerMap) { + this.headerMap = headerMap; + } + + public String getRoomTitle() { + return roomTitle; + } + + public void setRoomTitle(String roomTitle) { + this.roomTitle = roomTitle; + } + + public boolean isPlayEmpty() { + return playUrls == null || playUrls.isEmpty(); + } + + @NonNull + @Override + public String toString() { + return "LiveModel{" + + "roomId='" + roomId + '\'' + + ", name='" + name + '\'' + + ", logo='" + logo + '\'' + + ", index='" + index + '\'' + + ", playUrls=" + playUrls + + '}'; + } + + public String getClarity() { + String clarity = null; + if (currentQuality >= 0) { + clarity = qualites.get(currentQuality); + } + + return clarity == null ? "" : clarity; + } + + public String getLine() { + String line = null; + if (currentLineIndex >= 0) { + line = playUrls.get(currentLineIndex); + } + + return line == null ? "" : line; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/BinderNotFoundException.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/BinderNotFoundException.java new file mode 100644 index 00000000..8f27e105 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/BinderNotFoundException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +/** + * @author drakeet + */ +class BinderNotFoundException extends RuntimeException { + + BinderNotFoundException(@NonNull Class clazz) { + super("Have you registered {className}.class to the binder in the adapter/pool?" + .replace("{className}", clazz.getSimpleName())); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ClassLinker.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ClassLinker.java new file mode 100644 index 00000000..75f6636c --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ClassLinker.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +/** + * An interface to link the items and binders by the classes of binders. + * + * @author drakeet + */ +public interface ClassLinker { + + /** + * Returns the class of your registered binders for your item. + * + * @param position The position in items + * @param t The item + * @return The index of your registered binders + * @see OneToManyEndpoint#withClassLinker(ClassLinker) + */ + @NonNull Class> index(int position, @NonNull T t); +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ClassLinkerWrapper.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ClassLinkerWrapper.java new file mode 100644 index 00000000..6b9843e5 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ClassLinkerWrapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +/** + * @author drakeet + */ +final class ClassLinkerWrapper implements Linker { + + private final @NonNull ClassLinker classLinker; + private final @NonNull ItemViewBinder[] binders; + + + private ClassLinkerWrapper( + @NonNull ClassLinker classLinker, + @NonNull ItemViewBinder[] binders) { + this.classLinker = classLinker; + this.binders = binders; + } + + + static @NonNull ClassLinkerWrapper wrap( + @NonNull ClassLinker classLinker, + @NonNull ItemViewBinder[] binders) { + return new ClassLinkerWrapper(classLinker, binders); + } + + + @Override + public int index(int position, @NonNull T t) { + Class userIndexClass = classLinker.index(position, t); + for (int i = 0; i < binders.length; i++) { + if (binders[i].getClass().equals(userIndexClass)) { + return i; + } + } + throw new IndexOutOfBoundsException( + String.format("%s is out of your registered binders'(%s) bounds.", + userIndexClass.getName(), Arrays.toString(binders)) + ); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/DefaultLinker.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/DefaultLinker.java new file mode 100644 index 00000000..152848e1 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/DefaultLinker.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +/** + * @author drakeet + */ +final class DefaultLinker implements Linker { + + @Override + public int index(int position, @NonNull T t) { + return 0; + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ItemViewBinder.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ItemViewBinder.java new file mode 100644 index 00000000..767a7524 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/ItemViewBinder.java @@ -0,0 +1,228 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.LayoutManager; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import java.util.List; + +/*** + * @author drakeet + */ +public abstract class ItemViewBinder { + + /* internal */ MultiTypeAdapter adapter; + + + protected abstract @NonNull VH onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent); + + /** + * Called by MultiTypeAdapter to display the data with its view holder. This method should + * update the contents of the {@link ViewHolder#itemView} to reflect the given item. + *

+ * If you need the position of an item later on (e.g. in a click listener), use + * {@code ViewHolder#getAdapterPosition()} which will have the updated adapter position. + * + * Override {@code onBindViewHolder(ViewHolder, Object, List)} instead if your ItemViewBinder + * can handle efficient partial bind. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * given item in the items data set. + * @param item The item within the MultiTypeAdapter's items data set. + */ + protected abstract void onBindViewHolder(@NonNull VH holder, @NonNull T item); + + + /** + * Called by MultiTypeAdapter to display the data with its view holder. This method should + * update the contents of the {@link ViewHolder#itemView} to reflect the given item. + *

+ * If you need the position of an item later on (e.g. in a click listener), use + * {@link ViewHolder#getAdapterPosition()} which will have the updated adapter position. + *

+ * Partial bind vs full bind: + *

+ * The payloads parameter is a merge list from {@link MultiTypeAdapter#notifyItemChanged(int, + * Object)} {@link MultiTypeAdapter#notifyItemRangeChanged(int, int, Object)}. + * If the payloads list is not empty, the ViewHolder is currently bound to old data and + * ItemViewBinder may run an efficient partial update using the payload info. + * If the payload is empty, ItemViewBinder must run a full bind. + * ItemViewBinder should not assume that the payload passed in notify methods will be + * received by onBindViewHolder(). For example when the view is not attached to the screen, + * the payload in notifyItemChange() will be simply dropped. + * + * This implementation calls the {@code onBindViewHolder(ViewHolder, Object)} by default. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * given item in the items data set. + * @param item The item within the MultiTypeAdapter's items data set. + * @param payloads A non-null list of merged payloads. Can be empty list if requires full + * update. + * @since v2.5.0 + */ + protected void onBindViewHolder(@NonNull VH holder, @NonNull T item, @NonNull List payloads) { + onBindViewHolder(holder, item); + } + + + /** + * Get the adapter position of current item, + * the internal position equals to {@link ViewHolder#getAdapterPosition()}. + *

NOTE: Below v2.3.5 we may provide getPosition() method to get the position, + * It exists BUG, and sometimes can not get the correct position, + * it is recommended to immediately stop using it and use the new + * {@code getPosition(ViewHolder)} instead.

+ * + * @param holder The ViewHolder to call holder.getAdapterPosition(). + * @return The adapter position. + * @since v2.3.5. If below v2.3.5, use {@link ViewHolder#getAdapterPosition()} instead. + */ + protected final int getPosition(@NonNull final ViewHolder holder) { + return holder.getAdapterPosition(); + } + + + /** + * Get the {@link MultiTypeAdapter} for sending notifications or getting item count, etc. + *

+ * Note that if you need to change the item's parent items, you could call this method + * to get the {@link MultiTypeAdapter}, and call {@link MultiTypeAdapter#getItems()} to get + * a list that can not be added any new item, so that you should copy the items and just use + * {@link MultiTypeAdapter#setItems(List)} to replace the original items list and update the + * views. + *

+ * + * @return The MultiTypeAdapter this item is currently associated with. + * @since v2.3.4 + */ + protected final @NonNull MultiTypeAdapter getAdapter() { + if (adapter == null) { + throw new IllegalStateException("ItemViewBinder " + this + " not attached to MultiTypeAdapter. " + + "You should not call the method before registering the binder."); + } + return adapter; + } + + + /** + * Return the stable ID for the item. If {@link RecyclerView.Adapter#hasStableIds()} + * would return false this method should return {@link RecyclerView#NO_ID}. The default + * implementation of this method returns {@link RecyclerView#NO_ID}. + * + * @param item The item within the MultiTypeAdapter's items data set to query + * @return the stable ID of the item + * @see RecyclerView.Adapter#setHasStableIds(boolean) + * @since v3.2.0 + */ + protected long getItemId(@NonNull T item) { + return RecyclerView.NO_ID; + } + + + /** + * Called when a view created by this {@link ItemViewBinder} has been recycled. + * + *

A view is recycled when a {@link LayoutManager} decides that it no longer + * needs to be attached to its parent {@link RecyclerView}. This can be because it has + * fallen out of visibility or a set of cached views represented by views still + * attached to the parent RecyclerView. If an item view has large or expensive data + * bound to it such as large bitmaps, this may be a good place to release those + * resources.

+ *

+ * RecyclerView calls this method right before clearing ViewHolder's internal data and + * sending it to RecycledViewPool. + * + * @param holder The ViewHolder for the view being recycled + * @since v3.1.0 + */ + protected void onViewRecycled(@NonNull VH holder) {} + + + /** + * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled + * due to its transient state. Upon receiving this callback, Adapter can clear the + * animation(s) that effect the View's transient state and return true so that + * the View can be recycled. Keep in mind that the View in question is already removed from + * the RecyclerView. + *

+ * In some cases, it is acceptable to recycle a View although it has transient state. Most + * of the time, this is a case where the transient state will be cleared in + * {@link #onBindViewHolder(ViewHolder, Object)} call when View is rebound to a new item. + * For this reason, RecyclerView leaves the decision to the Adapter and uses the return + * value of this method to decide whether the View should be recycled or not. + *

+ * Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you + * should never receive this callback because RecyclerView keeps those Views as children + * until their animations are complete. This callback is useful when children of the item + * views create animations which may not be easy to implement using an {@link + * RecyclerView.ItemAnimator}. + *

+ * You should never fix this issue by calling + * holder.itemView.setHasTransientState(false); unless you've previously called + * holder.itemView.setHasTransientState(true);. Each + * View.setHasTransientState(true) call must be matched by a + * View.setHasTransientState(false) call, otherwise, the state of the View + * may become inconsistent. You should always prefer to end or cancel animations that are + * triggering the transient state instead of handling it manually. + * + * @param holder The ViewHolder containing the View that could not be recycled due to its + * transient state. + * @return True if the View should be recycled, false otherwise. Note that if this method + * returns true, RecyclerView will ignore the transient state of + * the View and recycle it regardless. If this method returns false, + * RecyclerView will check the View's transient state again before giving a final decision. + * Default implementation returns false. + * @since v3.1.0 + */ + protected boolean onFailedToRecycleView(@NonNull VH holder) { + return false; + } + + + /** + * Called when a view created by this {@link ItemViewBinder} has been attached to a window. + * + *

This can be used as a reasonable signal that the view is about to be seen + * by the user. If the {@link ItemViewBinder} previously freed any resources in + * {@link #onViewDetachedFromWindow(ViewHolder) onViewDetachedFromWindow} + * those resources should be restored here.

+ * + * @param holder Holder of the view being attached + * @since v3.1.0 + */ + protected void onViewAttachedToWindow(@NonNull VH holder) {} + + + /** + * Called when a view created by this {@link ItemViewBinder} has been detached from its + * window. + * + *

Becoming detached from the window is not necessarily a permanent condition; + * the consumer of an Adapter's views may choose to cache views offscreen while they + * are not visible, attaching and detaching them as appropriate.

+ * + * @param holder Holder of the view being detached + * @since v3.1.0 + */ + protected void onViewDetachedFromWindow(@NonNull VH holder) {} +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Items.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Items.java new file mode 100644 index 00000000..8f758d09 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Items.java @@ -0,0 +1,62 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * A convenient class for creating a {@code ArrayList}. + * + * @author drakeet + */ +public class Items extends ArrayList { + + /** + * Constructs an empty Items with an initial capacity of ten. + */ + public Items() { + super(); + } + + + /** + * Constructs an empty Items with the specified initial capacity. + * + * @param initialCapacity the initial capacity of the Items + * @throws IllegalArgumentException if the specified initial capacity + * is negative + */ + public Items(int initialCapacity) { + super(initialCapacity); + } + + + /** + * Constructs a Items containing the elements of the specified + * collection, in the order they are returned by the collection's + * iterator. + * + * @param c the collection whose elements are to be placed into this Items + * @throws NullPointerException if the specified collection is null + */ + public Items(@NonNull Collection c) { + super(c); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Linker.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Linker.java new file mode 100644 index 00000000..3b0bd777 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Linker.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +/** + * An interface to link the items and binders by array integer index. + * + * @author drakeet + */ +public interface Linker { + + /** + * Returns the index of your registered binders for your item. The result should be in range of + * {@code [0, one-to-multiple-binders.length)}. + * + *

Note: The argument of {@link OneToManyFlow#to(ItemViewBinder[])} is the + * one-to-multiple-binders.

+ * + * @param position The position in items + * @param t Your item data + * @return The index of your registered binders + * @see OneToManyFlow#to(ItemViewBinder[]) + * @see OneToManyEndpoint#withLinker(Linker) + */ + @IntRange(from = 0) int index(int position, @NonNull T t); +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypeAdapter.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypeAdapter.java new file mode 100644 index 00000000..da7832f6 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypeAdapter.java @@ -0,0 +1,365 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import static com.xycz.simple_live_tv.multitype.Preconditions.checkNotNull; + +/** + * @author drakeet + */ +public class MultiTypeAdapter extends RecyclerView.Adapter { + + private static final String TAG = "MultiTypeAdapter"; + + private @NonNull List items; + private @NonNull TypePool typePool; + + + /** + * Constructs a MultiTypeAdapter with an empty items list. + */ + public MultiTypeAdapter() { + this(Collections.emptyList()); + } + + + /** + * Constructs a MultiTypeAdapter with a items list. + * + * @param items the items list + */ + public MultiTypeAdapter(@NonNull List items) { + this(items, new MultiTypePool()); + } + + + /** + * Constructs a MultiTypeAdapter with a items list and an initial capacity of TypePool. + * + * @param items the items list + * @param initialCapacity the initial capacity of TypePool + */ + public MultiTypeAdapter(@NonNull List items, int initialCapacity) { + this(items, new MultiTypePool(initialCapacity)); + } + + + /** + * Constructs a MultiTypeAdapter with a items list and a TypePool. + * + * @param items the items list + * @param pool the type pool + */ + public MultiTypeAdapter(@NonNull List items, @NonNull TypePool pool) { + checkNotNull(items); + checkNotNull(pool); + this.items = items; + this.typePool = pool; + } + + + /** + * Registers a type class and its item view binder. If you have registered the class, + * it will override the original binder(s). Note that the method is non-thread-safe + * so that you should not use it in concurrent operation. + *

+ * Note that the method should not be called after + * {@link RecyclerView#setAdapter(RecyclerView.Adapter)}, or you have to call the setAdapter + * again. + *

+ * + * @param clazz the class of a item + * @param binder the item view binder + * @param the item data type + */ + public void register(@NonNull Class clazz, @NonNull ItemViewBinder binder) { + checkNotNull(clazz); + checkNotNull(binder); + checkAndRemoveAllTypesIfNeeded(clazz); + register(clazz, binder, new DefaultLinker()); + } + + + void register( + @NonNull Class clazz, + @NonNull ItemViewBinder binder, + @NonNull Linker linker) { + typePool.register(clazz, binder, linker); + binder.adapter = this; + } + + + /** + * Registers a type class to multiple item view binders. If you have registered the + * class, it will override the original binder(s). Note that the method is non-thread-safe + * so that you should not use it in concurrent operation. + *

+ * Note that the method should not be called after + * {@link RecyclerView#setAdapter(RecyclerView.Adapter)}, or you have to call the setAdapter + * again. + *

+ * + * @param clazz the class of a item + * @param the item data type + * @return {@link OneToManyFlow} for setting the binders + * @see #register(Class, ItemViewBinder) + */ + @CheckResult + public @NonNull OneToManyFlow register(@NonNull Class clazz) { + checkNotNull(clazz); + checkAndRemoveAllTypesIfNeeded(clazz); + return new OneToManyBuilder<>(this, clazz); + } + + + /** + * Registers all of the contents in the specified type pool. If you have registered a + * class, it will override the original binder(s). Note that the method is non-thread-safe + * so that you should not use it in concurrent operation. + *

+ * Note that the method should not be called after + * {@link RecyclerView#setAdapter(RecyclerView.Adapter)}, or you have to call the setAdapter + * again. + *

+ * + * @param pool type pool containing contents to be added to this adapter inner pool + * @see #register(Class, ItemViewBinder) + * @see #register(Class) + */ + public void registerAll(@NonNull final TypePool pool) { + checkNotNull(pool); + final int size = pool.size(); + for (int i = 0; i < size; i++) { + registerWithoutChecking( + pool.getClass(i), + pool.getItemViewBinder(i), + pool.getLinker(i) + ); + } + } + + + /** + * Sets and updates the items atomically and safely. It is recommended to use this method + * to update the items with a new wrapper list or consider using {@link CopyOnWriteArrayList}. + * + *

Note: If you want to refresh the list views after setting items, you should + * call {@link RecyclerView.Adapter#notifyDataSetChanged()} by yourself.

+ * + * @param items the new items list + * @since v2.4.1 + */ + public void setItems(@NonNull List items) { + checkNotNull(items); + this.items = items; + } + + + public @NonNull List getItems() { + return items; + } + + + /** + * Set the TypePool to hold the types and view binders. + * + * @param typePool the TypePool implementation + */ + public void setTypePool(@NonNull TypePool typePool) { + checkNotNull(typePool); + this.typePool = typePool; + } + + + public @NonNull TypePool getTypePool() { + return typePool; + } + + + @Override + public final int getItemViewType(int position) { + Object item = items.get(position); + return indexInTypesOf(position, item); + } + + + @Override + public final ViewHolder onCreateViewHolder(ViewGroup parent, int indexViewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + ItemViewBinder binder = typePool.getItemViewBinder(indexViewType); + return binder.onCreateViewHolder(inflater, parent); + } + + + /** + * This method is deprecated and unused. You should not call this method. + *

+ * If you need to call the binding, use {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, + * int, List)} instead. + *

+ * + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. + * @throws IllegalAccessError By default. + * @deprecated Call {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)} + * instead. + */ + @Override @Deprecated + public final void onBindViewHolder(@NonNull ViewHolder holder, int position) { + onBindViewHolder(holder, position, Collections.emptyList()); + } + + + @Override @SuppressWarnings("unchecked") + public final void onBindViewHolder(ViewHolder holder, int position, @NonNull List payloads) { + Object item = items.get(position); + ItemViewBinder binder = typePool.getItemViewBinder(holder.getItemViewType()); + binder.onBindViewHolder(holder, item, payloads); + } + + + @Override + public final int getItemCount() { + return items.size(); + } + + + /** + * Called to return the stable ID for the item, and passes the event to its associated binder. + * + * @param position Adapter position to query + * @return the stable ID of the item at position + * @see ItemViewBinder#getItemId(Object) + * @see RecyclerView.Adapter#setHasStableIds(boolean) + * @since v3.2.0 + */ + @Override @SuppressWarnings("unchecked") + public final long getItemId(int position) { + Object item = items.get(position); + int itemViewType = getItemViewType(position); + ItemViewBinder binder = typePool.getItemViewBinder(itemViewType); + return binder.getItemId(item); + } + + + /** + * Called when a view created by this adapter has been recycled, and passes the event to its + * associated binder. + * + * @param holder The ViewHolder for the view being recycled + * @see RecyclerView.Adapter#onViewRecycled(ViewHolder) + * @see ItemViewBinder#onViewRecycled(ViewHolder) + */ + @Override @SuppressWarnings("unchecked") + public final void onViewRecycled(@NonNull ViewHolder holder) { + getRawBinderByViewHolder(holder).onViewRecycled(holder); + } + + + /** + * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled + * due to its transient state, and passes the event to its associated item view binder. + * + * @param holder The ViewHolder containing the View that could not be recycled due to its + * transient state. + * @return True if the View should be recycled, false otherwise. Note that if this method + * returns true, RecyclerView will ignore the transient state of + * the View and recycle it regardless. If this method returns false, + * RecyclerView will check the View's transient state again before giving a final decision. + * Default implementation returns false. + * @see RecyclerView.Adapter#onFailedToRecycleView(ViewHolder) + * @see ItemViewBinder#onFailedToRecycleView(ViewHolder) + */ + @Override @SuppressWarnings("unchecked") + public final boolean onFailedToRecycleView(@NonNull ViewHolder holder) { + return getRawBinderByViewHolder(holder).onFailedToRecycleView(holder); + } + + + /** + * Called when a view created by this adapter has been attached to a window, and passes the + * event to its associated item view binder. + * + * @param holder Holder of the view being attached + * @see RecyclerView.Adapter#onViewAttachedToWindow(ViewHolder) + * @see ItemViewBinder#onViewAttachedToWindow(ViewHolder) + */ + @Override @SuppressWarnings("unchecked") + public final void onViewAttachedToWindow(@NonNull ViewHolder holder) { + getRawBinderByViewHolder(holder).onViewAttachedToWindow(holder); + } + + + /** + * Called when a view created by this adapter has been detached from its window, and passes + * the event to its associated item view binder. + * + * @param holder Holder of the view being detached + * @see RecyclerView.Adapter#onViewDetachedFromWindow(ViewHolder) + * @see ItemViewBinder#onViewDetachedFromWindow(ViewHolder) + */ + @Override @SuppressWarnings("unchecked") + public final void onViewDetachedFromWindow(@NonNull ViewHolder holder) { + getRawBinderByViewHolder(holder).onViewDetachedFromWindow(holder); + } + + + private @NonNull ItemViewBinder getRawBinderByViewHolder(@NonNull ViewHolder holder) { + return typePool.getItemViewBinder(holder.getItemViewType()); + } + + + int indexInTypesOf(int position, @NonNull Object item) throws BinderNotFoundException { + int index = typePool.firstIndexOf(item.getClass()); + if (index != -1) { + @SuppressWarnings("unchecked") + Linker linker = (Linker) typePool.getLinker(index); + return index + linker.index(position, item); + } + throw new BinderNotFoundException(item.getClass()); + } + + + private void checkAndRemoveAllTypesIfNeeded(@NonNull Class clazz) { + if (typePool.unregister(clazz)) { + Log.w(TAG, "You have registered the " + clazz.getSimpleName() + " type. " + + "It will override the original binder(s)."); + } + } + + + /** A safe register method base on the TypePool's safety for TypePool. */ + @SuppressWarnings("unchecked") + private void registerWithoutChecking(@NonNull Class clazz, @NonNull ItemViewBinder binder, @NonNull Linker linker) { + checkAndRemoveAllTypesIfNeeded(clazz); + register(clazz, binder, linker); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypeAsserts.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypeAsserts.java new file mode 100644 index 00000000..42dcb46f --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypeAsserts.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import static com.xycz.simple_live_tv.multitype.Preconditions.checkNotNull; + +/** + * @author drakeet + */ +public final class MultiTypeAsserts { + + private MultiTypeAsserts() { + throw new AssertionError(); + } + + + /** + * Makes the exception to occur in your class for debug and index. + * + * @param adapter the MultiTypeAdapter + * @param items the items list + * @throws BinderNotFoundException if check failed + * @throws IllegalArgumentException if your Items/List is empty + */ + @SuppressWarnings("unchecked") + public static void assertAllRegistered(@NonNull MultiTypeAdapter adapter, @NonNull List items) + throws BinderNotFoundException, IllegalArgumentException, IllegalAccessError { + checkNotNull(adapter); + checkNotNull(items); + if (items.isEmpty()) { + throw new IllegalArgumentException("Your Items/List is empty."); + } + for (int i = 0; i < items.size(); i++) { + adapter.indexInTypesOf(i, items.get(0)); + } + /* All passed. */ + } + + + /** + * @param recyclerView the RecyclerView + * @param adapter the MultiTypeAdapter + * @throws IllegalAccessError The assertHasTheSameAdapter() method must be placed after + * recyclerView.setAdapter(). + * @throws IllegalArgumentException If your recyclerView's adapter. + * is not the sample with the argument adapter. + */ + public static void assertHasTheSameAdapter(@NonNull RecyclerView recyclerView, @NonNull MultiTypeAdapter adapter) + throws IllegalArgumentException, IllegalAccessError { + checkNotNull(recyclerView); + checkNotNull(adapter); + if (recyclerView.getAdapter() == null) { + throw new IllegalAccessError("The assertHasTheSameAdapter() method must " + + "be placed after recyclerView.setAdapter()"); + } + if (recyclerView.getAdapter() != adapter) { + throw new IllegalArgumentException( + "Your recyclerView's adapter is not the sample with the argument adapter."); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypePool.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypePool.java new file mode 100644 index 00000000..1a151475 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/MultiTypePool.java @@ -0,0 +1,151 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +import static com.xycz.simple_live_tv.multitype.Preconditions.checkNotNull; + +/** + * An List implementation of TypePool. + * + * @author drakeet + */ +public class MultiTypePool implements TypePool { + + private final @NonNull List> classes; + private final @NonNull List> binders; + private final @NonNull List> linkers; + + + /** + * Constructs a MultiTypePool with default lists. + */ + public MultiTypePool() { + this.classes = new ArrayList<>(); + this.binders = new ArrayList<>(); + this.linkers = new ArrayList<>(); + } + + + /** + * Constructs a MultiTypePool with default lists and a specified initial capacity. + * + * @param initialCapacity the initial capacity of the list + */ + public MultiTypePool(int initialCapacity) { + this.classes = new ArrayList<>(initialCapacity); + this.binders = new ArrayList<>(initialCapacity); + this.linkers = new ArrayList<>(initialCapacity); + } + + + /** + * Constructs a MultiTypePool with specified lists. + * + * @param classes the list for classes + * @param binders the list for binders + * @param linkers the list for linkers + */ + public MultiTypePool( + @NonNull List> classes, + @NonNull List> binders, + @NonNull List> linkers) { + checkNotNull(classes); + checkNotNull(binders); + checkNotNull(linkers); + this.classes = classes; + this.binders = binders; + this.linkers = linkers; + } + + + @Override + public void register( + @NonNull Class clazz, + @NonNull ItemViewBinder binder, + @NonNull Linker linker) { + checkNotNull(clazz); + checkNotNull(binder); + checkNotNull(linker); + classes.add(clazz); + binders.add(binder); + linkers.add(linker); + } + + + @Override + public boolean unregister(@NonNull Class clazz) { + checkNotNull(clazz); + boolean removed = false; + while (true) { + int index = classes.indexOf(clazz); + if (index != -1) { + classes.remove(index); + binders.remove(index); + linkers.remove(index); + removed = true; + } else { + break; + } + } + return removed; + } + + + @Override + public int size() { + return classes.size(); + } + + + @Override + public int firstIndexOf(@NonNull final Class clazz) { + checkNotNull(clazz); + int index = classes.indexOf(clazz); + if (index != -1) { + return index; + } + for (int i = 0; i < classes.size(); i++) { + if (classes.get(i).isAssignableFrom(clazz)) { + return i; + } + } + return -1; + } + + + @Override + public @NonNull Class getClass(int index) { + return classes.get(index); + } + + + @Override + public @NonNull ItemViewBinder getItemViewBinder(int index) { + return binders.get(index); + } + + + @Override + public @NonNull Linker getLinker(int index) { + return linkers.get(index); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyBuilder.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyBuilder.java new file mode 100644 index 00000000..37a2fd72 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; + +import static com.xycz.simple_live_tv.multitype.Preconditions.checkNotNull; + +/** + * @author drakeet + */ +class OneToManyBuilder implements OneToManyFlow, OneToManyEndpoint { + + private final @NonNull MultiTypeAdapter adapter; + private final @NonNull Class clazz; + private ItemViewBinder[] binders; + + + OneToManyBuilder(@NonNull MultiTypeAdapter adapter, @NonNull Class clazz) { + this.clazz = clazz; + this.adapter = adapter; + } + + + @Override @CheckResult @SafeVarargs + public final @NonNull OneToManyEndpoint to(@NonNull ItemViewBinder... binders) { + checkNotNull(binders); + this.binders = binders; + return this; + } + + + @Override + public void withLinker(@NonNull Linker linker) { + checkNotNull(linker); + doRegister(linker); + } + + + @Override + public void withClassLinker(@NonNull ClassLinker classLinker) { + checkNotNull(classLinker); + doRegister(ClassLinkerWrapper.wrap(classLinker, binders)); + } + + + private void doRegister(@NonNull Linker linker) { + for (ItemViewBinder binder : binders) { + adapter.register(clazz, binder, linker); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyEndpoint.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyEndpoint.java new file mode 100644 index 00000000..6901c9ea --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyEndpoint.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +/** + * End-operators for one-to-many. + * + * @author drakeet + */ +public interface OneToManyEndpoint { + + /** + * Sets a linker to link the items and binders by array index. + * + * @param linker the row linker + * @see Linker + */ + void withLinker(@NonNull Linker linker); + + /** + * Sets a class linker to link the items and binders by the class instance of binders. + * + * @param classLinker the class linker + * @see ClassLinker + */ + void withClassLinker(@NonNull ClassLinker classLinker); +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyFlow.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyFlow.java new file mode 100644 index 00000000..62eaaf69 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/OneToManyFlow.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; + +/** + * Process and flow operators for one-to-many. + * + * @author drakeet + */ +public interface OneToManyFlow { + + /** + * Sets some item view binders to the item type. + * + * @param binders the item view binders + * @return end flow operator + */ + @CheckResult @SuppressWarnings("unchecked") + @NonNull + OneToManyEndpoint to(@NonNull ItemViewBinder... binders); +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Preconditions.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Preconditions.java new file mode 100644 index 00000000..04c86a74 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/Preconditions.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +/** + * @author drakeet + */ +@SuppressWarnings("WeakerAccess") +public final class Preconditions { + + @SuppressWarnings("ConstantConditions") + public static @NonNull T checkNotNull(@NonNull final T object) { + if (object == null) { + throw new NullPointerException(); + } + return object; + } + + + private Preconditions() {} +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/TypePool.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/TypePool.java new file mode 100644 index 00000000..5c3163c6 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/multitype/TypePool.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 drakeet. https://github.com/drakeet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xycz.simple_live_tv.multitype; + +import androidx.annotation.NonNull; + +/** + * An ordered collection to hold the types, binders and linkers. + * + * @author drakeet + */ +public interface TypePool { + + /** + * Registers a type class and its item view binder. + * + * @param clazz the class of a item + * @param binder the item view binder + * @param linker the linker to link the class and view binder + * @param the item data type + */ + void register( + @NonNull Class clazz, + @NonNull ItemViewBinder binder, + @NonNull Linker linker); + + /** + * Unregister all items with the specified class. + * + * @param clazz the class of items + * @return true if any items are unregistered from the pool + */ + boolean unregister(@NonNull Class clazz); + + /** + * Returns the number of items in this pool. + * + * @return the number of items in this pool + */ + int size(); + + /** + * For getting index of the item class. If the subclass is already registered, + * the registered mapping is used. If the subclass is not registered, then look + * for its parent class if is registered, if the parent class is registered, + * the subclass is regarded as the parent class. + * + * @param clazz the item class. + * @return The index of the first occurrence of the specified class + * in this pool, or -1 if this pool does not contain the class. + */ + int firstIndexOf(@NonNull Class clazz); + + /** + * Gets the class at the specified index. + * + * @param index the item index + * @return the class at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ + @NonNull Class getClass(int index); + + /** + * Gets the item view binder at the specified index. + * + * @param index the item index + * @return the item class at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ + @NonNull + ItemViewBinder getItemViewBinder(int index); + + /** + * Gets the linker at the specified index. + * + * @param index the item index + * @return the linker at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ + @NonNull + Linker getLinker(int index); +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/ExoPlayerView.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/ExoPlayerView.java new file mode 100644 index 00000000..4ff44837 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/ExoPlayerView.java @@ -0,0 +1,54 @@ +package com.xycz.simple_live_tv.player; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.Nullable; +import androidx.media3.ui.PlayerView; + +import com.xycz.simple_live_tv.core.IVideoPlayer; + +/** + * Created by wangyan on 2024/12/22 + */ +public class ExoPlayerView extends PlayerView implements IVideoPlayer { + public ExoPlayerView(Context context) { + super(context); + } + + public ExoPlayerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ExoPlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void prepare() { + if (getPlayer() != null) { + getPlayer().prepare(); + } + } + + @Override + public void start() { + if (getPlayer() != null) { + getPlayer().play(); + } + } + + @Override + public void stop() { + if (getPlayer() != null) { + getPlayer().stop(); + } + } + + @Override + public void release() { + if (getPlayer() != null) { + getPlayer().release(); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/IjkPlayerView.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/IjkPlayerView.java new file mode 100644 index 00000000..3ce3f7c9 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/IjkPlayerView.java @@ -0,0 +1,33 @@ +package com.xycz.simple_live_tv.player; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.ijk.widget.VideoView; + +import com.xycz.simple_live_tv.core.IVideoPlayer; + +/** + * Created by wangyan on 2024/12/22 + */ +public class IjkPlayerView extends VideoView implements IVideoPlayer { + + public IjkPlayerView(@NonNull Context context) { + super(context); + } + + public IjkPlayerView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public IjkPlayerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void prepare() { + this.prepareAsync(); + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/NativePlayerView.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/NativePlayerView.java new file mode 100644 index 00000000..306fc5f6 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/NativePlayerView.java @@ -0,0 +1,162 @@ +package com.xycz.simple_live_tv.player; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.Player; +import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.VideoSize; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.datasource.HttpDataSource; +import androidx.media3.datasource.okhttp.OkHttpDataSource; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.analytics.AnalyticsListener; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.core.FlutterManager; +import com.xycz.simple_live_tv.core.LogUtils; +import com.xycz.simple_live_tv.core.MessageManager; +import com.xycz.simple_live_tv.core.MethodCallModel; +import com.xycz.simple_live_tv.core.OkHttpManager; + +import java.util.Map; + +import io.flutter.plugin.platform.PlatformView; + +import static com.xycz.simple_live_tv.core.MessageManager.FLUTTER_TO_JAVA_CMD; + +/** + * Created by wangyan on 2024/12/29 + */ +public class NativePlayerView extends ExoPlayerView implements PlatformView, Player.Listener, Handler.Callback, AnalyticsListener { + + private static final String TAG = "NativePlayerView"; + + private final Context mContext; + + private ExoPlayer mExoPlayer; + + private boolean mIsPlaying; + + public NativePlayerView(Context context) { + super(context); + mContext = context; + setUseController(false); + setId(R.id.player_view); + setFocusable(false); + setClickable(false); + initExoPlayer(); + } + + protected void initExoPlayer() { + OkHttpDataSource.Factory okHttpDataSource = new OkHttpDataSource.Factory(OkHttpManager.getInstance().getOkHttpClient()); + DefaultDataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(mContext, okHttpDataSource); + mExoPlayer = new ExoPlayer.Builder(mContext) + .setMediaSourceFactory(new DefaultMediaSourceFactory(mContext).setDataSourceFactory(dataSourceFactory)) + .build(); + // 关联ExoPlayer与PlayerView + setPlayer(mExoPlayer); + + // 添加事件监听器 + mExoPlayer.addListener(this); + + // 自动开始播放 + mExoPlayer.setPlayWhenReady(true); + + mExoPlayer.addAnalyticsListener(this); + + // 注册播放停止函数 + FlutterManager.getInstance().registerMethod("startPlay"); + FlutterManager.getInstance().registerMethod("stopPlay"); + MessageManager.getInstance().registerCallback(this); + } + + public void startToPlay(String videoUrl, Map header) { + if (header != null) { + OkHttpManager.getInstance().resetRequestHeader(header); + } + + MediaItem mediaItem = MediaItem.fromUri(videoUrl); + mExoPlayer.setMediaItem(mediaItem); + // 准备播放 + mExoPlayer.prepare(); + mExoPlayer.play(); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + this.mIsPlaying = isPlaying; + if (!isPlaying) { + // 播放结束后的处理,比如自动播放下一集(如果有)等 + FlutterManager.getInstance().invokerFlutterMethod("mediaEnd", null); + } + } + + @Override + public void onPlayerError(PlaybackException error) { + // 详细的错误处理,根据不同错误类型提示用户或者记录日志等 + String errorMessage = error.getMessage(); + Throwable cause = error.getCause(); + if (cause instanceof HttpDataSource.HttpDataSourceException) { + // An HTTP error occurred. + HttpDataSource.HttpDataSourceException httpError = (HttpDataSource.HttpDataSourceException) cause; + // It's possible to find out more about the error both by casting and by querying + // the cause. + if (httpError instanceof HttpDataSource.InvalidResponseCodeException) { + // Cast to InvalidResponseCodeException and retrieve the response code, message + // and headers. + HttpDataSource.InvalidResponseCodeException invalidResponseCodeException = (HttpDataSource.InvalidResponseCodeException)httpError; + errorMessage = invalidResponseCodeException.getMessage(); + if (invalidResponseCodeException.responseCode == 302) { + return; + } + } else { + // Try calling httpError.getCause() to retrieve the underlying cause, although + // note that it may be null. + errorMessage = httpError.getCause() == null ? "" : httpError.getCause().getMessage(); + } + } + LogUtils.e(TAG, errorMessage, error); + FlutterManager.getInstance().invokerFlutterMethod("mediaError", errorMessage); + } + + @Override + public void onVideoSizeChanged(VideoSize videoSize) { + LogUtils.i(TAG, videoSize.width + "x" + videoSize.height); + } + + @Nullable + @Override + public View getView() { + return this; + } + + @Override + public void dispose() { + release(); + MessageManager.getInstance().unRegisterCallback(this); + } + + @Override + public boolean handleMessage(@NonNull Message message) { + if (message.what == FLUTTER_TO_JAVA_CMD) { + MethodCallModel model = (MethodCallModel)message.obj; + if (message.arg1 == "startPlay".hashCode()) { + String videoUrl = model.getMethodCall().argument("videoUrl"); + startToPlay(videoUrl, null); + } else { + return false; + } + + return true; + } else { + return false; + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/VideoPlayerListener.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/VideoPlayerListener.java new file mode 100644 index 00000000..92ad0074 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/player/VideoPlayerListener.java @@ -0,0 +1,37 @@ +package com.xycz.simple_live_tv.player; + +import androidx.ijk.listener.OnVideoListener; + +import tv.danmaku.ijk.media.player.IMediaPlayer; + +/** + * Created by wangyan on 2024/12/21 + */ +public interface VideoPlayerListener extends OnVideoListener { + + default void onVideoPrepared(IMediaPlayer var1) {} + + default void onVideoSizeChanged(IMediaPlayer mp, int width, int height, int sar_num, int sar_den) {} + + default void onVideoSeekEnable(boolean var1) {} + + default void onVideoBufferingStart(IMediaPlayer var1, int var2) {} + + default void onVideoBufferingEnd(IMediaPlayer var1, int var2) {} + + default void onVideoRenderingStart(IMediaPlayer var1, int var2) {} + + default void onVideoRotationChanged(IMediaPlayer var1, int var2) {} + + default void onVideoTrackLagging(IMediaPlayer var1, int var2) {} + + default void onVideoBadInterleaving(IMediaPlayer var1, int var2) {} + + default void onVideoSeekComplete(IMediaPlayer var1) {} + + default void onVideoProgress(IMediaPlayer var1, long var2, long var4) {} + + default void onVideoCompletion(IMediaPlayer var1) {} + + default void onVideoError(IMediaPlayer mp, int what, int extra) {} +} \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/widgets/BaseDialog.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/widgets/BaseDialog.java new file mode 100644 index 00000000..425a4f11 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/widgets/BaseDialog.java @@ -0,0 +1,64 @@ +package com.xycz.simple_live_tv.widgets; + +import android.app.Dialog; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +import com.xycz.simple_live_tv.R; + +public class BaseDialog extends Dialog { + public BaseDialog(@NonNull Context context) { + super(context, R.style.CustomDialogStyle); + } + + public BaseDialog(Context context, int customDialogStyle) { + super(context, customDialogStyle); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + adaptCutoutAboveAndroidP(this, true); // 设置刘海 + super.onCreate(savedInstanceState); + } + + @Override + public void show() { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + super.show(); + hideSysBar(); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + } + + private void hideSysBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + int uiOptions = getWindow().getDecorView().getSystemUiVisibility(); + uiOptions |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + // uiOptions |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + uiOptions |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + // uiOptions |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + uiOptions |= View.SYSTEM_UI_FLAG_FULLSCREEN; + uiOptions |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + getWindow().getDecorView().setSystemUiVisibility(uiOptions); + } + } + + /** + * 适配刘海屏,针对Android P以上系统 + */ + public static void adaptCutoutAboveAndroidP(Dialog dialog, boolean isAdapt) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WindowManager.LayoutParams lp = dialog.getWindow().getAttributes(); + if (isAdapt) { + lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } else { + lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; + } + dialog.getWindow().setAttributes(lp); + } + } +} diff --git a/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/widgets/SelectDialog.java b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/widgets/SelectDialog.java new file mode 100644 index 00000000..fc795f72 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/java/com/xycz/simple_live_tv/widgets/SelectDialog.java @@ -0,0 +1,84 @@ +package com.xycz.simple_live_tv.widgets; + +import android.content.Context; +import android.os.Bundle; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import com.xycz.simple_live_tv.R; +import com.xycz.simple_live_tv.adapter.SelectAdapter; +import com.owen.tvrecyclerview.widget.TvRecyclerView; + +import java.util.LinkedHashMap; + +public class SelectDialog extends BaseDialog { + + private boolean muteCheck = false; + + public SelectDialog(@NonNull Context context) { + super(context); + setContentView(R.layout.dialog_select); + } + + public SelectDialog(@NonNull Context context, int resId) { + super(context); + setContentView(resId); + } + + public void setItemCheckDisplay(boolean shouldShowCheck) { + muteCheck = !shouldShowCheck; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + public void setTip(String tip) { + ((TextView) findViewById(R.id.title)).setText(tip); + } + + public void setAdapter(TvRecyclerView tvRecyclerView, SelectAdapter.ISelectDialog selectDialogCb, DiffUtil.ItemCallback itemDiffCallback, LinkedHashMap data, T select) { + int selectIndex = 0; + if (select == null) { + for (T key : data.keySet()) { + select = key; + break; + } + } else { + int index = 0; + for (T key : data.keySet()) { + if (key == select) { + selectIndex = index; + break; + } + + index++; + } + } + + SelectAdapter adapter = new SelectAdapter<>(selectDialogCb, itemDiffCallback, muteCheck); + adapter.setData(data, select); + if (tvRecyclerView == null) { + tvRecyclerView = findViewById(R.id.list); + } + + tvRecyclerView.setAdapter(adapter); + tvRecyclerView.setSelectedPosition(selectIndex); + if (selectIndex < 10) { + tvRecyclerView.setSelection(selectIndex); + } + + TvRecyclerView finalTvRecyclerView = tvRecyclerView; + int finalSelectIndex = selectIndex; + tvRecyclerView.post(new Runnable() { + @Override + public void run() { + finalTvRecyclerView.smoothScrollToPosition(finalSelectIndex); + finalTvRecyclerView.setSelectionWithSmooth(finalSelectIndex); + } + }); + } +} diff --git a/simple_live_tv_app/android/app/src/main/kotlin/com/xycz/simple_live_tv/MainActivity.kt b/simple_live_tv_app/android/app/src/main/kotlin/com/xycz/simple_live_tv/MainActivity.kt deleted file mode 100644 index 424fe72d..00000000 --- a/simple_live_tv_app/android/app/src/main/kotlin/com/xycz/simple_live_tv/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.xycz.simple_live_tv - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/simple_live_tv_app/android/app/src/main/res/color/selector.xml b/simple_live_tv_app/android/app/src/main/res/color/selector.xml new file mode 100644 index 00000000..b5873674 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/color/selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/button_dialog_main.xml b/simple_live_tv_app/android/app/src/main/res/drawable/button_dialog_main.xml new file mode 100644 index 00000000..7c5efaa0 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/drawable/button_dialog_main.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_back.webp b/simple_live_tv_app/android/app/src/main/res/drawable/icon_back.webp new file mode 100644 index 00000000..784ff7a6 Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_back.webp differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_clarity.webp b/simple_live_tv_app/android/app/src/main/res/drawable/icon_clarity.webp new file mode 100644 index 00000000..618235ce Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_clarity.webp differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_danmaku_close.png b/simple_live_tv_app/android/app/src/main/res/drawable/icon_danmaku_close.png new file mode 100644 index 00000000..f4058f6f Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_danmaku_close.png differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_danmaku_open.png b/simple_live_tv_app/android/app/src/main/res/drawable/icon_danmaku_open.png new file mode 100644 index 00000000..f3b10918 Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_danmaku_open.png differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_like.webp b/simple_live_tv_app/android/app/src/main/res/drawable/icon_like.webp new file mode 100644 index 00000000..a19cee0e Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_like.webp differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_line.webp b/simple_live_tv_app/android/app/src/main/res/drawable/icon_line.webp new file mode 100644 index 00000000..5e1dbbf9 Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_line.webp differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_more.webp b/simple_live_tv_app/android/app/src/main/res/drawable/icon_more.webp new file mode 100644 index 00000000..66834b17 Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_more.webp differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_ratio.webp b/simple_live_tv_app/android/app/src/main/res/drawable/icon_ratio.webp new file mode 100644 index 00000000..7c80b0a4 Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_ratio.webp differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_refresh.xml b/simple_live_tv_app/android/app/src/main/res/drawable/icon_refresh.xml new file mode 100644 index 00000000..57f9f76d --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/drawable/icon_refresh.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_select.xml b/simple_live_tv_app/android/app/src/main/res/drawable/icon_select.xml new file mode 100644 index 00000000..161d72d8 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/drawable/icon_select.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/icon_unlike.webp b/simple_live_tv_app/android/app/src/main/res/drawable/icon_unlike.webp new file mode 100644 index 00000000..5be27407 Binary files /dev/null and b/simple_live_tv_app/android/app/src/main/res/drawable/icon_unlike.webp differ diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/shape_dialog_bg_main.xml b/simple_live_tv_app/android/app/src/main/res/drawable/shape_dialog_bg_main.xml new file mode 100644 index 00000000..c90a17dc --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/drawable/shape_dialog_bg_main.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/drawable/shape_model_focus.xml b/simple_live_tv_app/android/app/src/main/res/drawable/shape_model_focus.xml new file mode 100644 index 00000000..6c6e843c --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/drawable/shape_model_focus.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/layout/activity_live.xml b/simple_live_tv_app/android/app/src/main/res/layout/activity_live.xml new file mode 100644 index 00000000..216083e2 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/layout/activity_live.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/layout/dialog_select.xml b/simple_live_tv_app/android/app/src/main/res/layout/dialog_select.xml new file mode 100644 index 00000000..0444aad9 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/layout/dialog_select.xml @@ -0,0 +1,46 @@ + + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/layout/item_button.xml b/simple_live_tv_app/android/app/src/main/res/layout/item_button.xml new file mode 100644 index 00000000..6709b3bd --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/layout/item_button.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/layout/item_dialog_select.xml b/simple_live_tv_app/android/app/src/main/res/layout/item_dialog_select.xml new file mode 100644 index 00000000..065f3b0e --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/layout/item_dialog_select.xml @@ -0,0 +1,18 @@ + + + diff --git a/simple_live_tv_app/android/app/src/main/res/layout/item_line.xml b/simple_live_tv_app/android/app/src/main/res/layout/item_line.xml new file mode 100644 index 00000000..cc293c9e --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/layout/item_line.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/layout/item_select.xml b/simple_live_tv_app/android/app/src/main/res/layout/item_select.xml new file mode 100644 index 00000000..146f7755 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/layout/item_select.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/values-hdpi/distance_pool.xml b/simple_live_tv_app/android/app/src/main/res/values-hdpi/distance_pool.xml new file mode 100644 index 00000000..ed9270df --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/values-hdpi/distance_pool.xml @@ -0,0 +1,1084 @@ + + + 0.0dp + 0.4dp + 0.9dp + 1.3dp + 1.8dp + 2.2dp + 2.7dp + 3.1dp + 3.6dp + 4.0dp + 4.4dp + 4.9dp + 5.3dp + 5.8dp + 6.2dp + 6.7dp + 7.1dp + 7.6dp + 8.0dp + 8.4dp + 8.9dp + 9.3dp + 9.8dp + 10.2dp + 10.7dp + 11.1dp + 11.6dp + 12.0dp + 12.4dp + 12.9dp + 13.3dp + 13.8dp + 14.2dp + 14.7dp + 15.1dp + 15.6dp + 16.0dp + 16.4dp + 16.9dp + 17.3dp + 17.8dp + 18.2dp + 18.7dp + 19.1dp + 19.6dp + 20.0dp + 20.4dp + 20.9dp + 21.3dp + 21.8dp + 22.2dp + 22.7dp + 23.1dp + 23.6dp + 24.0dp + 24.4dp + 24.9dp + 25.3dp + 25.8dp + 26.2dp + 26.7dp + 27.1dp + 27.6dp + 28.0dp + 28.4dp + 28.9dp + 29.3dp + 29.8dp + 30.2dp + 30.7dp + 31.1dp + 31.6dp + 32.0dp + 32.4dp + 32.9dp + 33.3dp + 33.8dp + 34.2dp + 34.7dp + 35.1dp + 35.6dp + 36.0dp + 36.4dp + 36.9dp + 37.3dp + 37.8dp + 38.2dp + 38.7dp + 39.1dp + 39.6dp + 40.0dp + 40.4dp + 40.9dp + 41.3dp + 41.8dp + 42.2dp + 42.7dp + 43.1dp + 43.6dp + 44.0dp + 44.4dp + 44.9dp + 45.3dp + 45.8dp + 46.2dp + 46.7dp + 47.1dp + 47.6dp + 48.0dp + 48.4dp + 48.9dp + 49.3dp + 49.8dp + 50.2dp + 50.7dp + 51.1dp + 51.6dp + 52.0dp + 52.4dp + 52.9dp + 53.3dp + 53.8dp + 54.2dp + 54.7dp + 55.1dp + 55.6dp + 56.0dp + 56.4dp + 56.9dp + 57.3dp + 57.8dp + 58.2dp + 58.7dp + 59.1dp + 59.6dp + 60.0dp + 60.4dp + 60.9dp + 61.3dp + 61.8dp + 62.2dp + 62.7dp + 63.1dp + 63.6dp + 64.0dp + 64.4dp + 64.9dp + 65.3dp + 65.8dp + 66.2dp + 66.7dp + 67.1dp + 67.6dp + 68.0dp + 68.4dp + 68.9dp + 69.3dp + 69.8dp + 70.2dp + 70.7dp + 71.1dp + 71.6dp + 72.0dp + 72.4dp + 72.9dp + 73.3dp + 73.8dp + 74.2dp + 74.7dp + 75.1dp + 75.6dp + 76.0dp + 76.4dp + 76.9dp + 77.3dp + 77.8dp + 78.2dp + 78.7dp + 79.1dp + 79.6dp + 80.0dp + 80.4dp + 80.9dp + 81.3dp + 81.8dp + 82.2dp + 82.7dp + 83.1dp + 83.6dp + 84.0dp + 84.4dp + 84.9dp + 85.3dp + 85.8dp + 86.2dp + 86.7dp + 87.1dp + 87.6dp + 88.0dp + 88.4dp + 88.9dp + 89.3dp + 89.8dp + 90.2dp + 90.7dp + 91.1dp + 91.6dp + 92.0dp + 92.4dp + 92.9dp + 93.3dp + 93.8dp + 94.2dp + 94.7dp + 95.1dp + 95.6dp + 96.0dp + 96.4dp + 96.9dp + 97.3dp + 97.8dp + 98.2dp + 98.7dp + 99.1dp + 99.6dp + 100.0dp + 100.4dp + 100.9dp + 101.3dp + 101.8dp + 102.2dp + 102.7dp + 103.1dp + 103.6dp + 104.0dp + 104.4dp + 104.9dp + 105.3dp + 105.8dp + 106.2dp + 106.7dp + 107.1dp + 107.6dp + 108.0dp + 108.4dp + 108.9dp + 109.3dp + 109.8dp + 110.2dp + 110.7dp + 111.1dp + 111.6dp + 112.0dp + 112.4dp + 112.9dp + 113.3dp + 113.8dp + 114.2dp + 114.7dp + 115.1dp + 115.6dp + 116.0dp + 116.4dp + 116.9dp + 117.3dp + 117.8dp + 118.2dp + 118.7dp + 119.1dp + 119.6dp + 120.0dp + 120.4dp + 120.9dp + 121.3dp + 121.8dp + 122.2dp + 122.7dp + 123.1dp + 123.6dp + 124.0dp + 124.4dp + 124.9dp + 125.3dp + 125.8dp + 126.2dp + 126.7dp + 127.1dp + 127.6dp + 128.0dp + 128.4dp + 128.9dp + 129.3dp + 129.8dp + 130.2dp + 130.7dp + 131.1dp + 131.6dp + 132.0dp + 132.4dp + 132.9dp + 133.3dp + 133.8dp + 134.2dp + 134.7dp + 135.1dp + 135.6dp + 136.0dp + 136.4dp + 136.9dp + 137.3dp + 137.8dp + 138.2dp + 138.7dp + 139.1dp + 139.6dp + 140.0dp + 140.4dp + 140.9dp + 141.3dp + 141.8dp + 142.2dp + 142.7dp + 143.1dp + 143.6dp + 144.0dp + 144.4dp + 144.9dp + 145.3dp + 145.8dp + 146.2dp + 146.7dp + 147.1dp + 147.6dp + 148.0dp + 148.4dp + 148.9dp + 149.3dp + 149.8dp + 150.2dp + 150.7dp + 151.1dp + 151.6dp + 152.0dp + 152.4dp + 152.9dp + 153.3dp + 153.8dp + 154.2dp + 154.7dp + 155.1dp + 155.6dp + 156.0dp + 156.4dp + 156.9dp + 157.3dp + 157.8dp + 158.2dp + 158.7dp + 159.1dp + 159.6dp + 160.0dp + 160.4dp + 160.9dp + 161.3dp + 161.8dp + 162.2dp + 162.7dp + 163.1dp + 163.6dp + 164.0dp + 164.4dp + 164.9dp + 165.3dp + 165.8dp + 166.2dp + 166.7dp + 167.1dp + 167.6dp + 168.0dp + 168.4dp + 168.9dp + 169.3dp + 169.8dp + 170.2dp + 170.7dp + 171.1dp + 171.6dp + 172.0dp + 172.4dp + 172.9dp + 173.3dp + 173.8dp + 174.2dp + 174.7dp + 175.1dp + 175.6dp + 176.0dp + 176.4dp + 176.9dp + 177.3dp + 177.8dp + 178.2dp + 178.7dp + 179.1dp + 179.6dp + 180.0dp + 180.4dp + 180.9dp + 181.3dp + 181.8dp + 182.2dp + 182.7dp + 183.1dp + 183.6dp + 184.0dp + 184.4dp + 184.9dp + 185.3dp + 185.8dp + 186.2dp + 186.7dp + 187.1dp + 187.6dp + 188.0dp + 188.4dp + 188.9dp + 189.3dp + 189.8dp + 190.2dp + 190.7dp + 191.1dp + 191.6dp + 192.0dp + 192.4dp + 192.9dp + 193.3dp + 193.8dp + 194.2dp + 194.7dp + 195.1dp + 195.6dp + 196.0dp + 196.4dp + 196.9dp + 197.3dp + 197.8dp + 198.2dp + 198.7dp + 199.1dp + 199.6dp + 200.0dp + 200.4dp + 200.9dp + 201.3dp + 201.8dp + 202.2dp + 202.7dp + 203.1dp + 203.6dp + 204.0dp + 204.4dp + 204.9dp + 205.3dp + 205.8dp + 206.2dp + 206.7dp + 207.1dp + 207.6dp + 208.0dp + 208.4dp + 208.9dp + 209.3dp + 209.8dp + 210.2dp + 210.7dp + 211.1dp + 211.6dp + 212.0dp + 212.4dp + 212.9dp + 213.3dp + 213.8dp + 214.2dp + 214.7dp + 215.1dp + 215.6dp + 216.0dp + 216.4dp + 216.9dp + 217.3dp + 217.8dp + 218.2dp + 218.7dp + 219.1dp + 219.6dp + 220.0dp + 220.4dp + 220.9dp + 221.3dp + 221.8dp + 222.2dp + 222.7dp + 223.1dp + 223.6dp + 224.0dp + 224.4dp + 224.9dp + 225.3dp + 225.8dp + 226.2dp + 226.7dp + 227.1dp + 227.6dp + 228.0dp + 228.4dp + 228.9dp + 229.3dp + 229.8dp + 230.2dp + 230.7dp + 231.1dp + 231.6dp + 232.0dp + 232.4dp + 232.9dp + 233.3dp + 233.8dp + 234.2dp + 234.7dp + 235.1dp + 235.6dp + 236.0dp + 236.4dp + 236.9dp + 237.3dp + 237.8dp + 238.2dp + 238.7dp + 239.1dp + 239.6dp + 240.0dp + 240.4dp + 240.9dp + 241.3dp + 241.8dp + 242.2dp + 242.7dp + 243.1dp + 243.6dp + 244.0dp + 244.4dp + 244.9dp + 245.3dp + 245.8dp + 246.2dp + 246.7dp + 247.1dp + 247.6dp + 248.0dp + 248.4dp + 248.9dp + 249.3dp + 249.8dp + 250.2dp + 250.7dp + 251.1dp + 251.6dp + 252.0dp + 252.4dp + 252.9dp + 253.3dp + 253.8dp + 254.2dp + 254.7dp + 255.1dp + 255.6dp + 256.0dp + 256.4dp + 256.9dp + 257.3dp + 257.8dp + 258.2dp + 258.7dp + 259.1dp + 259.6dp + 260.0dp + 260.4dp + 260.9dp + 261.3dp + 261.8dp + 262.2dp + 262.7dp + 263.1dp + 263.6dp + 264.0dp + 264.4dp + 264.9dp + 265.3dp + 265.8dp + 266.2dp + 266.7dp + 267.1dp + 267.6dp + 268.0dp + 268.4dp + 268.9dp + 269.3dp + 269.8dp + 270.2dp + 270.7dp + 271.1dp + 271.6dp + 272.0dp + 272.4dp + 272.9dp + 273.3dp + 273.8dp + 274.2dp + 274.7dp + 275.1dp + 275.6dp + 276.0dp + 276.4dp + 276.9dp + 277.3dp + 277.8dp + 278.2dp + 278.7dp + 279.1dp + 279.6dp + 280.0dp + 280.4dp + 280.9dp + 281.3dp + 281.8dp + 282.2dp + 282.7dp + 283.1dp + 283.6dp + 284.0dp + 284.4dp + 284.9dp + 285.3dp + 285.8dp + 286.2dp + 286.7dp + 287.1dp + 287.6dp + 288.0dp + 288.4dp + 288.9dp + 289.3dp + 289.8dp + 290.2dp + 290.7dp + 291.1dp + 291.6dp + 292.0dp + 292.4dp + 292.9dp + 293.3dp + 293.8dp + 294.2dp + 294.7dp + 295.1dp + 295.6dp + 296.0dp + 296.4dp + 296.9dp + 297.3dp + 297.8dp + 298.2dp + 298.7dp + 299.1dp + 299.6dp + 300.0dp + 300.4dp + 300.9dp + 301.3dp + 301.8dp + 302.2dp + 302.7dp + 303.1dp + 303.6dp + 304.0dp + 304.4dp + 304.9dp + 305.3dp + 305.8dp + 306.2dp + 306.7dp + 307.1dp + 307.6dp + 308.0dp + 308.4dp + 308.9dp + 309.3dp + 309.8dp + 310.2dp + 310.7dp + 311.1dp + 311.6dp + 312.0dp + 312.4dp + 312.9dp + 313.3dp + 313.8dp + 314.2dp + 314.7dp + 315.1dp + 315.6dp + 316.0dp + 316.4dp + 316.9dp + 317.3dp + 317.8dp + 318.2dp + 318.7dp + 319.1dp + 319.6dp + 320.0dp + 320.4dp + 320.9dp + 321.3dp + 321.8dp + 322.2dp + 322.7dp + 323.1dp + 323.6dp + 324.0dp + 324.4dp + 324.9dp + 325.3dp + 325.8dp + 326.2dp + 326.7dp + 327.1dp + 327.6dp + 328.0dp + 328.4dp + 328.9dp + 329.3dp + 329.8dp + 330.2dp + 330.7dp + 331.1dp + 331.6dp + 332.0dp + 332.4dp + 332.9dp + 333.3dp + 333.8dp + 334.2dp + 334.7dp + 335.1dp + 335.6dp + 336.0dp + 336.4dp + 336.9dp + 337.3dp + 337.8dp + 338.2dp + 338.7dp + 339.1dp + 339.6dp + 340.0dp + 340.4dp + 340.9dp + 341.3dp + 341.8dp + 342.2dp + 342.7dp + 343.1dp + 343.6dp + 344.0dp + 344.4dp + 344.9dp + 345.3dp + 345.8dp + 346.2dp + 346.7dp + 347.1dp + 347.6dp + 348.0dp + 348.4dp + 348.9dp + 349.3dp + 349.8dp + 350.2dp + 350.7dp + 351.1dp + 351.6dp + 352.0dp + 352.4dp + 352.9dp + 353.3dp + 353.8dp + 354.2dp + 354.7dp + 355.1dp + 355.6dp + 356.0dp + 356.4dp + 356.9dp + 357.3dp + 357.8dp + 358.2dp + 358.7dp + 359.1dp + 359.6dp + 360.0dp + 360.4dp + 360.9dp + 361.3dp + 361.8dp + 362.2dp + 362.7dp + 363.1dp + 363.6dp + 364.0dp + 364.4dp + 364.9dp + 365.3dp + 365.8dp + 366.2dp + 366.7dp + 367.1dp + 367.6dp + 368.0dp + 368.4dp + 368.9dp + 369.3dp + 369.8dp + 370.2dp + 370.7dp + 371.1dp + 371.6dp + 372.0dp + 372.4dp + 372.9dp + 373.3dp + 373.8dp + 374.2dp + 374.7dp + 375.1dp + 375.6dp + 376.0dp + 376.4dp + 376.9dp + 377.3dp + 377.8dp + 378.2dp + 378.7dp + 379.1dp + 379.6dp + 380.0dp + 380.4dp + 380.9dp + 381.3dp + 381.8dp + 382.2dp + 382.7dp + 383.1dp + 383.6dp + 384.0dp + 384.4dp + 384.9dp + 385.3dp + 385.8dp + 386.2dp + 386.7dp + 387.1dp + 387.6dp + 388.0dp + 388.4dp + 388.9dp + 389.3dp + 389.8dp + 390.2dp + 390.7dp + 391.1dp + 391.6dp + 392.0dp + 392.4dp + 392.9dp + 393.3dp + 393.8dp + 394.2dp + 394.7dp + 395.1dp + 395.6dp + 396.0dp + 396.4dp + 396.9dp + 397.3dp + 397.8dp + 398.2dp + 398.7dp + 399.1dp + 399.6dp + 400.0dp + 400.4dp + 400.9dp + 401.3dp + 401.8dp + 402.2dp + 402.7dp + 403.1dp + 403.6dp + 404.0dp + 404.4dp + 404.9dp + 405.3dp + 405.8dp + 406.2dp + 406.7dp + 407.1dp + 407.6dp + 408.0dp + 408.4dp + 408.9dp + 409.3dp + 409.8dp + 410.2dp + 410.7dp + 411.1dp + 411.6dp + 412.0dp + 412.4dp + 412.9dp + 413.3dp + 413.8dp + 414.2dp + 414.7dp + 415.1dp + 415.6dp + 416.0dp + 416.4dp + 416.9dp + 417.3dp + 417.8dp + 418.2dp + 418.7dp + 419.1dp + 419.6dp + 420.0dp + 420.4dp + 420.9dp + 421.3dp + 421.8dp + 422.2dp + 422.7dp + 423.1dp + 423.6dp + 424.0dp + 424.4dp + 424.9dp + 425.3dp + 425.8dp + 426.2dp + 426.7dp + 427.1dp + 427.6dp + 428.0dp + 428.4dp + 428.9dp + 429.3dp + 429.8dp + 430.2dp + 430.7dp + 431.1dp + 431.6dp + 432.0dp + 432.4dp + 432.9dp + 433.3dp + 433.8dp + 434.2dp + 434.7dp + 435.1dp + 435.6dp + 436.0dp + 436.4dp + 436.9dp + 437.3dp + 437.8dp + 438.2dp + 438.7dp + 439.1dp + 439.6dp + 440.0dp + 440.4dp + 440.9dp + 441.3dp + 441.8dp + 442.2dp + 442.7dp + 443.1dp + 443.6dp + 444.0dp + 444.4dp + 444.9dp + 445.3dp + 445.8dp + 446.2dp + 446.7dp + 447.1dp + 447.6dp + 448.0dp + 448.4dp + 448.9dp + 449.3dp + 449.8dp + 450.2dp + 450.7dp + 451.1dp + 451.6dp + 452.0dp + 452.4dp + 452.9dp + 453.3dp + 453.8dp + 454.2dp + 454.7dp + 455.1dp + 455.6dp + 456.0dp + 456.4dp + 456.9dp + 457.3dp + 457.8dp + 458.2dp + 458.7dp + 459.1dp + 459.6dp + 460.0dp + 460.4dp + 460.9dp + 461.3dp + 461.8dp + 462.2dp + 462.7dp + 463.1dp + 463.6dp + 464.0dp + 464.4dp + 464.9dp + 465.3dp + 465.8dp + 466.2dp + 466.7dp + 467.1dp + 467.6dp + 468.0dp + 468.4dp + 468.9dp + 469.3dp + 469.8dp + 470.2dp + 470.7dp + 471.1dp + 471.6dp + 472.0dp + 472.4dp + 472.9dp + 473.3dp + 473.8dp + 474.2dp + 474.7dp + 475.1dp + 475.6dp + 476.0dp + 476.4dp + 476.9dp + 477.3dp + 477.8dp + 478.2dp + 478.7dp + 479.1dp + 479.6dp + 480.0dp + diff --git a/simple_live_tv_app/android/app/src/main/res/values-night/styles.xml b/simple_live_tv_app/android/app/src/main/res/values-night/styles.xml index 06952be7..16b65f44 100644 --- a/simple_live_tv_app/android/app/src/main/res/values-night/styles.xml +++ b/simple_live_tv_app/android/app/src/main/res/values-night/styles.xml @@ -15,4 +15,16 @@ + + + diff --git a/simple_live_tv_app/android/app/src/main/res/values-xxhdpi/distance_pool.xml b/simple_live_tv_app/android/app/src/main/res/values-xxhdpi/distance_pool.xml new file mode 100644 index 00000000..ce0ad693 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/values-xxhdpi/distance_pool.xml @@ -0,0 +1,1084 @@ + + + 0.0dp + 0.3dp + 0.7dp + 1.0dp + 1.3dp + 1.7dp + 2.0dp + 2.3dp + 2.7dp + 3.0dp + 3.3dp + 3.7dp + 4.0dp + 4.3dp + 4.7dp + 5.0dp + 5.3dp + 5.7dp + 6.0dp + 6.3dp + 6.7dp + 7.0dp + 7.3dp + 7.7dp + 8.0dp + 8.3dp + 8.7dp + 9.0dp + 9.3dp + 9.7dp + 10.0dp + 10.3dp + 10.7dp + 11.0dp + 11.3dp + 11.7dp + 12.0dp + 12.3dp + 12.7dp + 13.0dp + 13.3dp + 13.7dp + 14.0dp + 14.3dp + 14.7dp + 15.0dp + 15.3dp + 15.7dp + 16.0dp + 16.3dp + 16.7dp + 17.0dp + 17.3dp + 17.7dp + 18.0dp + 18.3dp + 18.7dp + 19.0dp + 19.3dp + 19.7dp + 20.0dp + 20.3dp + 20.7dp + 21.0dp + 21.3dp + 21.7dp + 22.0dp + 22.3dp + 22.7dp + 23.0dp + 23.3dp + 23.7dp + 24.0dp + 24.3dp + 24.7dp + 25.0dp + 25.3dp + 25.7dp + 26.0dp + 26.3dp + 26.7dp + 27.0dp + 27.3dp + 27.7dp + 28.0dp + 28.3dp + 28.7dp + 29.0dp + 29.3dp + 29.7dp + 30.0dp + 30.3dp + 30.7dp + 31.0dp + 31.3dp + 31.7dp + 32.0dp + 32.3dp + 32.7dp + 33.0dp + 33.3dp + 33.7dp + 34.0dp + 34.3dp + 34.7dp + 35.0dp + 35.3dp + 35.7dp + 36.0dp + 36.3dp + 36.7dp + 37.0dp + 37.3dp + 37.7dp + 38.0dp + 38.3dp + 38.7dp + 39.0dp + 39.3dp + 39.7dp + 40.0dp + 40.3dp + 40.7dp + 41.0dp + 41.3dp + 41.7dp + 42.0dp + 42.3dp + 42.7dp + 43.0dp + 43.3dp + 43.7dp + 44.0dp + 44.3dp + 44.7dp + 45.0dp + 45.3dp + 45.7dp + 46.0dp + 46.3dp + 46.7dp + 47.0dp + 47.3dp + 47.7dp + 48.0dp + 48.3dp + 48.7dp + 49.0dp + 49.3dp + 49.7dp + 50.0dp + 50.3dp + 50.7dp + 51.0dp + 51.3dp + 51.7dp + 52.0dp + 52.3dp + 52.7dp + 53.0dp + 53.3dp + 53.7dp + 54.0dp + 54.3dp + 54.7dp + 55.0dp + 55.3dp + 55.7dp + 56.0dp + 56.3dp + 56.7dp + 57.0dp + 57.3dp + 57.7dp + 58.0dp + 58.3dp + 58.7dp + 59.0dp + 59.3dp + 59.7dp + 60.0dp + 60.3dp + 60.7dp + 61.0dp + 61.3dp + 61.7dp + 62.0dp + 62.3dp + 62.7dp + 63.0dp + 63.3dp + 63.7dp + 64.0dp + 64.3dp + 64.7dp + 65.0dp + 65.3dp + 65.7dp + 66.0dp + 66.3dp + 66.7dp + 67.0dp + 67.3dp + 67.7dp + 68.0dp + 68.3dp + 68.7dp + 69.0dp + 69.3dp + 69.7dp + 70.0dp + 70.3dp + 70.7dp + 71.0dp + 71.3dp + 71.7dp + 72.0dp + 72.3dp + 72.7dp + 73.0dp + 73.3dp + 73.7dp + 74.0dp + 74.3dp + 74.7dp + 75.0dp + 75.3dp + 75.7dp + 76.0dp + 76.3dp + 76.7dp + 77.0dp + 77.3dp + 77.7dp + 78.0dp + 78.3dp + 78.7dp + 79.0dp + 79.3dp + 79.7dp + 80.0dp + 80.3dp + 80.7dp + 81.0dp + 81.3dp + 81.7dp + 82.0dp + 82.3dp + 82.7dp + 83.0dp + 83.3dp + 83.7dp + 84.0dp + 84.3dp + 84.7dp + 85.0dp + 85.3dp + 85.7dp + 86.0dp + 86.3dp + 86.7dp + 87.0dp + 87.3dp + 87.7dp + 88.0dp + 88.3dp + 88.7dp + 89.0dp + 89.3dp + 89.7dp + 90.0dp + 90.3dp + 90.7dp + 91.0dp + 91.3dp + 91.7dp + 92.0dp + 92.3dp + 92.7dp + 93.0dp + 93.3dp + 93.7dp + 94.0dp + 94.3dp + 94.7dp + 95.0dp + 95.3dp + 95.7dp + 96.0dp + 96.3dp + 96.7dp + 97.0dp + 97.3dp + 97.7dp + 98.0dp + 98.3dp + 98.7dp + 99.0dp + 99.3dp + 99.7dp + 100.0dp + 100.3dp + 100.7dp + 101.0dp + 101.3dp + 101.7dp + 102.0dp + 102.3dp + 102.7dp + 103.0dp + 103.3dp + 103.7dp + 104.0dp + 104.3dp + 104.7dp + 105.0dp + 105.3dp + 105.7dp + 106.0dp + 106.3dp + 106.7dp + 107.0dp + 107.3dp + 107.7dp + 108.0dp + 108.3dp + 108.7dp + 109.0dp + 109.3dp + 109.7dp + 110.0dp + 110.3dp + 110.7dp + 111.0dp + 111.3dp + 111.7dp + 112.0dp + 112.3dp + 112.7dp + 113.0dp + 113.3dp + 113.7dp + 114.0dp + 114.3dp + 114.7dp + 115.0dp + 115.3dp + 115.7dp + 116.0dp + 116.3dp + 116.7dp + 117.0dp + 117.3dp + 117.7dp + 118.0dp + 118.3dp + 118.7dp + 119.0dp + 119.3dp + 119.7dp + 120.0dp + 120.3dp + 120.7dp + 121.0dp + 121.3dp + 121.7dp + 122.0dp + 122.3dp + 122.7dp + 123.0dp + 123.3dp + 123.7dp + 124.0dp + 124.3dp + 124.7dp + 125.0dp + 125.3dp + 125.7dp + 126.0dp + 126.3dp + 126.7dp + 127.0dp + 127.3dp + 127.7dp + 128.0dp + 128.3dp + 128.7dp + 129.0dp + 129.3dp + 129.7dp + 130.0dp + 130.3dp + 130.7dp + 131.0dp + 131.3dp + 131.7dp + 132.0dp + 132.3dp + 132.7dp + 133.0dp + 133.3dp + 133.7dp + 134.0dp + 134.3dp + 134.7dp + 135.0dp + 135.3dp + 135.7dp + 136.0dp + 136.3dp + 136.7dp + 137.0dp + 137.3dp + 137.7dp + 138.0dp + 138.3dp + 138.7dp + 139.0dp + 139.3dp + 139.7dp + 140.0dp + 140.3dp + 140.7dp + 141.0dp + 141.3dp + 141.7dp + 142.0dp + 142.3dp + 142.7dp + 143.0dp + 143.3dp + 143.7dp + 144.0dp + 144.3dp + 144.7dp + 145.0dp + 145.3dp + 145.7dp + 146.0dp + 146.3dp + 146.7dp + 147.0dp + 147.3dp + 147.7dp + 148.0dp + 148.3dp + 148.7dp + 149.0dp + 149.3dp + 149.7dp + 150.0dp + 150.3dp + 150.7dp + 151.0dp + 151.3dp + 151.7dp + 152.0dp + 152.3dp + 152.7dp + 153.0dp + 153.3dp + 153.7dp + 154.0dp + 154.3dp + 154.7dp + 155.0dp + 155.3dp + 155.7dp + 156.0dp + 156.3dp + 156.7dp + 157.0dp + 157.3dp + 157.7dp + 158.0dp + 158.3dp + 158.7dp + 159.0dp + 159.3dp + 159.7dp + 160.0dp + 160.3dp + 160.7dp + 161.0dp + 161.3dp + 161.7dp + 162.0dp + 162.3dp + 162.7dp + 163.0dp + 163.3dp + 163.7dp + 164.0dp + 164.3dp + 164.7dp + 165.0dp + 165.3dp + 165.7dp + 166.0dp + 166.3dp + 166.7dp + 167.0dp + 167.3dp + 167.7dp + 168.0dp + 168.3dp + 168.7dp + 169.0dp + 169.3dp + 169.7dp + 170.0dp + 170.3dp + 170.7dp + 171.0dp + 171.3dp + 171.7dp + 172.0dp + 172.3dp + 172.7dp + 173.0dp + 173.3dp + 173.7dp + 174.0dp + 174.3dp + 174.7dp + 175.0dp + 175.3dp + 175.7dp + 176.0dp + 176.3dp + 176.7dp + 177.0dp + 177.3dp + 177.7dp + 178.0dp + 178.3dp + 178.7dp + 179.0dp + 179.3dp + 179.7dp + 180.0dp + 180.3dp + 180.7dp + 181.0dp + 181.3dp + 181.7dp + 182.0dp + 182.3dp + 182.7dp + 183.0dp + 183.3dp + 183.7dp + 184.0dp + 184.3dp + 184.7dp + 185.0dp + 185.3dp + 185.7dp + 186.0dp + 186.3dp + 186.7dp + 187.0dp + 187.3dp + 187.7dp + 188.0dp + 188.3dp + 188.7dp + 189.0dp + 189.3dp + 189.7dp + 190.0dp + 190.3dp + 190.7dp + 191.0dp + 191.3dp + 191.7dp + 192.0dp + 192.3dp + 192.7dp + 193.0dp + 193.3dp + 193.7dp + 194.0dp + 194.3dp + 194.7dp + 195.0dp + 195.3dp + 195.7dp + 196.0dp + 196.3dp + 196.7dp + 197.0dp + 197.3dp + 197.7dp + 198.0dp + 198.3dp + 198.7dp + 199.0dp + 199.3dp + 199.7dp + 200.0dp + 200.3dp + 200.7dp + 201.0dp + 201.3dp + 201.7dp + 202.0dp + 202.3dp + 202.7dp + 203.0dp + 203.3dp + 203.7dp + 204.0dp + 204.3dp + 204.7dp + 205.0dp + 205.3dp + 205.7dp + 206.0dp + 206.3dp + 206.7dp + 207.0dp + 207.3dp + 207.7dp + 208.0dp + 208.3dp + 208.7dp + 209.0dp + 209.3dp + 209.7dp + 210.0dp + 210.3dp + 210.7dp + 211.0dp + 211.3dp + 211.7dp + 212.0dp + 212.3dp + 212.7dp + 213.0dp + 213.3dp + 213.7dp + 214.0dp + 214.3dp + 214.7dp + 215.0dp + 215.3dp + 215.7dp + 216.0dp + 216.3dp + 216.7dp + 217.0dp + 217.3dp + 217.7dp + 218.0dp + 218.3dp + 218.7dp + 219.0dp + 219.3dp + 219.7dp + 220.0dp + 220.3dp + 220.7dp + 221.0dp + 221.3dp + 221.7dp + 222.0dp + 222.3dp + 222.7dp + 223.0dp + 223.3dp + 223.7dp + 224.0dp + 224.3dp + 224.7dp + 225.0dp + 225.3dp + 225.7dp + 226.0dp + 226.3dp + 226.7dp + 227.0dp + 227.3dp + 227.7dp + 228.0dp + 228.3dp + 228.7dp + 229.0dp + 229.3dp + 229.7dp + 230.0dp + 230.3dp + 230.7dp + 231.0dp + 231.3dp + 231.7dp + 232.0dp + 232.3dp + 232.7dp + 233.0dp + 233.3dp + 233.7dp + 234.0dp + 234.3dp + 234.7dp + 235.0dp + 235.3dp + 235.7dp + 236.0dp + 236.3dp + 236.7dp + 237.0dp + 237.3dp + 237.7dp + 238.0dp + 238.3dp + 238.7dp + 239.0dp + 239.3dp + 239.7dp + 240.0dp + 240.3dp + 240.7dp + 241.0dp + 241.3dp + 241.7dp + 242.0dp + 242.3dp + 242.7dp + 243.0dp + 243.3dp + 243.7dp + 244.0dp + 244.3dp + 244.7dp + 245.0dp + 245.3dp + 245.7dp + 246.0dp + 246.3dp + 246.7dp + 247.0dp + 247.3dp + 247.7dp + 248.0dp + 248.3dp + 248.7dp + 249.0dp + 249.3dp + 249.7dp + 250.0dp + 250.3dp + 250.7dp + 251.0dp + 251.3dp + 251.7dp + 252.0dp + 252.3dp + 252.7dp + 253.0dp + 253.3dp + 253.7dp + 254.0dp + 254.3dp + 254.7dp + 255.0dp + 255.3dp + 255.7dp + 256.0dp + 256.3dp + 256.7dp + 257.0dp + 257.3dp + 257.7dp + 258.0dp + 258.3dp + 258.7dp + 259.0dp + 259.3dp + 259.7dp + 260.0dp + 260.3dp + 260.7dp + 261.0dp + 261.3dp + 261.7dp + 262.0dp + 262.3dp + 262.7dp + 263.0dp + 263.3dp + 263.7dp + 264.0dp + 264.3dp + 264.7dp + 265.0dp + 265.3dp + 265.7dp + 266.0dp + 266.3dp + 266.7dp + 267.0dp + 267.3dp + 267.7dp + 268.0dp + 268.3dp + 268.7dp + 269.0dp + 269.3dp + 269.7dp + 270.0dp + 270.3dp + 270.7dp + 271.0dp + 271.3dp + 271.7dp + 272.0dp + 272.3dp + 272.7dp + 273.0dp + 273.3dp + 273.7dp + 274.0dp + 274.3dp + 274.7dp + 275.0dp + 275.3dp + 275.7dp + 276.0dp + 276.3dp + 276.7dp + 277.0dp + 277.3dp + 277.7dp + 278.0dp + 278.3dp + 278.7dp + 279.0dp + 279.3dp + 279.7dp + 280.0dp + 280.3dp + 280.7dp + 281.0dp + 281.3dp + 281.7dp + 282.0dp + 282.3dp + 282.7dp + 283.0dp + 283.3dp + 283.7dp + 284.0dp + 284.3dp + 284.7dp + 285.0dp + 285.3dp + 285.7dp + 286.0dp + 286.3dp + 286.7dp + 287.0dp + 287.3dp + 287.7dp + 288.0dp + 288.3dp + 288.7dp + 289.0dp + 289.3dp + 289.7dp + 290.0dp + 290.3dp + 290.7dp + 291.0dp + 291.3dp + 291.7dp + 292.0dp + 292.3dp + 292.7dp + 293.0dp + 293.3dp + 293.7dp + 294.0dp + 294.3dp + 294.7dp + 295.0dp + 295.3dp + 295.7dp + 296.0dp + 296.3dp + 296.7dp + 297.0dp + 297.3dp + 297.7dp + 298.0dp + 298.3dp + 298.7dp + 299.0dp + 299.3dp + 299.7dp + 300.0dp + 300.3dp + 300.7dp + 301.0dp + 301.3dp + 301.7dp + 302.0dp + 302.3dp + 302.7dp + 303.0dp + 303.3dp + 303.7dp + 304.0dp + 304.3dp + 304.7dp + 305.0dp + 305.3dp + 305.7dp + 306.0dp + 306.3dp + 306.7dp + 307.0dp + 307.3dp + 307.7dp + 308.0dp + 308.3dp + 308.7dp + 309.0dp + 309.3dp + 309.7dp + 310.0dp + 310.3dp + 310.7dp + 311.0dp + 311.3dp + 311.7dp + 312.0dp + 312.3dp + 312.7dp + 313.0dp + 313.3dp + 313.7dp + 314.0dp + 314.3dp + 314.7dp + 315.0dp + 315.3dp + 315.7dp + 316.0dp + 316.3dp + 316.7dp + 317.0dp + 317.3dp + 317.7dp + 318.0dp + 318.3dp + 318.7dp + 319.0dp + 319.3dp + 319.7dp + 320.0dp + 320.3dp + 320.7dp + 321.0dp + 321.3dp + 321.7dp + 322.0dp + 322.3dp + 322.7dp + 323.0dp + 323.3dp + 323.7dp + 324.0dp + 324.3dp + 324.7dp + 325.0dp + 325.3dp + 325.7dp + 326.0dp + 326.3dp + 326.7dp + 327.0dp + 327.3dp + 327.7dp + 328.0dp + 328.3dp + 328.7dp + 329.0dp + 329.3dp + 329.7dp + 330.0dp + 330.3dp + 330.7dp + 331.0dp + 331.3dp + 331.7dp + 332.0dp + 332.3dp + 332.7dp + 333.0dp + 333.3dp + 333.7dp + 334.0dp + 334.3dp + 334.7dp + 335.0dp + 335.3dp + 335.7dp + 336.0dp + 336.3dp + 336.7dp + 337.0dp + 337.3dp + 337.7dp + 338.0dp + 338.3dp + 338.7dp + 339.0dp + 339.3dp + 339.7dp + 340.0dp + 340.3dp + 340.7dp + 341.0dp + 341.3dp + 341.7dp + 342.0dp + 342.3dp + 342.7dp + 343.0dp + 343.3dp + 343.7dp + 344.0dp + 344.3dp + 344.7dp + 345.0dp + 345.3dp + 345.7dp + 346.0dp + 346.3dp + 346.7dp + 347.0dp + 347.3dp + 347.7dp + 348.0dp + 348.3dp + 348.7dp + 349.0dp + 349.3dp + 349.7dp + 350.0dp + 350.3dp + 350.7dp + 351.0dp + 351.3dp + 351.7dp + 352.0dp + 352.3dp + 352.7dp + 353.0dp + 353.3dp + 353.7dp + 354.0dp + 354.3dp + 354.7dp + 355.0dp + 355.3dp + 355.7dp + 356.0dp + 356.3dp + 356.7dp + 357.0dp + 357.3dp + 357.7dp + 358.0dp + 358.3dp + 358.7dp + 359.0dp + 359.3dp + 359.7dp + 360.0dp + diff --git a/simple_live_tv_app/android/app/src/main/res/values/colors.xml b/simple_live_tv_app/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..a0976535 --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/values/colors.xml @@ -0,0 +1,103 @@ + + + #FF6600 + + + #F80000 + #CCF80000 + #B3F80000 + #99F80000 + #80F80000 + #66F80000 + + #B3601216 + #FF424242 + + + #32364E + #CC32364E + #6632364E + #3D3D3D + #733D3D3D + #803D3D3D + #C83D3D3D + + + #E60E0E0E + + + #F26A6A6A + + #FFFFFF + #E6FFFFFF + #CCFFFFFF + #B3FFFFFF + #80FFFFFF + #66FFFFFF + #32FFFFFF + #26FFFFFF + + #00000000 + #FF000000 + #E6000000 + #CC000000 + #B3000000 + #99000000 + #66000000 + #48000000 + #32000000 + #26000000 + + #80000000 + #8A000000 + #94000000 + #9E000000 + #A8000000 + #B2000000 + #BC000000 + #C6000000 + #FF000000 + + + #FFB6C1 + #6CFFFFFF + #CCFFFFFF + + #FFFFFF + #E5E5E5 + #CCCCCC + #B2B2B2 + #999999 + #7F7F7F + #666666 + #4C4C4C + #333333 + + + + @color/subtitle_text_color_1 + @color/subtitle_text_color_2 + @color/subtitle_text_color_3 + @color/subtitle_text_color_4 + @color/subtitle_text_color_5 + @color/subtitle_text_color_6 + @color/subtitle_text_color_7 + @color/subtitle_text_color_8 + @color/subtitle_text_color_9 + @color/color_FFB6C1 + + + + @color/color_000000_80F + @color/color_000000_8A + @color/color_000000_94 + @color/color_000000_9E + @color/color_000000_A8 + @color/color_000000_B2 + @color/color_000000_BC + @color/color_000000_C6 + @color/color_000000_FF + @color/color_000000_80F + + + diff --git a/simple_live_tv_app/android/app/src/main/res/values/ids.xml b/simple_live_tv_app/android/app/src/main/res/values/ids.xml new file mode 100644 index 00000000..2315e9cf --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/values/strings.xml b/simple_live_tv_app/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..3305367c --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + 清晰度 + 播放线路 + 画面比例 + 已关注 + 未关注 + 弹幕开 + 弹幕关 + 弹幕大小 + 弹幕速度 + 弹幕区域 + 不透明度 + 描边宽度 + 刷新 + 横屏 + 竖屏 + + ]]> + \ No newline at end of file diff --git a/simple_live_tv_app/android/app/src/main/res/values/styles.xml b/simple_live_tv_app/android/app/src/main/res/values/styles.xml index cb1ef880..4bce0a7e 100644 --- a/simple_live_tv_app/android/app/src/main/res/values/styles.xml +++ b/simple_live_tv_app/android/app/src/main/res/values/styles.xml @@ -15,4 +15,26 @@ + + + + + diff --git a/simple_live_tv_app/android/app/src/main/res/xml/file_paths.xml b/simple_live_tv_app/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..b48c1c0d --- /dev/null +++ b/simple_live_tv_app/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/simple_live_tv_app/android/build.gradle b/simple_live_tv_app/android/build.gradle index f7eb7f63..6c0c1aff 100644 --- a/simple_live_tv_app/android/build.gradle +++ b/simple_live_tv_app/android/build.gradle @@ -1,18 +1,28 @@ + buildscript { ext.kotlin_version = '1.7.10' repositories { + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:8.3.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { + flatDir { + dirs project(':app').file('libs') //将xxx替换为引入aar文件的module名 + } + + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } + maven { url 'https://artifact.bytedance.com/repository/releases/' } google() mavenCentral() } @@ -24,6 +34,13 @@ subprojects { } subprojects { project.evaluationDependsOn(':app') + configurations.all { + resolutionStrategy { + force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + } + } } tasks.register("clean", Delete) { diff --git a/simple_live_tv_app/android/gradle/wrapper/gradle-wrapper.properties b/simple_live_tv_app/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99..5d6560a4 100644 --- a/simple_live_tv_app/android/gradle/wrapper/gradle-wrapper.properties +++ b/simple_live_tv_app/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip diff --git a/simple_live_tv_app/android/settings.gradle b/simple_live_tv_app/android/settings.gradle index 55c4ca8b..0375fa5c 100644 --- a/simple_live_tv_app/android/settings.gradle +++ b/simple_live_tv_app/android/settings.gradle @@ -12,9 +12,15 @@ pluginManagement { plugins { id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false } } include ":app" apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ':simple_live_core' +project(':simple_live_core').projectDir = file('./simple_live_core') + +include ':simple_live_tv_app' +project(':simple_live_tv_app').projectDir = file('./lib') \ No newline at end of file diff --git a/simple_live_tv_app/lib/app/controller/app_settings_controller.dart b/simple_live_tv_app/lib/app/controller/app_settings_controller.dart index 70ac8ac4..601131cc 100644 --- a/simple_live_tv_app/lib/app/controller/app_settings_controller.dart +++ b/simple_live_tv_app/lib/app/controller/app_settings_controller.dart @@ -76,6 +76,11 @@ class AppSettingsController extends GetxController { 0, ); + playerMode.value = LocalStorageService.instance.getValue( + LocalStorageService.kPlayerMode, + 0, + ); + pipHideDanmu.value = LocalStorageService.instance .getValue(LocalStorageService.kPIPHideDanmu, true); @@ -212,6 +217,12 @@ class AppSettingsController extends GetxController { .setValue(LocalStorageService.kPlayerCompatMode, e); } + var playerMode = 0.obs; + void setPlayerMode(int value) { + playerMode.value = value; + LocalStorageService.instance.setValue(LocalStorageService.kPlayerMode, value); + } + var playerBufferSize = 32.obs; void setPlayerBufferSize(int e) { playerBufferSize.value = e; diff --git a/simple_live_tv_app/lib/modules/live_room/live_controller.dart b/simple_live_tv_app/lib/modules/live_room/live_controller.dart new file mode 100644 index 00000000..d9210025 --- /dev/null +++ b/simple_live_tv_app/lib/modules/live_room/live_controller.dart @@ -0,0 +1,522 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:simple_live_core/simple_live_core.dart'; +import 'package:simple_live_tv_app/app/constant.dart'; +import 'package:simple_live_tv_app/app/controller/app_settings_controller.dart'; +import 'package:simple_live_tv_app/app/event_bus.dart'; +import 'package:simple_live_tv_app/app/log.dart'; +import 'package:simple_live_tv_app/app/sites.dart'; +import 'package:simple_live_tv_app/app/utils.dart'; +import 'package:simple_live_tv_app/models/db/follow_user.dart'; +import 'package:simple_live_tv_app/models/db/history.dart'; +import 'package:simple_live_tv_app/services/db_service.dart'; +import 'package:simple_live_tv_app/services/follow_user_service.dart'; + +class LiveController { + final Site pSite; + final String pRoomId; + late LiveDanmaku liveDanmaku; + MethodChannel platform = const MethodChannel('samples.flutter.jumpto.android'); + + LiveController({ + required this.pSite, + required this.pRoomId, + }) { + rxSite = pSite.obs; + rxRoomId = pRoomId.obs; + liveDanmaku = site.liveSite.getDanmaku(); + _initChannel(); + } + final FocusNode focusNode = FocusNode(); + late Rx rxSite; + Site get site => rxSite.value; + late Rx rxRoomId; + String get roomId => rxRoomId.value; + + Rx detail = Rx(null); + var online = 0.obs; + var followed = false.obs; + var liveStatus = false.obs; + + /// 清晰度数据 + RxList qualites = RxList(); + + /// 当前清晰度 + var currentQuality = -1; + var currentQualityInfo = "".obs; + + /// 线路数据 + RxList playUrls = RxList(); + + /// 当前线路 + var currentLineIndex = -1; + var currentLineInfo = "".obs; + + /// 是否处于后台 + var isBackground = false; + + var datetime = "00:00".obs; + + void initTimer() { + Timer.periodic(const Duration(seconds: 1), (timer) { + var now = DateTime.now(); + datetime.value = + "${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}"; + }); + } + + void _initChannel() { + platform.setMethodCallHandler((MethodCall call) async { + // 同样也是根据方法名分发不同的函数 + switch(call.method) { + case "onResume": { + didChangeAppLifecycleState(AppLifecycleState.resumed); + } + case "onPause": { + didChangeAppLifecycleState(AppLifecycleState.paused); + } + case "onCreate": { + onInit(); + } + case "onDestory": { + onClose(); + } + case "followUser": { + if (followed.value) { + removeFollowUser(); + } else { + followUser(); + } + return followed.value; + } + case "mediaError": { + mediaError(call.arguments); + } + case "mediaEnd": { + mediaEnd(); + } + case "changeQuality": { + currentQuality = call.arguments; + getPlayUrl(); + } + case "changeLine": { + currentLineIndex = call.arguments; + // 重置错误次数 + mediaErrorRetryCount = 0; + } + case "refresh": { + refreshRoom(); + } + } + return null; + }); + } + + /// 双击退出Flag + bool doubleClickExit = false; + + /// 双击退出Timer + Timer? doubleClickTimer; + + void openLivePage(int playerMode) { + platform.invokeMethod("openLivePage", playerMode); + } + + void onInit() { + initTimer(); + // showDanmakuState.value = AppSettingsController.instance.danmuEnable.value; + followed.value = DBService.instance.getFollowExist("${site.id}_$roomId"); + + loadData(); + } + + void refreshRoom() { + //messages.clear(); + + liveDanmaku.stop(); + + loadData(); + } + + /// 初始化弹幕接收事件 + void initDanmau() { + liveDanmaku.onMessage = onWSMessage; + } + + /// 接收到WebSocket信息 + void onWSMessage(LiveMessage msg) { + if (msg.type == LiveMessageType.chat) { + // 关键词屏蔽检查 + for (var keyword in AppSettingsController.instance.shieldList) { + Pattern? pattern; + if (Utils.isRegexFormat(keyword)) { + String removedSlash = Utils.removeRegexFormat(keyword); + try { + pattern = RegExp(removedSlash); + } catch (e) { + // should avoid this during add keyword + Log.d("关键词:$keyword 正则格式错误"); + } + } else { + pattern = keyword; + } + if (pattern != null && msg.message.contains(pattern)) { + Log.d("关键词:$keyword\n已屏蔽消息内容:${msg.message}"); + return; + } + } + + if (!liveStatus.value || isBackground) { + return; + } + } else if (msg.type == LiveMessageType.online) { + online.value = msg.data; + } else if (msg.type == LiveMessageType.superChat) { + //superChats.add(msg.data); + } + + if (msg.message.isEmpty) { + return; + } + + platform.invokeMethod("danmaku", + { + 'type': msg.type.name, + 'message': msg.message, + 'color': msg.color.toString() + }); + } + + /// 加载直播间信息 + void loadData() async { + try { + SmartDialog.showLoading(msg: ""); + // pageLoadding.value = true; + detail.value = await site.liveSite.getRoomDetail(roomId: roomId); + + addHistory(); + online.value = detail.value!.online; + liveStatus.value = detail.value!.status || detail.value!.isRecord; + if (liveStatus.value) { + getPlayQualites(); + } + if (detail.value!.isRecord) { + SmartDialog.showToast("当前主播未开播,正在轮播录像"); + } + + initDanmau(); + liveDanmaku.start(detail.value?.danmakuData); + } catch (e) { + SmartDialog.showToast(e.toString()); + } finally { + SmartDialog.dismiss(status: SmartStatus.loading); + // pageLoadding.value = false; + } + } + + /// 初始化播放器 + void getPlayQualites() async { + qualites.clear(); + currentQuality = -1; + try { + var playQualites = + await site.liveSite.getPlayQualites(detail: detail.value!); + + if (playQualites.isEmpty) { + SmartDialog.showToast("无法读取播放清晰度"); + return; + } + qualites.value = playQualites; + var qualityLevel = AppSettingsController.instance.qualityLevel.value; + if (qualityLevel == 2) { + //最高 + currentQuality = 0; + } else if (qualityLevel == 0) { + //最低 + currentQuality = playQualites.length - 1; + } else { + //中间值 + int middle = (playQualites.length / 2).floor(); + currentQuality = middle; + } + + getPlayUrl(); + } catch (e) { + Log.logPrint(e); + SmartDialog.showToast("无法读取播放清晰度"); + } + } + + void getPlayUrl() async { + playUrls.clear(); + currentQualityInfo.value = qualites[currentQuality].quality; + currentLineInfo.value = ""; + currentLineIndex = -1; + var playUrl = await site.liveSite + .getPlayUrls(detail: detail.value!, quality: qualites[currentQuality]); + if (playUrl.isEmpty) { + SmartDialog.showToast("无法读取播放地址"); + return; + } + Log.i("playUrl=> $playUrl"); + playUrls.value = playUrl; + currentLineIndex = 0; + currentLineInfo.value = "线路${currentLineIndex + 1}"; + //重置错误次数 + mediaErrorRetryCount = 0; + setPlayer(); + } + + void changePlayLine(int index) { + currentLineIndex = index; + //重置错误次数 + mediaErrorRetryCount = 0; + setPlayer(); + } + + void setPlayer() async { + currentLineInfo.value = "线路${currentLineIndex + 1}"; + // errorMsg.value = ""; + Map headers = {}; + if (site.id == Constant.kBiliBili) { + headers = { + "referer": "https://live.bilibili.com", + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188" + }; + } else if (site.id == Constant.kHuya) { + headers = { + // "referer": "https://m.huya.com", + // "user-agent": + // "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 Edg/130.0.0.0" + "user-agent": "HYSDK(Windows, 20000308)" + }; + } + + RxList qualiteNames = RxList(); + for (var element in qualites) { + qualiteNames.add(element.quality); + } + + bool result = await platform.invokeMethod("parseLiveUrl", + json.encode({ + 'liveUrl': playUrls, + 'id': site.id, + 'roomId': roomId, + 'name': site.name, + 'logo': site.logo, + 'index': site.index, + 'followed': followed.value, + 'qualites': qualiteNames, + 'currentQuality': currentQuality, + 'currentLineIndex': currentLineIndex, + 'headers': headers, + 'roomTitle': detail.value?.title + })); + if (!result) { + onClose(); + } + Log.d("播放链接\r\n:${playUrls[currentLineIndex]}"); + } + + void mediaEnd() async { + if (mediaErrorRetryCount < 2) { + Log.d("播放结束,尝试第${mediaErrorRetryCount + 1}次刷新"); + if (mediaErrorRetryCount == 1) { + //延迟一秒再刷新 + await Future.delayed(const Duration(seconds: 1)); + } + mediaErrorRetryCount += 1; + //刷新一次 + setPlayer(); + return; + } + + Log.d("播放结束"); + // 遍历线路,如果全部链接都断开就是直播结束了 + if (playUrls.length - 1 == currentLineIndex) { + liveStatus.value = false; + } else { + changePlayLine(currentLineIndex + 1); + + //setPlayer(); + } + } + + int mediaErrorRetryCount = 0; + void mediaError(String error) async { + if (mediaErrorRetryCount < 2) { + Log.d("播放失败,尝试第${mediaErrorRetryCount + 1}次刷新"); + if (mediaErrorRetryCount == 1) { + //延迟一秒再刷新 + await Future.delayed(const Duration(seconds: 1)); + } + mediaErrorRetryCount += 1; + //刷新一次 + setPlayer(); + return; + } + + if (playUrls.length - 1 == currentLineIndex) { + // errorMsg.value = "播放失败"; + SmartDialog.showToast("播放失败:$error"); + } else { + //currentLineIndex += 1; + //setPlayer(); + changePlayLine(currentLineIndex + 1); + } + } + + /// 添加历史记录 + void addHistory() { + if (detail.value == null) { + return; + } + var id = "${site.id}_$roomId"; + var history = DBService.instance.getHistory(id); + if (history != null) { + history.updateTime = DateTime.now(); + } + history ??= History( + id: id, + roomId: roomId, + siteId: site.id, + userName: detail.value?.userName ?? "", + face: detail.value?.userAvatar ?? "", + updateTime: DateTime.now(), + ); + + DBService.instance.addOrUpdateHistory(history); + } + + /// 关注用户 + void followUser() { + if (detail.value == null) { + return; + } + var id = "${site.id}_$roomId"; + DBService.instance.addFollow( + FollowUser( + id: id, + roomId: roomId, + siteId: site.id, + userName: detail.value?.userName ?? "", + face: detail.value?.userAvatar ?? "", + addTime: DateTime.now(), + ), + ); + followed.value = true; + EventBus.instance.emit(Constant.kUpdateFollow, id); + SmartDialog.showToast("已关注"); + } + + /// 取消关注用户 + void removeFollowUser() async { + if (detail.value == null) { + return; + } + // if (!await Utils.showAlertDialog("确定要取消关注该用户吗?", title: "取消关注")) { + // return; + // } + + var id = "${site.id}_$roomId"; + DBService.instance.deleteFollow(id); + followed.value = false; + EventBus.instance.emit(Constant.kUpdateFollow, id); + SmartDialog.showToast("已取消关注"); + } + + void resetRoom(Site site, String roomId) async { + if (this.site == site && this.roomId == roomId) { + return; + } + + rxSite.value = site; + rxRoomId.value = roomId; + + // 清除全部消息 + liveDanmaku.stop(); + + // danmakuController?.clear(); + + // 重新设置LiveDanmaku + liveDanmaku = site.liveSite.getDanmaku(); + + // 停止播放 + await platform.invokeMethod("stopPlay"); + // await player.stop(); + + // 刷新信息 + loadData(); + } + + void nextChannel() { + //读取正在直播的频道 + var liveChannels = FollowUserService.instance.livingList; + if (liveChannels.isEmpty) { + SmartDialog.showToast("没有正在直播的频道"); + return; + } + var index = liveChannels + .indexWhere((element) => element.id == "${site.id}_$roomId"); + // if (index == -1) { + // //当前频道不在列表中 + + // return; + // } + index += 1; + if (index >= liveChannels.length) { + index = 0; + } + var nextChannel = liveChannels[index]; + + resetRoom(Sites.allSites[nextChannel.siteId]!, nextChannel.roomId); + } + + void prevChannel() { + //读取正在直播的频道 + var liveChannels = FollowUserService.instance.livingList; + if (liveChannels.isEmpty) { + SmartDialog.showToast("没有正在直播的频道"); + return; + } + var index = liveChannels + .indexWhere((element) => element.id == "${site.id}_$roomId"); + // if (index == -1) { + // //当前频道不在列表中 + + // return; + // } + index -= 1; + if (index < 0) { + index = liveChannels.length - 1; + } + var nextChannel = liveChannels[index]; + + resetRoom(Sites.allSites[nextChannel.siteId]!, nextChannel.roomId); + } + + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + Log.d("进入后台"); + //进入后台,关闭弹幕 + // danmakuController?.clear(); + isBackground = true; + } else + //返回前台 + if (state == AppLifecycleState.resumed) { + Log.d("返回前台"); + isBackground = false; + } + } + + void onClose() { + liveDanmaku.stop(); + // danmakuController = null; + } +} diff --git a/simple_live_tv_app/lib/modules/live_room/player/player_controller.dart b/simple_live_tv_app/lib/modules/live_room/player/player_controller.dart index d503cdfd..7902b60d 100644 --- a/simple_live_tv_app/lib/modules/live_room/player/player_controller.dart +++ b/simple_live_tv_app/lib/modules/live_room/player/player_controller.dart @@ -157,6 +157,7 @@ mixin PlayerDanmakuMixin on PlayerStateMixin { duration: AppSettingsController.instance.danmuSpeed.value, opacity: AppSettingsController.instance.danmuOpacity.value, strokeWidth: AppSettingsController.instance.danmuStrokeWidth.value.w, + fontWeight: FontWeight.bold ), ); } diff --git a/simple_live_tv_app/lib/modules/live_room/player/player_controls.dart b/simple_live_tv_app/lib/modules/live_room/player/player_controls.dart index 008bfeb3..d7c730fd 100644 --- a/simple_live_tv_app/lib/modules/live_room/player/player_controls.dart +++ b/simple_live_tv_app/lib/modules/live_room/player/player_controls.dart @@ -576,6 +576,7 @@ void showPlayerSettings(LiveRoomController controller) { autofocus: danmakuSpeedFoucsNode.isFoucsed.value, title: "弹幕速度", items: { + 54.0: "超级慢", 18.0: "很慢", 14.0: "较慢", 12.0: "慢", diff --git a/simple_live_tv_app/lib/modules/settings/settings_controller.dart b/simple_live_tv_app/lib/modules/settings/settings_controller.dart index ed71bee9..f3685aab 100644 --- a/simple_live_tv_app/lib/modules/settings/settings_controller.dart +++ b/simple_live_tv_app/lib/modules/settings/settings_controller.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:simple_live_tv_app/app/app_focus_node.dart'; @@ -11,6 +12,7 @@ class SettingsController extends BaseController with GetTickerProviderStateMixin { late TabController tabController; var tabIndex = 0.obs; + MethodChannel platform = const MethodChannel('samples.flutter.jumpto.android'); SettingsController() { tabController = TabController(length: 5, vsync: this); @@ -39,6 +41,7 @@ class SettingsController extends BaseController } var hardwareDecodeFocusNode = AppFocusNode()..isFoucsed.value = true; var compatibleModeFocusNode = AppFocusNode(); + var playerFoucsNode = AppFocusNode(); var scaleFoucsNode = AppFocusNode(); var defaultQualityFocusNode = AppFocusNode(); var danmakuFoucsNode = AppFocusNode(); @@ -69,4 +72,9 @@ class SettingsController extends BaseController SmartDialog.showToast("检查更新中..."); Utils.checkUpdate(showMsg: true); } + + void checkTestUpdate() { + SmartDialog.showToast("检查更新中..."); + platform.invokeMethod("checkTestUpdate"); + } } diff --git a/simple_live_tv_app/lib/modules/settings/settings_page.dart b/simple_live_tv_app/lib/modules/settings/settings_page.dart index e55681ab..d8e11472 100644 --- a/simple_live_tv_app/lib/modules/settings/settings_page.dart +++ b/simple_live_tv_app/lib/modules/settings/settings_page.dart @@ -170,6 +170,24 @@ class SettingsPage extends GetView { ), ), AppStyle.vGap24, + Obx( + () => SettingsItemWidget( + foucsNode: controller.playerFoucsNode, + autofocus: controller.playerFoucsNode.isFoucsed.value, + title: "播放器", + items: const { + 0: "IjkPlayer", + 1: "ExoPlayer", + 2: "Flutter", + }, + value: + AppSettingsController.instance.playerMode.value, + onChanged: (e) { + AppSettingsController.instance.setPlayerMode(e); + }, + ), + ), + AppStyle.vGap24, Obx( () => SettingsItemWidget( foucsNode: controller.scaleFoucsNode, @@ -488,6 +506,13 @@ class SettingsPage extends GetView { subtitle: "v${Utils.packageInfo.version}", onTap: controller.checkUpdate, ), + AppStyle.vGap24, + HighlightListTile( + focusNode: AppFocusNode(), + title: "本地更新", + subtitle: "v${Utils.packageInfo.version}", + onTap: controller.checkTestUpdate, + ), ], ); } diff --git a/simple_live_tv_app/lib/routes/app_navigation.dart b/simple_live_tv_app/lib/routes/app_navigation.dart index 7b998194..dc7b7c9f 100644 --- a/simple_live_tv_app/lib/routes/app_navigation.dart +++ b/simple_live_tv_app/lib/routes/app_navigation.dart @@ -1,9 +1,11 @@ +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:simple_live_tv_app/app/constant.dart'; import 'package:simple_live_tv_app/app/controller/app_settings_controller.dart'; import 'package:simple_live_tv_app/app/sites.dart'; import 'package:simple_live_tv_app/modules/category/category_controller.dart'; +import 'package:simple_live_tv_app/modules/live_room/live_controller.dart'; import 'package:simple_live_tv_app/routes/route_path.dart'; import 'package:simple_live_tv_app/services/bilibili_account_service.dart'; @@ -24,9 +26,17 @@ class AppNavigator { } } - Get.toNamed(RoutePath.kLiveRoomDetail, arguments: site, parameters: { - "roomId": roomId, - }); + if (AppSettingsController.instance.playerMode.value == 0) { + LiveController liveController = LiveController(pSite: site, pRoomId: roomId); + liveController.openLivePage(0); + } else if (AppSettingsController.instance.playerMode.value == 1) { + LiveController liveController = LiveController(pSite: site, pRoomId: roomId); + liveController.openLivePage(1); + } else { + Get.toNamed(RoutePath.kLiveRoomDetail, arguments: site, parameters: { + "roomId": roomId, + }); + } } /// 跳转至哔哩哔哩登录 diff --git a/simple_live_tv_app/lib/services/local_storage_service.dart b/simple_live_tv_app/lib/services/local_storage_service.dart index b9a3f875..5f4acc9d 100644 --- a/simple_live_tv_app/lib/services/local_storage_service.dart +++ b/simple_live_tv_app/lib/services/local_storage_service.dart @@ -11,6 +11,9 @@ class LocalStorageService extends GetxService { /// 缩放模式 static const String kPlayerScaleMode = "ScaleMode"; + /// 播放器模式 + static const String kPlayerMode = "PlayerMode"; + /// 网站排序 static const String kSiteSort = "SiteSort"; diff --git a/simple_live_tv_app/lib/widgets/native_video_view.dart b/simple_live_tv_app/lib/widgets/native_video_view.dart new file mode 100644 index 00000000..137211c9 --- /dev/null +++ b/simple_live_tv_app/lib/widgets/native_video_view.dart @@ -0,0 +1,26 @@ + + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; + +class NativeVideoView extends StatelessWidget { + final MethodChannel _methodChannel = const MethodChannel("samples.flutter.jumpto.android"); + const NativeVideoView({super.key}); + + @override + Widget build(BuildContext context) { + return AndroidView( + viewType: "nativeVideoView", onPlatformViewCreated: (int id) {}); + } + + void startPlayVideo(String videoUrl, Map? httpHeaders) { + _methodChannel.invokeMethod("startPlay", { + "videoUrl": videoUrl, + "headers": httpHeaders + }); + } + + void stopPlayVideo() { + _methodChannel.invokeMethod("stopPlay"); + } +} \ No newline at end of file diff --git a/simple_live_tv_app/pubspec.lock b/simple_live_tv_app/pubspec.lock index c30d228e..25df4a7b 100644 --- a/simple_live_tv_app/pubspec.lock +++ b/simple_live_tv_app/pubspec.lock @@ -6,23 +6,23 @@ packages: description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.6.1" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" - url: "https://pub.dev" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: transitive description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.0" boolean_selector: @@ -30,7 +30,7 @@ packages: description: name: boolean_selector sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" brotli: @@ -38,7 +38,7 @@ packages: description: name: brotli sha256: "7f891558ed779aab2bed874f0a36b8123f9ff3f19cf6efbee89e18ed294945ae" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.0" characters: @@ -46,7 +46,7 @@ packages: description: name: characters sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" clock: @@ -54,48 +54,39 @@ packages: description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + url: "https://pub.flutter-io.cn" source: hosted - version: "1.18.0" + version: "1.19.0" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.8" - dart_tars_protocol: - dependency: transitive - description: - path: "." - ref: HEAD - resolved-ref: b7bd596f50d1267018cb84d558c814c0365e153b - url: "https://github.com/xiaoyaocz/dart_tars_protocol.git" - source: git - version: "1.0.0" dbus: dependency: transitive description: name: dbus sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.10" device_info_plus: @@ -103,55 +94,55 @@ packages: description: name: device_info_plus sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" - url: "https://pub.dev" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" + url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.1" + version: "7.0.2" dio: dependency: "direct main" description: name: dio - sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" - url: "https://pub.dev" + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.6.0" + version: "5.7.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" extended_image: dependency: "direct main" description: name: extended_image - sha256: "9786aab821aac117763d6e4419cd49f5031fbaacfe3fd212c5b313d0334c37a9" - url: "https://pub.dev" + sha256: "69d4299043334ecece679996e47d0b0891cd8c29d8da0034868443506f1d9a78" + url: "https://pub.flutter-io.cn" source: hosted - version: "8.2.1" + version: "8.3.1" extended_image_library: dependency: transitive description: name: extended_image_library - sha256: c9caee8fe9b6547bd41c960c4f2d1ef8e34321804de6a1777f1d614a24247ad6 - url: "https://pub.dev" + sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.4" + version: "4.0.5" fading_edge_scrollview: dependency: "direct overridden" description: name: fading_edge_scrollview sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" fake_async: @@ -159,7 +150,7 @@ packages: description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" ffi: @@ -167,25 +158,25 @@ packages: description: name: ffi sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.dev" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -196,7 +187,7 @@ packages: description: name: flutter_easyrefresh sha256: "5d161ee5dcac34da9065116568147d742dd25fb9bff3b10024d9054b195087ad" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.2" flutter_lints: @@ -204,7 +195,7 @@ packages: description: name: flutter_lints sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" flutter_screenutil: @@ -212,7 +203,7 @@ packages: description: name: flutter_screenutil sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.3" flutter_smart_dialog: @@ -220,7 +211,7 @@ packages: description: name: flutter_smart_dialog sha256: e9ee69eeac16165d142f1974b4db05ca9846cffafb7c94674a38ec07d7e6cda1 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.9.6" flutter_staggered_grid_view: @@ -228,7 +219,7 @@ packages: description: name: flutter_staggered_grid_view sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_test: @@ -246,7 +237,7 @@ packages: description: name: get sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.6" hive: @@ -254,7 +245,7 @@ packages: description: name: hive sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" hive_flutter: @@ -262,7 +253,7 @@ packages: description: name: hive_flutter sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" html_unescape: @@ -270,7 +261,7 @@ packages: description: name: html_unescape sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" http: @@ -278,7 +269,7 @@ packages: description: name: http sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" http_client_helper: @@ -286,7 +277,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" http_methods: @@ -294,31 +285,31 @@ packages: description: name: http_methods sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.2" + version: "4.1.1" image: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" - url: "https://pub.dev" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.flutter-io.cn" source: hosted - version: "4.2.0" + version: "4.3.0" intl: dependency: "direct main" description: name: intl sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.18.1" js: @@ -326,31 +317,31 @@ packages: description: name: js sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.7" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" - url: "https://pub.dev" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.flutter-io.cn" source: hosted - version: "10.0.4" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" - url: "https://pub.dev" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.3" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" lints: @@ -358,72 +349,71 @@ packages: description: name: lints sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" logger: dependency: "direct main" description: name: logger - sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" - url: "https://pub.dev" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.0" + version: "2.5.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.3.0" lottie: dependency: "direct main" description: name: lottie - sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b" - url: "https://pub.dev" + sha256: "377d87b8dcef640c04717e93afb86a510f0e1117a399ab94dc4b3f39c85eaa87" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.2" + version: "3.3.0" marquee: dependency: "direct main" description: name: marquee - sha256: "4b5243d2804373bdc25fc93d42c3b402d6ec1f4ee8d0bb72276edd04ae7addb8" - url: "https://pub.dev" + sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.3" + version: "2.3.0" matcher: dependency: transitive description: name: matcher sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" - url: "https://pub.dev" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.0" + version: "0.11.1" media_kit: dependency: "direct main" description: - path: media_kit - ref: main - resolved-ref: "50c510d018cc5286eb6730f3ea165290f19dc5f6" - url: "https://github.com/media-kit/media-kit.git" - source: git - version: "1.1.10+1" + name: media_kit + sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.11" media_kit_libs_android_video: dependency: transitive description: name: media_kit_libs_android_video sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.6" media_kit_libs_ios_video: @@ -431,7 +421,7 @@ packages: description: name: media_kit_libs_ios_video sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.4" media_kit_libs_linux: @@ -439,7 +429,7 @@ packages: description: name: media_kit_libs_linux sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.3" media_kit_libs_macos_video: @@ -447,66 +437,63 @@ packages: description: name: media_kit_libs_macos_video sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.4" media_kit_libs_video: dependency: "direct main" description: - path: "libs/universal/media_kit_libs_video" - ref: main - resolved-ref: "50c510d018cc5286eb6730f3ea165290f19dc5f6" - url: "https://github.com/media-kit/media-kit.git" - source: git - version: "1.0.4" + name: media_kit_libs_video + sha256: "20bb4aefa8fece282b59580e1cd8528117297083a6640c98c2e98cfc96b93288" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" media_kit_libs_windows_video: dependency: transitive description: name: media_kit_libs_windows_video - sha256: "7bace5f35d9afcc7f9b5cdadb7541d2191a66bb3fc71bfa11c1395b3360f6122" - url: "https://pub.dev" + sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.9" + version: "1.0.10" media_kit_native_event_loop: - dependency: "direct overridden" + dependency: transitive description: - path: media_kit_native_event_loop - ref: main - resolved-ref: "50c510d018cc5286eb6730f3ea165290f19dc5f6" - url: "https://github.com/media-kit/media-kit.git" - source: git - version: "1.0.8" + name: media_kit_native_event_loop + sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.9" media_kit_video: dependency: "direct main" description: - path: media_kit_video - ref: main - resolved-ref: "50c510d018cc5286eb6730f3ea165290f19dc5f6" - url: "https://github.com/media-kit/media-kit.git" - source: git - version: "1.2.4" + name: media_kit_video + sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.5" message_pack_dart: dependency: transitive description: name: message_pack_dart sha256: "71b9f0ff60e5896e60b337960bb535380d7dba3297b457ac763ccae807385b59" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" - url: "https://pub.dev" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.12.0" + version: "1.15.0" network_info_plus: dependency: "direct main" description: name: network_info_plus sha256: "4601b815b1c6a46d84839f65cd774a7d999738471d910fae00d813e9e98b04e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0+1" network_info_plus_platform_interface: @@ -514,7 +501,7 @@ packages: description: name: network_info_plus_platform_interface sha256: "881f5029c5edaf19c616c201d3d8b366c5b1384afd5c1da5a49e4345de82fb8b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.3" nm: @@ -522,7 +509,7 @@ packages: description: name: nm sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.0" ns_danmaku: @@ -538,56 +525,56 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 - url: "https://pub.dev" + sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" + url: "https://pub.flutter-io.cn" source: hosted - version: "8.0.2" + version: "8.1.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 - url: "https://pub.dev" + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.1" + version: "3.0.2" path: dependency: transitive description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.0" path_provider: dependency: transitive description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 - url: "https://pub.dev" + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" - url: "https://pub.dev" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.10" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 - url: "https://pub.dev" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -595,7 +582,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" path_provider_windows: @@ -603,7 +590,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" petitparser: @@ -611,23 +598,23 @@ packages: description: name: petitparser sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.2" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" - url: "https://pub.dev" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" pool: @@ -635,7 +622,7 @@ packages: description: name: pool sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" protobuf: @@ -643,7 +630,7 @@ packages: description: name: protobuf sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" qr: @@ -651,7 +638,7 @@ packages: description: name: qr sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" qr_flutter: @@ -659,23 +646,23 @@ packages: description: name: qr_flutter sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" remixicon: dependency: "direct main" description: name: remixicon - sha256: "78b367d2d46cb4496f4a2a433110ce946578616f3ac7e31935b7a365c2c7c43d" - url: "https://pub.dev" + sha256: "6556b0487cd3d990f74e9cbcbe92a7ba60dd9132c97f2bfba252ac7e76e69ff3" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.3.0" safe_local_storage: dependency: transitive description: name: safe_local_storage sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" screen_brightness: @@ -683,7 +670,7 @@ packages: description: name: screen_brightness sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2+1" screen_brightness_android: @@ -691,7 +678,7 @@ packages: description: name: screen_brightness_android sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0+2" screen_brightness_ios: @@ -699,7 +686,7 @@ packages: description: name: screen_brightness_ios sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" screen_brightness_macos: @@ -707,7 +694,7 @@ packages: description: name: screen_brightness_macos sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0+1" screen_brightness_platform_interface: @@ -715,7 +702,7 @@ packages: description: name: screen_brightness_platform_interface sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" screen_brightness_windows: @@ -723,33 +710,33 @@ packages: description: name: screen_brightness_windows sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.3" shelf: dependency: "direct main" description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_router: dependency: "direct main" description: name: shelf_router sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.4" signalr_netcore: dependency: "direct main" description: name: signalr_netcore - sha256: c41052a7b3dfa9c5f3fa292d6fc0f44556dff988c385bf09c5dcf64257eb0c2d - url: "https://pub.dev" + sha256: bf42db085aee4adeafb772e436fb51a4af0baa06dee91bb193d7ca3cdfa55518 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.9" + version: "1.4.0" simple_live_core: dependency: "direct main" description: @@ -761,13 +748,13 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" sprintf: @@ -775,39 +762,39 @@ packages: description: name: sprintf sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.0" sse: dependency: transitive description: name: sse - sha256: "111a05843ea9035042975744fe61d5e8b95bc4d38656dbafc5532da77a0bb89a" - url: "https://pub.dev" + sha256: "4389a01d5bc7ef3e90fbc645f8e7c6d8711268adb1f511e14ae9c71de47ee32b" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.1.6" + version: "4.1.7" sse_channel: dependency: transitive description: name: sse_channel sha256: "9aad5d4eef63faf6ecdefb636c0f857bd6f74146d2196087dcf4b17ab5b49b1b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.11.1" + version: "1.12.0" sticky_headers: dependency: "direct main" description: name: sticky_headers sha256: "9b3dd2cb0fd6a7038170af3261f855660cbb241cb56c501452cb8deed7023ede" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0+2" stream_channel: @@ -815,63 +802,70 @@ packages: description: name: stream_channel sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.3.0" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" - url: "https://pub.dev" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" + tars_dart: + dependency: transitive + description: + path: "../simple_live_core/packages/tars_dart" + relative: true + source: path + version: "0.1.0" term_glyph: dependency: transitive description: name: term_glyph sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" - url: "https://pub.dev" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.0" + version: "0.7.3" tuple: dependency: transitive description: name: tuple sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.2" + version: "1.4.0" udp: dependency: "direct main" description: name: udp sha256: "50ea45d7ee80ad4c62de4ec0e8ed3ae65c36e9fe8cd0655a2bcd1503d2708e5a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.3" universal_platform: @@ -879,7 +873,7 @@ packages: description: name: universal_platform sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" uri_parser: @@ -887,55 +881,55 @@ packages: description: name: uri_parser sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" - url: "https://pub.dev" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" - url: "https://pub.dev" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.3.8" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e - url: "https://pub.dev" + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af - url: "https://pub.dev" + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" - url: "https://pub.dev" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" url_launcher_web: @@ -943,47 +937,47 @@ packages: description: name: url_launcher_web sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" - url: "https://pub.dev" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.2" + version: "3.1.3" uuid: dependency: "direct main" description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" - url: "https://pub.dev" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.flutter-io.cn" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" - url: "https://pub.dev" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.flutter-io.cn" source: hosted - version: "14.2.1" + version: "14.3.0" volume_controller: dependency: transitive description: name: volume_controller sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.8" wakelock_plus: @@ -991,7 +985,7 @@ packages: description: name: wakelock_plus sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.8" wakelock_plus_platform_interface: @@ -999,23 +993,23 @@ packages: description: name: wakelock_plus_platform_interface sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" - url: "https://pub.dev" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.flutter-io.cn" source: hosted - version: "0.5.1" + version: "1.1.0" web_socket: dependency: transitive description: name: web_socket sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.6" web_socket_channel: @@ -1023,41 +1017,41 @@ packages: description: name: web_socket_channel sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" win32: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" - url: "https://pub.dev" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.5.4" + version: "5.9.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" - url: "https://pub.dev" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.4" + version: "1.1.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d - url: "https://pub.dev" + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.5.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0"