个人桌面版ChatGPT——ChatPTQ【Compose Desktop试水】

chat.png

最近学习之余注意到了Compose MultiPlatform,然后就想试试水,正好最近越来越依赖ChatGPT,这东西是真香啊,但是总觉得每次都要找套壳网站,想用还得打开浏览器,我很懒 ̄へ ̄,然后我大概找了一下,网上好像也没有人做私人桌面版的小工具(虽然这玩意完全不难做吧,但就是好像没看到有),正好又想玩玩Compose Desktop,于是就花了两天写了这个ChatPTQ。

因为这个项目比较简单,没啥特别好提的,所以这篇文章会更多地偏聊天式、口语化。

GitHub传送门

1 使用

  • Windows应用,无需安装,开箱即用
  • 使用前先在Setting里配置
  • 个人私用,安全、高效、方便

具体的介绍和使用部分就直接看GitHub的文档吧,不想再写一遍了,下面简单说说项目的实现和Compose Desktop的试水体验。

2 实现

因为是个很小的项目,很多地方都很粗糙,能用就行,没有仔细想了。

2.1 UI

就弄了俩页面,Chat和Setting,组件也基本上都是Material的组件。

写起来感受和Compose Android差不多 (除了有些东西不支持,比较简陋)

但是捏,我好像没找到Toast怎么弄出来,报错的时候我想弹Toast提示用户,那行,只能自己实现一个了。

2.2 Toast实现

定义Toaster接口,随便写几个常用的重载方法。

interface Toaster {  
    fun toast(message: String) = toast(message, 2500L, true)  
  
    fun toast(message: String, duration: Long) = toast(message, duration, true)  
  
    fun toast(message: String, duration: Long, success: Boolean)  
  
    fun toastFailure(message: String) = toast(message, 2500L, false)  
}

创建LocalAppToaster。

val LocalAppToaster = compositionLocalOf { Toaster.Default }

封装Toast作用域。在这个Composable里,定义toast的显示,然后把App这个Composable扔进CompositionLocalProvider里,让它给整个App域都提供Toaster接口,这样,任意的子Composable都能通过LocalAppToaster.current获取到当前的toaster,然后触发toast回调,然后设置个小动画(淡入淡出),Toast就完成了。

package view

@Composable
fun Toast(App: @Composable () -> Unit) {
    var showToast by remember { mutableStateOf(false) }
    var toastColor by remember { mutableStateOf(toastColors[0]) }
    val coroutineScope = rememberCoroutineScope()
    var toastText by remember { mutableStateOf("") }
    val toaster by remember {
        mutableStateOf(object : Toaster {
            override fun toast(message: String, duration: Long, success: Boolean) {
                if (showToast) {
                    return
                }
                coroutineScope.launch {
                    toastText = message
                    toastColor = toastColors[if (success) 0 else 1]
                    showToast = true
                    delay(duration)
                    showToast = false
                }
            }
        })
    }

    CompositionLocalProvider(LocalAppToaster provides toaster) {
        Box(modifier = Modifier.fillMaxSize()) {
            App()

            AnimatedVisibility(showToast, modifier = Modifier.align(Alignment.Center)) {
                Box(modifier = Modifier.wrapContentSize().clip(shape =   RoundedCornerShape(6.dp)).background(toastColor)) {
                    Text(toastText, modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp))
                }
            }
        }
    }
}

这里我偷了懒,没有处理多条Toast的情况,只是简单的拦截了一下,当前正在显示的话,就不显示。

Main.kt中,用Toast把RoutePage(真正的页面内容)包一层,就行了:

@Composable



fun App() {
    MaterialTheme {
        Toast {
            RoutePage()
        }
    }
}

2.3 Config

讲到Toast就顺便说一下Config全局配置,因为它的实现思路和Toast差不多。

全局配置包括这么几项:

data class AppConfig(
    val enableSystemProxy: Boolean = false,
    val userProxy: UserProxy = UserProxy("0.0.0.0", 0),
    val apiKey: String = "",
    val autoStart: Boolean = false,
    val gptName: String = "小彭"
)

需要做到:

  • 在Setting界面能修改设置,并自动保存到本地且生效
  • 任意页面能获取到想要的配置项

实现方式也和Toast差不多。

定义一个回调OnConfigChange,参数代表新的AppConfig,再定义一个AppConfigContext类封装一下AppConifg,给它加个回调。然后把AppConfigContext定义成CompositionLocal的。

typealias OnConfigChange = (AppConfig) -> Unit
class AppConfigContext(val appConfig: AppConfig = AppConfig(), val onConfigChange: OnConfigChange)

val LocalAppConfig = compositionLocalOf { AppConfigContext { } }

同样地,定义一个Config的封装Composable块。设一个LaunchedEffect,一启动就读一次配置,然后应用配置,然后把配置暂记下来。然后,CompositionLocalProvider套住App块,这样就能给全局提供AppConfigContext,其它页面能通过LocalAppConfig.current获取。若接收到页面发来的更改配置事件,统一在AppConfigContext的回调中处理,然后更新本地文件和appConfig变量,appConfig的改变会导致UI刷新。

@Composable



fun AppConfig(App: @Composable () -> Unit) {
    var appConfig by remember { mutableStateOf(AppConfig()) }
    val coroutineScope = rememberCoroutineScope()
    val toast = LocalAppToaster.current

    LaunchedEffect(Unit) {
        val jsonConfig = readConfig() ?: run {
            toast.toastFailure("配置文件未找到")
            return@LaunchedEffect
        }
        applyChanges(null, jsonConfig, onSuccess = {

        }, onFailure = {
            toast.toastFailure(it)
        })
        appConfig = jsonConfig
    }

    CompositionLocalProvider(LocalAppConfig provides AppConfigContext(appConfig) { new ->
        coroutineScope.launch {
            writeConfig(appConfig, new, onSuccess = {

            }, onFailure = {
                toast.toastFailure(it)
            })
            appConfig = new
        }
    }) {
        App()
    }
}

Main.Kt中:

@Composable



@Preview
fun App() {
    MaterialTheme {
        Toast {
            AppConfig {
                RoutePage()
            }
        }
    }
}

Setting页面更改配置示例:

@Composable



fun ApiKey(appConfig: AppConfig, onChange: OnConfigChange) {
    SettingPanel("请求设置") {
        OutlinedTextField(appConfig.apiKey,
            label = {
                Text("APIKey")
            },
            onValueChange = {
                //onChnage就是LocalAppConfig.current.onConfigChange
                onChange(appConfig.copy(apiKey = it)) 
            }
        )
    }
}

2.4 网络请求和数据存储

Compose Desktop的网络请求可以用Retrofit,小数据的存储可以用DataStore,具体地就不多说了,项目里只是很简陋地封装了一下,基本上Android怎么用,Desktop就怎么用。

源码

数据的存储为了偷懒我都存项目根路径了(即File(“.”)),如果是打包的exe就是exe所在的路径。

2.5 快捷Enter键发送

键盘的监听可以用Modifier.onPreviewKeyEvent或者onKeyEvent,只是感觉键盘事件处理起来很棘手,因为搜狗输入法中文已经有字的时候敲Enter、普通的敲Enter换行、快捷Enter发送,这三种场景都要Enter,得想办法处理好快捷键事件,这里我想的是长按就触发快捷发送,但是代码写起来很不顺手,不知道还有没有更好的处理方式。(直接在输入框敲Enter,会触发一个awt无法识别的事件,具体地可以自己println出来试一试。)

2.6 打包

关于打包的参数什么的,可以看官方文档
(官方文档的教程还有很多别的功能,我没仔细看,有相对应的需求可以自己去找找)

但是呢,官方打出来的包好像只能是一个安装包,就是有安装程序,然后安装到自己电脑上用,但是呢,我不知道是什么原因,一运行exe安装程序,他就自动变成了后台进程,然后什么反应都没有了,也就是无法安装。

那我就换一种思路吧,反正整个应用也不大,能不能直接生成一个可执行的exe呢,直接双击就运行,不搞什么安装。这样也不会往c盘乱塞东西。

最终找到了办法,执行compose desktop下的CreateDistributable Task就可以了,会在build/compose/binaries底下输出直接可执行的exe。

3 结语

这次通过这个小项目试水了Compose Desktop,然后也做出了一个确实可以给自己工作用的ChatGPT桌面端应用。Compose MultiPlatform感觉还是很香的,基本上就是零学习成本写UI,直接就能上手写,而且kotlin是真的好用,爽爽,但是感觉目前有些地方还是不完善,毕竟还没成长起来嘛,社区也没怎么起来,但是确实是感觉未来可期!

最后贴一个我实现过程中参考了的项目:从0到1搞一个 Compose Desktop 版本的天气应用

就写到这里吧~

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYlRpa3C' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片