commit ec8289d0f5c76343c57d74993e0314cd87c7ec0c Author: BBIT-Kai <2911862937@qq.com> Date: Mon May 25 15:27:47 2026 +0800 初始化项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11e6be0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +.kotlin/ +.idea/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..952b930 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..20e2a01 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..55d8458 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,45 @@ +[versions] +agp = "8.10.1" +kotlin = "2.0.21" +coreKtx = "1.10.1" +junit = "4.13.2" +junitVersion = "1.1.5" +espressoCore = "3.5.1" +appcompat = "1.6.1" +material = "1.10.0" +activity = "1.8.0" +constraintlayout = "2.1.4" +composeBom = "2024.09.00" +tvFoundation = "1.0.0-alpha07" +tvMaterial = "1.0.1" +lifecycleRuntimeKtx = "2.6.1" +activityCompose = "1.8.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" } +androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } + +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3d3fa12 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Aug 18 14:55:29 CST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/phone/.gitignore b/phone/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/phone/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/phone/build.gradle.kts b/phone/build.gradle.kts new file mode 100644 index 0000000..51fe9c6 --- /dev/null +++ b/phone/build.gradle.kts @@ -0,0 +1,68 @@ +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.phone" + compileSdk = 35 + + defaultConfig { + applicationId = "com.bbitcn.phone" + minSdk = 24 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + implementation( "org.java-websocket:Java-WebSocket:1.6.0") + //lifecycle-viewmodel-compose + implementation(libs.androidx.lifecycle.viewmodel.compose) + //MMKV + implementation("com.tencent:mmkv:2.2.2") + implementation("com.google.code.gson:gson:2.13.1") + +} \ No newline at end of file diff --git a/phone/proguard-rules.pro b/phone/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/phone/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/phone/src/androidTest/java/com/bbitcn/phone/ExampleInstrumentedTest.kt b/phone/src/androidTest/java/com/bbitcn/phone/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..98ee1e9 --- /dev/null +++ b/phone/src/androidTest/java/com/bbitcn/phone/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.bbitcn.phone + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.bbitcn.phone", appContext.packageName) + } +} \ No newline at end of file diff --git a/phone/src/main/AndroidManifest.xml b/phone/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b2cf34f --- /dev/null +++ b/phone/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/MyApp.kt b/phone/src/main/java/com/bbitcn/phone/MyApp.kt new file mode 100644 index 0000000..3d2b01a --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/MyApp.kt @@ -0,0 +1,34 @@ +package com.bbitcn.phone + +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.phone.utls.MMKVUtil + + +/** + * @Description APPLICATION类 + * @Author DuanKaiji + * @CreateTime 2024年03月27日 13:43 + */ +val M = Modifier +val MD = Modifier + .animateContentSize( + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessLow + ) + ) + + +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + // 初始化MMKV + MMKVUtil.init(applicationContext) + } +} diff --git a/phone/src/main/java/com/bbitcn/phone/base/BaseListTempDataBase.kt b/phone/src/main/java/com/bbitcn/phone/base/BaseListTempDataBase.kt new file mode 100644 index 0000000..68475cb --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/base/BaseListTempDataBase.kt @@ -0,0 +1,75 @@ +package com.bbitcn.sericulture.base + +import com.bbitcn.phone.utls.MMKVUtil +import com.google.gson.Gson +import java.lang.reflect.Type + +/** + * + * @Description 列表临时存储库 + * @Author DuanKaiji + * @CreateTime 2024年08月06日 11:39:31 + */ +abstract class BaseListTempDataBase { + protected val gson: Gson = Gson() + + abstract fun getKey(): String + + abstract fun getType(): Type + + fun getAll(): MutableList { + val json = MMKVUtil.get(getKey(), "[]") + return Gson().fromJson(json, getType()) + } + + suspend fun init(data: List) { + MMKVUtil.put(getKey(), gson.toJson(data)) + } + + suspend fun insert(t: T, afterInsert: (T) -> Unit = {}): Boolean { +// val type: Type = ParameterizedTypeImpl(T::class.java) + val temp: MutableList = getAll() + if (temp.contains(t)) { + return false + } + temp.add(t) + MMKVUtil.put(getKey(), gson.toJson(temp)) + afterInsert(t) + return true + } + + /** + * 更新 + */ + fun update(predicate: (T) -> Boolean, update: (T) -> Unit = {}, afterUpdate: () -> Unit = {}): Boolean { + val temp: MutableList = getAll() + for (mode in temp) { + if (predicate(mode)) { + update(mode) + MMKVUtil.put(getKey(), gson.toJson(temp)) + afterUpdate() + return true + } + } + return false + } + + fun delete(predicate: (T) -> Boolean,afterDel: (T) -> Unit = { }): Boolean { + val temp: MutableList = getAll() + val iterator = temp.iterator() + while (iterator.hasNext()) { + val mode = iterator.next() + if (predicate(mode)) { + iterator.remove() + MMKVUtil.put(getKey(), gson.toJson(temp)) + afterDel(mode) + return true + } + } + return false + } + fun clear(): Boolean { + MMKVUtil.put(getKey(), "[]") + return true + } +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/base/BaseViewModel.kt b/phone/src/main/java/com/bbitcn/phone/base/BaseViewModel.kt new file mode 100644 index 0000000..d5562cd --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/base/BaseViewModel.kt @@ -0,0 +1,194 @@ +package com.bbitcn.phone.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.bbitcn.phone.utls.PollingTask +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +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.Companion.getInstance(javaClass.simpleName) // 使用类名作为ID + + protected fun doInUIThread(task: () -> Unit) { + viewModelScope.launch { + withContext(Dispatchers.Main) { + task() + } + } + } + + protected fun 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 doInIoThread( + loadingTips: String = "正在加载中", + showDialog: Boolean = true, + onError: (Throwable) -> Unit = { }, + onFinish: () -> Unit = { }, + doInIO: suspend () -> T, + ) { + doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO, onFinish) { } + } + + fun 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() + + /** + * 放弃旧任务,执行新任务 + * + * @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 { 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() + } + +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/base/MyAnimatedVisibility.kt b/phone/src/main/java/com/bbitcn/phone/base/MyAnimatedVisibility.kt new file mode 100644 index 0000000..023c5ef --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/base/MyAnimatedVisibility.kt @@ -0,0 +1,30 @@ +package com.bbitcn.phone.base + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bbitcn.phone.M + +@Composable +fun MyAnimatedVisibility( + visible: Boolean, + modifier: Modifier = M, + enter: EnterTransition = fadeIn(), + exit: ExitTransition = fadeOut(), + label: String = "AnimatedVisibility", + content: @Composable AnimatedVisibilityScope.() -> Unit +) { + AnimatedVisibility( + visible = visible, + modifier = modifier, + enter = enter, + exit = exit, + label = label, + content = content + ) +} diff --git a/phone/src/main/java/com/bbitcn/phone/base/MyBottomSheet.kt b/phone/src/main/java/com/bbitcn/phone/base/MyBottomSheet.kt new file mode 100644 index 0000000..26c9c67 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/base/MyBottomSheet.kt @@ -0,0 +1,110 @@ +package com.bbitcn.phone.base + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.bbitcn.phone.M +import kotlinx.coroutines.delay +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.padding + +@Composable +fun MyBottomSheet( + showBottomSheet: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetHeight: Dp = 500.dp, + content: @Composable ColumnScope.() -> Unit +) { + var showSheet by remember { mutableStateOf(false) } + + // 拦截返回键 + if (showBottomSheet) { + BackHandler { + onDismissRequest() + } + } + + LaunchedEffect(showBottomSheet) { + if (showBottomSheet) { + showSheet = true + } else { + showSheet = false + } + } + + if (showBottomSheet || showSheet) { + Box(modifier = Modifier.fillMaxSize().padding(bottom = 100.dp)) { + AnimatedVisibility( + visible = showBottomSheet, + enter = fadeIn(animationSpec = tween(200)), + exit = fadeOut(animationSpec = tween(200)) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x99000000)) + .noVisualFeedbackClickable { onDismissRequest() } + ) + } + + AnimatedVisibility( + visible = showSheet, + enter = slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(250, easing = FastOutSlowInEasing) + ), + exit = slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(200, easing = FastOutSlowInEasing) + ), + modifier = Modifier.align(Alignment.BottomCenter) + ) { + Box( + modifier = modifier + .fillMaxWidth() + .height(sheetHeight) + .background( + Color.White, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) {} + ) { + Column( + modifier = Modifier.fillMaxSize(), + content = content + ) + } + } + } + } +} diff --git a/phone/src/main/java/com/bbitcn/phone/base/ParameterizedTypeImpl.kt b/phone/src/main/java/com/bbitcn/phone/base/ParameterizedTypeImpl.kt new file mode 100644 index 0000000..c13ba54 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/base/ParameterizedTypeImpl.kt @@ -0,0 +1,18 @@ +package com.bbitcn.phone.base + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ParameterizedTypeImpl(private var clazz: Class<*>) : ParameterizedType { + override fun getActualTypeArguments(): Array { + return arrayOf(clazz) + } + + override fun getRawType(): Type { + return MutableList::class.java + } + + override fun getOwnerType(): Type? { + return null + } +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/base/noVisualFeedbackClickable.kt b/phone/src/main/java/com/bbitcn/phone/base/noVisualFeedbackClickable.kt new file mode 100644 index 0000000..1db9356 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/base/noVisualFeedbackClickable.kt @@ -0,0 +1,21 @@ +package com.bbitcn.phone.base + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput + +/** + * 自定义点击事件 + * 不使用clickable的点击效果 + */ +fun Modifier.noVisualFeedbackClickable( + onClick: () -> Unit +): Modifier { + return this.pointerInput(Unit) { + detectTapGestures( + onTap = { + onClick() + } + ) + } +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/model/Page.kt b/phone/src/main/java/com/bbitcn/phone/model/Page.kt new file mode 100644 index 0000000..1ac5268 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/model/Page.kt @@ -0,0 +1,10 @@ +package com.bbitcn.phone.model + +import java.util.UUID + +data class Page( + var id: UUID, + var sort: Int, + var name: String, + var url: String +) \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/model/Plan.kt b/phone/src/main/java/com/bbitcn/phone/model/Plan.kt new file mode 100644 index 0000000..0e0d3be --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/model/Plan.kt @@ -0,0 +1,13 @@ +package com.bbitcn.phone.model + +import java.util.Date +import java.util.UUID + +data class Plan( + val id: UUID, + var name: String, + val createTime: Date, + var cycleRefreshTime :Int, + var pages: List + +) diff --git a/phone/src/main/java/com/bbitcn/phone/ui/PhoneActivity.kt b/phone/src/main/java/com/bbitcn/phone/ui/PhoneActivity.kt new file mode 100644 index 0000000..6cc9d8c --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/PhoneActivity.kt @@ -0,0 +1,157 @@ +package com.bbitcn.phone.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.phone.M +import com.bbitcn.phone.base.MyBottomSheet +import com.bbitcn.phone.ui.compose.ConnectScreen +import com.bbitcn.phone.ui.compose.ControlScreen +import com.bbitcn.phone.ui.compose.SetScreen +import kotlinx.coroutines.launch + + +class PhoneActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + val viewModel = viewModel() + viewModel.startDiscovery(this) + val pagerState = rememberPagerState { 10 } + val scope = rememberCoroutineScope() + val showBottomSheet by viewModel.isDrawerOpen.collectAsState() + + // 连接、控制、设置 + val navList = listOf( + "连接" to Icons.Default.Phone, + "控制" to Icons.Default.PlayArrow, + "设置" to Icons.Default.Settings + ) + Scaffold( + modifier = M + .fillMaxSize(), + floatingActionButton = { + if( pagerState.currentPage == 2 && !showBottomSheet) { + FloatingActionButton(onClick = { + // 新增计划 + viewModel.addPlan() + }) { + Icon(Icons.Default.Add, contentDescription = null) + } + } + }, + bottomBar = { + NavigationBar(windowInsets = NavigationBarDefaults.windowInsets) { + navList.forEachIndexed { index, item -> + NavigationBarItem( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + icon = { + Icon( + item.second, + contentDescription = null + ) + }, + label = { Text(item.first) } + ) + } + } + } + ) { contentPadding -> + HorizontalPager(state = pagerState, contentPadding = contentPadding) { + Column(modifier = M.fillMaxSize()) { + when (it) { + 0 -> ConnectScreen(viewModel) + 1 -> ControlScreen(viewModel) + 2 -> SetScreen(viewModel) + } + } + } + MyBottomSheet( + showBottomSheet = showBottomSheet, + onDismissRequest = { + viewModel.closeDrawer() + } + ) { + Box( + modifier = M.fillMaxSize() + ) { + // 底部抽屉 + viewModel.drawerContent.value() + } + } + } + } + } +} + +@Composable +fun FunctionModel(name: String, content: @Composable () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + Text( + name, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + modifier = Modifier + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + content() + } + } +} + +@Composable +fun MyFuncButtonRow(content: @Composable () -> Unit) { + LazyRow { + item { + Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) { + content() + } + } + } +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/ui/PhoneViewModel.kt b/phone/src/main/java/com/bbitcn/phone/ui/PhoneViewModel.kt new file mode 100644 index 0000000..5108cf6 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/PhoneViewModel.kt @@ -0,0 +1,525 @@ +package com.bbitcn.phone.ui + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.bbitcn.phone.M +import com.bbitcn.phone.base.BaseViewModel +import com.bbitcn.phone.model.Page +import com.bbitcn.phone.model.Plan +import com.bbitcn.phone.utls.PlanTempDatabase +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.java_websocket.client.WebSocketClient +import org.java_websocket.handshake.ServerHandshake +import org.json.JSONObject +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket +import java.net.URI +import java.util.Date +import java.util.UUID + +class PhoneViewModel : BaseViewModel() { + // 控制 Drawer 内容的 MutableStateFlow + private val _drawerContent = MutableStateFlow<@Composable () -> Unit> { } + val drawerContent: StateFlow<@Composable () -> Unit> = _drawerContent.asStateFlow() + + /** + * 控制 Drawer 状态的 Boolean Flow + */ + private val _isDrawerOpen = MutableStateFlow(false) + val isDrawerOpen: StateFlow = _isDrawerOpen.asStateFlow() + + /** + * 扫描到的所有设备 + */ + private val _devices = MutableStateFlow>(emptyList()) + val devices = _devices.asStateFlow() + + /** + * 设备当前页面列表 + */ + private val _devicePagesList = MutableStateFlow(null) + val devicePagesList = _devicePagesList.asStateFlow() + + /** + * 自己的计划列表 + */ + private val _plans = MutableStateFlow>(emptyList()) + val plans = _plans.asStateFlow() + + /** + * 当前连接的设备信息 + */ + private val _device = MutableStateFlow(null) + val device = _device.asStateFlow() + + /** + * 自动刷新状态 + */ + private val _autoSearching = MutableStateFlow(true) + val autoSearching = _autoSearching.asStateFlow() + + /** + * 发现监听器 + */ + private var discoveryListener: NsdManager.DiscoveryListener? = null + private var client: WebSocketClient? = null + lateinit var nsdManager: NsdManager + val port = 8080 + + + init { + // 刷新个人配置 + _plans.value = PlanTempDatabase.getAll() + } + + fun connectToTv(nsdServiceInfo: NsdServiceInfo) { + connectToTvByIp(nsdServiceInfo.host.hostAddress, nsdServiceInfo) + } + + fun connectToTvByIp(tvIp: String, nsdServiceInfo: NsdServiceInfo? = null) { + doInIoThread { + val uri = URI("ws://$tvIp:$port") + client = object : WebSocketClient(uri) { + override fun onOpen(handshakedata: ServerHandshake?) { + _device.value = nsdServiceInfo + ?: NsdServiceInfo().apply { + serviceName = "手动连接设备" + host = + URI("http://$tvIp").host?.let { InetAddress.getByName(it) } + port = port + } + } + + override fun onMessage(message: String?) { + message?.let { + try { + val json = JSONObject(it) + if (json["type"] == "tv") { + val listType = object : TypeToken() {}.type + val plan: Plan = Gson().fromJson(json["data"].toString(), listType) + plan.pages.sortedBy { it.id } + _devicePagesList.value = plan + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + override fun onClose(code: Int, reason: String?, remote: Boolean) { + initConnectedDeviceData() + client?.close() + client = null + } + + override fun onError(ex: Exception?) { + ex?.printStackTrace() + } + } + client?.connect() + } + } + + // 开始发现服务 + fun startDiscovery(context: Context) { + nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + discoveryListener = object : NsdManager.DiscoveryListener { + override fun onServiceFound(service: NsdServiceInfo) { + // 异步解析服务 + nsdManager.resolveService(service, object : NsdManager.ResolveListener { + override fun onServiceResolved(nsdServiceInfo: NsdServiceInfo) { + val host = nsdServiceInfo.host.hostAddress + val port = nsdServiceInfo.port + if (isServiceAvailable(host, port)) { + _devices.value = + (_devices.value + nsdServiceInfo).distinctBy { it.serviceName } + connectToTv(nsdServiceInfo) + } else { + Log.w("NSD", "服务不可达: $host:$port") + } + } + + override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {} + }) + } + + override fun onServiceLost(service: NsdServiceInfo) { + _devices.value = _devices.value.filter { it.serviceName != service.serviceName } + } + + override fun onDiscoveryStarted(serviceType: String) {} + override fun onDiscoveryStopped(serviceType: String) {} + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {} + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {} + } + + nsdManager.discoverServices( + "_bbit_show._tcp.", + NsdManager.PROTOCOL_DNS_SD, + discoveryListener + ) + _autoSearching.value = true + } + + fun isServiceAvailable(ip: String, port: Int, timeout: Int = 1000): Boolean { + return try { + Socket().use { socket -> + socket.connect(InetSocketAddress(ip, port), timeout) + } + true + } catch (e: IOException) { + e.printStackTrace() + false + } + } + + /** + * 手动断开连接 + */ + fun disconnect() { + client?.close() + client = null + initConnectedDeviceData() + } + + // 停止发现 + fun stopDiscovery() { + discoveryListener?.let { + nsdManager.stopServiceDiscovery(it) + discoveryListener = null + } + _autoSearching.value = false + } + + /** + * 基础控制 + */ + fun next() = sendAction("next") + fun prev() = sendAction("prev") + fun reload() = sendAction("reload") + fun stop() = sendAction("stop") + + /** + * 多页控制 + */ + fun nextPage() = sendAction("nextPage") + fun prevPage() = sendAction("prevPage") + fun reloadAll() = sendAction("reload_all") + fun switchPage(id: UUID) = sendAction("switchPage", id.toString()) + fun loadList(content: String) = sendAction("load_list", content) + + private fun sendAction(action: String, data: String? = null) { + val json = JSONObject() + json.put("action", action) + if (data != null) json.put("data", data) + client?.send(json.toString()) + Log.d("WebSocket", "发送消息: $json") + } + + + fun closeDrawer() { + _isDrawerOpen.value = false + } + + fun public() { + doInIoThread { + _drawerContent.value = { + LazyColumn { + item { + Text( + text = "选择方案", + modifier = M + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + } + items(items = _plans.value) { plan -> + Button( + modifier = M.fillMaxWidth(), + onClick = { + // 发送方案数据 + val gson = GsonBuilder() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + .create() + + val jsonString = gson.toJson(plan) + loadList(jsonString) + _isDrawerOpen.value = false + } + ) { + Text(plan.name) + } + } + } + } + _isDrawerOpen.value = true + } + } + + fun addPlan() { + doInIoThread { + _drawerContent.value = { + var name by remember { mutableStateOf("") } + var autoRefreshTimes by remember { mutableStateOf("0") } + Column { + Text( + text = "新建方案", + modifier = M + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + Column(modifier = M.padding(horizontal = 16.dp)) { + TextField( + modifier = M.fillMaxWidth(), + value = name, + onValueChange = { name = it }, + label = { Text("方案名称") }, + singleLine = true + ) + TextField( + modifier = M.fillMaxWidth(), + value = autoRefreshTimes, + onValueChange = { autoRefreshTimes = it }, + label = { Text("自动刷新时间(0为不刷新)") }, + singleLine = true, + // 限制为数字输入 + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + Button(modifier = M.fillMaxWidth(), onClick = { + doInIoThread { + PlanTempDatabase.insert( + Plan( + id = UUID.randomUUID(), + name = name, + createTime = Date(), + cycleRefreshTime = autoRefreshTimes.toIntOrNull() ?: 0, + pages = emptyList() + ) + ) + _plans.value = PlanTempDatabase.getAll() + _isDrawerOpen.value = false + } + }) { Text("新建") } + } + } + } + _isDrawerOpen.value = true + } + } + + fun editPlan(plan: Plan) { + doInIoThread { + _drawerContent.value = { + var name by remember { mutableStateOf(plan.name) } + var autoRefreshTimes by remember { mutableStateOf(plan.cycleRefreshTime.toString()) } + Column { + Text( + text = "编辑方案", + modifier = M + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + Column(modifier = M.padding(horizontal = 16.dp)) { + TextField( + modifier = M.fillMaxWidth(), + value = name, + onValueChange = { name = it }, + label = { Text("方案名称") }, + singleLine = true + ) + TextField( + modifier = M.fillMaxWidth(), + value = autoRefreshTimes, + onValueChange = { autoRefreshTimes = it }, + label = { Text("自动刷新时间(0为不刷新)") }, + singleLine = true, + // 限制为数字输入 + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Button(modifier = M.weight(1f), onClick = { + doInIoThread { + PlanTempDatabase.update({ it.id == plan.id }, update = { + it.name = name + it.cycleRefreshTime = autoRefreshTimes.toIntOrNull() ?: 0 + }) { + _plans.value = PlanTempDatabase.getAll() + _isDrawerOpen.value = false + } + } + }) { Text("确定") } + Button(modifier = M.weight(1f), onClick = { + PlanTempDatabase.delete({ + it.id == plan.id + }) { + _plans.value = PlanTempDatabase.getAll() + _isDrawerOpen.value = false + } + }) { Text("删除") } + } + } + } + } + _isDrawerOpen.value = true + } + } + + fun editPage(plan: Plan) { + doInIoThread {_drawerContent.value = { + // 保存原始快照用于比较 + val originalList = remember { plan.pages.map { it.copy() } } + var list by remember { mutableStateOf(plan.pages.map { it.copy() }) } + + // 判断列表是否被修改 + val isModified by remember(list) { + derivedStateOf { + if (list.size != originalList.size) return@derivedStateOf true + list.zip(originalList).any { (cur, orig) -> + cur.id != orig.id || cur.name != orig.name || cur.url != orig.url || cur.sort != orig.sort + } + } + } + + Column(modifier = M.fillMaxSize()) { + Text( + text = "方案细则 - ${plan.name}", + modifier = M.padding(horizontal = 16.dp, vertical = 8.dp), + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + + Button(modifier = M.fillMaxWidth(), onClick = { + list = list + Page( + id = UUID.randomUUID(), + sort = list.size + 1, + name = "新页面", + url = "" + ) + }) { Text("新增") } + + LazyColumn(modifier = M.weight(1f)) { + itemsIndexed(items = list) { index, page -> + var pageSort by remember { mutableStateOf(page.sort.toString()) } + var pageName by remember { mutableStateOf(page.name) } + var pageUrl by remember { mutableStateOf(page.url) } + + Row(modifier = M.fillMaxWidth().padding(vertical = 5.dp)) { + TextField( + modifier = M.weight(2f).padding(horizontal = 4.dp), + value = pageSort, + onValueChange = { newSort -> + pageSort = newSort + list = list.toMutableList().also { l -> + l[index] = l[index].copy(sort = newSort.toIntOrNull() ?: 0) + } + }, + label = { Text("序号") }, + singleLine = true + ) + TextField( + modifier = M.weight(3f).padding(horizontal = 4.dp), + value = pageName, + onValueChange = { newName -> + pageName = newName + list = list.toMutableList().also { l -> + l[index] = l[index].copy(name = newName) + } + }, + label = { Text("名称") }, + singleLine = true + ) + } + + Row(modifier = M.fillMaxWidth().padding(vertical = 5.dp)) { + TextField( + modifier = M.weight(4f).padding(horizontal = 4.dp), + value = pageUrl, + onValueChange = { newUrl -> + pageUrl = newUrl + list = list.toMutableList().also { l -> + l[index] = l[index].copy(url = newUrl) + } + }, + label = { Text("网址") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri) + ) + Button( + modifier = M.padding(horizontal = 4.dp), + onClick = { + list = list.filter { it.id != page.id } + } + ) { Text("删除") } + } + } + } + + if (isModified) { + Button(modifier = M.fillMaxWidth(), onClick = { + doInIoThread { + PlanTempDatabase.update({ it.id == plan.id }, update = { + it.pages = list.sortedBy { it.sort } + }) { + _plans.value = PlanTempDatabase.getAll() + _isDrawerOpen.value = false + } + } + }) { Text("保存") } + } + } + } + + _isDrawerOpen.value = true + } + } + + fun initConnectedDeviceData() { + _devicePagesList.value = null + _device.value = null + _plans.value = PlanTempDatabase.getAll() + + } +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/ui/compose/ConnectScreen.kt b/phone/src/main/java/com/bbitcn/phone/ui/compose/ConnectScreen.kt new file mode 100644 index 0000000..6801cc6 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/compose/ConnectScreen.kt @@ -0,0 +1,101 @@ +package com.bbitcn.phone.ui.compose + +import android.R.attr.port +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.Button +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.phone.ui.FunctionModel +import com.bbitcn.phone.ui.MyFuncButtonRow +import com.bbitcn.phone.ui.PhoneViewModel + +@Composable +fun ConnectScreen(viewModel: PhoneViewModel) { + var ip by remember { mutableStateOf("10.0.4.74") } + val device by viewModel.device.collectAsState() + val autoSearching by viewModel.autoSearching.collectAsState() + + FunctionModel("状态信息") { + Column { + Text( + "连接状态:" + if (device == null) "未连接" else "已连接到设备<${device?.serviceName}>", + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + Text( + "扫描状态:" + if (autoSearching) "正在自动搜索中" else "", + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + } + } + FunctionModel("自动连接") { + Column { + val devicesList by viewModel.devices.collectAsState() + LazyRow { + items(items = devicesList) { + Button(onClick = { + viewModel.connectToTv(it) + }) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + it.serviceName, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + Text( + it.host.hostAddress + ":" + it.port, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + } + } + val context = LocalContext.current + MyFuncButtonRow { + Button(enabled = !autoSearching, onClick = { + viewModel.startDiscovery(context) + }) { + Text("开始搜索") + } + Button(enabled = autoSearching, onClick = { + viewModel.stopDiscovery() + }) { + Text("停止搜索") + } + } + } + } + FunctionModel("手动连接") { + Column { + TextField( + value = ip, + onValueChange = { ip = it }, + label = { Text("设备IP地址(可通过点击电视菜单键查看)") }) + MyFuncButtonRow { + Button(onClick = { + viewModel.connectToTvByIp(ip) + }) { Text("连接") } + Button(onClick = { viewModel.disconnect() }) { + Text("断开") + } + } + } + } +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/ui/compose/ControlScreen.kt b/phone/src/main/java/com/bbitcn/phone/ui/compose/ControlScreen.kt new file mode 100644 index 0000000..03b26fb --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/compose/ControlScreen.kt @@ -0,0 +1,92 @@ +package com.bbitcn.phone.ui.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.bbitcn.phone.M +import com.bbitcn.phone.ui.FunctionModel +import com.bbitcn.phone.ui.MyFuncButtonRow +import com.bbitcn.phone.ui.PhoneViewModel + +@Composable +fun ControlScreen(viewModel: PhoneViewModel) { + FunctionModel("设备状态") { + val device by viewModel.device.collectAsState() + if (device == null) { + Text("连接已断开") + } else { + Column { + Text("设备名称:${device?.serviceName}") + Text("设备地址:${device?.host?.hostAddress}") + } + } + } + FunctionModel("基础控制") { + MyFuncButtonRow { + Button(onClick = { + viewModel.prev() + }) { Text("后退") } + Button(onClick = { + viewModel.next() + }) { Text("前进") } + Button(onClick = { + viewModel.reload() + }) { Text("刷新") } + Button(onClick = { + viewModel.stop() + }) { Text("停止") } + } + } + FunctionModel("多页控制") { + Column { + MyFuncButtonRow { + Button(onClick = { + viewModel.prevPage() + }) { Text("上一页") } + Button(onClick = { + viewModel.nextPage() + }) { Text("下一页") } + Button(onClick = { + viewModel.reloadAll() + }) { Text("刷新") } + Button(onClick = { + viewModel.public() + }) { Text("发布") } + } + } + } + val devicePagesList by viewModel.devicePagesList.collectAsState() + devicePagesList?.let { + FunctionModel("当前设备页面") { + LazyColumn { + items(items = it.pages) { + Button(modifier = M.fillMaxWidth(), onClick = { + viewModel.switchPage(it.id) + }) { + Text( + modifier = M + .fillMaxWidth(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = "${it.sort}.${it.name} - ${it.url}", + textAlign = TextAlign.Left + ) + } + } + } + } + } +} diff --git a/phone/src/main/java/com/bbitcn/phone/ui/compose/SetScreen.kt b/phone/src/main/java/com/bbitcn/phone/ui/compose/SetScreen.kt new file mode 100644 index 0000000..bc26d7f --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/compose/SetScreen.kt @@ -0,0 +1,45 @@ +package com.bbitcn.phone.ui.compose + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.input.pointer.pointerInput +import com.bbitcn.phone.M +import com.bbitcn.phone.ui.FunctionModel +import com.bbitcn.phone.ui.PhoneViewModel + +@Composable +fun SetScreen(viewModel: PhoneViewModel) { + FunctionModel("页面配置") { + val plans by viewModel.plans.collectAsState() + LazyColumn { + items(items = plans) { plan -> + Row(modifier = M.fillMaxWidth()) { + Button( + modifier = M.weight(1f), onClick = { + // 编辑计划 + viewModel.editPlan(plan) + }) { Text(modifier = M.fillMaxWidth(), text = plan.name) } + Button(onClick = { + viewModel.editPage(plan) + }) { Text("编辑细则") } + } + } + } + } +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/ui/theme/Color.kt b/phone/src/main/java/com/bbitcn/phone/ui/theme/Color.kt new file mode 100644 index 0000000..051e705 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.bbitcn.phone.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) \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/ui/theme/Theme.kt b/phone/src/main/java/com/bbitcn/phone/ui/theme/Theme.kt new file mode 100644 index 0000000..9b35784 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.bbitcn.phone.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun BBITShowTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/ui/theme/Type.kt b/phone/src/main/java/com/bbitcn/phone/ui/theme/Type.kt new file mode 100644 index 0000000..4275b32 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.bbitcn.phone.ui.theme + +import androidx.compose.material3.Typography +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 + +// Set of Material typography styles to start with +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 + ) + */ +) \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/utls/MMKVUtil.java b/phone/src/main/java/com/bbitcn/phone/utls/MMKVUtil.java new file mode 100644 index 0000000..8851557 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/utls/MMKVUtil.java @@ -0,0 +1,107 @@ +package com.bbitcn.phone.utls; + + +import android.content.Context; +import android.os.Parcelable; + +import com.tencent.mmkv.MMKV; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * MMKV工具类 + */ +public class MMKVUtil { + private static MMKV mmkvInstance; + + private MMKVUtil() { + } + + /** + * 初始化MMKV,只能在Application中初始化一次 + */ + public static void init(Context applicationContext) { + MMKV.initialize(applicationContext); + mmkvInstance = MMKV.defaultMMKV(); + } + + /** + * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法 + */ + public static void put(String key, T object) { + if (object instanceof String) { + mmkvInstance.encode(key, (String) object); + } else if (object instanceof Integer) { + mmkvInstance.encode(key, (Integer) object); + } else if (object instanceof Boolean) { + mmkvInstance.encode(key, (Boolean) object); + } else if (object instanceof Float) { + mmkvInstance.encode(key, (Float) object); + } else if (object instanceof Long) { + mmkvInstance.encode(key, (Long) object); + } else if (object instanceof Double) { + mmkvInstance.encode(key, (Double) object); + } else if (object instanceof Set) { + mmkvInstance.encode(key, (Set) object); + } else if (object instanceof Parcelable) { + mmkvInstance.encode(key, (Parcelable) object); + } else { + mmkvInstance.encode(key, object == null ? "" : object.toString()); + } + } + + /** + * 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值 + */ + public static T get(String key, T defaultObject) { + if (defaultObject instanceof String || defaultObject == null) { + return (T) mmkvInstance.decodeString(key, (String) defaultObject); + } else if (defaultObject instanceof Integer) { + return (T) Integer.valueOf(mmkvInstance.decodeInt(key, (Integer) defaultObject)); + } else if (defaultObject instanceof Boolean) { + return (T) Boolean.valueOf(mmkvInstance.decodeBool(key, (Boolean) defaultObject)); + } else if (defaultObject instanceof Float) { + return (T) Float.valueOf(mmkvInstance.decodeFloat(key, (Float) defaultObject)); + } else if (defaultObject instanceof Long) { + return (T) Long.valueOf(mmkvInstance.decodeLong(key, (Long) defaultObject)); + } else if (defaultObject instanceof Double) { + return (T) Double.valueOf(mmkvInstance.decodeDouble(key, (Double) defaultObject)); + } else if (defaultObject instanceof Parcelable) { + Parcelable p = (Parcelable) defaultObject; + return (T) mmkvInstance.decodeParcelable(key, p.getClass()); + } else { + return null; + } + } + + /** + * 获得字符串 + */ + public static String get(String key) { + return mmkvInstance.decodeString(key, ""); + } + + /** + * 移除某个key值已经对应的值 + */ + public static void remove(String key) { + mmkvInstance.remove(key); + } + + /** + * 查询某个key是否已经存在 + */ + public boolean contains(String key) { + return mmkvInstance.contains(key); + } + + /** + * 清除所有数据 + */ + public static void clear() { + mmkvInstance.clear(); + } + +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/utls/PlanTempDatabase.kt b/phone/src/main/java/com/bbitcn/phone/utls/PlanTempDatabase.kt new file mode 100644 index 0000000..90d7332 --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/utls/PlanTempDatabase.kt @@ -0,0 +1,19 @@ +package com.bbitcn.phone.utls + +import com.bbitcn.phone.base.ParameterizedTypeImpl +import com.bbitcn.phone.model.Plan +import com.bbitcn.sericulture.base.BaseListTempDataBase +import kotlin.jvm.java +import java.lang.reflect.Type + +object PlanTempDatabase : BaseListTempDataBase() { + + override fun getKey(): String { + return "MENU_PERMISSION_LIST2" + } + + override fun getType(): Type { + return ParameterizedTypeImpl(Plan::class.java) + } + +} \ No newline at end of file diff --git a/phone/src/main/java/com/bbitcn/phone/utls/PollingTask.kt b/phone/src/main/java/com/bbitcn/phone/utls/PollingTask.kt new file mode 100644 index 0000000..3a3dd4a --- /dev/null +++ b/phone/src/main/java/com/bbitcn/phone/utls/PollingTask.kt @@ -0,0 +1,160 @@ +package com.bbitcn.phone.utls + +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>? = 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 IO任务返回结果的类型 + */ + fun startPollingTask( + taskId: String, + intervalSeconds: Long, + ioTask: IRxIOTask, + uiTask: IRxUITask + ) { + 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 { + fun doInIOThread(): T + } + + // 定义UI任务接口 + interface IRxUITask { + fun doInUIThread(t: T) + } + + companion object { + private val instances: MutableMap = HashMap() + + @Synchronized + fun getInstance(): PollingTask = getInstance("MAIN") + + // 获取基于ID的单例实例 + @Synchronized + fun getInstance(id: String): PollingTask { + if (!instances.containsKey(id)) { + instances[id] = PollingTask() + } + return instances[id]!! + } + } +} \ No newline at end of file diff --git a/phone/src/main/res/mipmap-hdpi/logo.png b/phone/src/main/res/mipmap-hdpi/logo.png new file mode 100644 index 0000000..de4102a Binary files /dev/null and b/phone/src/main/res/mipmap-hdpi/logo.png differ diff --git a/phone/src/main/res/values/colors.xml b/phone/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/phone/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/phone/src/main/res/values/strings.xml b/phone/src/main/res/values/strings.xml new file mode 100644 index 0000000..d4e99e0 --- /dev/null +++ b/phone/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + BBIT展示屏控制工具 + \ No newline at end of file diff --git a/phone/src/main/res/values/themes.xml b/phone/src/main/res/values/themes.xml new file mode 100644 index 0000000..6db3f5f --- /dev/null +++ b/phone/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +