# [FVV](https://app.niggergo.work/fw) > FVV 语言 FVV 的相关文档目前已迁移至 [FVV 官网](https://fvvlang.sbs) NGA 文档目前不再发布 FVV 包与开发文档 # [项目汇总](https://app.niggergo.work/) > NGA 系列、FVV 系列、PureJoy 欢律遗愉 系列 —— 开发文档 **NGA SDK** 以及其他项目的文档均编写在本开发文档,以帮助使用各个项目 ## 项目 [#项目] *** *** ## Re:Next - 从非零开始的代码生活 [#renext---从非零开始的代码生活] 本教程文档用于讲述部分开发实践要点 # [C++ SDK](https://app.niggergo.work/nga/cpp) > NGA SDK - C++ SDK C++ 部分主要提供一些辅助函数 主要分为如下功能: * 标准库通用: `nga-std.hh` * POSIX 通用: `nga-posix.hh` * Android 按键事件监听: `nga-key.hh` ## 引入依赖 [#引入依赖] 在项目中添加文件后,直接 `#include` 即可: ```cpp #include "nga-std.hh" // 标准库通用 #include "nga-posix.hh" // POSIX 通用 #include "nga-key.hh" // Android 按键事件监听 ``` ## 使用方法 [#使用方法] 文件中已写注释,直接参考注释即可 # [Flutter SDK](https://app.niggergo.work/nga/flutter) > NGA SDK - Flutter(Dart) SDK Flutter SDK 部分主要提供一些 UI 组件 > [Pub 发布页面](https://pub-web.flutter-io.cn/packages/nga_sdk) > > [Dart API 文档](https://pub-web.flutter-io.cn/documentation/nga_sdk/latest/nga_sdk/) 主要分为如下功能: * CU NGA 风格组件: `cu.dart` * 类型拓展: `ext.dart` * 开屏过渡动画: `splash.dart` * 杂项工具: `tool.dart` * 全屏水印 (可彩虹色): `watermark.dart` * NGA 风格组件: `widget.dart` ## 引入依赖 [#引入依赖] > [Pub 参考](https://pub-web.flutter-io.cn/packages/nga_sdk/install) 先添加依赖: ```shell flutter pub add nga_sdk ``` 然后直接 `import` 即可: ```dart import 'package:nga_sdk/cu.dart'; // CU NGA 风格组件 import 'package:nga_sdk/nga.dart'; // 其他功能,也可单独 import 文件 ``` ## 使用方法 [#使用方法] > 仅概述,详细内容请自行通过源码理解 ### CU NGA 风格组件 [#cu-nga-风格组件] * `CUHeadLabel`: CU 风格的文字标签 * `CUCard`: 简易的 CU NGA 风格卡片 * `CUProCard`: 支持更多自定义的 CU NGA 风格卡片 * `CUNavBar`: CU NGA 风格的侧边竖条导航栏 * `CUNavBarGroup`: 导航栏组 * `CUNavBarGroupSub`: 具体导航内容 * `CUTxtButton`: CU NGA 风格的文字按钮 * `CUWidget`: 将传入组件限制到合适大小 * 颜色: CU NGA 风格组件要用到的颜色 * 数值: CU NGA 风格组件要用到的填充、圆角等数值 * `lightColorScheme`、`darkColorScheme`: CU NGA 风格的主题色配置 ### 类型拓展 [#类型拓展] * `let`: 类似于 Kotlin 的 `let` * `ifEmpty`: 类似于 Kotlin 的 `ifEmpty` ### 开屏过渡动画 [#开屏过渡动画] * `NGASplash.view`: 用于包裹页面 * `NGASplash.remove`: 暂时移除动画 * `NGASplash.removeAll`: 完全移除动画 (会额外清理资源) * `NGASplash.show`: 再现动画 * `withLoadingView`: `Widget` 的类型拓展,用于简化 `NGASplash.view` 调用 ### 杂项工具 [#杂项工具] * `NGATool.isDesktop`: 通过 getter 判断当前平台是否是桌面平台 ### 全屏水印 [#全屏水印] * `NGAWatermark.add`: 添加全屏水印 * `NGAWatermark.remove`: 移除全屏水印 ### NGA 风格组件 [#nga-风格组件] * `NGAMsg.show`: NGA 风格的提示,从上往下弹出,从下往上收回 * `NGAMsgType`: 提示类型 # [Go SDK](https://app.niggergo.work/nga/go) > NGA SDK - Go SDK Go SDK 部分主要提供一些功能与辅助函数 主要分为如下功能: * 日志记录: `logger.go` * 文件操作: `io.go` * 网络文件: `http.go` ## 引入依赖 [#引入依赖] 先添加依赖 在 `go.mod` 中通过 `replace` 指向本地路径,并通过 `require` 添加依赖 ```plaintext title='示例' replace app.niggergo.work/sdk/nga => path/nga-go require app.niggergo.work/sdk/nga v0.0.0 ``` 通过命令行添加依赖: ```shell go get -u app.niggergo.work/sdk/nga@latest ``` 然后 `import` 即可: ```go import "app.niggergo.work/sdk/nga" ``` ## 使用方法 [#使用方法] > 仅概述,详细内容请自行通过源码理解 ### 日志记录 [#日志记录] > 该功能主要实现与 [CU Logger](https://github.com/chenzyadb/CU-Utils) 类似的功能 为确保适配 Android 系统,默认会引入 `time/tzdata` ,会略微增加编译后大小 * `Logger`: 记录器类型 * `LogLevel`: 日志等级 * `LastLogLevel`: 最后一个日志的日志等级 * `OutputMode`: 输出模式 * `TimeLoc`: 时区 * `TimeFmt`: 时间格式化 * 函数 `LogX`: 对应类型的日志输出 * 函数 `Flush`: 等待日志输出完毕 * 函数 `Close`: 关闭记录器 (会调用 `Flush`) * `LogLevel`: 日志等级 * `LOG_NONE`: `?` (正常使用中不应当使用,仅代表最后一次日志的占位符) * `LOG_ERROR`: `E` * `LOG_WARN`: `W` * `LOG_INFO`: `I` * `LOG_DEBUG`: `D` * `LOG_VERBOSE`: `V` * `LogMode`: 日志模式 * `LOG_APPEND`: 追加模式 * `LOG_TRUNC`: 清空模式 * `LogOutput`: 输出模式 * `LOG_PRINT`: 仅打印 * `LOG_FILE`: 仅写入文件 * `LOG_ALL`: 全部 ### 文件操作 [#文件操作] * `PathExist`: 判断文件存在 * `MoveFile`: 移动单个文件 (支持跨文件系统,会保持文件时间一致) * `IsDir`: 判断路径是目录 * `IsEmptyDir`: 判断路径是空目录 * `IsFile`: 判断路径是文件 * `IsEmptyFile`: 判断路径是空文件 * `IsHiddenPath`: 判断路径是隐藏路径 (路径中包含 `.` 开头路径) * `CopyFile`: 复制单个文件 (会保持文件时间一致) * `CopyDir`: 复制目录 (会保持文件/目录时间一致) ### 网络文件 [#网络文件] 该功能实现了一个有 `ReadAt` 函数的 `HttpReader` 类型 # [NGA SDK](https://app.niggergo.work/nga) > NGA 系列的主线项目 NGA SDK 目前支持如下语言/框架 C++ `20`+ Dart `2.12`+ Go `1.18`+ *Android* (*仍在开发中*) 基于 `POSIX`/`ksh` # [Kotlin SDK](https://app.niggergo.work/nga/kotlin) > NGA SDK - Kotlin SDK Kotlin SDK 提供了一些小功能 ## 引入依赖 [#引入依赖] 先依赖源中有 [PkgPub](/purejoy/pkgpub/use) 的依赖源 然后添加对应的依赖: ```groovy dependencies { implementation("work.niggergo.app:gendoki:+") // GenDoki implementation("work.niggergo.app.sandocube:moesa:+") // MoeSa implementation("work.niggergo.app:futago:+") // Futago } ``` **功能列表**: * `GenDoki`: 用于非常简单地配置用于初始化内容的内容提供器 * `MoeSa`: 用于在不传入任何参数的情况下通过 `GenDoki` 在软件启动时校验软件真实性 * `Futago`: 用于在不继承任何 **`Application` 子类** 的情况下通过 [`KavaRef`](https://highcapable.github.io/KavaRef/zh-cn/) 在 `Application` 中创建多个 **`Application` 子类** 的实例 ## 使用方法 [#使用方法] ### GenDoki [#gendoki] 首先编写一个继承自 `GenDokiInitializer` 的内容提供器, 重写 `Application.onInit` 函数即可执行初始化操作 ```kotlin title='示例' import android.app.Application import work.niggergo.app.gendoki.GenDokiInitializer class AppInitializer : GenDokiInitializer() { override fun Application.onInit() { // 干些什么事情... } } ``` `GenDokiInitializer` 在运行 `Application.onInit` 函数时,会在 `runCatching` 中执行, 所以一般来说不需要自己再加一层 `runCatching` 或 `try` 然后在 `AndroidManifest.xml` 中注册内容提供器即可实现在软件启动时执行内容 ```xml title='示例' ``` ### MoeSa [#moesa] 使用 MoeSa 直接引入依赖即可,无需任何额外操作 引入 MoeSa,会自动判断 `applicationInfo.sourceDir` 是否与 `pm path <包名>` 返回的 `base.apk` 路径相同, 并且判断路径是否以 `/data/app/` 开头,以 `/base.apk` 结尾,并且其中包含包名 还会判断 `base.apk` 的 `uid` 与 `gid` 是否都为 `1000`(即 `system`) 并且判断文件权限是否为 `644`(常规情况下) 或 `555`(**`V4` 签名** 安装情况下) (实际的判断方式是判断文件权限是否在 `544`\~`655` 范围之内) 如果校验失败,会调用 `killProcess(myPid())` 和 `exitProcess(-1)` 强制停止软件运行 ### Futago [#futago] 在自己的 `Application` 中实现 `FutagoAppsLoader` 然后再在 `attachBaseContext`、`onCreate`、`onTerminate`、`onConfigurationChanged`、`onLowMemory`、`onTrimMemory` 函数中调用即可 ```kotlin title='示例' import android.app.Application import android.content.Context import android.content.res.Configuration import work.niggergo.app.futago.FutagoAppsDelegate import work.niggergo.app.futago.FutagoAppsLoader class AppApplication : Application(), FutagoAppsLoader { override val futagoDelegate by lazy { FutagoAppsDelegate( listOf(TargetApplication::class) ) } override fun attachBaseContext(base: Context) = super.attachBaseContext(base).also { appsAttachBaseContext() } override fun onCreate() = super.onCreate().also { appsCreate() } override fun onTerminate() = super.onTerminate().also { appsTerminate() } override fun onConfigurationChanged(newConfig: Configuration) = super.onConfigurationChanged(newConfig).also { appsConfigurationChanged(newConfig) } override fun onLowMemory() = super.onLowMemory().also { appsLowMemory() } override fun onTrimMemory(level: Int) = super.onTrimMemory(level).also { appsTrimMemory(level) } } ``` 为确保在 R8 的发力下运行正常,已在 **使用者 ProGuard 规则** 中添加了对所有 **`Application` 子类** 的 `keepclassmembers` 规则,将会保留 *构造函数*、`attachBaseContext`、`onCreate`、`onTerminate`、`onConfigurationChanged`、`onLowMemory`、`onTrimMemory` 以确保反射运行正常 # [Rust SDK](https://app.niggergo.work/nga/rust) > NGA SDK - Rust SDK Rust SDK 仍在开发中 # [Shell SDK](https://app.niggergo.work/nga/shell) > NGA SDK - Shell SDK Shell SDK 部分主要提供一些辅助函数与混淆加密工具 主要分为如下功能: * 实用代码: `nga-utils.sh` (基于 `POSIX` 语法编写) * AW 加密: `nga-enc.sh` (基于 `ksh` 语法编写,生成代码基于 `POSIX` 语法) ## 引入依赖 [#引入依赖] ### 引入实用代码 [#引入实用代码] ShiroSU 模块构建工具 会 **默认添加** 实用代码,建议通过它来构建模块 在项目中添加文件后,推荐使用如下方法引入: ```shell baseDir="$(dirname "$(readlink -f "$0")")" [ -f "$baseDir/nga-utils.sh" ] && . "$baseDir/nga-utils.sh" || exit ``` ## 使用方法 [#使用方法] ### 实用代码 [#实用代码] #### 重定向 [#重定向] `run2null`: 将 `标准输出` 和 `标准错误` 重定向到 `/dev/null` ```shell title='示例' run2null echo "这句话将消失" ``` `run22null`: 将 `标准错误` 重定向到 `/dev/null` ```shell title='示例' run22null echo "这句话不会消失" run22null eval 'echo "这句话会消失" 1>&2' ``` #### 按键 [#按键] `until_key`: 获取按下的按键 大部分人写的按键监听都有问题,没有考虑到按键事件的 `EV_KEY` 有 `DOWN` 和 `UP` 两个状态(即 **按下** 和 **松开**) <> 这会导致如果按键时松开过快,会**连续两次监听到相同事件**,从而导致错按问题 <> NGA SDK 则完全没有该问题,在函数中已经判断了具体的按压状态 ```shell title='示例' echo $(until_key) # 输出按下的按键 ``` | 按键名称 | 代码 | `until_key` 输出名称 | | ------- | --------------- | ---------------- | | 音量+ | KEY\_VOLUMEUP | up | | 音量- | KEY\_VOLUMEDOWN | down | | 电源键 | KEY\_POWER | power | | 静音键 | KEY\_MUTE | mute | | 肩键等额外按键 | KEY\_F`X` | f`X` | 由于大部分情况下,不需要再监听更多按键,所以目前仅监听这些常用按键 `until_key_any`: 按下任意键 ```shell title='示例' echo "按下任意键后继续..." until_key_any # 按下任意按键后继续执行 ``` `until_key_up_down`: 仅获取 `音量+键` 或 `音量-键` 按下 ```shell title='示例' echo $(until_key_up_down) # 输出按下的按键,只能为 up 或 down ``` `until_key_up_down_power`: 仅获取 `音量+键` 或 `音量-键` 或 `电源键` 按下 ```shell title='示例' echo $(until_key_up_down_power) # 输出按下的按键,只能为 up 或 down 或 power ``` `until_key_up`: 仅获取 `音量+键` 按下 ```shell title='示例' echo $(until_key_up) # 输出按下的按键,只能为 up ``` `until_key_down`: 仅获取 `音量-键` 按下 ```shell title='示例' echo $(until_key_down) # 输出按下的按键,只能为 down ``` `until_key_power`: 仅获取 `电源键` 按下 ```shell title='示例' echo $(until_key_power) # 输出按下的按键,只能为 power ``` #### 跳转 [#跳转] `goto_url`: 跳转链接 ```shell title='示例' goto_url "https://app.niggergo.work" # 跳转 NGA 文档 ``` `goto_app`: 跳转软件活动 ```shell title='示例' goto_app "ren.shiror.su/dev.oom_wg.ssu.SSUUI" # 跳转 SSU ``` #### 字符串 [#字符串] `str_eq`: 判断传入的第一个字符串与其他字符串中是否有相等的 ```shell title='示例' str_eq "oom" "oow" "ssu" "suu" && \ echo "'oom' 与其他有相等的!" || \ echo "'oom' 与其他没有相等的!" # 由于通常认为 echo 不可能出错,所以这里直接使用了 || ``` #### 打印 [#打印] 因为实用代码面对的环境有 直接执行、模块安装/执行 等,所以会有一些封装函数 `pure_print`: 打印文字 ```shell title='示例' pure_print "无论是在系统还是 Recovery,这句话都能正常显示" ``` `nga_abort`: 报错退出 ```shell title='示例' nga_abort "我不行了..." # 输出后将会是 “⚠️ 我不行了...” 并在尝试删除缓存后退出 ``` `nga_print`: 带一个 `>` 地打印文字 ```shell title='示例' nga_print "有 '>' 君在我身边,我真的会很安心呢" # 输出后将会是 “> 有 '>' 君在我身边,我真的会很安心呢” ``` `newline`: 打印空行 ```shell title='示例' newline # 不传入内容,默认打印一行空行 newline 3 # 传入内容,打印指定行数的空行 ``` `print_lines`: 打印每一个传入内容为一行 此功能为 `run_install_list` 的辅助功能,并非为直接打印所写,故使用 `echo` 而非 `pure_print` ```shell title='示例' print_lines "这是第一行" "这是第二行" ``` #### 文件 [#文件] `get_work_dir`: 获取父目录 ```shell title='示例' echo "我现在在 '$(get_work_dir .)' 正好好待着呢" # 输出后将会是 “我现在在 '<当前目录的父目录路径>' 正好好待着呢” ``` `set_dir_perm`: 将传入的目录递归设置正确权限 当使用 OverlayFS 实现 Systemless 时,正确设置目录的权限变得尤其重要 ```shell title='示例' set_dir_perm path/dir1 path/dir2 ``` `set_system_file`: 将传入的目录递归设置正确 SELinux 上下文 当使用 OverlayFS 实现 Systemless 时,正确设置目录的权限变得尤其重要 ```shell title='示例' set_system_file path/dir1 path/dir2 ``` `pre_bin`: 将单个可执行文件设置执行权限 ```shell title='示例' pre_bin path/exe ``` `pre_bins`: 将多个可执行文件设置执行权限 ```shell title='示例' pre_bins path/exe1 path/exe2 ``` `run_bin`: 运行单个可执行文件 (执行前会通过 `pre_bin` 设置权限) ```shell title='示例' run_bin path/exe arg ``` `nohup_bin`: 后台运行单个可执行文件 (执行前会通过 `pre_bin` 设置权限) ```shell title='示例' nohup_bin path/exe arg ``` `get_arch`: 获取当前设备会在 `lib` 目录使用的架构名称 (仅可能为 `arm64`/`arm`/`x86_64`/`x86`/`riscv64`/`mips64`/`mips` 之一) ```shell title='示例' echo $(get_arch) ``` `get_app_lib`: 获取指定包名软件的指定共享库路径 获取到的架构优先使用 `get_arch` 获取到的(即当前设备架构) <> 若当前架构的共享库文件不存在,会获取 `lib` 目录内第一个架构的,若还是不存在,则不会继续查找 ```shell title='示例' echo $(get_app_lib ren.shiror.su ssuus) # 输出后将会是 “/lib/<当前设备架构>/lib<指定共享库名称>.so” ``` #### 等待 [#等待] `until_boot`: 等待开机完毕 (即退出第二屏) > 此功能通过 `resetprop` 实现 ```shell title='示例' until_boot # 不传入内容,默认在开机完毕后立即继续执行 until_boot 30 # 传入内容,在开机完毕并等待指定秒数后继续执行 ``` `until_unlock`: 等待设备解锁 (会通过 `until_boot` 确保开机完毕) 如果设备的数据分区未加密,此功能可能无法正常运行(仅能确保开机完毕) ```shell title='示例' until_unlock # 不传入内容,默认在设备解锁后立即继续执行 until_unlock 30 # 传入内容,在设备解锁并等待指定秒数后继续执行 ``` #### root [#root] `is_ssu`/`is_shirosu`: 判断是否是 [ShiroSU](https://ssu.oom-wg.dev/) ```shell title='示例' is_ssu && echo "是 ShiroSU 哦" # 简写函数 is_shirosu && echo "是 ShiroSU 哦" # 全称函数 ``` `is_ksu`/`is_kernelsu`: 判断是否是 [KernelSU](https://kernelsu.org/zh_CN/) ```shell title='示例' is_ksu && echo "是 KernelSU 哦" # 简写函数 is_kernelsu && echo "是 KernelSU 哦" # 全称函数 ``` `is_ap`/`is_apatch`: 判断是否是 [APatch](https://apatch.dev/zh_CN/) ```shell title='示例' is_ap && echo "是 APatch 哦" # 简写函数 is_apatch && echo "是 APatch 哦" # 全称函数 ``` `not_magisk`: 判断是否不是 [Magisk](https://topjohnwu.github.io/Magisk/) ```shell title='示例' not_magisk && echo "不是 Magisk 哦" ``` `is_magisk`: 判断是否是 [Magisk](https://topjohnwu.github.io/Magisk/) ```shell title='示例' is_magisk && echo "是 Magisk 哦" ``` `nga_install_module`: 安装单个模块 为了兼容多种 root 实现,检测顺序是 `Magisk`、`APatch`、`KernelSU` <> 这个顺序是基于各种 root 实现的策略与易安装程度而决定的 ```shell title='示例' nga_install_module path/mod.zip ``` `nga_install_modules`: 通过 `nga_install_module` 安装多个模块 ```shell title='示例' nga_install_modules path/mod1.zip path/mod2.zip ``` #### 模块 [#模块] `magisk_run_completed`: 是 [Magisk](https://topjohnwu.github.io/Magisk/) 的情况下运行 `boot-completed.sh` 通常不建议编写 `boot-completed.sh` 而是在 `service.sh` 中使用 `until_boot`,除非完全不考虑适配 Magisk ```shell title='示例' magisk_run_completed "$(get_work_dir "$0")" ``` `get_target_bin`: 在 **模块安装时** 获取当前架构的指定名称可执行文件,移动至模块目录 仅支持获取以 ShiroSU 模块构建工具 格式放置的可执行文件 ```shell title='示例' get_target_bin exe ``` `get_target_bins`: 通过 `get_target_bin` 获取多个可执行文件 ```shell title='示例' get_target_bins exe1 exe2 ``` `run_install_list`: 通过音量键选择安装多种功能 ```shell title='示例' inst_1() { local func_head="method_" # 函数头 local opt_name="功能一" # 功能名称 local opt_num=2 # 选项个数 local cancel=true # 是否可取消 local opt_names="选项一 选项二" # 各选项名称 print_lines "$func_head" "$opt_name" "$opt_num" "$cancel" $opt_names # 输出功能信息 } method_1() { echo "这个是功能一的选项一哦" } method_2() { echo "这个是功能一的选项二哦" } inst_2() { local func_head="method2_" # 函数头 local opt_name="功能二" # 功能名称 local opt_num=2 # 选项个数 local cancel=false # 是否可取消 local opt_names="选项一 选项二" # 各选项名称 print_lines "$func_head" "$opt_name" "$opt_num" "$cancel" $opt_names # 输出功能信息 } method2_1() { echo "这个是功能二的选项一哦" } method2_2() { echo "这个是功能二的选项二哦" } run_install_list inst_ 2 # 传入 功能函数头 与 个数,调用安装 ``` `run_install_list` 传入的内容: 1. 功能函数头 2. 功能函数的个数 故在编写各个功能的函数时必须以 `函数头`+`次序` 的形式定义,**次序以 `1` 开始** 每个功能函数应当输出的内容(以行排序): 1. 选项函数头 2. 功能名称 3. 选项个数 4. 是否可取消安装该功能(开启则会多一个用于取消安装的选项零) 5. 选项名称(一行一个) 故在编写各个选项的函数时必须以 `函数头`+`次序` 的形式定义,**次序以 `1` 开始** 安装时通过 ***`音量+` 键* 切换选项**,***`音量-` 键* 确认选项** `nga_install_init`: 初始化由 ShiroSU 模块构建工具 构建的模块 此功能应当在安装脚本的开头就调用! <> 调用时会校验文件哈希值,如有需要忽略的文件,可传入文件的**相对路径** ```shell title='示例' [ -f "$MODPATH/nga-utils.sh" ] && . "$MODPATH/nga-utils.sh" || abort '! File "nga-utils.sh" does not exist!' nga_install_init path/ignore1 path/ignore2 # 传入需要忽略校验的文件相对路径 ``` `nga_install_done`: 收尾由 ShiroSU 模块构建工具 构建的模块 此功能应当在安装脚本的结尾调用! 具体工序: * 清理 `bin` 目录(即多余架构的可执行文件) * 递归设置 `system` 目录的 `权限` 与 `SELinux 上下文`,并将可能存在的 `system/vendor/odm` 目录移动至 `system/odm` * 清理多余架构的 [Zygisk](https://topjohnwu.github.io/Magisk/guides.html#zygisk) 共享库文件 * 清理可能存在的多余文件(自述文件、更新日志等) ```shell title='示例' nga_install_done ``` #### 导出变量 [#导出变量] 实用代码还会在调用后导出如下变量: * `BOOTMODE`: 是否为开机模式(即是否有 `zygote` 进程在运行) * `ARCH`、`ABI`、`ABI32`、`IS64BIT`: 当前设备的 架构、ABI、32 位 ABI、是否是 64 位 如果当前 root 实现是 Magisk 并且**版本低于 27008**(即不支持 `操作`), 则会显示一个检测到低版本 Magisk 的警告, 因为 Magisk 分支 Kitsune Mask(即原来的 Magisk Delta)的用户较多,但已经停止更新 如果当前 root 实现是 KernelSU 分支 **SukiSU Ultra**, 则会显示一个检测到 SukiSU Ultra 的警告, 因为在综合考究之下,其项目质量差、稳定性低、可用性低、开发者维护能力不足,不具备任何可靠性 警告**不会影响任何行为**,仅作为提醒而存在 ### AW 加密 [#aw-加密] AW 加密 的目的并不是高强度的 Shell 混淆加密 (虽然里面添加了强度较高的防破解手段,只不过是能防一些通解手段而已) AW 加密 的目的是实现可以防低技术人群的同时还具有一定的艺术性 故 AW 加密 的艺术性是其重要的组成部分, 恳请各路开发者在参考时不要参考其艺术性部分,其他部分大可拿去用, 只不过还是要遵守许可证规定 AW 加密 的开发目的是为了适配 ShiroSU 模块构建工具,故采取的是覆写策略 AW 加密 有以下优点: * 雑魚ですね♡ * 可读性差 * 较为美观的外层壳 * 自带简单防破解 * 无残留函数/变量,干净执行 * 基于 `POSIX` 语法解密执行,兼容性极佳 * 各方面功能兼容性好,在 `set -e` 情况下也可正常执行 使用 AW 加密 很简单,只需要传入脚本路径,即可混淆加密指定的脚本 ```shell title='示例' bash nga-enc.sh path/script1.sh path/script2.sh ``` AW 加密 本身基于 `ksh` 语法编写,不兼容 `POSIX` 语法,切勿使用 `dash` 等解释器执行! 如果对 Shell 的安全防护感兴趣,可见 [此篇文章](https://oom-wg.dev/posts/shell-obf-enc) # [FYTxt 多语言框架](https://app.niggergo.work/purejoy/fytxt) > 基于 FVV 实现的支持 KMP/CMP 的多语言框架 FYTxt 是支持 **Kotlin MultiPlatform** 以及 **Compose MultiPlatform** 的多语言框架 完全做到了 *方便易用*、*高性能*、*全平台支持*,可以监听系统语言变化,还支持锁定语言列表以便于自定义语言列表, 并且有翻译率统计与语言组设定 FYTxt 基于 [`FVV`](/fw/) 而不是传统的 xml 存放文本, 原理是通过 Gradle 插件自动生成 Kotlin 文件, 可以通过配置来让指定语言的文本以 `KDoc` 形式为文本注释,通过 IDE 即可快速查看文本具体内容 并且 Android 平台的 **Compose** 方式实现了默认使用 [**Pangu Text**](https://betterandroid.github.io/PanguText/zh-cn/) 来达成优化中英文字符间距 ## 支持范围 [#支持范围] 对于各平台的支持度如下: | 平台 | 运行环境 | 获取方式 | 自动更新语言列表 | 备注 | | :---------- | :------------ | :------------ | :------- | :-------------------------------------- | | **Android** | **JVM** | Java API | **支持** | Compose 可自动重绘;常规使用需要手动在 `Activity` 添加监听 | | **Android** | **Native** | `getprop` 命令行 | **不支持** | | | **Desktop** | **JVM** | Java API | **不支持** | | | **iOS** | **Native** | Native API | **不支持** | iOS 在切换语言后会重启,故无需监听。 | | **Linux** | **Native** | `LANG` 环境变量 | **不支持** | | | **Windows** | **Native** | Win32 API | **不支持** | | | **Web** | **JS / Wasm** | JS API | **支持** | | > 对于部分不支持自动更新语言列表的平台,如有需要请自行循环更新 也就是说,FYTxt 完全可在 *常规 UI*、*Compose UI* 以至于 *共享库*、*可执行* 均可实现多语言 [Compose 与 CLI 多语言示例](https://github.com/OOM-WG/OOM-Demos/tree/oom/fytxt) ## 引入依赖 [#引入依赖] 先依赖源中有 [PkgPub](/purejoy/pkgpub/use) 的依赖源 然后添加 FYTxt 的 Gradle 插件: ```groovy plugins { id("dev.oom-wg.purejoy.fyl.fytxt") version "+" apply false } ``` FYTxt 可在 KMP(`org.jetbrains.kotlin.multiplatform`) 或 Android(`com.android.application`/`com.android.library`) 中使用 在项目中引入并配置内容: ```groovy plugins { id("dev.oom-wg.purejoy.fyl.fytxt") } fytxt { // 配置内容... } ``` 支持的配置内容如下: ## 使用方法 [#使用方法] ### 配置概念 [#配置概念] FYTxt 采用了 *语言组* 概念来存放各个语言变种(例如 *常规语言* 与 *喵言喵语*), **第一个语言组**将被视为**默认语言组**,其他语言组的语言支持范围需**小于等于**默认语言组, 当其他语言组出现**文本缺失**时,将**优先采用默认语言组的同语言文本** FYTxt 将所有平台的语言标签均转为了**下划线连接**的**全大写**文本(例如 `ZH_HANS_CN`、`EN_GB`) 不过当 Gradle 插件在搜索时,会自动将文件夹名称转为全大写,您依旧可以将文件夹名称命名为 `zh_CN` 等内容, 但**不可使用连字符连接** 默认语言也会在运行时自动转为全大写,它将作为 `KDoc` 生成时的默认语言与所有语言的回退文本使用 如果将默认语言设为 *中文* ,且软件仅支持 *中文* 、 *英文* ,那么当系统语言为其他语言时,将显示 **中文** 由于部分语言可能难以匹配,所以支持配置语言与其对应的正则表达式,依此优化语言匹配效果 正则表达式对应的语言标签必须是全大写,所以建议所有语言标签都使用全大写 虽然 Android 已经支持了很多年的脚本标签(例如 `zh-Hans`),但是其匹配顺序难以推测,并且格式难写 大部分系统也只会返回 `zh-CN` 这样的语言与地区的标签,而不是 `zh-Hans-CN` 这样的完整标签 并且由于中文的体系复杂,*中国大陆地区* 与 *新加坡* 使用**简体中文**,中国的大陆以外的 *其他地区* 使用**繁体中文**, 使用单纯的前缀匹配很难完整覆盖,故推出正则匹配 当该语言有正则规则时,只匹配其正则规则,若无,则使用前缀匹配, 如下规则可仅 *中国大陆* 与 *新加坡* 使用**简体中文**,其他中文地区均使用**繁体中文**: ```kotlin langAliases = mapOf( "ZH_HANS" to "^ZH_.*(HANS|CN|SG)", "ZH_HANT" to "^ZH_(?!.*(HANS|CN|SG)).*" ) ``` ### 编写多语言文件 [#编写多语言文件] Gradle 插件会遍历传入的文件夹,以传入的文件夹的子文件夹作为语言标签,子文件夹内所有 FVV 文件作为语言数据 在目录下创建例如: common/zh/lang.fvv common/en/lang.fvv ```plaintext Hello = "你好" Home = { Welcome = "欢迎%s" } ``` ```plaintext Hello = "Hello" ``` 以上内容会自动生成如下 kotlin 代码 (若以 `common` 目录作为语言组 *Common*,开启了 Compose 生成,且其他可选选项均为默认配置): ```kotlin internal enum class FYTxtGroups : FYTxtGroup { Common { override val stats: Map = mapOf( FYTxtTags.EN to 0.5, FYTxtTags.ZH_CN to 1.0, ) }, ; public companion object { init { FYTxtConfig.init(FYTxtGroups.Common, FYTxtTags.entries) } } } internal enum class FYTxtTags : FYTxtTag { EN { override val pattern: Regex? = null }, ZH_CN { override val pattern: Regex? = "^ZH_.*(HANS|CN|SG)".toRegex() }, } internal object FYTxt { init { FYTxtGroups } /** * 你好 */ public val Hello: String get() = FYTxtConfig.activeTags.value.firstNotNullOfOrNull { it as FYTxtTags when (it) { FYTxtTags.EN -> "Hello" FYTxtTags.ZH_CN -> "你好" else -> null } } ?: "你好" /** * 你好 */ @Composable public fun Hello(vararg args: Any?): String = FYTxtConfig.observe { Hello.fmt(args) } public object Home { init { FYTxtGroups } /** * 欢迎%s * @suppress Common: EN */ public val Welcome: String get() = FYTxtConfig.activeTags.value.firstNotNullOfOrNull { it as FYTxtTags when (it) { FYTxtTags.ZH_CN -> "欢迎%s" else -> null } } ?: "欢迎%s" /** * 欢迎%s * @suppress Common: EN */ @Composable public fun Welcome(vararg args: Any?): String = FYTxtConfig.observe { Welcome.fmt(args) } } } ``` 对如上代码的解释: * `FYTxt` 为自动生成的根类 * `FYTxtGroups` 为语言组 `enum`,其中包含了各个语言的翻译率 * `FYTxtTags` 为各个语言标签 具体的语言文本采用 `firstNotNullOfOrNull` 遍历 `when` 匹配 `enum`,性能好, 在经过 *ProGuard*/*R8* 优化后会变成性能更好的判断匹配 ### 在代码中使用 [#在代码中使用] 由于要兼顾常规方式与 Compose 方式两个场景,以 **`getter` 调用**为**常规方式**,**函数调用** 为 **Compose 方式** 如果要调用文本,只需要调用 `FYTxt.Hello`(常规方式) 或 `FYTxt.Home.Welcome()`(Compose 方式) 即可 调用 Compose 方式时,可以直接传入内容来实现格式化文本 仅 *Android JVM*/*JVM* 支持格式化,其他平台仅有简单的 `%s` 替换 <> *Android Jetpack Compose* 输出后的文本默认经过 **Pangu Text** 处理 当 *Android JVM* 使用常规方式需手动调用 `FYTxtConfig.updateTags()` 以更新 ```kotlin title='示例' import dev.oom_wg.purejoy.fyl.fytxt.FYTxtConfig class AppActivity : Activity() { override fun onConfigurationChanged(newConfig: Configuration) = super.onConfigurationChanged(newConfig).also { FYTxtConfig.updateTags() } } ``` 当 *Compose* 使用时,需要在 UI 外层使用 `FYTxtProvider` 以监听语言变化 ### 详细功能说明 [#详细功能说明] #### 自定义语言列表 [#自定义语言列表] 若需要自定义语言列表,而不是一直跟随系统,可使用 `FYTxtConfig.updateTags()`: * `tags`: 自定义的语言列表,需要**全大写**且**下划线连接** * `lock`: 锁定语言列表,锁定后将不会自动更新语言列表,仅可通过传入 `tags` 更新 (*锁定*/*解锁* 均执行一次即可,无需每次调用时都传入) #### 切换语言组 [#切换语言组] 可使用 `FYTxtConfig.updateGroup()` 传入由插件自动生成的语言组 `enum` 以实现切换 #### 获取翻译率 [#获取翻译率] 由插件自动生成的语言组 `enum` 中的每个语言组均已记录每个语言组中的每个语言的翻译率 # [PureJoy 欢律遗愉](https://app.niggergo.work/purejoy) > 回忆溢出工作组的主线系列 欢律遗愉 目前有如下项目 多语言框架 (*KMP*/*CMP*/*Android* 可用) 包发布系统 # [Re:Next - Flutter](https://app.niggergo.work/ren/flutter) > 非零基础的 Flutter 教程 Flutter 是一个基于 Dart 实现的前端框架, 但是由于 Dart 本身在除了开发一些包之外,很少需要单独使用,所以主要讲 Flutter Flutter 与 CMP 在某些方面比较相似,但是 CMP 相较于 Flutter 有较多的劣势 首先就是跨平台方面,CMP 需要 **JVM**,所以在绝大部分情况下,只会在 Android 上见到 Compose (KMP 倒不至于,但是开发也并不简单) 尤其是在网页方面,两者都支持 `js`/`wasm`,但是 CMP 有个非常严重的硬伤,编译即送 **Skiko** 依赖大礼包,无论 js 还是 wasm,无论代码多少,都会依赖上这么个庞然大物 虽然 Flutter 也会有 **Canvaskit** 的依赖,但是 Flutter 默认会走**谷歌 CDN**,并不需要过多操心,谷歌 CDN 无论是在中国大陆还是其他地方,速度都是相当可观的(*IP 太脏当我没说*) 然而 **Skiko 并没有任何 CDN**,只能靠自己,这是一个非常大的硬伤,因为 Flutter 和 CMP 的编译产物都是比较大的,再加上比较大的核心依赖,速度会一拖再拖 还有个硬伤,也是非常需要重视的,那就是 CMP **不支持中文**(就是没有其他语言的字体,常规字符外的文字均无法显示),也不会调用系统字体,虽然 Flutter 也是如此,但是 Flutter 做的相当智能了,会自动从**谷歌字体 CDN** 下载字体 所以 CMP 只能自己依赖字体,要依赖一个中文字体也会占相当大的大小,并且 CMP 使用字体不知道为什么做的有一丝怪异,和 Jetpack Compose 有些不同,用起来会更麻烦 还有个非常关键的点,CMP 无法使用谷歌字体,Jetpack Compose 的谷歌字体是通过 **GMS** 实现的,在没 GMS 的 Android 设备上都是残废的,更不用说在 CMP 上了(Flutter 的谷歌字体则不需要,因为是纯 Dart 实现的跨平台谷歌字体,根本都不需要为了 Android 单独再走 GMS) 自己手动通过网络获取字体或许有些许可能,但是远远不如 Flutter 来的方便 还有就是 Compose 本身的问题,先讲一个让我入门即放弃的问题 Compose 的焦点管理相当麻烦,需要自己手动清理,不清理轻则内存泄漏,重则无法操作界面 还有就是在设计上的几个问题,Flutter 是万物皆 `Widget` 这么个基本类,无论是 `main` 的入口,还是具体到每个组件都是这样 然而 Compose 是由一堆意义不明的 `Unit`(就是什么都不返回的函数,等同于 `void`)组成的, 在这么个整体架构上就很难让人理解,在组件上看起来是与 Flutter 比较相像的嵌套,但是实际上并不是这样 Compose 中比较常见的一个东西是 `content: @Composable () -> Unit`,实际上就是传入了一个 Lambda 函数, 并非 Flutter 那样的 `Widget`/`List`,所以在组件分工上就会非常不明确,并且会导致在体系上非常混乱, 因为可以 “**随地大小变**(*量*)” 随地设变量虽然很多时候确实比较有需求,但是这样就不太约束人了(*代码质量*`--`),并且这也导致了 Compose 有了一种另样的界面架构:弹窗 Flutter 的弹窗是可以直接在函数中创建的,例如在按钮的回调中显示弹窗,这样非常直观,也非常人性化 但是 Compose 的弹窗是直接嵌在界面内部的,可以存在于几乎任何地方,然后通过一个**布尔值**管理状态, 其他的类似组件原理皆是如此 这样的各类问题就会导致整个界面的架构能多不明确就能多不明确,因为约束性差,状态管理上也莫名其妙 Flutter 与 Compose 的状态更新也很不一样,Compose 由于就像先前说过的架构混乱,所以在状态管理上会更加吃力些, 并且 Compose 的值很多时候与 `remember` 息息相关,没有细致管理可能就会有很多额外的性能消耗, **裸**值、**remember** 的值、**全局**值、**ViewModel** 中的值在使用时也要考虑许多 然而 Flutter 只要是在不太滥用的情况下,可以把重绘约束到具体值或具体组件上,并不需要波及到整个组件, 可以让组件只监听它需要的值,然后单独变化 还有一个比较大的差异,Compose 的基本组件很少,大部分组件都是通过基本组件加上 `Modifier` 实现的, 通过它来设置每个组件的具体效果,而非 Flutter 那样每个组件各自分工,通过嵌套实现组合效果 并且 `Modifier` 有个比较反直觉的点,设置给它的属性并不会顺序执行,而是倒序的 例如给一个组件通过 `clickable` 设置可点击,此时该组件就会有水波纹的按压效果,但是并没有圆角 但是按照常规直觉,应当是 `Modifier.clickable {...}.clip(...)`,但这样并无成效, 正确的反而是 `Modifier.clip(...).clickable {...}`,这样才能裁剪出圆角 因为 `Modifier` 的原理是先内后外的链式调用,得反着来才可以正常用 但是如果把 Flutter 只与 Jetpack Compose 对比,Flutter 的问题也不少 体积问题是一个比较重要的问题,Flutter 本身的依赖就已经很大了,再加上代码的构建产物也挺大的, 完全比不过编译成 `dex` 可以直接在 JVM 中运行的 Kotlin 代码 并且 Kotlin 可以直接与 **JVM** 或是 **JNI** 这些交互, 然而 Dart 虽然也有 **ffi** 或是**与 JVM 的通信通道**,但自然是远不及别人 Android 原生代码本身的 还有一个最大的硬伤,那就是 Dart 本身很难实现较为容易的多线程运行 Dart 的每个 **Isolate** 都是内存隔离的,无法直接交互,调用麻烦,通信也麻烦,能干的活也很少, 涉及一点界面就会报错 Flutter 引擎未初始化(因为 Flutter 引擎仅存在于**主 Isolate**) 所以大部分情况下,都是通过**异步**实现一些操作,代价就是性能差,占用多了就卡了,很多大厂软件也长期以来都有这个问题, 很难解决,因为是底层设计的问题,要是 Dart 有 Kotlin 那样的协程,软件性能都不知道能翻多少倍 由于并不是零基础教程,所以不讲基础 * [SDK 镜像](https://docs.flutter.cn/install/archive) * [Pub 镜像](https://docs.flutter.cn/community/china/) 非常建议学习 Flutter 时通关一遍 [Codelab](https://codelabs.developers.google.cn/codelabs/flutter-codelab-first) ## 版本选择 [#版本选择] 除非有什么特殊需求,那么 **Stable 版本**是不太必要的,用 **Beta 版本**即可 ## 项目起名 [#项目起名] 再新建项目时,尽可能地不要起太简单的名字,起 `dart`、`flutter`、`web` 这种名字会和官方包名称冲突! ## 从 main 到具体界面 [#从-main-到具体界面] `main` 自然就是 main,默认它不会有任何操作,要干什么当然是一步一步写啦 `runApp` 是一个非常关键的函数,向它传入的 `Widget` 即软件的根组件 但是,在此之前了,如果用了一些包,有些包可能需要初始化组件,大概会是这样的 ```dart title='示例' final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized(); // 初始化第三方包... runApp(/* 根组件 */); ``` 字面上就能看出来,是为了确保绑定初始化成功,有些第三方包也会使用这个绑定 实际上 `runApp` 内部就会执行一次,只不过没有给个接口调用好让不重复执行而已 可以随便封装一下,比如说像这样 ```dart title='封装示例' void runAppWithBinding(final Widget app, final void Function(WidgetsBinding) initializer) { initializer(WidgetsFlutterBinding.ensureInitialized()); runApp(app); } ``` 然后就可以这样调用 ```dart title='调用示例' runAppWithBinding(/* 根组件 */, (final WidgetsBinding binding) { // 初始化第三方包... }); ``` (和直接调用是一样的,怎么顺心怎么来?) 根组件往往不是一个直接的组件,而是一个用于配置内容的组件再嵌套具体内容的组件 ```dart title='示例' runApp( MaterialApp( title: /* 软件标题 */, theme: ThemeData.light(useMaterial3: true), // MD3 的日间主题 darkTheme: ThemeData.dark(useMaterial3: false), // MD2 的夜间主题(只是随便写的) home: /* 主页组件 */, ), ); ``` 在面临多页面需求时,可以使用 `MaterialApp.router` 等页面路由方案,而非直接的主页面组件 落实到具体页面具体组件时,往往会使用 `StatefulWidget` 或 `StatelessWidget`, 但是为了尽可能优化性能,**绝大部分情况下都不建议使用 `StatefulWidget`** 实际上还有一种组件的创建方式,那就是通过函数返回一个 `Widget`,这种方式实际上和直接定义继承 `StatelessWidget` 的类是一样的,按需使用即可 很多时候,可能会通过 `StatefulWidget` 然后通过 `setState` 更新状态, 这种做法相当不合适,会很大地浪费性能 最好是除非实在没办法,那不然绝不用任何 `StatefulWidget`,不使用它也有很多管理状态的办法 ### 原生状态管理 [#原生状态管理] Flutter 提供了 `ValueNotifier` 等类型,以及对应的 `ValueListenableBuilder` 等组件, 用于较为轻量地监听值的变化,可以满足很多需要,去掉了大部分使用 `StatefulWidget` 的情况 ```dart title='示例' class Page extends StatelessWidget { Page({super.key}); final meow = ValueNotifier('meow'); @override Widget build(final context) => Center( child: ValueListenableBuilder( valueListenable: meow, builder: (final _, final String value, final _) => Text(value), ), ); } ``` ### 第三方包状态管理 [#第三方包状态管理] #### GetX [#getx] > [Pub 地址](https://pub-web.flutter-io.cn/packages/get) 当值较多时,使用 Flutter 自带方案可能就会力不从心了,这时候可以选择 GetX 这个状态管理包 使用 GetX 非常简单,只需要把需要监听的值加上一个 `.obs`,然后通过 `Obx` 组件包裹需要使用值的组件即可 ```dart title='GetX 示例' class Page extends StatelessWidget { Page({super.key}); final meow = 'meow'.obs; @override Widget build(final context) => Center(child: Obx(() => Text(meow.value))); } ``` 当值较多或需要跨页面使用时,也可以定义一个继承 `GetxController` 的类来统一存放, 然后通过 `Get.put` 来创建,通过 `Get.find` 来获取 #### Flutter Hooks [#flutter-hooks] > [Pub 地址](https://pub-web.flutter-io.cn/packages/flutter_hooks) 通过 Flutter 自带的东西或者 GetX 的确可以解决绝大部分状态管理了,但是还有一个硬伤没有解决 有些控制器,比如说动画控制器,文本编辑控制器,它们需要手动管理生命周期,创建、释放等都需要手动管理 如果直接在 `StatelessWidget` 使用,那么生命周期管理不当,直接内存泄漏 通常情况下,通过 `StatefulWidget` 的 `initState`、`dispose` 等实现创建、释放等操作 但只是为了控制器而这样大动干戈实在不应该,Flutter Hooks 提供了一个非常好用的方法, 只需要让组件继承自 `HookWidget`(`StatelessWidget` 的子类), 然后再使用对应的方法即可获取会自动管理生命周期的控制器 这样多管齐下,`StatefulWidget` 基本上是可以完全杜绝了 通过其他方法也可以实现对于控制器的生命周期管理,但是 Flutter Hooks 是一个相当方便的解决方案 ## 懒加载 [#懒加载] Dart 有一个懒加载机制,`import` 时加上一个 `deferred as` 就可以按需加载, 调用它的 `loadLibrary` 即可加载 虽然它仅生效于网页与 Android(并且 Android 的作用有限,基本用不到),但是 Flutter 作为跨平台框架,好好利用特性的相当必要的 <> 尤其是在写跨平台软件或包时,更应当使用(*虽然不知道写包在什么情况下能用*) 虽然并不是很必要,但是还是建议从 `main` 开始就使用懒加载 ```dart title='main 懒加载示例' import 'xxx.dart' deferred as xxx; void main() async => await xxx.loadLibrary().then((_) => xxx.xxx()); ``` 这样做能在网页平台减少 `main.dart.js` 的大小,虽然其他平台用不到,但是这样写也不会对其他平台有什么影响 虽然只是把代码从 `main.dart.js` 转移到其他 `main.dart.js_X.part.js`,并不会减少整体大小,但是基于多方面因素嘛,这样做总归是好的 以此类推,在 `runApp` 之前,以及它运行的页面,都可以懒加载一下 并且在使用 router 时,也可以懒加载一下,这样会对加载速度有巨大提升(当然是得看代码量了) ```dart title='GoRouter 懒加载示例' import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'page1.dart' deferred as page1; import 'page2.dart' deferred as page2; final router = GoRouter( routes: [ GoRoute( path: '/', pageBuilder: (final _, final state) => MaterialPage( key: state.pageKey, child: FutureBuilder( future: page1.loadLibrary(), builder: (final _, final snapshot) => snapshot.connectionState == ConnectionState.done ? page1.Page1() : const Center(child: CircularProgressIndicator()), ), ), routes: [ GoRoute( path: 'page2', pageBuilder: (final _, final state) => MaterialPage( key: state.pageKey, child: FutureBuilder( future: page2.loadLibrary(), builder: (final _, final snapshot) => snapshot.connectionState == ConnectionState.done ? page2.Page2() : const Center(child: CircularProgressIndicator()), ), ), ), ], ), ], ); ``` 这样加载单个页面的速度将会大大加快(因为不使用懒加载的话,所有页面的代码都会放在一起,加载时会一口气全部加载完) 加载其他页面的速度倒是会慢了些,但是对软件的整体体验会提升非常大(毕竟一开始的速度快了不少嘛) # [Re:Next - 从非零开始的代码生活](https://app.niggergo.work/ren) > 非零基础的代码教程 Re:Next 目前有如下教程 Dart `3`+ & Flutter `3`+ 跨平台 # [YumeStarUI](https://app.niggergo.work/yesui) > 简称 YesUI,是为 Flutter/CMP 设计的 UI 库 YumeStarUI 是支持 **Flutter** 以及 **Compose MultiPlatform** 的 UI 库, 将会是 [FWW](https://fvvlang.sbs/widgets/) 的首选 UI 是 NGA / CU NGA 风格的继任,主要为**移动端**与**桌面端**提供良好的**视觉**体验与**交互**体验 UI 风格参考: * [洛可可系列](https://www.latestfile.zip/module/rokyokyo/): 原本的 StarsUI * [NGA SDK](https://app.niggergo.work/nga/): 最初的 NGA 风格 * [Cumulus](https://github.com/chenzyadb/Cumulus-APP): Chenzyadb 的 CU 风格 的 MD3 风格 * [YumeBox](https://yumebox.oom-wg.dev/): 底板 UI 风格实现参考 # [开发 PkgPub](https://app.niggergo.work/purejoy/pkgpub/dev) > PkgPub 的相关配置 PkgPub 旨在快速、便利地建立自己的 Maven 仓库来发布依赖 PkgPub 构建使用 **GitHub Actions**(*手动 & 定时构建*),版本基于 **标签** 通过 Git 仓库远程获取标签,与已构建的标签列表对比, 筛选出**未在配置文件忽略的**并且是**不存在的**或**哈希不对应的**标签 将构建后的仓库部署至独立分支,以便于 Pages 直接部署至自己的域名 ## 配置 [#配置] 在 `maven.fvv` 中可配置以下内容: * `Domain`(`string`): 自定义域名,用于写入至根目录的 `CNAME` * `Repos`(`FVVV` `list`): Git 仓库列表 * `Url`(`string`): Git 仓库链接 * `Dir`(`string`): 可选配置,Gradle 项目所处的子目录 * `Ignore`(`string` `list`): 要忽略的标签列表 (支持 *正则表达式*) # [PkgPub](https://app.niggergo.work/purejoy/pkgpub) > 通过 GitHub Actions 部署的包发布系统 PkgPub 目前有如下文档 开发 *PkgPub* 本身
使用通过 *PkgPub* 部署的 [*回忆溢出工作组*](https://oom-wg.dev/) 的仓库
# [使用 PkgPub](https://app.niggergo.work/purejoy/pkgpub/use) > 使用通过 PkgPub 部署的依赖 在 maven 仓库列表中添加 [*回忆溢出工作组*](https://oom-wg.dev/) 的仓库以使用其依赖 ## 引入依赖源 [#引入依赖源] ```groovy pluginManagement { repositories { maven { url 'https://oom-maven.sawahara.host' content { includeGroupAndSubgroups 'ren.shiror' includeGroupAndSubgroups 'sbs.fvvlang' includeGroupAndSubgroups 'work.niggergo' includeGroupAndSubgroups 'dev.oom-wg' includeGroupAndSubgroups 'dev.oom_wg' } } } } dependencyResolutionManagement { repositories { maven { url 'https://oom-maven.sawahara.host' content { includeGroupAndSubgroups 'ren.shiror' includeGroupAndSubgroups 'sbs.fvvlang' includeGroupAndSubgroups 'work.niggergo' includeGroupAndSubgroups 'dev.oom-wg' includeGroupAndSubgroups 'dev.oom_wg' } } } } ``` ```kotlin pluginManagement { repositories { maven("https://oom-maven.sawahara.host") { content { includeGroupAndSubgroups("ren.shiror") includeGroupAndSubgroups("sbs.fvvlang") includeGroupAndSubgroups("work.niggergo") includeGroupAndSubgroups("dev.oom-wg") includeGroupAndSubgroups("dev.oom_wg") } } } } dependencyResolutionManagement { repositories { maven("https://oom-maven.sawahara.host") { content { includeGroupAndSubgroups("ren.shiror") includeGroupAndSubgroups("sbs.fvvlang") includeGroupAndSubgroups("work.niggergo") includeGroupAndSubgroups("dev.oom-wg") includeGroupAndSubgroups("dev.oom_wg") } } } } ``` ### 镜像源 [#镜像源] 目前有如下源: * `https://oom-maven.sawahara.host`: EdgeOne Pages (全球,包括中国大陆) * `https://maven.oom-wg.dev`: GitHub Pages * `https://raw.githubusercontent.com/OOM-WG/PureJoy/maven`: GitHub