mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
feat: native widget url settings dialog (#856)
This commit is contained in:
@@ -46,6 +46,15 @@
|
|||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".widget.WidgetConfigureActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@android:style/Theme.Material.Light.Dialog">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.HomeWidget"
|
android:name=".widget.HomeWidget"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -13,13 +13,24 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import org.json.JSONException
|
||||||
import tech.lolli.toolbox.R
|
import tech.lolli.toolbox.R
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class HomeWidget : AppWidgetProvider() {
|
class HomeWidget : AppWidgetProvider() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "HomeWidget"
|
||||||
|
private const val NETWORK_TIMEOUT = 10_000L // 10 seconds
|
||||||
|
private const val COROUTINE_TIMEOUT = 15_000L // 15 seconds
|
||||||
|
private val activeUpdates = ConcurrentHashMap<Int, Boolean>()
|
||||||
|
}
|
||||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
for (appWidgetId in appWidgetIds) {
|
for (appWidgetId in appWidgetIds) {
|
||||||
updateAppWidget(context, appWidgetManager, appWidgetId)
|
updateAppWidget(context, appWidgetManager, appWidgetId)
|
||||||
@@ -27,105 +38,184 @@ class HomeWidget : AppWidgetProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
|
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
|
||||||
val views = RemoteViews(context.packageName, R.layout.home_widget)
|
// Prevent concurrent updates for the same widget
|
||||||
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
if (activeUpdates.putIfAbsent(appWidgetId, true) == true) {
|
||||||
var url = sp.getString("widget_$appWidgetId", null)
|
Log.d(TAG, "Widget $appWidgetId is already updating, skipping")
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
url = sp.getString("$appWidgetId", null)
|
|
||||||
}
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
val gUrl = sp.getString("widget_*", null)
|
|
||||||
url = gUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
Log.e("HomeWidget", "URL not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
val intentUpdate = Intent(context, HomeWidget::class.java)
|
|
||||||
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
|
||||||
val ids = intArrayOf(appWidgetId)
|
|
||||||
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
|
||||||
|
|
||||||
var flag = PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
|
|
||||||
flag = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
}
|
|
||||||
|
|
||||||
val pendingUpdate: PendingIntent = PendingIntent.getBroadcast(
|
|
||||||
context,
|
|
||||||
appWidgetId,
|
|
||||||
intentUpdate,
|
|
||||||
flag)
|
|
||||||
views.setOnClickPendingIntent(R.id.widget_container, pendingUpdate)
|
|
||||||
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
views.setTextViewText(R.id.widget_name, "No URL")
|
|
||||||
// Update the widget to display a message for missing URL
|
|
||||||
views.setViewVisibility(R.id.error_message, View.VISIBLE)
|
|
||||||
views.setTextViewText(R.id.error_message, "Please configure the widget URL.")
|
|
||||||
views.setViewVisibility(R.id.widget_content, View.GONE)
|
|
||||||
views.setFloat(R.id.widget_name, "setAlpha", 1f)
|
|
||||||
views.setFloat(R.id.error_message, "setAlpha", 1f)
|
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
views.setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
|
|
||||||
views.setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
|
|
||||||
views.setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
|
|
||||||
views.setViewVisibility(R.id.widget_net_label, View.VISIBLE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.home_widget)
|
||||||
|
val url = getWidgetUrl(context, appWidgetId)
|
||||||
|
|
||||||
|
if (url.isNullOrEmpty()) {
|
||||||
|
Log.w(TAG, "URL not found for widget $appWidgetId")
|
||||||
|
showErrorState(views, appWidgetManager, appWidgetId, "Please configure the widget URL.")
|
||||||
|
activeUpdates.remove(appWidgetId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setupClickIntent(context, views, appWidgetId)
|
||||||
|
|
||||||
|
showLoadingState(views, appWidgetManager, appWidgetId)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
withTimeoutOrNull(COROUTINE_TIMEOUT) {
|
||||||
val connection = URL(url).openConnection() as HttpURLConnection
|
try {
|
||||||
connection.requestMethod = "GET"
|
val serverData = fetchServerData(url)
|
||||||
val responseCode = connection.responseCode
|
if (serverData != null) {
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
withContext(Dispatchers.Main) {
|
||||||
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
|
showSuccessState(views, appWidgetManager, appWidgetId, serverData)
|
||||||
val jsonObject = JSONObject(jsonStr)
|
}
|
||||||
val data = jsonObject.getJSONObject("data")
|
} else {
|
||||||
val server = data.getString("name")
|
withContext(Dispatchers.Main) {
|
||||||
val cpu = data.getString("cpu")
|
showErrorState(views, appWidgetManager, appWidgetId, "Invalid server data received.")
|
||||||
val mem = data.getString("mem")
|
|
||||||
val disk = data.getString("disk")
|
|
||||||
val net = data.getString("net")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (mem.isEmpty() || disk.isEmpty()) {
|
|
||||||
Log.e("HomeWidget", "Failed to retrieve status: Memory or disk information is empty")
|
|
||||||
return@withContext
|
|
||||||
}
|
}
|
||||||
views.setTextViewText(R.id.widget_name, server)
|
|
||||||
views.setTextViewText(R.id.widget_cpu, cpu)
|
|
||||||
views.setTextViewText(R.id.widget_mem, mem)
|
|
||||||
views.setTextViewText(R.id.widget_disk, disk)
|
|
||||||
views.setTextViewText(R.id.widget_net, net)
|
|
||||||
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
|
|
||||||
views.setTextViewText(R.id.widget_time, timeStr)
|
|
||||||
views.setFloat(R.id.widget_name, "setAlpha", 1f)
|
|
||||||
views.setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
|
|
||||||
views.setFloat(R.id.widget_mem_label, "setAlpha", 1f)
|
|
||||||
views.setFloat(R.id.widget_disk_label, "setAlpha", 1f)
|
|
||||||
views.setFloat(R.id.widget_net_label, "setAlpha", 1f)
|
|
||||||
views.setFloat(R.id.widget_time, "setAlpha", 1f)
|
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
throw FileNotFoundException("HTTP response code: $responseCode")
|
Log.e(TAG, "Error updating widget $appWidgetId: ${e.message}", e)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val errorMessage = when (e) {
|
||||||
|
is SocketTimeoutException -> "Connection timeout. Please check your network."
|
||||||
|
is IOException -> "Network error. Please check your connection."
|
||||||
|
is JSONException -> "Invalid data format received from server."
|
||||||
|
else -> "Failed to retrieve data: ${e.message}"
|
||||||
|
}
|
||||||
|
showErrorState(views, appWidgetManager, appWidgetId, errorMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} ?: run {
|
||||||
Log.e("HomeWidget", "Error updating widget: ${e.localizedMessage}", e)
|
Log.w(TAG, "Widget update timed out for widget $appWidgetId")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
views.setTextViewText(R.id.widget_name, "Error")
|
showErrorState(views, appWidgetManager, appWidgetId, "Update timed out. Please try again.")
|
||||||
// Update the widget to display a message for data retrieval failure
|
|
||||||
views.setViewVisibility(R.id.error_message, View.VISIBLE)
|
|
||||||
views.setTextViewText(R.id.error_message, "Failed to retrieve data.")
|
|
||||||
views.setViewVisibility(R.id.widget_content, View.GONE)
|
|
||||||
views.setFloat(R.id.widget_name, "setAlpha", 1f)
|
|
||||||
views.setFloat(R.id.error_message, "setAlpha", 1f)
|
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
activeUpdates.remove(appWidgetId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getWidgetUrl(context: Context, appWidgetId: Int): String? {
|
||||||
|
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||||
|
return sp.getString("widget_$appWidgetId", null)
|
||||||
|
?: sp.getString("$appWidgetId", null)
|
||||||
|
?: sp.getString("widget_*", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupClickIntent(context: Context, views: RemoteViews, appWidgetId: Int) {
|
||||||
|
val intentConfigure = Intent(context, WidgetConfigureActivity::class.java).apply {
|
||||||
|
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingConfigure = PendingIntent.getActivity(context, appWidgetId, intentConfigure, flag)
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_container, pendingConfigure)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchServerData(url: String): ServerData? = withContext(Dispatchers.IO) {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
|
requestMethod = "GET"
|
||||||
|
connectTimeout = NETWORK_TIMEOUT.toInt()
|
||||||
|
readTimeout = NETWORK_TIMEOUT.toInt()
|
||||||
|
setRequestProperty("User-Agent", "ServerBox-Widget/1.0")
|
||||||
|
setRequestProperty("Accept", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
throw IOException("HTTP ${connection.responseCode}: ${connection.responseMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
|
||||||
|
parseServerData(jsonStr)
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseServerData(jsonStr: String): ServerData? {
|
||||||
|
return try {
|
||||||
|
val jsonObject = JSONObject(jsonStr)
|
||||||
|
val data = jsonObject.getJSONObject("data")
|
||||||
|
|
||||||
|
val server = data.optString("name", "Unknown Server")
|
||||||
|
val cpu = data.optString("cpu", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||||
|
val mem = data.optString("mem", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||||
|
val disk = data.optString("disk", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||||
|
val net = data.optString("net", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||||
|
|
||||||
|
// Return data even if some fields are missing, providing defaults
|
||||||
|
// Only reject if we can't parse the JSON structure properly
|
||||||
|
ServerData(server, cpu, mem, disk, net)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.e(TAG, "JSON parsing error: ${e.message}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoadingState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
|
||||||
|
views.apply {
|
||||||
|
setTextViewText(R.id.widget_name, "Loading...")
|
||||||
|
setViewVisibility(R.id.error_message, View.GONE)
|
||||||
|
setViewVisibility(R.id.widget_content, View.VISIBLE)
|
||||||
|
setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
|
||||||
|
setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
|
||||||
|
setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
|
||||||
|
setViewVisibility(R.id.widget_net_label, View.VISIBLE)
|
||||||
|
setViewVisibility(R.id.widget_progress, View.VISIBLE)
|
||||||
|
setFloat(R.id.widget_name, "setAlpha", 0.7f)
|
||||||
|
}
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSuccessState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, data: ServerData) {
|
||||||
|
views.apply {
|
||||||
|
setTextViewText(R.id.widget_name, data.name)
|
||||||
|
setTextViewText(R.id.widget_cpu, data.cpu)
|
||||||
|
setTextViewText(R.id.widget_mem, data.mem)
|
||||||
|
setTextViewText(R.id.widget_disk, data.disk)
|
||||||
|
setTextViewText(R.id.widget_net, data.net)
|
||||||
|
|
||||||
|
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
|
||||||
|
setTextViewText(R.id.widget_time, timeStr)
|
||||||
|
|
||||||
|
setViewVisibility(R.id.error_message, View.GONE)
|
||||||
|
setViewVisibility(R.id.widget_content, View.VISIBLE)
|
||||||
|
setViewVisibility(R.id.widget_progress, View.GONE)
|
||||||
|
|
||||||
|
// Smooth fade-in animation
|
||||||
|
setFloat(R.id.widget_name, "setAlpha", 1f)
|
||||||
|
setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
|
||||||
|
setFloat(R.id.widget_mem_label, "setAlpha", 1f)
|
||||||
|
setFloat(R.id.widget_disk_label, "setAlpha", 1f)
|
||||||
|
setFloat(R.id.widget_net_label, "setAlpha", 1f)
|
||||||
|
setFloat(R.id.widget_time, "setAlpha", 1f)
|
||||||
|
}
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showErrorState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, errorMessage: String) {
|
||||||
|
views.apply {
|
||||||
|
setTextViewText(R.id.widget_name, "Error")
|
||||||
|
setViewVisibility(R.id.error_message, View.VISIBLE)
|
||||||
|
setTextViewText(R.id.error_message, errorMessage)
|
||||||
|
setViewVisibility(R.id.widget_content, View.GONE)
|
||||||
|
setViewVisibility(R.id.widget_progress, View.GONE)
|
||||||
|
setFloat(R.id.widget_name, "setAlpha", 1f)
|
||||||
|
setFloat(R.id.error_message, "setAlpha", 1f)
|
||||||
|
}
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ServerData(
|
||||||
|
val name: String,
|
||||||
|
val cpu: String,
|
||||||
|
val mem: String,
|
||||||
|
val disk: String,
|
||||||
|
val net: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package tech.lolli.toolbox.widget
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Patterns
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import tech.lolli.toolbox.R
|
||||||
|
|
||||||
|
class WidgetConfigureActivity : Activity() {
|
||||||
|
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
private lateinit var urlEditText: EditText
|
||||||
|
private lateinit var saveButton: Button
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.widget_configure)
|
||||||
|
|
||||||
|
// 设置结果为取消,以防用户在完成配置前退出
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
|
||||||
|
// 获取 widget ID
|
||||||
|
val extras = intent.extras
|
||||||
|
if (extras != null) {
|
||||||
|
appWidgetId = extras.getInt(
|
||||||
|
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||||
|
AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有有效的 widget ID,完成 activity
|
||||||
|
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 UI 元素
|
||||||
|
urlEditText = findViewById(R.id.url_edit_text)
|
||||||
|
saveButton = findViewById(R.id.save_button)
|
||||||
|
|
||||||
|
// 从 SharedPreferences 加载现有配置
|
||||||
|
val sp = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
|
||||||
|
val existingUrl = sp.getString("widget_$appWidgetId", "")
|
||||||
|
urlEditText.setText(existingUrl)
|
||||||
|
|
||||||
|
// 设置保存按钮点击事件
|
||||||
|
saveButton.setOnClickListener {
|
||||||
|
val url = urlEditText.text.toString().trim()
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
urlEditText.error = "Please enter a URL"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 URL 格式
|
||||||
|
if (!Patterns.WEB_URL.matcher(url).matches()) {
|
||||||
|
urlEditText.error = "Please enter a valid URL"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存 URL 到 SharedPreferences
|
||||||
|
val editor = sp.edit()
|
||||||
|
editor.putString("widget_$appWidgetId", url)
|
||||||
|
editor.apply()
|
||||||
|
|
||||||
|
// 更新 widget 使用 AppWidgetManager
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(this)
|
||||||
|
val updateIntent = Intent(this, HomeWidget::class.java).apply {
|
||||||
|
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
|
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
|
||||||
|
}
|
||||||
|
sendBroadcast(updateIntent)
|
||||||
|
|
||||||
|
// 设置结果并结束 activity
|
||||||
|
val resultValue = Intent()
|
||||||
|
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||||
|
setResult(RESULT_OK, resultValue)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,14 +10,17 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widget_name"
|
android:id="@+id/widget_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/widgetText"
|
android:textColor="@color/widgetText"
|
||||||
android:textSize="23sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
android:alpha="0"
|
android:alpha="0"
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
|
android:fadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
tools:text="Server Name" />
|
tools:text="Server Name" />
|
||||||
|
|
||||||
<!-- Wrap the content in a LinearLayout for easy visibility management -->
|
<!-- Wrap the content in a LinearLayout for easy visibility management -->
|
||||||
@@ -27,121 +30,138 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_below="@id/widget_name"
|
android:layout_below="@id/widget_name"
|
||||||
android:paddingTop="13dp">
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/widget_container_inner"
|
android:id="@+id/widget_container_inner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:paddingTop="13dp"
|
|
||||||
android:animateLayoutChanges="true">
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/widget_cpu_label"
|
android:id="@+id/widget_cpu_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="2.7dp"
|
android:layout_marginBottom="4dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:alpha="0"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="17dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="17dp"
|
android:layout_height="16dp"
|
||||||
android:src="@drawable/speed_24">
|
android:src="@drawable/speed_24"
|
||||||
</ImageView>
|
android:layout_gravity="center_vertical"
|
||||||
|
android:contentDescription="CPU usage" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widget_cpu"
|
android:id="@+id/widget_cpu"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="11dp"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:ellipsize = "marquee"
|
android:ellipsize="end"
|
||||||
android:textColor="@color/widgetSummaryText"
|
android:textColor="@color/widgetSummaryText"
|
||||||
android:textSize="12.7sp"
|
android:textSize="12sp"
|
||||||
tools:text="CPU" />
|
tools:text="CPU: 25.6%" />
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/widget_mem_label"
|
android:id="@+id/widget_mem_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="2.7dp"
|
android:layout_marginBottom="4dp"
|
||||||
android:layout_below="@id/widget_cpu_label"
|
android:layout_below="@id/widget_cpu_label"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:alpha="0"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="17dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="17dp"
|
android:layout_height="16dp"
|
||||||
android:src="@drawable/memory_24">
|
android:src="@drawable/memory_24"
|
||||||
</ImageView>
|
android:layout_gravity="center_vertical"
|
||||||
|
android:contentDescription="Memory usage" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widget_mem"
|
android:id="@+id/widget_mem"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="11dp"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
android:textColor="@color/widgetSummaryText"
|
android:textColor="@color/widgetSummaryText"
|
||||||
android:textSize="12.7sp"
|
android:textSize="12sp"
|
||||||
tools:text="Mem" />
|
tools:text="Memory: 4.2GB / 8GB" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/widget_disk_label"
|
android:id="@+id/widget_disk_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="2.7dp"
|
android:layout_marginBottom="4dp"
|
||||||
android:layout_below="@id/widget_mem_label"
|
android:layout_below="@id/widget_mem_label"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:alpha="0"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="17dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="17dp"
|
android:layout_height="16dp"
|
||||||
android:src="@drawable/storage_24">
|
android:src="@drawable/storage_24"
|
||||||
</ImageView>
|
android:layout_gravity="center_vertical"
|
||||||
|
android:contentDescription="Disk usage" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widget_disk"
|
android:id="@+id/widget_disk"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="11dp"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
android:textColor="@color/widgetSummaryText"
|
android:textColor="@color/widgetSummaryText"
|
||||||
android:textSize="12.7sp"
|
android:textSize="12sp"
|
||||||
tools:text="Disk" />
|
tools:text="Disk: 125GB / 250GB" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/widget_net_label"
|
android:id="@+id/widget_net_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/widget_disk_label"
|
android:layout_below="@id/widget_disk_label"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:alpha="0"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="17dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="17dp"
|
android:layout_height="16dp"
|
||||||
android:src="@drawable/net_24">
|
android:src="@drawable/net_24"
|
||||||
</ImageView>
|
android:layout_gravity="center_vertical"
|
||||||
|
android:contentDescription="Network usage" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widget_net"
|
android:id="@+id/widget_net"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="11dp"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
android:textColor="@color/widgetSummaryText"
|
android:textColor="@color/widgetSummaryText"
|
||||||
android:textSize="12.7sp"
|
android:textSize="12sp"
|
||||||
tools:text="Net" />
|
tools:text="Network: 15MB/s ↓ 8MB/s ↑" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -149,29 +169,45 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Add a TextView for error messages -->
|
<!-- Error message display -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/error_message"
|
android:id="@+id/error_message"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/widget_name"
|
android:layout_below="@id/widget_name"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
android:textColor="@color/widgetSummaryText"
|
android:textColor="@color/widgetSummaryText"
|
||||||
android:textSize="12sp"
|
android:textSize="11sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:alpha="0"
|
android:alpha="0"
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
tools:text="Error message" />
|
android:lineSpacingMultiplier="1.2"
|
||||||
|
android:maxLines="3"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="Error message text that might be longer than usual" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widget_time"
|
android:id="@+id/widget_time"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:maxLines="2"
|
android:layout_alignParentEnd="true"
|
||||||
|
android:maxLines="1"
|
||||||
android:textColor="@color/widgetSummaryText"
|
android:textColor="@color/widgetSummaryText"
|
||||||
android:textSize="11sp"
|
android:textSize="10sp"
|
||||||
android:alpha="0"
|
android:alpha="0"
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
tools:text="UpdateTime" />
|
android:fontFamily="monospace"
|
||||||
|
tools:text="12:34" />
|
||||||
|
|
||||||
|
<!-- Progress indicator for loading state -->
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/widget_progress"
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:indeterminate="true" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
38
android/app/src/main/res/layout/widget_configure.xml
Normal file
38
android/app/src/main/res/layout/widget_configure.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Widget URL"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textColor="@android:color/black" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/url_edit_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="https://server/status"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="@android:drawable/edit_text"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:textColorHint="@android:color/darker_gray" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/save_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Save"
|
||||||
|
android:background="#8b2252"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:padding="12dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
android:minHeight="110dp"
|
android:minHeight="110dp"
|
||||||
android:updatePeriodMillis="1800001"
|
android:updatePeriodMillis="1800001"
|
||||||
android:initialLayout="@layout/home_widget"
|
android:initialLayout="@layout/home_widget"
|
||||||
|
android:configure="tech.lolli.toolbox.widget.WidgetConfigureActivity"
|
||||||
android:resizeMode="none"
|
android:resizeMode="none"
|
||||||
android:widgetCategory="home_screen">
|
android:widgetCategory="home_screen">
|
||||||
</appwidget-provider>
|
</appwidget-provider>
|
||||||
Reference in New Issue
Block a user