初始化项目
This commit is contained in:
+17
@@ -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/
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" }
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+6
@@ -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
|
||||
@@ -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" "$@"
|
||||
Vendored
+89
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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")
|
||||
|
||||
}
|
||||
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,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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/logo"
|
||||
android:label="@string/app_name"
|
||||
android:name=".MyApp"
|
||||
android:roundIcon="@mipmap/logo"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.BBITShow">
|
||||
<activity
|
||||
android:name=".ui.PhoneActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.BBITShow">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<T> {
|
||||
protected val gson: Gson = Gson()
|
||||
|
||||
abstract fun getKey(): String
|
||||
|
||||
abstract fun getType(): Type
|
||||
|
||||
fun getAll(): MutableList<T> {
|
||||
val json = MMKVUtil.get(getKey(), "[]")
|
||||
return Gson().fromJson(json, getType())
|
||||
}
|
||||
|
||||
suspend fun init(data: List<T>) {
|
||||
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<T> = 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<T> = 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<T> = 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
|
||||
}
|
||||
}
|
||||
@@ -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 <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,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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Type> {
|
||||
return arrayOf(clazz)
|
||||
}
|
||||
|
||||
override fun getRawType(): Type {
|
||||
return MutableList::class.java
|
||||
}
|
||||
|
||||
override fun getOwnerType(): Type? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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<Page>
|
||||
|
||||
)
|
||||
@@ -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<PhoneViewModel>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean> = _isDrawerOpen.asStateFlow()
|
||||
|
||||
/**
|
||||
* 扫描到的所有设备
|
||||
*/
|
||||
private val _devices = MutableStateFlow<List<NsdServiceInfo>>(emptyList())
|
||||
val devices = _devices.asStateFlow()
|
||||
|
||||
/**
|
||||
* 设备当前页面列表
|
||||
*/
|
||||
private val _devicePagesList = MutableStateFlow<Plan?>(null)
|
||||
val devicePagesList = _devicePagesList.asStateFlow()
|
||||
|
||||
/**
|
||||
* 自己的计划列表
|
||||
*/
|
||||
private val _plans = MutableStateFlow<List<Plan>>(emptyList())
|
||||
val plans = _plans.asStateFlow()
|
||||
|
||||
/**
|
||||
* 当前连接的设备信息
|
||||
*/
|
||||
private val _device = MutableStateFlow<NsdServiceInfo?>(null)
|
||||
val device = _device.asStateFlow()
|
||||
|
||||
/**
|
||||
* 自动刷新状态
|
||||
*/
|
||||
private val _autoSearching = MutableStateFlow<Boolean>(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<Plan>() {}.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()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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("断开")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("编辑细则") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -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 <T> 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<String>) object);
|
||||
} else if (object instanceof Parcelable) {
|
||||
mmkvInstance.encode(key, (Parcelable) object);
|
||||
} else {
|
||||
mmkvInstance.encode(key, object == null ? "" : object.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值
|
||||
*/
|
||||
public static <T> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Plan>() {
|
||||
|
||||
override fun getKey(): String {
|
||||
return "MENU_PERMISSION_LIST2"
|
||||
}
|
||||
|
||||
override fun getType(): Type {
|
||||
return ParameterizedTypeImpl(Plan::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<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]!!
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">BBIT展示屏控制工具</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.BBITShow" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bbitcn.phone
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://maven.mozilla.org/maven2/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "BBITShow"
|
||||
include(":tv")
|
||||
include(":phone")
|
||||
@@ -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