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

最近学习之余注意到了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 版本的气候运用

就写到这儿吧~