初始化项目
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.bbitcn.f8.pad
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bbitcn.f8.pad.utils.MMKVUtil
|
||||
import com.bbitcn.f8.pad.utils.TTSManager
|
||||
import com.bbitcn.f8.pad.utils.global.Global
|
||||
import com.bbitcn.f8.pad.utils.log.CrashHandlerUtil
|
||||
import com.bbitcn.f8.pad.utils.log.MyLog
|
||||
import com.blankj.utilcode.util.ActivityUtils
|
||||
import com.iflytek.cloud.SpeechConstant
|
||||
import com.iflytek.cloud.SpeechUtility
|
||||
|
||||
import org.xutils.x
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
/**
|
||||
* @Description APPLICATION类
|
||||
* @Author DuanKaiji
|
||||
* @CreateTime 2024年03月27日 13:43
|
||||
*/
|
||||
val M = Modifier
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
val MD = Modifier
|
||||
|
||||
val IS_DEBUG_DRYCOCOON = true // 是否是调试状态
|
||||
|
||||
|
||||
class MyApp : android.app.Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// 初始化MMKV
|
||||
MMKVUtil.init(android.content.ContextWrapper.getApplicationContext)
|
||||
// 初始化崩溃捕捉
|
||||
CrashHandlerUtil.init()
|
||||
// 初始化日志库
|
||||
Timber.plant(MyLog())
|
||||
// 初始化网络请求库
|
||||
x.Ext.init(this)
|
||||
// 初始化全局变量
|
||||
MMKVUtil.put(Global.DEVICE_ID, android.provider.Settings.Secure.getString(android.content.ContextWrapper.getContentResolver, android.provider.Settings.Secure.ANDROID_ID))
|
||||
// 初始化讯飞语音
|
||||
SpeechUtility.createUtility(android.content.ContextWrapper.getApplicationContext, SpeechConstant.APPID +"=5d0fed03")
|
||||
// 初始化文本转语音
|
||||
TTSManager.init(android.content.ContextWrapper.getApplicationContext)
|
||||
|
||||
MyLog.test("设备唯一码:${Global.getDeviceId()}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val appContext: android.content.Context
|
||||
get() = ActivityUtils.getTopActivity()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import org.gradle.kotlin.dsl.implementation
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.bbitcn.bbitshow"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.bbitcn.bbitshow"
|
||||
minSdk = 23
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
configurations.all {
|
||||
resolutionStrategy.eachDependency {
|
||||
if (requested.group == "org.jetbrains.kotlin") {
|
||||
useVersion("2.0.21")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.tv.foundation)
|
||||
implementation(libs.androidx.tv.material)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation( "org.java-websocket:Java-WebSocket:1.6.0")
|
||||
//lifecycle-viewmodel-compose
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
// MD3
|
||||
implementation("androidx.compose.material3:material3:1.4.0-alpha12")
|
||||
implementation("com.google.code.gson:gson:2.13.1")
|
||||
//util
|
||||
implementation("com.blankj:utilcodex:1.31.1")
|
||||
|
||||
implementation("org.mozilla.geckoview:geckoview:142.0.20250811145442")
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="CoarseFineLocation">
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/logo"
|
||||
android:icon="@mipmap/logo"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:name=".MyApp"
|
||||
android:label="@string/app_name"
|
||||
android:hardwareAccelerated="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.BBITShow">
|
||||
<activity
|
||||
android:name=".TvActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.bbitcn.bbitshow
|
||||
|
||||
import android.media.MediaPlayer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.SocketException
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
open class BaseViewModel : ViewModel() {
|
||||
|
||||
|
||||
protected var pollingTask: PollingTask =
|
||||
PollingTask.getInstance(javaClass.simpleName) // 使用类名作为ID
|
||||
|
||||
protected fun doInUIThread(task: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
task()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun <T> doInIoThreadThenUI(
|
||||
loadingTips: String = "正在加载中",
|
||||
showDialog: Boolean = true,
|
||||
onError: (Throwable) -> Unit = { },
|
||||
onIO: suspend () -> T,
|
||||
onFinish: () -> Unit = { },
|
||||
onUI: (T) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (showDialog) {
|
||||
// Toasty.showLoadingDialog(loadingTips)
|
||||
}
|
||||
onIO()
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
// hideLoadingDialog()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
result.onSuccess { data ->
|
||||
onUI(data)
|
||||
}.onFailure { exception ->
|
||||
// ✅ 如果是协程取消,不处理,只记录日志
|
||||
if (exception is CancellationException || exception.cause is SocketException && exception.cause?.message?.contains(
|
||||
"Socket closed"
|
||||
) == true
|
||||
) {
|
||||
return@onFailure
|
||||
}
|
||||
// 其他异常继续处理
|
||||
exception.printStackTrace()
|
||||
onError(exception)
|
||||
exception.message?.let {
|
||||
// Toasty.error(it)
|
||||
}
|
||||
}.also {
|
||||
// ✅ 最终执行的操作
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T> doInIoThread(
|
||||
loadingTips: String = "正在加载中",
|
||||
showDialog: Boolean = true,
|
||||
onError: (Throwable) -> Unit = { },
|
||||
onFinish: () -> Unit = { },
|
||||
doInIO: suspend () -> T,
|
||||
) {
|
||||
doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO, onFinish) { }
|
||||
}
|
||||
|
||||
fun <T> doInIoThreadNoDialog(
|
||||
onError: (Throwable) -> Unit = { },
|
||||
task: suspend () -> T,
|
||||
) {
|
||||
doInIoThread(showDialog = false, doInIO = task, onError = onError)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在IO线程中执行任务,可选择是否显示加载对话框
|
||||
*/
|
||||
fun doInIoThreadWith(showLoading: Boolean, loadingTips: String, function: suspend () -> Unit) {
|
||||
if (showLoading) {
|
||||
doInIoThread(loadingTips) { function() }
|
||||
} else {
|
||||
doInIoThreadNoDialog { function() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一个无限轮询任务
|
||||
*
|
||||
* @param pollingInterval 轮询间隔时间(单位:秒)
|
||||
* @param pollingTask 轮询任务的挂起函数
|
||||
*/
|
||||
fun polling(intervalSeconds: Long, task: suspend () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
while (true) {
|
||||
task()
|
||||
delay(intervalSeconds * 1000L) // 转换秒为毫秒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟开始轮询任务
|
||||
*/
|
||||
suspend fun delayPolling(delaySeconds: Long, intervalSeconds: Long, task: suspend () -> Unit) {
|
||||
delay(delaySeconds * 1000L)
|
||||
polling(intervalSeconds, task)
|
||||
}
|
||||
|
||||
private val taskMap = mutableMapOf<String, Job>()
|
||||
|
||||
/**
|
||||
* 放弃旧任务,执行新任务
|
||||
*
|
||||
* @param key 任务的唯一标识符
|
||||
* @param block 任务的挂起函数,必须使用协程,不能开启新的协程,否则无法取消任务
|
||||
*/
|
||||
fun launchTaskNewFirst(
|
||||
key: String,
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
taskMap[key]?.cancel() // 取消旧任务
|
||||
taskMap[key] = viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 有新任务时,取消。优先执行旧任务直到完成
|
||||
*
|
||||
* @param key 任务的唯一标识符
|
||||
* @param block 任务函数,可以自由新建协程,但一定要在任务完成时调用 onFinished 回调,否则会导致后续任务永远无法执行
|
||||
*/
|
||||
fun launchTaskOldFirst(
|
||||
key: String,
|
||||
block: (onFinished: () -> Unit) -> Unit
|
||||
) {
|
||||
val existing = taskMap[key]
|
||||
if (existing?.isActive == true) {
|
||||
return
|
||||
}
|
||||
val job = viewModelScope.launch {
|
||||
try {
|
||||
suspendCancellableCoroutine<Unit> { continuation ->
|
||||
block {
|
||||
if (continuation.isActive) {
|
||||
// continuation.resume(Unit) { cause, _, _ ->
|
||||
// println("resume 后任务被取消: $cause")
|
||||
// }
|
||||
}
|
||||
taskMap.remove(key)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
taskMap.remove(key)
|
||||
}
|
||||
}
|
||||
taskMap[key] = job
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
taskMap.values.forEach { it.cancel() }
|
||||
taskMap.clear()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.bbitcn.bbitshow
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
|
||||
/**
|
||||
* @Description APPLICATION类
|
||||
* @Author DuanKaiji
|
||||
* @CreateTime 2024年03月27日 13:43
|
||||
*/
|
||||
val M = Modifier
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
val MD = Modifier
|
||||
|
||||
|
||||
class MyApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.bbitcn.bbitshow
|
||||
|
||||
import com.bbitcn.bbitshow.utls.MyUtils
|
||||
import com.bbitcn.bbitshow.utls.PlanTempDB
|
||||
import com.google.gson.GsonBuilder
|
||||
import org.java_websocket.WebSocket
|
||||
import org.java_websocket.handshake.ClientHandshake
|
||||
import org.java_websocket.server.WebSocketServer
|
||||
import org.json.JSONObject
|
||||
import java.net.InetSocketAddress
|
||||
import kotlin.toString
|
||||
|
||||
class MyWebSocketServer(
|
||||
address: InetSocketAddress,
|
||||
val onMessageReceived: (conn: WebSocket?,Map<String, String>) -> Unit
|
||||
) : WebSocketServer(address) {
|
||||
override fun onMessage(conn: WebSocket?, message: String?) {
|
||||
message?.let {
|
||||
val json = JSONObject(it)
|
||||
val map = mutableMapOf<String, String>()
|
||||
json.keys().forEach { key -> map[key] = json.optString(key) }
|
||||
onMessageReceived(conn,map)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) {// 发送方案数据
|
||||
conn?.send(MyUtils.getMyPlanJsonStr())
|
||||
}
|
||||
|
||||
override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) {}
|
||||
override fun onError(conn: WebSocket?, ex: Exception?) {}
|
||||
override fun onStart() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.bbitcn.bbitshow
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class PollingTask private constructor() {
|
||||
private var scheduler: ScheduledExecutorService? = null
|
||||
private var taskMap: MutableMap<String, ScheduledFuture<*>>? = null
|
||||
|
||||
// 私有化构造函数,确保外部不能直接实例化
|
||||
init {
|
||||
createNewScheduler()
|
||||
}
|
||||
|
||||
// 创建新的 ScheduledExecutorService 实例
|
||||
private fun createNewScheduler() {
|
||||
if (scheduler != null && !scheduler!!.isShutdown) {
|
||||
scheduler!!.shutdown()
|
||||
}
|
||||
scheduler = Executors.newScheduledThreadPool(3)
|
||||
taskMap = HashMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动延迟任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param delaySeconds 延迟时间(以秒为单位)
|
||||
* @param task 需要执行的任务
|
||||
*/
|
||||
fun startDelayedTask(taskId: String, delaySeconds: Long, task: Runnable) {
|
||||
stopTask(taskId) // 如果已有相同ID的任务,先停止
|
||||
|
||||
val future = scheduler!!.schedule({
|
||||
task.run() // 执行任务
|
||||
}, delaySeconds, TimeUnit.SECONDS)
|
||||
taskMap!![taskId] = future // 保存任务的ScheduledFuture
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动IO线程的轮询任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param task 需要在IO线程中执行的任务
|
||||
*/
|
||||
fun startPollingTaskOnIOThread(taskId: String, intervalSeconds: Long, task: Runnable) {
|
||||
startPollingTask(taskId, intervalSeconds) {
|
||||
task.run()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动UI线程的轮询任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param task 需要在UI线程中执行的任务
|
||||
*/
|
||||
fun startPollingTaskOnUIThread(taskId: String, intervalSeconds: Long, task: Runnable) {
|
||||
startPollingTask(taskId, intervalSeconds) {
|
||||
// 执行UI线程任务(需要在UI线程中执行)
|
||||
Handler(Looper.getMainLooper()).post(task)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动IO线程任务并在UI线程中处理结果的轮询任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param ioTask 需要在IO线程中执行的任务
|
||||
* @param uiTask 需要在UI线程中执行的任务
|
||||
* @param <T> IO任务返回结果的类型
|
||||
</T> */
|
||||
fun <T> startPollingTask(
|
||||
taskId: String,
|
||||
intervalSeconds: Long,
|
||||
ioTask: IRxIOTask<T>,
|
||||
uiTask: IRxUITask<T>
|
||||
) {
|
||||
startPollingTask(taskId, intervalSeconds) {
|
||||
// 执行IO线程任务
|
||||
val result = ioTask.doInIOThread()
|
||||
// 将结果传递给UI线程任务
|
||||
Handler(Looper.getMainLooper()).post { uiTask.doInUIThread(result) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动基础轮询任务,供内部使用
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param task 需要执行的任务
|
||||
*/
|
||||
private fun startPollingTask(taskId: String, intervalSeconds: Long, task: Runnable) {
|
||||
stopTask(taskId) // 如果已有相同ID的任务,先停止
|
||||
val future = scheduler!!.scheduleWithFixedDelay(task, 0, intervalSeconds, TimeUnit.SECONDS)
|
||||
|
||||
// val future = scheduler!!.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS)
|
||||
taskMap!![taskId] = future // 保存任务的ScheduledFuture
|
||||
}
|
||||
|
||||
// 停止所有任务
|
||||
fun stopAllTasks() {
|
||||
for (taskId in taskMap!!.keys) {
|
||||
// test("关闭轮询任务$taskId")
|
||||
}
|
||||
if (scheduler != null && !scheduler!!.isShutdown) {
|
||||
scheduler!!.shutdownNow() // 超时后强制停止
|
||||
taskMap!!.clear() // 清空任务映射
|
||||
}
|
||||
// 从实例映射中移除当前实例
|
||||
instances.values.remove(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止轮询任务
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
*/
|
||||
fun stopTask(taskId: String) {
|
||||
val future = taskMap!![taskId]
|
||||
if (future != null) {
|
||||
future.cancel(true) // 取消任务,中断正在执行的任务
|
||||
taskMap!!.remove(taskId) // 从任务列表中移除
|
||||
}
|
||||
}
|
||||
|
||||
// 定义IO任务接口
|
||||
interface IRxIOTask<T> {
|
||||
fun doInIOThread(): T
|
||||
}
|
||||
|
||||
// 定义UI任务接口
|
||||
interface IRxUITask<T> {
|
||||
fun doInUIThread(t: T)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val instances: MutableMap<String, PollingTask> = HashMap()
|
||||
|
||||
@Synchronized
|
||||
fun getInstance(): PollingTask = getInstance("MAIN")
|
||||
|
||||
// 获取基于ID的单例实例
|
||||
@Synchronized
|
||||
fun getInstance(id: String): PollingTask {
|
||||
if (!instances.containsKey(id)) {
|
||||
instances[id] = PollingTask()
|
||||
}
|
||||
return instances[id]!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.bbitcn.bbitshow
|
||||
|
||||
import android.R.attr.port
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.format.Formatter
|
||||
import android.view.KeyEvent
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||
import androidx.tv.material3.Surface
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.mozilla.geckoview.GeckoView
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
class TvActivity : ComponentActivity() {
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
CookieManager.getInstance().setAcceptCookie(true)
|
||||
val viewModel = viewModel<TvViewModel>()
|
||||
registerService(8080, this)
|
||||
Surface(
|
||||
modifier = M.fillMaxSize()
|
||||
) {
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
ModalNavigationDrawer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
if (keyEvent.type == KeyEventType.KeyDown) {
|
||||
when (keyEvent.nativeKeyEvent.keyCode) {
|
||||
KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_BACK -> { // 遥控器菜单键
|
||||
scope.launch {
|
||||
if (drawerState.isClosed) drawerState.open()
|
||||
else drawerState.close()
|
||||
}
|
||||
true // 拦截事件
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
},
|
||||
gesturesEnabled = drawerState.isOpen,
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
Column(
|
||||
modifier = M
|
||||
.fillMaxHeight()
|
||||
.selectableGroup()
|
||||
.width(200.dp)
|
||||
) {
|
||||
Text(
|
||||
"TV IP: ${getLocalIpAddress() ?: "未知IP"}",
|
||||
fontSize = 20.sp,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
// val currentIndex by viewModel.currentIndex.collectAsState()
|
||||
// val pages by viewModel.pages.collectAsState()
|
||||
// val state = rememberPagerState { pages.size }
|
||||
// LaunchedEffect(currentIndex) {
|
||||
// if (currentIndex in 0 until pages.size) {
|
||||
// state.animateScrollToPage(currentIndex)
|
||||
// }
|
||||
// }
|
||||
// VerticalPager(state = state) { it ->
|
||||
// // 拿出对应 WebView
|
||||
// pages.getOrNull(it)?.let { wv ->
|
||||
// AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
|
||||
// // 确保 WebView 没有父布局
|
||||
// (wv.parent as? ViewGroup)?.removeView(wv)
|
||||
// wv
|
||||
// } )
|
||||
// }
|
||||
// }
|
||||
val currentIndex by viewModel.currentIndex.collectAsState()
|
||||
val pages by viewModel.pages.collectAsState()
|
||||
val state = rememberPagerState { pages.size }
|
||||
|
||||
LaunchedEffect(currentIndex) {
|
||||
if (currentIndex in 0 until pages.size) {
|
||||
state.animateScrollToPage(currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
VerticalPager(state = state, beyondViewportPageCount = pages.size) { index ->
|
||||
pages.getOrNull(index)?.let { session ->
|
||||
AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
|
||||
GeckoView(context).apply {
|
||||
// 每个页面实例化一个 GeckoView,然后 attach 已存在的 Session
|
||||
setSession(session.session)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLocalIpAddress(): String? {
|
||||
val wm = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
|
||||
return Formatter.formatIpAddress(wm.connectionInfo.ipAddress)
|
||||
}
|
||||
|
||||
|
||||
private var registrationListener: NsdManager.RegistrationListener? = null
|
||||
private var nsdManager: NsdManager? = null
|
||||
|
||||
fun registerService(port: Int, context: Context) {
|
||||
val serviceInfo = NsdServiceInfo().apply {
|
||||
serviceName = Settings.Global.getString(context.contentResolver, "device_name")
|
||||
serviceType = "_bbit_show._tcp."
|
||||
setPort(port)
|
||||
}
|
||||
|
||||
nsdManager = context.getSystemService(NSD_SERVICE) as NsdManager
|
||||
registrationListener = object : NsdManager.RegistrationListener {
|
||||
override fun onServiceRegistered(serviceInfo: NsdServiceInfo?) {}
|
||||
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {}
|
||||
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo?) {}
|
||||
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {}
|
||||
}
|
||||
nsdManager?.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
|
||||
}
|
||||
|
||||
fun unregisterService() {
|
||||
registrationListener?.let {
|
||||
nsdManager?.unregisterService(it)
|
||||
}
|
||||
registrationListener = null
|
||||
nsdManager = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterService()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.bbitcn.bbitshow
|
||||
|
||||
import android.R.attr.tag
|
||||
import android.content.Context
|
||||
import android.net.http.SslError
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.bbitcn.bbitshow.model.GeckoPage
|
||||
import com.bbitcn.bbitshow.model.Plan
|
||||
import com.bbitcn.bbitshow.utls.MyUtils
|
||||
import com.bbitcn.bbitshow.utls.PlanTempDB
|
||||
import com.blankj.utilcode.util.ActivityUtils
|
||||
import com.blankj.utilcode.util.AppUtils
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.UUID
|
||||
import kotlin.to
|
||||
import org.mozilla.geckoview.AllowOrDeny
|
||||
import org.mozilla.geckoview.GeckoResult
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoSession
|
||||
import kotlin.collections.getOrNull
|
||||
import kotlin.collections.indexOfFirst
|
||||
import kotlin.collections.plus
|
||||
|
||||
class TvViewModel : BaseViewModel() {
|
||||
|
||||
private var server: MyWebSocketServer? = null
|
||||
private val _currentIndex = MutableStateFlow(0)
|
||||
val currentIndex = _currentIndex.asStateFlow()
|
||||
|
||||
/**
|
||||
* 自己的计划
|
||||
*/
|
||||
private val _plan = MutableStateFlow<Plan?>(null)
|
||||
val plan = _plan.asStateFlow()
|
||||
val port = 8080
|
||||
|
||||
/**
|
||||
* 页面列表
|
||||
*/
|
||||
// private val _pages = MutableStateFlow<List<WebView>>(listOf())
|
||||
// val pages = _pages.asStateFlow()
|
||||
private val _pages = MutableStateFlow<List<GeckoPage>>(listOf())
|
||||
val pages = _pages.asStateFlow()
|
||||
|
||||
init {
|
||||
if (server == null) {
|
||||
server = MyWebSocketServer(InetSocketAddress(port)) { conn, msg ->
|
||||
doInUIThread {
|
||||
when (msg["action"]) {
|
||||
// 基本控制
|
||||
"next" -> goForward()
|
||||
"prev" -> goBack()
|
||||
"reload" -> reloadCurrent()
|
||||
"stop" -> stopLoad()
|
||||
|
||||
// 多页控制
|
||||
"nextPage" -> switchPage(1)
|
||||
"prevPage" -> switchPage(-1)
|
||||
"reload_all" -> reloadAll()
|
||||
"switchPage" -> switchPageById(UUID.fromString(msg["data"] ?: ""))
|
||||
"load_list" -> {
|
||||
val jsonString = msg["data"].toString()
|
||||
val listType = object : TypeToken<Plan>() {}.type
|
||||
val plan: Plan = Gson().fromJson(jsonString, listType)
|
||||
_plan.value = plan
|
||||
PlanTempDB.init(plan)
|
||||
plan.pages.sortedBy { it.sort }
|
||||
initGeckoPages(plan)
|
||||
conn?.send(MyUtils.getMyPlanJsonStr())
|
||||
}
|
||||
}
|
||||
Log.d("TvViewModel", "Received message: $msg")
|
||||
}
|
||||
}
|
||||
server?.start()
|
||||
}
|
||||
// 读取本地保存的页面
|
||||
doInIoThreadThenUI(onIO = {
|
||||
PlanTempDB.getData()
|
||||
}) {
|
||||
initGeckoPages(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页的停止读取方法
|
||||
*/
|
||||
private fun stopLoad() {
|
||||
_pages.value.getOrNull(_currentIndex.value)?.session?.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页后退
|
||||
*/
|
||||
fun goBack() {
|
||||
_pages.value.getOrNull(_currentIndex.value)?.let { webView ->
|
||||
webView.session.goBack()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页前进
|
||||
*/
|
||||
fun goForward() {
|
||||
_pages.value.getOrNull(_currentIndex.value)?.let { webView ->
|
||||
webView.session.goForward()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载当前页面
|
||||
*/
|
||||
fun reloadCurrent() {
|
||||
_pages.value.getOrNull(_currentIndex.value)?.session?.reload()
|
||||
}
|
||||
|
||||
fun reloadAll() {
|
||||
_pages.value.forEach {
|
||||
it.session.reload()
|
||||
}
|
||||
}
|
||||
|
||||
fun switchPage(delta: Int) {
|
||||
val newIndex = (_currentIndex.value + delta).coerceIn(0, _pages.value.size - 1)
|
||||
_currentIndex.value = newIndex
|
||||
}
|
||||
|
||||
fun switchPageById(id: UUID) {
|
||||
val newIndex = _pages.value.indexOfFirst { it.id == id }
|
||||
_currentIndex.value = newIndex
|
||||
}
|
||||
|
||||
private val runtime by lazy {
|
||||
GeckoRuntime.create(ActivityUtils.getTopActivity())
|
||||
}
|
||||
|
||||
private fun initGeckoPages(plan: Plan) {
|
||||
_pages.value = listOf()
|
||||
plan.pages.forEach { page ->
|
||||
val session = GeckoSession().apply { open(runtime) }
|
||||
session.loadUri(page.url)
|
||||
// SSL 错误/URL 拦截 → GeckoView 走的是 ProgressDelegate、PermissionDelegate、NavigationDelegate
|
||||
session.navigationDelegate = object : GeckoSession.NavigationDelegate {
|
||||
override fun onLoadRequest(
|
||||
session: GeckoSession,
|
||||
request: GeckoSession.NavigationDelegate.LoadRequest
|
||||
): GeckoResult<AllowOrDeny> {
|
||||
// 相当于 shouldOverrideUrlLoading
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
}
|
||||
val geckoPage = GeckoPage(id = page.id, session = session)
|
||||
_pages.update { current -> current + geckoPage }
|
||||
}
|
||||
_currentIndex.value = 0
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.bbitcn.sericulture.base
|
||||
|
||||
import android.content.Context
|
||||
import com.bbitcn.bbitshow.utls.SPUtils
|
||||
import com.google.gson.Gson
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
*
|
||||
* @Description 临时数据库(单实体,列表请用@BaseListTempDataBase)
|
||||
* @Author DuanKaiji
|
||||
* @CreateTime 2024年08月06日 11:39:31
|
||||
*/
|
||||
abstract class BaseTempDataBase<T> {
|
||||
|
||||
abstract fun getKey(): String
|
||||
abstract fun defaultData(): T
|
||||
abstract fun getType(): Type
|
||||
|
||||
fun init(value :T): Boolean {
|
||||
SPUtils.put(getKey(), Gson().toJson(value))
|
||||
return true
|
||||
}
|
||||
|
||||
fun getData(): T {
|
||||
val json = SPUtils.get(getKey(), Gson().toJson(defaultData()))
|
||||
return Gson().fromJson(json, getType())
|
||||
}
|
||||
|
||||
fun clear(): Boolean {
|
||||
SPUtils.put(getKey(), "")
|
||||
return true
|
||||
}
|
||||
fun putString(context: Context, key: String, value: String) {
|
||||
val sp = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
|
||||
sp.edit().putString(key, value).apply() // apply() 异步提交
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.bbitcn.bbitshow.base
|
||||
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class ParameterizedTypeImpl(private var clazz: Class<*>) : ParameterizedType {
|
||||
override fun getActualTypeArguments(): Array<Type> {
|
||||
return arrayOf(clazz)
|
||||
}
|
||||
|
||||
override fun getRawType(): Type {
|
||||
return MutableList::class.java
|
||||
}
|
||||
|
||||
override fun getOwnerType(): Type? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bbitcn.bbitshow.model
|
||||
|
||||
import org.mozilla.geckoview.GeckoSession
|
||||
import java.util.UUID
|
||||
|
||||
data class GeckoPage(
|
||||
val id: UUID,
|
||||
val session: GeckoSession
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bbitcn.bbitshow.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class Page(
|
||||
var id: UUID,
|
||||
var sort: Int,
|
||||
var name: String,
|
||||
var url: String
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bbitcn.bbitshow.model
|
||||
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
data class Plan(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val createTime: Date,
|
||||
val cycleRefreshTime :Int,
|
||||
val pages: List<Page>
|
||||
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.bbitcn.bbitshow.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.bbitcn.bbitshow.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.darkColorScheme
|
||||
import androidx.tv.material3.lightColorScheme
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
@Composable
|
||||
fun BBITShowTheme(
|
||||
isInDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = if (isInDarkTheme) {
|
||||
darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
} else {
|
||||
lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
)
|
||||
}
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.bbitcn.bbitshow.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||
import androidx.tv.material3.Typography
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bbitcn.bbitshow.utls
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import org.json.JSONObject
|
||||
|
||||
object MyUtils {
|
||||
fun getMyPlanJsonStr(): String {
|
||||
val gson = GsonBuilder()
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
.create()
|
||||
val map = mutableMapOf<String, String>()
|
||||
map["type"] = "tv"
|
||||
map["data"] = gson.toJson(PlanTempDB.getData())
|
||||
return JSONObject(map).toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.bbitcn.bbitshow.utls
|
||||
|
||||
import com.bbitcn.bbitshow.model.Page
|
||||
import com.bbitcn.bbitshow.model.Plan
|
||||
import com.bbitcn.sericulture.base.BaseTempDataBase
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import java.lang.reflect.Type
|
||||
|
||||
object PlanTempDB :BaseTempDataBase<Plan>(){
|
||||
override fun getKey(): String {
|
||||
return "FRP_CONFIG_TEMP"
|
||||
}
|
||||
|
||||
override fun defaultData(): Plan {
|
||||
return Plan(id = UUID.randomUUID(),
|
||||
name ="默认配置",
|
||||
createTime = Date(),
|
||||
cycleRefreshTime = 0,
|
||||
pages = listOf(Page(
|
||||
id = UUID.randomUUID(),
|
||||
sort = 0,
|
||||
name = "默认页面",
|
||||
url = "https://www.baidu.com",
|
||||
)))
|
||||
}
|
||||
|
||||
override fun getType(): Type {
|
||||
return object : TypeToken<Plan>() {}.type
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.bbitcn.bbitshow.utls
|
||||
|
||||
import android.content.Context
|
||||
import com.blankj.utilcode.util.ActivityUtils
|
||||
import com.blankj.utilcode.util.AppUtils
|
||||
|
||||
object SPUtils {
|
||||
|
||||
private const val PREF_NAME = "app_prefs"
|
||||
|
||||
// 存储 String
|
||||
fun put( key: String, value: String) {
|
||||
val sp = ActivityUtils.getTopActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
sp.edit().putString(key, value).apply()
|
||||
}
|
||||
|
||||
// 读取 String
|
||||
fun get(key: String, defaultValue: String = ""): String {
|
||||
val sp = ActivityUtils.getTopActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
return sp.getString(key, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
// 可选:清空所有
|
||||
fun clear() {
|
||||
val sp = ActivityUtils.getTopActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
sp.edit().clear().apply()
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">BBIT展示屏</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
|
||||
<style name="Theme.BBITShow" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
||||
</resources>
|
||||
Reference in New Issue
Block a user