diff --git a/android/app/build.gradle b/android/app/build.gradle index b3337ec..e392095 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -3,6 +3,7 @@ plugins { id "kotlin-android" id "kotlin-parcelize" id "kotlinx-serialization" + id "com.google.devtools.ksp" id "dev.flutter.flutter-gradle-plugin" } @@ -47,6 +48,12 @@ android { versionCode gitCommits versionName version + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + arg("room.incremental", "true") + arg("room.expandProjection", "true") + } + buildConfigField("String", "ALIST_VERSION", "\"${alistVersion}\"") } @@ -135,5 +142,11 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + // Room +// implementation("androidx.room:room-runtime:$room_version") +// implementation("androidx.room:room-ktx:$room_version") +// ksp("androidx.room:room-compiler:$room_version") +// androidTestImplementation("androidx.room:room-testing:$room_version") + } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 28e38fb..cedbe6e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,45 @@ + + + + + + + + + + + + + + + + + + android:label="AList" + android:networkSecurityConfig="@xml/network_security_config" + android:requestLegacyExternalStorage="true" + android:roundIcon="@mipmap/ic_launcher" + android:supportsRtl="true" + + + android:usesCleartextTraffic="true" + tools:ignore="UnusedAttribute"> + channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.alist_flutter.Android.addShortcut", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.addShortcut(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -289,6 +315,30 @@ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable Android ap api.startService(); wrapped.add(0, null); } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.alist_flutter.Android.setAdminPwd", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String pwdArg = (String) args.get(0); + try { + api.setAdminPwd(pwdArg); + wrapped.add(0, null); + } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; @@ -470,5 +520,25 @@ public void onServiceStatusChanged(@NonNull Boolean isRunningArg, @NonNull VoidR } }); } + public void onServerLog(@NonNull Long levelArg, @NonNull String timeArg, @NonNull String logArg, @NonNull VoidResult result) { + final String channelName = "dev.flutter.pigeon.alist_flutter.Event.onServerLog"; + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList(Arrays.asList(levelArg, timeArg, logArg)), + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error(new FlutterError((String) listReply.get(0), (String) listReply.get(1), (String) listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } } } diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt index 36d0217..52d2062 100644 --- a/android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt @@ -10,25 +10,25 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.github.jing332.alistflutter.bridge.AndroidBridge import com.github.jing332.alistflutter.bridge.AppConfigBridge import com.github.jing332.alistflutter.model.ShortCuts +import com.github.jing332.alistflutter.model.alist.Logger import com.github.jing332.pigeon.GeneratedApi import com.github.jing332.pigeon.GeneratedApi.VoidResult import io.flutter.embedding.android.FlutterActivity -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch class MainActivity : FlutterActivity() { companion object { - private val TAG = "MainActivity" - const val BRIDGE_CHANNEL = "alistflutter/bridge" - const val CONFIG_CHANNEL = "alistflutter/config" - - const val EVENT_CHANNEL = "alistflutter/event" + private const val TAG = "MainActivity" } private val receiver by lazy { MyReceiver() } private var mEvent: GeneratedApi.Event? = null + @OptIn(DelicateCoroutinesApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -43,6 +43,22 @@ class MainActivity : FlutterActivity() { GeneratedApi.Android.setUp(binaryMessage, AndroidBridge(this)) mEvent = GeneratedApi.Event(binaryMessage) + Logger.addListener(object : Logger.Listener { + override fun onLog(level: Int, time: String, msg: String) { + GlobalScope.launch(Dispatchers.Main) { + mEvent?.onServerLog(level.toLong(), time, msg, object : VoidResult { + override fun success() { + + } + + override fun error(error: Throwable) { + } + + }) + } + } + + }) } override fun onDestroy() { diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt index 7f3813f..ecc0b11 100644 --- a/android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt @@ -10,12 +10,10 @@ class SwitchServerActivity : Activity() { super.onCreate(savedInstanceState) if (AListService.isRunning) { - toast(R.string.alist_shut_downing) startService(Intent(this, AListService::class.java).apply { action = AListService.ACTION_SHUTDOWN }) } else { - toast(R.string.alist_starting) startService(Intent(this, AListService::class.java)) } diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/bridge/AndroidBridge.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/bridge/AndroidBridge.kt index bf8866f..4119d05 100644 --- a/android/app/src/main/kotlin/com/github/jing332/alistflutter/bridge/AndroidBridge.kt +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/bridge/AndroidBridge.kt @@ -4,15 +4,33 @@ import android.content.Context import android.content.Intent import com.github.jing332.alistflutter.AListService import com.github.jing332.alistflutter.BuildConfig +import com.github.jing332.alistflutter.R +import com.github.jing332.alistflutter.SwitchServerActivity +import com.github.jing332.alistflutter.model.alist.AList +import com.github.jing332.alistflutter.utils.MyTools import com.github.jing332.alistflutter.utils.ToastUtils.longToast import com.github.jing332.alistflutter.utils.ToastUtils.toast import com.github.jing332.pigeon.GeneratedApi class AndroidBridge(private val context: Context) : GeneratedApi.Android { + override fun addShortcut() { + MyTools.addShortcut( + context, + context.getString(R.string.app_switch), + "alist_flutter_switch", + R.drawable.ic_launcher_foreground, + Intent(context, SwitchServerActivity::class.java) + ) + } + override fun startService() { context.startService(Intent(context, AListService::class.java)) } + override fun setAdminPwd(pwd: String) { + AList.setAdminPassword(pwd) + } + override fun isRunning() = AListService.isRunning override fun getAListVersion() = BuildConfig.ALIST_VERSION override fun getVersionName() = BuildConfig.VERSION_NAME diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/LogLevel.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/LogLevel.kt new file mode 100644 index 0000000..0f60253 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/LogLevel.kt @@ -0,0 +1,38 @@ +package com.github.jing332.alistflutter.constant + +import androidx.annotation.IntDef + +@IntDef( + LogLevel.PANIC, + LogLevel.FATAL, + LogLevel.ERROR, + LogLevel.WARN, + LogLevel.INFO, + LogLevel.DEBUG, + LogLevel.TRACE +) +annotation class LogLevel { + companion object { + const val PANIC = 0 + const val FATAL = 1 + const val ERROR = 2 + const val WARN = 3 + const val INFO = 4 + const val DEBUG = 5 + const val TRACE = 6 + + fun Int.toLevelString(): String { + return when (this) { + PANIC -> "PANIC" + FATAL -> "FATAL" + ERROR -> "ERROR" + WARN -> "WARN" + INFO -> "INFO" + DEBUG -> "DEBUG" + TRACE -> "TRACE" + else -> "UNKNOWN" + } + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/data/AppDatabase.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/data/AppDatabase.kt new file mode 100644 index 0000000..8def7c9 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/data/AppDatabase.kt @@ -0,0 +1,33 @@ +/* +package com.github.jing332.alistflutter.data + +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.github.jing332.alistandroid.data.dao.ServerLogDao +import com.github.jing332.alistflutter.data.entities.ServerLog +import com.github.jing332.alistflutter.App.Companion.app + +val appDb by lazy { AppDatabase.create() } + +@Database( + version = 2, + entities = [ServerLog::class], + autoMigrations = [ + AutoMigration(from = 1, to = 2) + ] +) +abstract class AppDatabase : RoomDatabase() { + abstract val serverLogDao: ServerLogDao + + companion object { + fun create() = Room.databaseBuilder( + app, + AppDatabase::class.java, + "alistandroid.db" + ) + .allowMainThreadQueries() + .build() + } +}*/ diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/data/entities/ServerLog.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/data/entities/ServerLog.kt new file mode 100644 index 0000000..62f0565 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/data/entities/ServerLog.kt @@ -0,0 +1,31 @@ +package com.github.jing332.alistflutter.data.entities + +import com.github.jing332.alistflutter.constant.LogLevel + +data class ServerLog( + + @LogLevel val level: Int, + val message: String, + val time: String, +) { + companion object { + + @Suppress("RegExpRedundantEscape") + fun String.evalLog(): ServerLog? { + val logPattern = """(\w+)\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (.+)""".toRegex() + val result = logPattern.find(this) + if (result != null) { + val (level, time, msg) = result.destructured + val l = when (level[0].toString()) { + "D" -> LogLevel.DEBUG + "I" -> LogLevel.INFO + "W" -> LogLevel.WARN + "E" -> LogLevel.ERROR + else -> LogLevel.INFO + } + return ServerLog(level = l, message = msg, time = time) + } + return null + } + } +} diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt index 38391c4..735c4c4 100644 --- a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt @@ -4,7 +4,10 @@ import android.annotation.SuppressLint import android.util.Log import com.github.jing332.alistflutter.R import com.github.jing332.alistflutter.app +import com.github.jing332.alistflutter.constant.LogLevel +import com.github.jing332.alistflutter.data.entities.ServerLog.Companion.evalLog import com.github.jing332.alistflutter.utils.FileUtils.readAllText +import com.github.jing332.alistflutter.utils.StringUtils.removeAnsiCodes import com.github.jing332.alistflutter.utils.ToastUtils.longToast import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -13,6 +16,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.io.File import java.io.IOException +import java.io.InputStream import kotlin.coroutines.coroutineContext object AList { @@ -50,73 +54,37 @@ object AList { private var mProcess: Process? = null - private suspend fun errorLogWatcher(onNewLine: (String) -> Unit) { - mProcess?.apply { - errorStream.bufferedReader().use { - while (coroutineContext.isActive) { - val line = it.readLine() ?: break - Log.d(TAG, "Process errorStream: $line") + private suspend fun InputStream.logWatcher(onNewLine: (String) -> Unit) { + bufferedReader().use { + while (coroutineContext.isActive) { + runCatching { + val line = it.readLine() ?: return@use onNewLine(line) + }.onFailure { + Log.e(TAG, "logWatcher: ", it) + return@use } } - } - } - private suspend fun logWatcher(onNewLine: (String) -> Unit) { - mProcess?.apply { - inputStream.bufferedReader().use { - while (coroutineContext.isActive) { - val line = it.readLine() ?: break - Log.d(TAG, "Process inputStream: $line") - onNewLine(line) - } - } } } + private val mScope = CoroutineScope(Dispatchers.IO + Job()) private fun initOutput() { -// val dao = appDb.serverLogDao - mScope.launch { - runCatching { - logWatcher { msg -> -// msg.removeAnsiCodes().evalLog()?.let { -// dao.insert( -// ServerLog( -// level = it.level, -// message = it.message -// ) -// ) -// return@logWatcher -// } - -// dao.insert( -// ServerLog( -// level = if (msg.startsWith("fail")) LogLevel.ERROR else LogLevel.INFO, -// message = msg -// ) -// ) - - } - }.onFailure { - it.printStackTrace() + val onNewLine = { msg: String -> + msg.removeAnsiCodes().evalLog()?.let { + Logger.log(level = it.level, time = it.time, msg = it.message) + } ?: run { + Logger.log(level = LogLevel.INFO, time = "", msg = msg) } } + mScope.launch { - runCatching { - errorLogWatcher { msg -> -// val log = msg.removeAnsiCodes().evalLog() ?: return@errorLogWatcher -// dao.insert( -// ServerLog( -// level = log.level, -// message = log.message, -//// description = log.time + "\n" + log.code -// ) -// ) - } - }.onFailure { - it.printStackTrace() - } + mProcess?.inputStream?.logWatcher(onNewLine) + } + mScope.launch { + mProcess?.errorStream?.logWatcher(onNewLine) } } diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/Logger.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/Logger.kt new file mode 100644 index 0000000..279922f --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/Logger.kt @@ -0,0 +1,23 @@ +package com.github.jing332.alistflutter.model.alist + +object Logger { + private var listeners = mutableListOf() + + fun addListener(listener: Listener) { + listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + interface Listener { + fun onLog(level: Int, time: String, msg: String) + } + + fun log(level: Int, time: String, msg: String) { + listeners.forEach { + it.onLog(level, time, msg) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/MyTools.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/MyTools.kt new file mode 100644 index 0000000..c129831 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/MyTools.kt @@ -0,0 +1,92 @@ +package com.github.jing332.alistflutter.utils + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Build +import android.provider.Settings +import com.github.jing332.alistflutter.utils.ToastUtils.longToast +import splitties.systemservices.powerManager + +object MyTools { + fun Context.isIgnoringBatteryOptimizations(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + powerManager.isIgnoringBatteryOptimizations(packageName) + } + + @SuppressLint("BatteryLife") + fun Context.killBattery() { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isIgnoringBatteryOptimizations()) { + startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:$packageName") + }) + } + } + } + + /* 添加快捷方式 */ + @SuppressLint("UnspecifiedImmutableFlag") + @Suppress("DEPRECATION") + fun addShortcut( + ctx: Context, + name: String, + id: String, + iconResId: Int, + launcherIntent: Intent + ) { + if (Build.VERSION.SDK_INT < 26) { /* Android8.0 */ + ctx.longToast("如失败 请手动授予权限") + + val addShortcutIntent = Intent("com.android.launcher.action.INSTALL_SHORTCUT") + // 不允许重复创建 + addShortcutIntent.putExtra("duplicate", false) // 经测试不是根据快捷方式的名字判断重复的 + addShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name) + addShortcutIntent.putExtra( + Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext( + ctx, iconResId + ) + ) + + launcherIntent.action = Intent.ACTION_MAIN + launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER) + addShortcutIntent + .putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent) + + // 发送广播 + ctx.sendBroadcast(addShortcutIntent) + } else { + val shortcutManager: ShortcutManager = ctx.getSystemService(ShortcutManager::class.java) + if (shortcutManager.isRequestPinShortcutSupported) { + launcherIntent.action = Intent.ACTION_VIEW + val pinShortcutInfo = ShortcutInfo.Builder(ctx, id) + .setIcon( + Icon.createWithResource(ctx, iconResId) + ) + .setIntent(launcherIntent) + .setShortLabel(name) + .build() + val pinnedShortcutCallbackIntent = shortcutManager + .createShortcutResultIntent(pinShortcutInfo) + //Get notified when a shortcut is pinned successfully// + val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_IMMUTABLE + } else { + 0 + } + val successCallback = PendingIntent.getBroadcast( + ctx, 0, pinnedShortcutCallbackIntent, pendingIntentFlags + ) + shortcutManager.requestPinShortcut( + pinShortcutInfo, successCallback.intentSender + ) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt index 6e4836a..693ea61 100644 --- a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt @@ -22,9 +22,9 @@ object StringUtils { return this.replace(Regex("[^0-9]"), "").toIntOrNull() ?: 0 } + private val mAnsiRegex = Regex("""\x1b(\[.*?[@-~]|].*?(\x07|\x1b\\))""") fun String.removeAnsiCodes(): String { - val ansiRegex = Regex("\\x1B\\[[0-9;]*[m|K]") - return this.replace(ansiRegex, "") + return mAnsiRegex.replace(this, "") } fun String.parseToMap(): Map { diff --git a/lib/contant/log_level.dart b/lib/contant/log_level.dart new file mode 100644 index 0000000..dbbc02b --- /dev/null +++ b/lib/contant/log_level.dart @@ -0,0 +1,39 @@ +import 'dart:ui'; + +class LogLevel { + static const int panic = 0; + static const int fatal = 1; + static const int error = 2; + + static const int warn = 3; + static const int info = 4; + static const int debug = 5; + static const int trace = 6; + + static Color toColor(int level) { + //Color.fromARGB(a, r, g, b) + return switch(level) { + LogLevel.panic => const Color.fromARGB(255, 255, 0, 0), + LogLevel.fatal => const Color.fromARGB(255, 255, 0, 0), + LogLevel.error => const Color.fromARGB(255, 255, 0, 0), + LogLevel.warn => const Color.fromARGB(255, 255, 165, 0), + LogLevel.info => const Color.fromARGB(255, 0, 0, 255), + LogLevel.debug => const Color.fromARGB(255, 0, 255, 0), + LogLevel.trace => const Color.fromARGB(255, 0, 255, 0), + _ => const Color.fromARGB(255, 0, 0, 0) + }; + } + + static String toStr(int level) { + return switch(level) { + LogLevel.panic => "Panic", + LogLevel.fatal => "Fatal", + LogLevel.error => "Error", + LogLevel.warn => "Warn", + LogLevel.info => "Info", + LogLevel.debug => "Debug", + LogLevel.trace => "Trace", + _ => "" + }; + } +} diff --git a/lib/generated_api.dart b/lib/generated_api.dart index 5e269a3..f37793c 100644 --- a/lib/generated_api.dart +++ b/lib/generated_api.dart @@ -193,6 +193,28 @@ class Android { static const MessageCodec pigeonChannelCodec = StandardMessageCodec(); + Future addShortcut() async { + const String __pigeon_channelName = 'dev.flutter.pigeon.alist_flutter.Android.addShortcut'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else { + return; + } + } + Future startService() async { const String __pigeon_channelName = 'dev.flutter.pigeon.alist_flutter.Android.startService'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( @@ -215,6 +237,28 @@ class Android { } } + Future setAdminPwd(String pwd) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.alist_flutter.Android.setAdminPwd'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([pwd]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else { + return; + } + } + Future isRunning() async { const String __pigeon_channelName = 'dev.flutter.pigeon.alist_flutter.Android.isRunning'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( @@ -373,6 +417,8 @@ abstract class Event { void onServiceStatusChanged(bool isRunning); + void onServerLog(int level, String time, String log); + static void setup(Event? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel __pigeon_channel = BasicMessageChannel( @@ -399,5 +445,36 @@ abstract class Event { }); } } + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.alist_flutter.Event.onServerLog', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + __pigeon_channel.setMessageHandler(null); + } else { + __pigeon_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.alist_flutter.Event.onServerLog was null.'); + final List args = (message as List?)!; + final int? arg_level = (args[0] as int?); + assert(arg_level != null, + 'Argument for dev.flutter.pigeon.alist_flutter.Event.onServerLog was null, expected non-null int.'); + final String? arg_time = (args[1] as String?); + assert(arg_time != null, + 'Argument for dev.flutter.pigeon.alist_flutter.Event.onServerLog was null, expected non-null String.'); + final String? arg_log = (args[2] as String?); + assert(arg_log != null, + 'Argument for dev.flutter.pigeon.alist_flutter.Event.onServerLog was null, expected non-null String.'); + try { + api.onServerLog(arg_level!, arg_time!, arg_log!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } diff --git a/lib/main.dart b/lib/main.dart index 4562ce1..007d40e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:alist_flutter/pages/settings.dart'; import 'package:alist_flutter/pages/web.dart'; import 'package:alist_flutter/router.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; void main() { runApp(const MyApp()); @@ -14,7 +15,7 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( + return GetMaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. @@ -34,6 +35,9 @@ class MyApp extends StatelessWidget { // tested with just a hot reload. colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, + inputDecorationTheme: const InputDecorationTheme( + border: OutlineInputBorder(), + ), ), home: const MyHomePage(title: 'AList'), ); @@ -59,7 +63,6 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - String _text = ""; int _selectedIndex = 0; final PageController _pageController = PageController(); diff --git a/lib/pages/alist.dart b/lib/pages/alist.dart index f95b0ed..4e9264c 100644 --- a/lib/pages/alist.dart +++ b/lib/pages/alist.dart @@ -1,62 +1,105 @@ import 'package:alist_flutter/generated_api.dart'; +import 'package:alist_flutter/pages/pwd_edit_dialog.dart'; import 'package:alist_flutter/widgets/switch_floating_action_button.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'alist/log_list_view.dart'; + class AListScreen extends StatelessWidget { const AListScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final ui = Get.put(AListController()); - return Obx(() => - Scaffold( - appBar: AppBar( - backgroundColor: Theme - .of(context) - .colorScheme - .inversePrimary, - title: Text("AList - ${ui.alistVersion.value}"), + + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Obx(() => Text("AList - ${ui.alistVersion.value}")), actions: [ IconButton( - onPressed: () {}, + onPressed: () { + Android().addShortcut(); + }, icon: const Icon(Icons.add_home), ), IconButton( - onPressed: () {}, + onPressed: () { + showDialog( + context: context, + builder: (context) => PwdEditDialog(onConfirm: (pwd) { + Android().setAdminPwd(pwd); + })); + }, icon: const Icon(Icons.password), ), - ], - ), - floatingActionButton: SwitchFloatingButton( - isSwitch: ui.isSwitch.value, - onSwitchChange: (s) { - ui.isSwitch.value = s; - Android().startService(); - }, - ), - )); + PopupMenuButton( + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: 1, + child: Text("检查更新"), + ), + const PopupMenuItem( + value: 2, + child: Text("关于"), + ), + ]; + }, + icon: const Icon(Icons.more_vert), + ) + ]), + floatingActionButton: Obx( + () => SwitchFloatingButton( + isSwitch: ui.isSwitch.value, + onSwitchChange: (s) { + ui.clearLog(); + ui.isSwitch.value = s; + Android().startService(); + }), + ), + body: Obx(() => LogListView(logs: ui.logs.value))); } } class MyEventReceiver extends Event { - RxBool isRunning; + Function(Log log) logCb; + Function(bool isRunning) statusCb; + + MyEventReceiver(this.statusCb, this.logCb); - MyEventReceiver(this.isRunning); + @override + void onServiceStatusChanged(bool isRunning) { + statusCb(isRunning); + } @override - void onServiceStatusChanged(bool b) { - isRunning.value = b; + void onServerLog(int level, String time, String log) { + logCb(Log(level, time, log)); } } class AListController extends GetxController { + final ScrollController _scrollController = ScrollController(); var isSwitch = false.obs; var alistVersion = "".obs; + var logs = [].obs; + + void clearLog() { + logs.clear(); + } + + void addLog(Log log) { + logs.add(log); + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + @override void onInit() { - Event.setup(MyEventReceiver(isSwitch)); + Event.setup(MyEventReceiver( + (isRunning) => isSwitch.value = isRunning, (log) => addLog(log))); Android().getAListVersion().then((value) => alistVersion.value = value); super.onInit(); diff --git a/lib/pages/alist/log_level_view.dart b/lib/pages/alist/log_level_view.dart new file mode 100644 index 0000000..fe094a4 --- /dev/null +++ b/lib/pages/alist/log_level_view.dart @@ -0,0 +1,21 @@ +import 'package:flutter/cupertino.dart'; + +import '../../contant/log_level.dart'; + +class LogLevelView extends StatefulWidget { + final int level; + + const LogLevelView({super.key, required this.level}); + + @override + State createState() => _LogLevelViewState(); +} + +class _LogLevelViewState extends State { + @override + Widget build(BuildContext context) { + final s = LogLevel.toStr(widget.level); + final c = LogLevel.toColor(widget.level); + return Text(s, style: TextStyle(color: c)); + } +} diff --git a/lib/pages/alist/log_list_view.dart b/lib/pages/alist/log_list_view.dart new file mode 100644 index 0000000..ca84699 --- /dev/null +++ b/lib/pages/alist/log_list_view.dart @@ -0,0 +1,39 @@ +import 'package:alist_flutter/pages/alist/log_level_view.dart'; +import 'package:flutter/material.dart'; + +class Log { + final int level; + final String time; + final String content; + + Log(this.level, this.time, this.content); +} + +class LogListView extends StatefulWidget { + const LogListView({Key? key, required this.logs, this.controller}) : super(key: key); + + final List logs; + final ScrollController? controller; + + @override + State createState() => _LogListViewState(); +} + +class _LogListViewState extends State { + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: widget.logs.length, + controller: widget.controller, + itemBuilder: (context, index) { + final log = widget.logs[index]; + return ListTile( + dense: true, + title: Text(log.content), + subtitle: Text(log.time), + leading: LogLevelView(level: log.level), + ); + }, + ); + } +} diff --git a/lib/pages/pwd_edit_dialog.dart b/lib/pages/pwd_edit_dialog.dart new file mode 100644 index 0000000..a109441 --- /dev/null +++ b/lib/pages/pwd_edit_dialog.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class PwdEditDialog extends StatefulWidget { + final ValueChanged onConfirm; + + const PwdEditDialog({super.key, required this.onConfirm}); + + @override + State createState() { + return _PwdEditDialogState(); + } +} + +class _PwdEditDialogState extends State + with SingleTickerProviderStateMixin { + final TextEditingController pwdController = TextEditingController(); + + + @override + void dispose() { + pwdController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("修改admin密码"), + content: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: InputDecoration( + labelText: "admin密码", + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + widget.onConfirm(pwdController.text); + }, + child: const Text("取消"), + ), + FilledButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("确定"), + ), + ], + ); + } +} diff --git a/pigeons/pigeon.dart b/pigeons/pigeon.dart index 69e25cc..5e1468d 100644 --- a/pigeons/pigeon.dart +++ b/pigeons/pigeon.dart @@ -17,8 +17,12 @@ abstract class AppConfig { @HostApi() abstract class Android { + void addShortcut(); + void startService(); + void setAdminPwd(String pwd); + bool isRunning(); String getAListVersion(); @@ -35,4 +39,10 @@ abstract class Android { @FlutterApi() abstract class Event { void onServiceStatusChanged(bool isRunning); + + void onServerLog( + int level, + String time, + String log, + ); }