# [项目汇总](https://app.niggergo.work/) > NGA 系列、FVV 系列、PureJoy 欢律遗愉 系列 —— 开发文档 **NGA SDK** 以及其他项目的文档均编写在本开发文档,以帮助使用各个项目 项目 [#项目]
***
***
Re:Next - 从非零开始的代码生活 [#renext---从非零开始的代码生活] 本教程文档用于讲述部分开发实践要点 # [FVV 2 的变化](https://app.niggergo.work/fw/fvv-1-to-2) > FVV 1 至 2 的巨大改进 FVV 作为已经投入生产使用超过一年的语言,在它诞生的第二年,第二个大版本已经完全完成 FVV 2 是 FVV 1 的完全重构版本,在原有语法不变的基础上新增了更多语法,也完善了很多机制 具体变动内容如下: 代码优化 [#代码优化] `C++`/`Dart`/`Go`/`Kotlin` 版本全部完整实现 **解析**/**格式化** 以及 **序列化**/**反序列化** 由于 `Go` 的特性, `map` 是无序的,在格式化时会 `sort` 这里的格式化指的是把 FVV 对象转为字符串,并非直接将原始 FVV 文本重新格式化 解析由原本的扁平循环判断改为词法分析与语法分析分离的递归下降解析器,代码质量大幅提高 FVV 1 在语法出错时只会硬解析,不会报错, FVV 2 引入了 *异常机制*,并且会有具体的行号提示,各语言实现如下: * `C++`: 解析函数会返回字符串,若非空,则为报错内容 * `Dart`: 在解析出错时会直接 `throw` * `Go`: 在解析出错时会返回非 `nil` 的 `error` * `Kotlin`: 在解析出错时会直接 `throw` FVV 2 在代码使用上新增了 *序列化*/*反序列化* (`from`/`to` 或 `Marshal`/`Unmarshal`),使用更加方便 对于 *序列化*/*反序列化*,各语言实现如下: * `C++`: 直接的基本值引用传入或在类中定义绑定函数以传入类 * `Dart`: 继承基本类以定义绑定函数 * `Go`: 基于 `tag` 的反射 * `Kotlin`: 基于 `kotlinx.serialization` 的直接转换 语法优化 [#语法优化] 底层规则 [#底层规则] FVV 2 在解析时能够正常处理 `\r`/`\n`/`\r\n` 文本,不会再强制转为 `\n` 并且 FVV 2 能够正常处理 *转义*,支持 `\b`/`\f`/`\n`/`\r`/`\t`/`\\` 的转义 在 *文本* / *注释* 情况下会依情况额外支持 `\"` / `\”` 或 `\>` 在 FVV 2 中,整数均以 `int64` 存储,而非原本的 `int` 值解析 [#值解析] FVV 2 还支持了更完善的数字机制,支持 *二进制*/*八进制*/*十六进制* 整数解析, 支持在十进制数字中添加 `'`/`’` 作为分隔符 *八进制* 支持包括 `0` / `0o` 开头 在解析布尔值时,可以忽略大小写解析 组闭合的花括号可以用来省略分隔符了(`{ k = v; }` => `{ k = v }`) 字符串与拼接 [#字符串与拼接] 早日列在 *TODO* 的字符串拼接也支持了,可通过基本值或值名称以 `+` 拼接 仅支持基本值拼接!列表由于有赋值展平机制,故不支持直接拼接 非字符串基本值在拼接时为自动转为字符串(而非数学运算), 在列表中也有这样的类似规则,一个混合类型的基本类型列表会类型提升为最高优先级的类型,优先级是这样的: > `字符串` > `浮点数` > `整数` > `布尔值` 不支持基本值与组列表混合 类型提升时是直接提升为最高类型(`布尔值`(`true`) => `字符串`(`"true"`)), **而非**逐级提升(`布尔值`(`true`) => `整数`(`1`) => `浮点数`(`1.0`) => `字符串`(`"true"`)) 拼接时会以原始字符串拼接,而不会解析后再拼接 (原始值 `1'0000`/`TRUE` 拼接为字符串时仍为 `1'0000`/`TRUE`,而非 `10000`/`true`) <> 但列表混合类型由于是解析完成后统一类型提升,是以解析后的值提升的 对于字符串本身,支持了 *原始多行字符串*(使用 ` `` ` 包裹), 解析时不会触发任何转义,并且解析后自动去除最小缩进与头尾空白 字符 [#字符] FVV 2 支持使用 `:` 而非 `=` 来定义值(更 *YAML* 一点?) 全角字符 [#全角字符] 部分常见的全角字符也可在 FVV 2 使用,目前支持如下: * `{}` 与 `{}` * `[]` 与 `[]` * `""` 与 `“”` * `'` 与 `’` * `:` 与 `:` * `;` 与 `;` * `,` 与 `,` 除 `""`/`“”` 外,其他均支持混用(因为当引号混用时,转义将更复杂) # [基本用法](https://app.niggergo.work/fw/fvv) > FVV 的基本用法 **FVV** 全称 **Fresh View for Viewer**,是一个较为简单易上手的语言,名字显而易见,是个 *~~废物~~* **清新** 的文本格式,那么有多清新呢,请看示例 ```plaintext <基本类型> BoolVal = true <布尔值,解析时会忽略大小写> IntVal1 = 1 <十进制整数(整数、浮点数均不支持正号)> IntVal2 = 1000'0000 <有引号的十进制整数> IntVal3 = 0x5 <十六进制整数> IntVal4 = 0o2 <八进制整数,也可写作“02”> IntVal5 = 0b0 <二进制整数> FloatVal = -1.0 <浮点数> StrVal = "字符串" <字符串> <列表> StrList = ["1", "2", "3"] <字符串列表> <不再多写...> <组> Group = { SubVal = "子值" <子值> SubGroup = { SubVal = "子值" <子组子值> } } <普通组> GroupList = [ { SubVal = "组一子值" } <组一> { SubVal = "组二子值" } <组二> ] <组列表> <赋值> Value1 = StrVal <常规赋值> Value2 = "字符串" <这里把注释给赋值了,注释的赋值只会生效于目标是字符串类型的情况,不会隐式转换> List1 = [Value1, Value2] <常规列表赋值> List2 = [List1] <列表赋值列表,会自动展开> Group1.SubVal1 = Value1 <通过点连接组名赋值到子值> Group2.SubVal = Group1.SubVal1 <用同样的方式赋值到另一个子值> Group1 = { SubGroup = { SubVal2 = SubVal1 <展开赋值> } } <赋值遵守就近原则,并且赋值的字符串将按原样存储> <仅有常规赋值会保留原样,列表或注释中的赋值不会保留> <拼接与类型提升> StrVal1 = "字符串" + "字符串" <常规拼接> StrVal2 = StrVal1 + true + 1 + 1.0 <所有非字符串基本类型进行拼接都会提升为字符串> <解析时不会记录拼接的原样,所有拼接中的赋值都会丢失> StrList1 = ["字符串" + "字符串"] <拼接在列表中也可用> StrList2 = [StrVal1, StrList1, true, 1, 1.0] <在列表中也会提升类型为优先级最高的字符串> FloatList1 = [true, 1, 1.0] <字符串之下是浮点数> IntList1 = [true, 1] <浮点数之下是整数> BoolList1 = [true] <整数之下只有布尔值了> <组不支持拼接,列表也不支持拼接(因为已经支持在列表中展开其他列表了)> <并且列表仅允许单一类型,组与基本类型不可以混合存储> <分隔符> Val1 = 1 <用换行省略了分号> Val2 = 2 <显式的分号声明>; ValList1 = [ 1 2 3 ] <用换行省略了逗号> ValList2 = [ 1, 2, 3, ] <显式的逗号声明(末尾是否有逗号不影响解析)> <其他字符> Key1: "Value" <冒号与等号等价> Key2:"Value" <全角冒号同样可用> IntVal = 1000’0000 <有全角引号的整数> Say1 = "\"Hello\" “Hello”" <常规字符串> Say2 = “"Hello" “Hello\”” <用全角引号包裹的字符串> <可以看到转译也会因此改变> Say3 = ` "Hello" “Hello” ` <用反引号包裹的原始字符串,解析时会去掉开头与结尾的空白,并且去掉最小缩进> Grp ={Lst1 =[Key1, Key2]}<全角括号(用花括号省略了应该写在“Lst1”定义后的分号)> Lst2 = ["Value",];<全角分隔符> <除引号外,其他全角字符均可与半角字符混用> ``` 显而易见,支持 `字符串`、`浮点数`、`整数`、`布尔值` 以及它们的 `列表` 的定义,还有 `组` 本身的 `列表` **命名定义** ``` 组 = { 文本 = "" 文本列表 = [""] 组列表 = [{}] } ``` * `组` 为 *组名称*,`文本`/`文本列表`/`组列表` 为 *值名称* * `""`/`[""]`/`[{}]` 为值,`{}` 称为组 故基本类型名称为 `字符串`/`文本`、`浮点数`、`整数`、`布尔值`,`组` 为特殊类型 列表类型即为 `字符串列表`/`文本列表`、`浮点数列表`、`整数列表`、`布尔值列表` 以及 `组列表` FVV `2` 在 FVV `1` 的基础上有了大幅度改进,详见 [此处](./fvv-1-to-2.mdx) 值的命名也是没有什么忌口的,但需要注意的是, 如果一个值被命名为**纯数字**(仅 `整数`,因为 `浮点数` 会被分割为双层组), 那么这个值将**无法用于赋值**,因为没法判断到底给的是值还是值的名称 同样的,还需要注意正常命名不要带 `.` 啊喂,会被认为是多层组的定义的 值的定义使用 `;` 或 `换行` 进行, 所有的值都可以置于根路径的 `{}` 里面(放不放都没区别,只不过加一个 `{}` 看起来类 *JSON* 一点?), 块注释使用 `<>`,没有行注释 `换行` 可以省略掉以下字符: * `;`: 当定义值时有 `换行` 则无需在行尾添加 `;` * `,`: 当定义组时有 `换行` 则无需在两个值之间添加 `,` `}` 也可省略分隔符 注释是 FVV 一个比较特色的功能,它可以放到任何地方,请看示例: ``` <注释>{<注释>a<注释>=<注释>1<注释>;<注释>}<注释> ``` 可以非常直接地看出来,注释完全是想怎么写就怎么写,但是它的作用不止于此 介于 `=` 与 `;`(或 `换行`)之间的**最后一个**注释,将会被认定为是该值的 `描述`,在解析时将会被留存为该值的附属值 因为 `换行` 可以替代 `;`,`描述` 又是和定义一样通过 `换行`/`;` 来解析的, 所以 `描述` 不可以写到定义的**下一行**! 为了让 **FWW** 有更好的开发体验, `组列表` 及其子值的 `描述` 是放在前面的 `描述` 是支持**赋值**的,也就是说任意字符串都有可能赋值到 `描述` 上,只要 `描述` 与该字符串的值名称相同 所以在实践中,建议在编写 `描述` 时,在开头加一个 `- `,从而实现类似于 `<- 这是注释>` 的效果, 如果字体支持,`<-` 会变成一个好看的箭头,这样既不会与赋值机制有冲突,还可以提升观感 代码层使用方法 [#代码层使用方法] 仅支持 `UTF-8` 文本(或 `UTF-8 with BOM` ), `\r` / `\n` / `\r\n` 文本均可正常解析 引入依赖 [#引入依赖] 在项目中添加文件后,直接 `#include` 即可: ```cpp #include "fvv.hh" ``` > [Pub 发布页面](https://pub-web.flutter-io.cn/packages/fvv) > > [Dart API 文档](https://pub-web.flutter-io.cn/documentation/fvv/latest/fvv/) 先添加依赖: ```shell dart pub add fvv ``` ```shell flutter pub add fvv ``` 然后直接 `import` 即可: ```dart import 'package:fvv/fvv.dart'; ``` 先添加依赖 在 `go.mod` 中通过 `replace` 指向本地路径,并通过 `require` 添加依赖 ```plaintext title='示例' replace app.niggergo.work/fvv => path/fvv/go require app.niggergo.work/fvv v0.0.0 ``` 远程依赖仅可在 Go `1.25` \+ 可用! 通过命令行添加依赖: ```shell go get -u app.niggergo.work/fvv@latest ``` 然后 `import` 即可: ```go import "app.niggergo.work/fvv" ``` 先依赖源中有 [PkgPub](/purejoy/pkgpub/use) 的依赖源 然后添加依赖: ```groovy dependencies { implementation("ren.shiror.fvv:core:2.+") } ``` 再直接 `import` 即可: ```kotlin import ren.shiror.fvv.FVVV ``` 基本功能 [#基本功能] 示例代码都是瞎写的,不要依此为参照 对于开发体验着重在 `C++` 与 `Go` 上以及 `Dart` 与 `Kotlin` 上,在一起的语言开发体验较为相似 解析 [#解析] 由于 `Go` 的语言特性,其函数名称不得不大写 <> 由于 `Dart` 与 `Go` 不支持函数重载,为了保证长期维护的可能性,额外添加了 `String` 后缀 ```cpp FVV::FVVV fvv; fvv.parse(txt); ``` ```dart final fvv = FVVV()..parseString(txt); ``` ```go fvv := NewFVVV() fvv.ParseString(txt) ``` ```kotlin val fvv = FVVV().apply { parse(txt) } ``` 格式化 [#格式化] 当传入以下内容时,会有不同的格式化效果: * `Common`: 默认 * `UseWrapper`: 最外面包裹一层 *花括号* * `Minify`: 最小化,移除所有 *缩进*、*空格*、*换行* * `UseCRLF`: 使用 `\r\n` 换行,默认 `\n` * `UseCR`: 使用 `\r` 换行,默认同上 * `UseSpace2`: 使用 `2` 个 **空格** 作为 **缩进** ,默认是 `\t` * `UseSpace4`: 使用 `4` 个 **空格** 作为 **缩进** * `IntBinary`: 输出整数为 *二进制* 格式,默认是 *十进制* * `IntOctal`: 输出整数为 *八进制* 格式,默认同上 * `IntHex`: 输出整数为 *十六进制* 格式,默认同上 * `DigitSep3`: 输出 *十进制整数* 或 *浮点数的整数部分* 时每 `3` 个数字插入一个分隔符 * `DigitSep4`: 同上,每 `4` 个数字插入一个分隔符 * `UseColon`: 使用 *冒号* 而非默认的 *等号* 作为定义符 * `FullWidth`: 将部分符号改为 **全角** * `KeepListSingle`: 强制 *列表* 保持为一行(默认长度达到 `16` 的内容达到 `6` 则一行一个) * `ForceUseSeparator`: 强制添加 *分隔符*(最小化时无效) * `RawMultilineString`: 当 *字符串* 有多行时改为 *原始多行缩进字符串*(最小化时无效) * `NoDescs`: 移除所有 `描述`(在 *FWW 风格* 下对 `组列表` 与 `组` 无效) * `NoLinks`: 移除所有 `链接` * `FlattenPaths`: 递归展平只有 `1` 个值的 `组` * `FWWStyle`: FWW 风格,前置 `组` 的 `描述` 各个语言的格式化选项位置如下: * `C++`: `FVV::FormatOpt` * `Dart`: `FormatOpt` * `Go`: 以 `FmtOpt` 开头的值 * `Kotlin`: `FVVV.FormatOpt` 不同的语言在命名上略有不同,但实际行为一致 传入均支持通过 `|`/`or` 传入为一个参数或通过 `,` 传入为多个参数 当使用 `Dart` / `Kotlin` 默认的 `toString` 时( `"$fvv"` ),将使用默认的格式化效果 一切去除 `描述`/`链接` 的行为均仅为在输出的字符串中去除,不会影响到代码中的实际内容 ```cpp fvv.to_string(opts...); ``` ```dart fvv.toString(opts...); ``` ```go fvv.ToString(opts...) ``` ```kotlin fvv.toString(opts...) ``` 使用值 [#使用值] 由于 `JVM` 机制,在 `Kotlin` 上 *判断*/*获取* 列表时请使用对应的 `isList()`/`list()` 方法,否则会类型出错 首先是对值的基本判断 由于 `Go` 的语法可用性太低了,便又封装了 `IsNodesEmpty` 和 `IsNodesNotEmpty` 来判断子项是否为空, 但使用前最好还是先判断一下值本身是否为 `nil` ```cpp bool is_empty = fvv.empty(); // 判断值是否为空 bool is_type = fvv.is(); // 判断类型 is_type = fvv.is_list(); // 判断列表类型(相当于 fvv.is>()) ``` ```dart final isEmpty = fvv.isEmpty && !fvv.isNotEmpty; // 判断值是否为空 var isType = fvv.isType(); // 判断类型 isType = fvv.isList(); // 判断列表类型(相当于 fvv.isType>()) ``` ```go is_empty := fvv.IsEmpty() && !fvv.IsNotEmpty() // 判断值是否为空 is_empty = fvv.IsNodesEmpty() && !fvv.IsNodesNotEmpty() // 判断子值是否为空 is_type := fvv.IsBool() // 判断类型(也可使用 fvv.Is[T](fwv),此处的 fvv 是 fvv 包) is_type = fvv.IsBoolList() // 判断列表类型(也可使用 fvv.IsList[T](fwv),此处的 fvv 是 fvv 包) // 其他依此类推,不再示范... ``` ```kotlin val isEmpty = fvv.isEmpty() && !fvv.isNotEmpty() // 判断值是否为空 var isType = fvv.`is`() || fvv.isType() // 判断类型(不适用于列表) isType = fvv.isList() // 判断列表类型 ``` 由于 FVV 2 统一采用 `int64` 为整数类型,对于各语言的兼容性如下: * `C++`: 使用 `long long`/`vector` 类型, 存储时有兼容其他整数类型,但判断时需要使用 `long long`/`vector` * `Dart`: 由于没有细分类型,无变化 * `Go`: 由于 `Go` 没有重载,在判断中对 `int`/`[]int` 做了兼容,使用时尽量用 `int64`/`[]int64` * `Kotlin`: 在存储与判断时均有 `Int`/`List` 转 `Long`/`List` 与 `Float`/`List` 转 `Double`/`List` *** 然后是读取值 由于 `Dart` 机制,常规获取 `List` 类型得到的是引用,拷贝请再调用 `toList()` <> <>在 `Kotlin` 获取 `List` 类型时由于 `JVM` 机制无法确认类型,总会创建一个新的固定类型的 `List`,不会获取到引用 ```cpp T value = fvv.value(/*默认值*/); // 指定类型获取值 vector list = fvv.list(/*默认值*/); // 指定类型获取列表 ``` ```dart final value = fvv.as(/*默认值*/) ?: fvv.asType(/*默认值*/) ?: fvv.get(); // 指定类型获取值 //(当不确定类型时,使用 as/asType,返回 T?,确定时则可使用 get,返回 T) final list = fvv.list(/*默认值*/); // 指定类型获取列表 final boolValue = fvv.boolean; // 获取布尔值 final boolList = fvv.bools; // 获取布尔值列表 // 其他依此类推,不再示范... ``` ```go value := fvv.Value[T](fwv); // 指定类型获取值(此处的 fvv 是 fvv 包) list := fvv.List[T](); // 指定类型获取列表(此处的 fvv 是 fvv 包) bool_value := fvv.Bool(/*默认值*/); // 获取布尔值 bool_list := fvv.BoolList(/*默认值*/); // 获取布尔值列表 // 其他依此类推,不再示范... ``` ```kotlin val value = fvv.`as`(/*默认值*/) ?: fvv.asType(/*默认值*/) ?: fvv.get(); // 指定类型获取值 //(当不确定类型时,使用 as/asType,返回 T?,确定时则可使用 get,返回 T) val list = fvv.list(/*默认值*/); // 指定类型获取列表 val boolValue = fvv.bool; // 获取布尔值 val boolList = fvv.bools; // 获取布尔值列表 // 其他依此类推,不再示范... ``` 由于 `Dart` 的基本类型名称会冲突,但为了尽可能与 `Kotlin` 一致,目前的 getter 如下: * `bool`(`Kotlin`)、`boolean`(`Dart`&`Kotlin`) * `int`(`Kotlin`)、`integer`(`Dart`&`Kotlin`) * `double`(`Kotlin`)、`float`(`Dart`&`Kotlin`) * `string`、`bools`、`ints`、`doubles`、`strings`、`fvvvs`(`Dart`&`Kotlin`) 赋值只需要在有重写运算符功能的语言上直接赋值即可,没有则需要手动调用内部的值进行赋值 序列化/反序列化 [#序列化反序列化] *序列化* 时会去除所有链接 *序列化*/*反序列化* 在各语言上均可使用,但差异较大 `C++` 可以直接通过 `to`/`from` 传入值引用,或在类中定义 `fvv_values` 函数以绑定名称与引用 <> `parse` 支持传入第二个参数以相当于执行 `parse`+\`to `Dart` 需要定义继承 `FVVStruct` 的类,在 `fvvValues` 函数中绑定名称并设置 `getter`/`setter`, 当需要处理嵌套的 `FVVStruct` 列表时,需要传入 `factory` 以创建该 `FVVStruct` <> 调用只需要通过 `to`/`from` 传入即可 <> `parseString` 支持传入第二个参数以相当于执行 `parseString`+`to` `Go` 可在 `struct` 使用 `` `fvv:"值名称"` ``,支持 `omitempty` <> 调用只需要通过 `Unmarshal`/`Marshal` 传入即可 <> `ParseString` 支持传入多个参数以相当于执行 `parseString`+`Unmarshal` `Kotlin` 基于 `serialization` 实现 <> `parse` 支持传入类型以相当于执行 `parse`+`Unmarshal` 拓展插件 [#拓展插件] [GitHub](https://github.com/OOM-WG/FVV/tree/fw/ext/mt) 中有 *MT 管理器* 的拓展 *VS Code* 的拓展则可以直接在应用商店中搜索到 # [FWW](https://app.niggergo.work/fw/fww) > FWW 的基本用法 **FWW** 全称 **FVV Widget**,是基于 FVV 语法为图形化所立的标准, 是在**不变更任何 FVV 原有功能**的情况下,为编写界面而仅在**语法规则**上所立的标准 尽管 FVV 本身并不是独立语言,但是 FWW 通过其他语言解析 FWW 的配置文件从而生成界面 因为只是在语法规则上所立的标准,所以大部分都只是在值名称等地方有所规定, 如果已经了解如何编写 FVV,那么编写 FWW 也是十分容易的 基本规则 [#基本规则] FWW 的所有页面都是基于 `组列表` 实现的(类似于 `Column`),例如如下示例 ```plaintext title='示例' Page = <这是页面标题呢> [ { Txt = "这是第一个文本呢" } { Txt = "这是第二个文本呢" } ] ``` 所有页面都是基于如上的 `组列表` 实现的,`组列表` 可以读取外部 FVV 的定义,因此在实现多语言等方面也是比较方便的 具体的规范如下: * `组列表` 本身的 `描述` 是其 `页面标题` * `组列表` 内的每个 `组` 的 `描述` 是其 `组件类型` * `组列表` 内的每个 `组` 的具体定义则因具体 `组件类型` 而异 * `页面标题` 应当写在 `=` 与 `[]` 之间 * `组件类型` 应当写在 `{}` 前面(但应与 `{` 在同一行,否则语法错误) # [FVV](https://app.niggergo.work/fw) FVV 目前支持如下语言 C++ `11` \+ Dart `3` \+ Go `1.18` \+ *Kotlin 多平台* *** 快速开始! *** FVV 图形化 # [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/src/go require app.niggergo.work/sdk/nga v0.0.0 ``` 远程依赖仅可在 Go `1.25` \+ 可用! 通过命令行添加依赖: ```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 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.e_war.sandbox: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` 以确保反射运行正常 # [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) # [Re:Next - Flutter](https://app.niggergo.work/ren/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`+ 跨平台 # [FYTxt 多语言框架](https://app.niggergo.work/purejoy/fytxt) 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 { // 配置内容... } ``` 支持的配置内容如下: ' }, langAliases: { required: false, description: '语言与其对应的正则表达式', type: 'Map' }, defaultLang: { required: true, description: '默认语言', type: 'String' }, composeGen: { required: true, description: '为 Compose MultiPlatform/Jetpack Compose 生成代码', type: 'Boolean' }, internalClass: { required: true, description: '生成的代码使用 internal 修饰符', type: 'Boolean', default: 'true' } }} /> 使用方法 [#使用方法] 配置概念 [#配置概念] 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*,且其他可选选项均为默认配置): ```kotlin internal object`FYTxt`{init{`FYTxtGroups`} internal enum class`FYTxtGroups`:FYTxtGroup{`Common`{override val stats=mapOf(`FYTxtTags`.EN to 0.5,`FYTxtTags`.ZH to 1.0)};companion object{init{FYTxtConfig.init(`Common`,`FYTxtTags`.entries)}}} internal enum class`FYTxtTags`:FYTxtTag{EN{override val pattern=null},ZH{override val pattern="""^ZH_(?!.*(HANS|CN|SG)).*""".toRegex()}} /**你好 */ val`Hello`get()=FYTxtConfig.activeTags.value.firstNotNullOfOrNull{it as`FYTxtTags` when(it){ `FYTxtTags`.EN->"""Hello""" `FYTxtTags`.ZH->"""你好""" else -> null} }?:"""你好""" object`Home`{init{`FYTxtGroups`} /**欢迎%s *@suppress Common: EN */ val`Welcome`get()=FYTxtConfig.activeTags.value.firstNotNullOfOrNull{it as`FYTxtTags` when(it){ `FYTxtTags`.ZH->"""欢迎%s""" else -> null} }?:"""欢迎%s""" } } ``` 对如上代码的解释: * `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* 可用) 包发布系统 # [开发 PkgPub](https://app.niggergo.work/purejoy/pkgpub/dev) 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) PkgPub 目前有如下文档 开发 *PkgPub* 本身
使用通过 *PkgPub* 部署的 [*回忆溢出工作组*](https://oom-wg.dev/) 的仓库
# [使用 PkgPub](https://app.niggergo.work/purejoy/pkgpub/use) 在 maven 仓库列表中添加 [*回忆溢出工作组*](https://oom-wg.dev/) 的仓库以使用其依赖 引入依赖源 [#引入依赖源] ```groovy pluginManagement { repositories { maven { url 'https://oom-maven.sawahara.host' content { includeGroupAndSubgroups 'ren.shiror' includeGroupAndSubgroups 'work.niggergo' includeGroupAndSubgroups 'dev.oom-wg' } } } } dependencyResolutionManagement { repositories { maven { url 'https://oom-maven.sawahara.host' content { includeGroupAndSubgroups 'ren.shiror' includeGroupAndSubgroups 'work.niggergo' includeGroupAndSubgroups 'dev.oom-wg' } } } } ``` ```kotlin pluginManagement { repositories { maven("https://oom-maven.sawahara.host") { content { includeGroupAndSubgroups("ren.shiror") includeGroupAndSubgroups("work.niggergo") includeGroupAndSubgroups("dev.oom-wg") } } } } dependencyResolutionManagement { repositories { maven("https://oom-maven.sawahara.host") { content { includeGroupAndSubgroups("ren.shiror") includeGroupAndSubgroups("work.niggergo") 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