diff --git a/.github/scripts/lzy_web.py b/.github/scripts/lzy_web.py
new file mode 100644
index 000000000..5ce7b30d9
--- /dev/null
+++ b/.github/scripts/lzy_web.py
@@ -0,0 +1,98 @@
+import requests, os, datetime, sys
+
+# Cookie 中 phpdisk_info 的值
+cookie_phpdisk_info = os.environ.get('phpdisk_info')
+# Cookie 中 ylogin 的值
+cookie_ylogin = os.environ.get('ylogin')
+
+# 请求头
+headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45',
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
+ 'Referer': 'https://pc.woozooo.com/account.php?action=login'
+}
+
+# 小饼干
+cookie = {
+ 'ylogin': cookie_ylogin,
+ 'phpdisk_info': cookie_phpdisk_info
+}
+
+
+# 日志打印
+def log(msg):
+ utc_time = datetime.datetime.utcnow()
+ china_time = utc_time + datetime.timedelta(hours=8)
+ print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}")
+
+
+# 检查是否已登录
+def login_by_cookie():
+ url_account = "https://pc.woozooo.com/account.php"
+ if cookie['phpdisk_info'] is None:
+ log('ERROR: 请指定 Cookie 中 phpdisk_info 的值!')
+ return False
+ if cookie['ylogin'] is None:
+ log('ERROR: 请指定 Cookie 中 ylogin 的值!')
+ return False
+ res = requests.get(url_account, headers=headers, cookies=cookie, verify=True)
+ if '网盘用户登录' in res.text:
+ log('ERROR: 登录失败,请更新Cookie')
+ return False
+ else:
+ log('登录成功')
+ return True
+
+
+# 上传文件
+def upload_file(file_dir, folder_id):
+ file_name = os.path.basename(file_dir)
+ url_upload = "https://up.woozooo.com/fileup.php"
+ headers['Referer'] = f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={cookie_ylogin}'
+ post_data = {
+ "task": "1",
+ "folder_id": folder_id,
+ "id": "WU_FILE_0",
+ "name": file_name,
+ }
+ files = {'upload_file': (file_name, open(file_dir, "rb"), 'application/octet-stream')}
+ res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120).json()
+ log(f"{file_dir} -> {res['info']}")
+ return res['zt'] == 1
+
+
+# 上传文件夹内的文件
+def upload_folder(folder_dir, folder_id):
+ file_list = sorted(os.listdir(folder_dir), reverse=True)
+ for file in file_list:
+ path = os.path.join(folder_dir, file)
+ if os.path.isfile(path):
+ upload_file(path, folder_id)
+ else:
+ upload_folder(path, folder_id)
+
+
+# 上传
+def upload(dir, folder_id):
+ if dir is None:
+ log('ERROR: 请指定上传的文件路径')
+ return
+ if folder_id is None:
+ log('ERROR: 请指定蓝奏云的文件夹id')
+ return
+ if os.path.isfile(dir):
+ upload_file(dir, str(folder_id))
+ else:
+ upload_folder(dir, str(folder_id))
+
+
+if __name__ == '__main__':
+ argv = sys.argv[1:]
+ if len(argv) != 2:
+ log('ERROR: 参数错误,请以这种格式重新尝试\npython lzy_web.py 需上传的路径 蓝奏云文件夹id')
+ # 需上传的路径
+ upload_path = argv[0]
+ # 蓝奏云文件夹id
+ lzy_folder_id = argv[1]
+ if login_by_cookie():
+ upload(upload_path, lzy_folder_id)
\ No newline at end of file
diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml
new file mode 100644
index 000000000..9f991df64
--- /dev/null
+++ b/.github/workflows/automerge.yml
@@ -0,0 +1,29 @@
+name: Auto merge
+on:
+ pull_request:
+ types:
+ - labeled
+ - unlabeled
+ - synchronize
+ - opened
+ - edited
+ - ready_for_review
+ - reopened
+ - unlocked
+ pull_request_review:
+ types:
+ - submitted
+ check_suite:
+ types:
+ - completed
+ status: {}
+jobs:
+ automerge:
+ runs-on: ubuntu-latest
+ steps:
+ - id: automerge
+ name: automerge
+ uses: "pascalgn/automerge-action@v0.16.2"
+ env:
+ MERGE_FILTER_AUTHOR: "jing332"
+ GITHUB_TOKEN: ${{ secrets.TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml
new file mode 100644
index 000000000..3bc960bdb
--- /dev/null
+++ b/.github/workflows/debug.yml
@@ -0,0 +1,72 @@
+name: Build Debug
+
+on:
+ workflow_dispatch:
+
+jobs:
+ go-lib:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-go@v4
+ with:
+ go-version: 1.19.1
+ cache-dependency-path: ${{ github.workspace }}/tts-server-lib
+
+ - name: Build tts-server-lib
+ run: |
+ cd tts-server-lib
+ go install golang.org/x/mobile/cmd/gomobile@latest
+ gomobile init
+ go get golang.org/x/mobile/bind
+ gomobile bind -ldflags "-s -w" -v -androidapi=19
+ cp -f *.aar $GITHUB_WORKSPACE/app/libs
+
+ - uses: actions/upload-artifact@v3.1.0
+ with:
+ name: tts-server-lib
+ path: tts-server-lib/*.aar
+
+ build:
+ needs: go-lib
+ runs-on: ubuntu-latest
+ env:
+ outputs_dir: "${{ github.workspace }}/app/build/outputs"
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/download-artifact@v3
+ with:
+ name: tts-server-lib
+ path: ${{ github.workspace }}/app/libs
+
+ - uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 17
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Init Signature
+ run: |
+ touch local.properties
+ echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties
+ echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties
+ echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties
+ echo KEY_PATH='./key.jks' >> local.properties
+ # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks
+ echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks
+
+ - name: Build with Gradle
+ run: |
+ chmod +x gradlew
+ ./gradlew assembleAppDebug --build-cache --parallel --daemon --warning-mode all
+
+ - name: Upload APK To Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: "TTS-Server_debug"
+ path: ${{env.outputs_dir}}/apk/app/debug/*debug.apk
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6876be1fd..e4fe329d4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -2,97 +2,108 @@ name: Build Release
on:
push:
- branches:
+ branches:
- "master"
paths:
- "CHANGELOG.md"
workflow_dispatch:
jobs:
+ golib:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-go@v4
+ with:
+ go-version: 1.20.3
+ cache-dependency-path: ${{ github.workspace }}/tts-server-lib
+
+ - name: Build tts-server-lib
+ run: |
+ cd tts-server-lib
+ go install golang.org/x/mobile/cmd/gomobile
+ gomobile init
+ go get golang.org/x/mobile/bind
+ gomobile bind -ldflags "-s -w" -v -androidapi=21
+ cp -f *.aar $GITHUB_WORKSPACE/app/libs
+
+ - uses: actions/upload-artifact@v3.1.0
+ with:
+ name: tts-server-lib
+ path: tts-server-lib/*.aar
+
build:
+ needs: golib
+ strategy:
+ matrix:
+ product: [ { name: "App原版", value: app } ]
+
+ fail-fast: false
runs-on: ubuntu-latest
+ env:
+ product: ${{ matrix.product.value }}
+ product_name: ${{matrix.product.value}}
+ outputs_dir: "${{ github.workspace }}/app/build/outputs"
+ ver_name: ""
+
steps:
- - uses: actions/checkout@v3
-
- - name: Set up Go
- uses: actions/setup-go@v3
- with:
- go-version: 1.19.1
-
- - uses: actions/cache@v3
- with:
- path: |
- ~/.cache/go-build
- ~/go/pkg/mod
- key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ runner.os }}-go-
-
- - name: Build Go Lib
- run: |
- cd tts-server-lib
- go install golang.org/x/mobile/cmd/gomobile@latest
- gomobile init
- go get golang.org/x/mobile/bind
- gomobile bind -ldflags "-s -w" -v -target="android/arm,android/arm64" -androidapi=19
- cp -f *.aar $GITHUB_WORKSPACE/app/libs
-
- - name: Upload to Artifact
- uses: actions/upload-artifact@v3.1.0
- with:
- name: tts-server-lib
- path: |
- tts-server-lib/*.aar
- tts-server-lib/*.jar
-
- - uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
-
- - name: Init Sign
- run: |
- touch local.properties
- echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties
- echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties
- echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties
- echo KEY_PATH='./key.jks' >> local.properties
- # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks
- echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks
-
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
- - name: Build with Gradle
- run: ./gradlew assembleRelease --build-cache --parallel --daemon --warning-mode all
-
- - name: Organize the Files
- run: |
- mkdir -p ${{ github.workspace }}/apk/
- rm -f ${{ github.workspace }}/apk/*
- cp ${{ github.workspace }}/app/build/outputs/apk/*/*.apk ${{ github.workspace }}/apk/
- cp ${{ github.workspace }}/app/build/outputs/apk/*/*.json ${{ github.workspace }}/apk/
-
- - name: Get VerName
- run: |
- echo "ver_name=$(grep 'versionName' apk/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV
-
- - name: Upload App To Artifact
- uses: actions/upload-artifact@v3
- with:
- name: TTS-Server_${{ env.ver_name }}
- path: ${{ github.workspace }}/apk/*.apk
-
- - uses: softprops/action-gh-release@v0.1.14
- with:
- name: ${{ env.ver_name }}
- tag_name: ${{ env.ver_name }}
- body_path: ${{ github.workspace }}/CHANGELOG.md
- draft: false
- prerelease: false
- files: ${{ github.workspace }}/apk/*apk
- env:
- GITHUB_TOKEN: ${{ secrets.TOKEN }}
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - uses: actions/download-artifact@v3
+ with:
+ name: tts-server-lib
+ path: ${{ github.workspace }}/app/libs
+
+ - uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 17
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Init Signature
+ run: |
+ touch local.properties
+ echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties
+ echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties
+ echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties
+ echo KEY_PATH='./key.jks' >> local.properties
+ # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks
+ echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks
+
+ - name: Build with Gradle
+ run: |
+ chmod +x gradlew
+ ./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all
+
+ - name: Init environment variable
+ run: |
+ echo "ver_name=$(grep -m 1 'versionName' ${{ env.outputs_dir }}/apk/${{ env.product }}/release/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV
+
+ - name: Upload Mappings to Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: mappings_${{ env.product }}_${{ env.ver_name }}
+ path: ${{ env.outputs_dir }}/mapping/*/*.txt
+
+ - name: Upload APK To Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: "TTS-Server_${{ env.product }}_${{ env.ver_name }}"
+ path: ${{env.outputs_dir}}/apk/${{ env.product }}/release/*${{ env.ver_name }}.apk
+
+
+ - uses: softprops/action-gh-release@v0.1.15
+ with:
+ name: ${{ env.ver_name }}
+ tag_name: ${{ env.ver_name }}
+ body_path: ${{ github.workspace }}/CHANGELOG.md
+ draft: false
+ prerelease: false
+ files: ${{env.outputs_dir}}/apk/${{ env.product }}/release/*.apk
+ env:
+ GITHUB_TOKEN: ${{ secrets.TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d88c958d1..04ad2e6e2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,59 +4,72 @@ on:
push:
branches:
- "master"
+ - "compose"
paths-ignore:
- - "README.md"
- - "CHANGELOG.md"
-
+ - "**.md"
workflow_dispatch:
jobs:
- build:
+ go-lib:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- - name: Set up Go
- uses: actions/setup-go@v3
+ - uses: actions/setup-go@v4
with:
- go-version: 1.19.1
+ go-version: 1.20.3
+ cache-dependency-path: ${{ github.workspace }}/tts-server-lib
- - uses: actions/cache@v3
- with:
- path: |
- ~/.cache/go-build
- ~/go/pkg/mod
- key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ runner.os }}-go-
-
- - name: Build Go Lib
+ - name: Build tts-server-lib
run: |
cd tts-server-lib
- go install golang.org/x/mobile/cmd/gomobile@latest
+ go install golang.org/x/mobile/cmd/gomobile
gomobile init
go get golang.org/x/mobile/bind
- gomobile bind -ldflags "-s -w" -v -target="android/arm,android/arm64" -androidapi=19
+ gomobile bind -ldflags "-s -w" -v -androidapi=21
cp -f *.aar $GITHUB_WORKSPACE/app/libs
- - name: Upload to Artifact
- uses: actions/upload-artifact@v3.1.0
+ - uses: actions/upload-artifact@v3.1.0
with:
name: tts-server-lib
- path: |
- tts-server-lib/*.aar
- tts-server-lib/*.jar
+ path: tts-server-lib/*.aar
+
+ build:
+ needs: go-lib
+ strategy:
+ matrix:
+ product: [ { name: "App原版", value: app, lzyId: "9493563" }, { name: "Dev共存版", value: dev, lzyId: "7381570" } ]
+
+ fail-fast: false
+ runs-on: ubuntu-latest
+ env:
+ product: ${{ matrix.product.value }}
+ product_name: ${{ matrix.product.value }}
+ lzy_folder_id: ${{ matrix.product.lzyId }}
+ ylogin: ${{ secrets.LANZOU_ID }}
+ phpdisk_info: ${{ secrets.LANZOU_PSD }}
+ outputs_dir: "${{ github.workspace }}/app/build/outputs"
+ ver_name: ""
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
- - uses: actions/cache@v3
+ - uses: actions/download-artifact@v3
with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
-
- - name: Init Sign
+ name: tts-server-lib
+ path: ${{ github.workspace }}/app/libs
+
+ - uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 17
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Init Signature
run: |
touch local.properties
echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties
@@ -65,24 +78,40 @@ jobs:
echo KEY_PATH='./key.jks' >> local.properties
# 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks
echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
- - name: Build with Gradle
- run: ./gradlew assembleRelease --build-cache --parallel --daemon --warning-mode all
- - name: Organize the Files
+ - name: Build with Gradle
run: |
- mkdir -p ${{ github.workspace }}/apk/
- rm -f ${{ github.workspace }}/apk/*
- cp ${{ github.workspace }}/app/build/outputs/apk/*/*.apk ${{ github.workspace }}/apk/
- cp ${{ github.workspace }}/app/build/outputs/apk/*/*.json ${{ github.workspace }}/apk/
+ chmod +x gradlew
+ ./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all
- - name: Get VerName
+ - name: Set Version Name
run: |
- echo "ver_name=$(grep 'versionName' apk/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV
+ echo "ver_name=$(grep -m 1 'versionName' ${{ env.outputs_dir }}/apk/${{ env.product }}/release/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV
+
+ # - name: Set APK Path
+ # run: |
+ # echo "apk_path=${{ env.outputs_dir }}/apk/${{ env.product }}/release/${{ env.product }}-release.apk" >> $GITHUB_ENV
+
+ # - name: Upload Mappings to Artifact
+ # uses: actions/upload-artifact@v3
+ # with:
+ # name: mappings_${{ env.product }}_${{ env.ver_name }}
+ # path: ${{ env.outputs_dir }}/mapping/*/*.txt
- - name: Upload App To Artifact
+ - name: Upload APK To Artifact
uses: actions/upload-artifact@v3
with:
- name: TTS-Server_${{ env.ver_name }}
- path: ${{ github.workspace }}/apk/*.apk
\ No newline at end of file
+ name: "TTS-Server_${{ env.product }}_${{ env.ver_name }}"
+ path: ${{env.outputs_dir}}/apk/${{ env.product }}/release/*${{ env.ver_name }}.apk
+
+
+ - name: Upload APK To Lanzouyun
+ run: |
+ mkdir apk
+ cp ${{ env.outputs_dir }}/apk/${{ env.product }}/release/*${{ env.ver_name }}.apk "${{ github.workspace }}/apk"
+ path="${{ github.workspace }}/apk"
+ python3 ${{ github.workspace }}/.github/scripts/lzy_web.py $path "${{ env.lzy_folder_id }}"
+
+# echo "app: https://jing332.lanzn.com/b09jpjd2d"
+# echo "dev: https://jing332.lanzn.com/b09ig9qla"
+# echo "密码PWD: 1234"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 951e883e1..4852ad8e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,6 @@
*.iml
+.idea
.gradle
-/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d33521a..000000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index bdda35ad9..000000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-TTS Server
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index fb7f4a8a4..000000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 526b4c25c..000000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 48719d61d..000000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f4..000000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43f3b0611..07b1d312e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
-1. 🥰 新增Creation接口(来自微软Speech Studio Demo),本接口基本与Azure相同,且服务器在东南亚,延迟低、更稳定。
-打开网页版(Azure),开启测试按钮上方的 "使用Creation接口" 选项以使用。
-2. 根据日志等级上色。
-3. 新的桌面快捷方式图标。
-4. 长按快捷开关(Android7+)自动跳转到APP
+- 有好的配置导入提示
+- 添加三种主题色
+- 新用户自动添加默认配置
+- 添加帮助文档,并自动显示
+- 朗读规则支持单条导出
+- 支持由调用者通过API指定发音配置
+- 修复朗读规则导出无拓展名
+- 修复本地TTS无法在编辑界面试听
+- 修复插件TTS附加数据不更新(解决Azure插件风格和角色变化问题)
+- 修复Android8及以下版本的备份问题
\ No newline at end of file
diff --git a/README.md b/README.md
index 94a5a48f1..58d4f025a 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,145 @@
![MIT](https://img.shields.io/badge/license-MIT-green)
+[![Crowdin](https://img.shields.io/badge/Localization-Crowdin-blueviolet?logo=Crowdin)](https://crowdin.com/project/tts-server)
+
[![CI](https://github.com/jing332/tts-server-android/actions/workflows/release.yml/badge.svg)](https://github.com/jing332/tts-server-android/actions/workflows/release.yml)
[![CI](https://github.com/jing332/tts-server-android/actions/workflows/test.yml/badge.svg)](https://github.com/jing332/tts-server-android/actions/workflows/test.yml)
+
+![GitHub release](https://img.shields.io/github/downloads/jing332/tts-server-android/total)
![GitHub release (latest by date)](https://img.shields.io/github/downloads/jing332/tts-server-android/latest/total)
-# 介绍
-这是 [tts-server-go](https://github.com/jing332/tts-server-go) 的Android版本。使用Kotlin开发,通过 [Gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile) 将Go编译为Lib以供Android APP调用. 关于本项目的Go Lib,见 [tts-server-lib](./tts-server-lib).
+# TTS Server [![](https://img.shields.io/badge/Q%E7%BE%A4-124841768-blue)](https://jq.qq.com/?_wv=1027&k=y7WCDjEA)
+
+本APP起初为阅读APP的网络朗读所用,在原有基础上,现已支持:
+* 内置微软接口(Edge大声朗读、~~Azure演示API~~(已猝) ),可自定义HTTP请求,可导入其他本地TTS引擎,以及根据中文双引号的简单旁白/对话识别朗读
+ ,还有自动重试,备用配置,文本替换等更多功能。
点击展开查看截图
-
- ![Screenshot_20220917-102952972](https://user-images.githubusercontent.com/42014615/190837053-24550576-fddf-49a8-b3b6-f99743fe4f27.jpg)
-
+
+
+
+
+
+
+
+
+# Download
+
+* [Stable - 稳定版(Releases)](https://github.com/jing332/tts-server-android/releases)
+
+* [Dev - 开发版(Actions 需登陆Github账户)](https://github.com/jing332/tts-server-android/actions)
+
+## Actions mirror
+
+app: https://jing332.lanzn.com/b09jpjd2d
+
+dev: https://jing332.lanzn.com/b09ig9qla
+
+密码Password: 1234
+
+
+# JS
+
+#### 朗读规则
+
+程序已内置旁白对话规则,通过 朗读规则管理 -> 加号 添加。
+
+由用户制作的朗读规则:
+
+1. 可识别角色名的旁白对话规则:
+ 打开[此链接](https://www.gitlink.org.cn/geek/src/tree/master/ttsrv-speechRules-multiVoice.json),
+ 复制全部内容到剪贴板,然后在规则管理界面导入。
+
+2. 5种语言检测: 复制 [此链接](https://jt12.de/SYV2_1/2023/04/16/10/08/08/1681610888643b588876c09.json),
+ 规则管理界面选择网络链接导入。
+
+##### Small Example Js Rule:
+```javascript
+let SpeechRuleJS = {
+ name: "Fesgheli" ,
+ id: "ir.masoudsoft.ttsfarsi.rr.fesgheli",
+ author: "Masoud Azizi",
+ telegram: "@ttsfarsi",
+ version: 1,
+ tags: {en: "English", fa: "Farsi"},
+
+ handleText(text) {
+ return text.split(/([a-zA-Z]+)/).map(part => ({
+ text: part,
+ tag: /[a-zA-Z]+/.test(part) ? "en" : "fa"
+ }));
+ },
+};
+```
+
+#### TTS插件
+
+程序已内置Azure官方接口的TTS插件: 插件管理 -> 右上角添加 -> 保存 -> 设置变量 -> 填入Key与Region即可
+
+讯飞WebAPI插件:复制 [此链接](https://jt12.de/SYV2_1/2023/04/16/10/25/17/1681611917643b5c8d61313.json),
+插件管理界面选择网络链接导入,随后设置变量 AppId, ApiKey, ApiSecret即可。
+
+# Grateful
+
+
+ 开源项目
+
+| Application | Microsoft TTS |
+|---------------------------------------------------------------------------------|-----------------------------------------------------------------------|
+| [gedoor/legado](https://github.com/gedoor/legado) | [wxxxcxx/ms-ra-forwarder](https://github.com/wxxxcxx/ms-ra-forwarder) |
+| [ag2s20150909/TTS](https://github.com/ag2s20150909/TTS) | [litcc/tts-server](https://github.com/litcc/tts-server) |
+| [benjaminwan/ChineseTtsTflite](https://github.com/benjaminwan/ChineseTtsTflite) | [asters1/tts](https://github.com/asters1/tts) |
+| [yellowgreatsun/MXTtsEngine](https://github.com/yellowgreatsun/MXTtsEngine) |
+| [2dust/v2rayNG](https://github.com/2dust/v2rayNG) |
+
+| Library | Description |
+|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [dromara/hutool](https://github.com/dromara/hutool/) | 🍬A set of tools that keep Java sweet. |
+| [LouisCAD/Splitties](https://github.com/LouisCAD/Splitties) | A collection of hand-crafted extensions for your Kotlin projects. |
+| [getactivity/logcat](https://github.com/getactivity/logcat) | Android 日志打印框架,在手机上可以直接看到 Logcat 日志啦 |
+| [rosuH/AndroidFilePicker](https://github.com/rosuH/AndroidFilePicker) | FilePicker is a small and fast file selector library that is constantly evolving with the goal of rapid integration, high customization, and configurability~ |
+| [androidbroadcast/ViewBindingPropertyDelegate](https://github.com/androidbroadcast/ViewBindingPropertyDelegate) | Make work with Android View Binding simpler |
+| [zhanghai/AndroidFastScroll](https://github.com/zhanghai/AndroidFastScroll) | Fast scroll for Android RecyclerView and more |
+| [Rosemoe/sora-editor](https://github.com/Rosemoe/sora-editor) | sora-editor is a cool and optimized code editor on Android platform |
+| [gedoor/rhino-android](https://github.com/gedoor/rhino-android) | Give access to RhinoScriptEngine from the JSR223 interfaces on Android JRE. |
+| [liangjingkanji/BRV](https://github.com/liangjingkanji/BRV) | Android上最好的RecyclerView框架, 比 BRVAH 更简单强大 |
+| [liangjingkanji/Net](https://github.com/liangjingkanji/Net) | Android最好的网络请求工具, 比 Retrofit/OkGo 更简单易用 |
+| [chibatching/kotpref](https://github.com/chibatching/kotpref) | Android SharedPreferences delegation library for Kotlin |
+| [google/ExoPlayer](https://github.com/google/ExoPlayer) | An extensible media player for Android |
+| [material-components-android](https://github.com/material-components/material-components-android) | Modular and customizable Material Design UI components for Android |
+| [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization/) | Kotlin multiplatform / multi-format serialization |
+| [kotlinx.coroutine](https://github.com/Kotlin/kotlinx.coroutines) | Library support for Kotlin coroutines |
+
+
+其他资源:
+
+* [阿里巴巴IconFont](https://www.iconfont.cn/)
+
+* [酷安@沉默_9520](http://www.coolapk.com/u/25956307) 本APP图标作者
+
+# Build
+
+### Android Studio:
+在项目根目录下新建文件 `local.properties` 并写入如下内容:
+```
+KEY_PATH=E\:\\Android\\key\\sign.jks (签名文件)
+KEY_PASSWORD= 密码
+ALIAS_NAME= 别名
+ALIAS_PASSWORD= 别名密码
+```
+
+
+
+### Github Actions:
+> 详见 https://www.cnblogs.com/jing332/p/17452492.html
+
+使用 Git Bash 对签名文件进行无换行Base64编码: `openssl base64 < key.jks | tr -d '\r\n' | tee key.jks.base64.txt`
+
+分别添加如下四个安全变量 (Repository secrets):
+> 前往以下链接:https://github.com/你的用户名/tts-server-android/settings/secrets/actions
+* `ALIAS_NAME` 别名
+* `ALIAS_PASSWORD` 别名密码
+* `KEY_PASSWORD` 密码
+* `KEY_STORE` 前面生成的 sign.jks.base64.txt 内容
diff --git a/app/build.gradle b/app/build.gradle
index 454e3b866..f0c3db791 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,99 +1,286 @@
plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
-// id 'kotlin-kapt'
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("kotlinx-serialization")
+ id("kotlin-kapt")
+ id("kotlin-parcelize")
+ id("com.google.devtools.ksp")
+ id("com.mikepenz.aboutlibraries.plugin")
}
+static def buildTime() {
+ def t = new Date().time / 1000
+ return (long) t
+}
+
+static def releaseTime() {
+ return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8"))
+}
+
+def version = "1." + releaseTime()
+def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim())
+
android {
- compileSdk 33
+ compileSdk 34
+ namespace 'com.github.jing332.tts_server_android'
defaultConfig {
applicationId 'com.github.jing332.tts_server_android'
minSdk 21
- targetSdk 33
- versionCode 2
- versionName "0.1_${releaseTime()}"
+ targetSdk 34
+ versionCode gitCommits
+ versionName version
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.incremental", "true")
+ arg("room.expandProjection", "true")
+ }
+
+ // 读取strings.xml所在文件夹 获得应用支持的语言
+ tasks.register('buildTranslationArray') {
+ def defaultCode = "zh-CN"
+ def foundLocales = new StringBuilder()
+ foundLocales.append("new String[]{")
+
+ fileTree("src/main/res").visit { details ->
+ if (details.file.path.endsWith("strings.xml")) {
+ def path = details.file.parent.replaceAll('\\\\', "/")
+ def languageCode = path.tokenize('/').last().replaceAll('values-', '').replaceAll('-r', '-')
+ languageCode = (languageCode == "values") ? defaultCode : languageCode;
+ foundLocales.append("\"").append(languageCode).append("\"").append(",")
+ }
+ }
+ foundLocales.append("}")
+ def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
+ buildConfigField "String[]", "TRANSLATION_ARRAY", foundLocalesString
+ }
+ preBuild.dependsOn buildTranslationArray
+ buildConfigField "long", "BUILD_TIME", buildTime().toString()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
- signingConfigs{
- release{
- v1SigningEnabled true
- v2SigningEnabled true
- enableV3Signing = true
- enableV4Signing = true
+ signingConfigs {
+ release {
//签名文件 从local.properties取值
- Properties pro = new Properties()
+ Properties pro = new Properties()
InputStream input = project.rootProject.file("local.properties").newDataInputStream()
pro.load(input)
storeFile file(pro.getProperty("KEY_PATH"))
storePassword pro.getProperty("KEY_PASSWORD")
keyAlias pro.getProperty("ALIAS_NAME")
keyPassword pro.getProperty("ALIAS_PASSWORD")
-
}
}
buildTypes {
release {
- //签名apk
- signingConfig signingConfigs.release
+ signingConfig signingConfigs.release
minifyEnabled true
- //资源缩减
-// shrinkResources true
- // Zipalign优化
- zipAlignEnabled true
+ shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
+ debug {
+ signingConfig signingConfigs.release
+ applicationIdSuffix ".debug"
+ versionNameSuffix "_debug"
+ splits.abi.enable = false
+ splits.density.enable = false
+ }
+ }
+ packagingOptions {
+ resources {
+ excludes += ['META-INF/INDEX.LIST', 'META-INF/*.md']
+ }
+ }
+
+
+ // 分别打包APK 原版 和 dev共存版
+ flavorDimensions += "version"
+ productFlavors {
+ app {
+ dimension = "version"
+ }
+ dev {
+ dimension = "version"
+ applicationIdSuffix ".dev"
+ }
}
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'x86_64', 'x86', 'armeabi-v7a', 'arm64-v8a'
+ universalApk true
+ }
+ }
compileOptions {
coreLibraryDesugaringEnabled true
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
-// sourceCompatibility JavaVersion.VERSION_1_8
-// targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = '11'
+ jvmTarget = '17'
}
- android.applicationVariants.all {
- variant ->
- variant.outputs.all {//修改apk文件名
- outputFileName = "TTS-Server-v${variant.versionName}.apk"
+ kotlin {
+ jvmToolchain(17)
+
+ /*sourceSets.all {
+ languageSettings {
+ languageVersion = "2.0"
}
+ }*/
+ }
+
+ // 修改apk文件名
+ android.applicationVariants.all { variant ->
+ variant.outputs.all { output ->
+ //noinspection GrDeprecatedAPIUsage
+ def abiName = output.getFilter(com.android.build.OutputFile.ABI)
+ if (abiName == null)
+ output.outputFileName = "TTS-Server-v${variant.versionName}.apk"
+ else
+ output.outputFileName = "TTS-Server-v${variant.versionName}_${abiName}.apk"
+ }
+ }
+
+// sourceSets {
+// main {
+// java {
+// exclude 'tts_server_android/ui'
+// }
+// }
+// }
+
+ buildFeatures {
+ viewBinding true
+ dataBinding true
+
+ compose true
+ buildConfig true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$compose_compiler"
}
}
dependencies {
- coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.6')
+ implementation 'androidx.activity:activity-ktx:1.8.2'
+ coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:2.0.4')
+
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
- implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+
+ //noinspection GradleDependency
+ implementation 'com.google.android.material:material:1.9.0-beta01'
+
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
+
+ def markwon_version = '4.6.2'
+ implementation "com.caverock:androidsvg:1.4"
+ implementation "io.noties.markwon:core:$markwon_version"
+ implementation "io.noties.markwon:image:$markwon_version"
+// implementation "io.noties.markwon:html:$markwon_version"
+ implementation "io.noties.markwon:linkify:$markwon_version"
+
+ // RecyclerView
+ implementation 'com.github.liangjingkanji:BRV:1.5.8'
+ implementation "androidx.recyclerview:recyclerview:1.3.2"
+ implementation 'me.zhanghai.android.fastscroll:library:1.2.0'
+
+ // Code Editor
+ implementation 'io.github.Rosemoe.sora-editor:editor:0.21.1'
+ implementation 'io.github.Rosemoe.sora-editor:language-textmate:0.21.1'
+
+ // Room
+ ksp("androidx.room:room-compiler:$room_version")
+ implementation("androidx.room:room-runtime:$room_version")
+ implementation("androidx.room:room-ktx:$room_version")
+ androidTestImplementation("androidx.room:room-testing:$room_version")
+
+ // IO & NET
+ implementation 'com.squareup.okio:okio:3.3.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.11.0'
+ implementation 'com.github.liangjingkanji:Net:3.6.4'
+
+ implementation 'me.rosuh:AndroidFilePicker:1.0.1'
+ implementation 'com.louiscad.splitties:splitties-systemservices:3.0.0'
+
+ implementation 'cn.hutool:hutool-crypto:5.8.19'
+
+ implementation("com.hankcs:hanlp:portable-1.8.4")
- //UI
- implementation 'androidx.appcompat:appcompat:1.3.0'
- implementation 'com.google.android.material:material:1.4.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ // Media
+ implementation("androidx.media3:media3-exoplayer:1.2.1")
+ implementation("androidx.media3:media3-ui:1.2.1")
+ // https://github.com/gyf-dev/ImmersionBar
+ implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2'
+ implementation 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2'
- implementation 'com.squareup.okhttp3:okhttp:4.10.0'
-// implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
+ // https://github.com/FunnySaltyFish/ComposeDataSaver
+ implementation "com.github.FunnySaltyFish.ComposeDataSaver:data-saver:v1.1.5"
+ implementation("com.mikepenz:aboutlibraries-core:${about_lib_version}")
+ implementation("com.mikepenz:aboutlibraries-compose:${about_lib_version}")
-// kapt('com.squareup.moshi:moshi-kotlin-codegen:1.14.0')
-// implementation('com.squareup.moshi:moshi:1.14.0')
+ def accompanistVersion = "0.33.0-alpha"
+ implementation("com.google.accompanist:accompanist-systemuicontroller:${accompanistVersion}")
+ implementation("com.google.accompanist:accompanist-navigation-animation:${accompanistVersion}")
+ implementation("com.google.accompanist:accompanist-webview:${accompanistVersion}")
+ implementation("com.google.accompanist:accompanist-permissions:${accompanistVersion}")
-// implementation 'androidx.work:work-runtime-ktx:2.7.1'
-// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
+
+
+ implementation("org.burnoutcrew.composereorderable:reorderable:0.9.6")
+
+ implementation 'androidx.activity:activity-compose:1.8.2'
+ implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
+ implementation("androidx.navigation:navigation-compose:2.7.6")
+
+ implementation("io.github.dokar3:sheets-m3:0.5.4")
+
+
+
+
+ def lifecycle_version = "2.7.0"
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:${lifecycle_version}"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${lifecycle_version}"
+
+// def composeBom = platform('androidx.compose:compose-bom:2023.08.00')
+ def composeBom = platform("dev.chrisbanes.compose:compose-bom:2024.01.00-alpha01")
+ implementation composeBom
+ androidTestImplementation composeBom
+
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.foundation:foundation")
+ implementation("androidx.compose.ui:ui")
+
+ implementation 'androidx.compose.material:material-icons-core'
+ implementation 'androidx.compose.material:material-icons-extended'
+ implementation 'androidx.compose.material3:material3-window-size-class'
+
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
+ debugImplementation 'androidx.compose.ui:ui-test-manifest'
+ implementation 'androidx.compose.ui:ui-tooling-preview'
+ debugImplementation 'androidx.compose.ui:ui-tooling'
+
+ // Logcat (Debug)
+// debugImplementation 'com.github.getActivity:Logcat:11.2'
testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
-
-static def releaseTime() {
- return new Date().format("yyyyMMddHHmm", TimeZone.getTimeZone("GMT+08:00"))
-}
\ No newline at end of file
diff --git a/app/libs/rhino-1.7.13-1.jar b/app/libs/rhino-1.7.13-1.jar
new file mode 100644
index 000000000..1f490006e
Binary files /dev/null and b/app/libs/rhino-1.7.13-1.jar differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index b7aca515c..075872eac 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,47 +1,31 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+-keepattributes SourceFile,LineNumberTable
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
-
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+# Rhino
+-keep class javax.script.** { *; }
+-keep class com.sun.script.javascript.** { *; }
+-keep class org.mozilla.javascript.** { *; }
+-keep class com.script.javascript.** { *; }
+
+# 插件相关
+-keep class com.github.jing332.tts_server_android.model.rhino.core.** { *; }
+-keep class cn.hutool.crypto.** { *; }
+-keep class com.hankcs.hanlp.** { *; }
+
+#-keep class cn.hutool.core.** { *; }
+
+-keepnames class * extends java.lang.Exception
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
+# 判断SVG库是否存在 (io.noties.markwon.image.svg.SvgSupport)
+-keepnames class com.caverock.androidsvg.SVG
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
+# OKIO
+-keep class okio.* { *; }
+
+# 保持 ViewBinding 实现类中的所有名称以 “inflate” 开头的方法不被混淆
+-keepclassmembers class * implements androidx.viewbinding.ViewBinding {
+ public static ** inflate(...);
+}
#-------------- 去掉所有打印 -------------
@@ -96,3 +80,235 @@ public *** println(...);
public *** print(...);
}
+
+# Keep `Companion` object fields of serializable classes.
+# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
+-if @kotlinx.serialization.Serializable class **
+-keepclassmembers class <1> {
+ static <1>$Companion Companion;
+}
+
+# Keep `serializer()` on companion objects (both default and named) of serializable classes.
+-if @kotlinx.serialization.Serializable class ** {
+ static **$* *;
+}
+-keepclassmembers class <2>$<3> {
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# Keep `INSTANCE.serializer()` of serializable objects.
+-if @kotlinx.serialization.Serializable class ** {
+ public static ** INSTANCE;
+}
+-keepclassmembers class <1> {
+ public static <1> INSTANCE;
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+
+# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
+# If you have any, uncomment and replace classes with those containing named companion objects.
+#-keepattributes InnerClasses # Needed for `getDeclaredClasses`.
+#-if @kotlinx.serialization.Serializable class
+#com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
+#com.example.myapplication.HasNamedCompanion2
+#{
+# static **$* *;
+#}
+#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
+# static <1>$$serializer INSTANCE;
+#}
+
+-dontwarn com.bumptech.glide.Glide
+-dontwarn com.bumptech.glide.RequestBuilder
+-dontwarn com.bumptech.glide.RequestManager
+-dontwarn com.bumptech.glide.request.BaseRequestOptions
+-dontwarn com.bumptech.glide.request.target.ViewTarget
+-dontwarn com.squareup.picasso.Picasso
+-dontwarn com.squareup.picasso.RequestCreator
+-dontwarn java.awt.AWTException
+-dontwarn java.awt.AlphaComposite
+-dontwarn java.awt.BasicStroke
+-dontwarn java.awt.Color
+-dontwarn java.awt.Composite
+-dontwarn java.awt.Desktop
+-dontwarn java.awt.Dimension
+-dontwarn java.awt.Font
+-dontwarn java.awt.FontFormatException
+-dontwarn java.awt.FontMetrics
+-dontwarn java.awt.Graphics2D
+-dontwarn java.awt.Graphics
+-dontwarn java.awt.GraphicsConfiguration
+-dontwarn java.awt.GraphicsDevice
+-dontwarn java.awt.GraphicsEnvironment
+-dontwarn java.awt.Image
+-dontwarn java.awt.Point
+-dontwarn java.awt.Rectangle
+-dontwarn java.awt.RenderingHints$Key
+-dontwarn java.awt.RenderingHints
+-dontwarn java.awt.Robot
+-dontwarn java.awt.Shape
+-dontwarn java.awt.Stroke
+-dontwarn java.awt.Toolkit
+-dontwarn java.awt.color.ColorSpace
+-dontwarn java.awt.datatransfer.Clipboard
+-dontwarn java.awt.datatransfer.ClipboardOwner
+-dontwarn java.awt.datatransfer.DataFlavor
+-dontwarn java.awt.datatransfer.StringSelection
+-dontwarn java.awt.datatransfer.Transferable
+-dontwarn java.awt.datatransfer.UnsupportedFlavorException
+-dontwarn java.awt.font.FontRenderContext
+-dontwarn java.awt.geom.AffineTransform
+-dontwarn java.awt.geom.Ellipse2D$Double
+-dontwarn java.awt.geom.Rectangle2D
+-dontwarn java.awt.geom.RoundRectangle2D$Double
+-dontwarn java.awt.image.AffineTransformOp
+-dontwarn java.awt.image.BufferedImage
+-dontwarn java.awt.image.BufferedImageOp
+-dontwarn java.awt.image.ColorConvertOp
+-dontwarn java.awt.image.ColorModel
+-dontwarn java.awt.image.CropImageFilter
+-dontwarn java.awt.image.DataBuffer
+-dontwarn java.awt.image.DataBufferByte
+-dontwarn java.awt.image.DataBufferInt
+-dontwarn java.awt.image.FilteredImageSource
+-dontwarn java.awt.image.ImageFilter
+-dontwarn java.awt.image.ImageObserver
+-dontwarn java.awt.image.ImageProducer
+-dontwarn java.awt.image.RenderedImage
+-dontwarn java.awt.image.SampleModel
+-dontwarn java.awt.image.WritableRaster
+-dontwarn java.beans.BeanInfo
+-dontwarn java.beans.FeatureDescriptor
+-dontwarn java.beans.IntrospectionException
+-dontwarn java.beans.Introspector
+-dontwarn java.beans.PropertyDescriptor
+-dontwarn java.beans.PropertyEditor
+-dontwarn java.beans.PropertyEditorManager
+-dontwarn java.beans.Transient
+-dontwarn java.beans.XMLEncoder
+-dontwarn java.lang.management.ManagementFactory
+-dontwarn java.lang.management.RuntimeMXBean
+-dontwarn javax.imageio.IIOImage
+-dontwarn javax.imageio.ImageIO
+-dontwarn javax.imageio.ImageReader
+-dontwarn javax.imageio.ImageTypeSpecifier
+-dontwarn javax.imageio.ImageWriteParam
+-dontwarn javax.imageio.ImageWriter
+-dontwarn javax.imageio.metadata.IIOMetadata
+-dontwarn javax.imageio.stream.ImageInputStream
+-dontwarn javax.imageio.stream.ImageOutputStream
+-dontwarn javax.naming.InitialContext
+-dontwarn javax.naming.NamingEnumeration
+-dontwarn javax.naming.NamingException
+-dontwarn javax.naming.directory.Attribute
+-dontwarn javax.naming.directory.Attributes
+-dontwarn javax.naming.directory.InitialDirContext
+-dontwarn javax.swing.ImageIcon
+-dontwarn javax.tools.DiagnosticCollector
+-dontwarn javax.tools.DiagnosticListener
+-dontwarn javax.tools.FileObject
+-dontwarn javax.tools.ForwardingJavaFileManager
+-dontwarn javax.tools.JavaCompiler$CompilationTask
+-dontwarn javax.tools.JavaCompiler
+-dontwarn javax.tools.JavaFileManager$Location
+-dontwarn javax.tools.JavaFileManager
+-dontwarn javax.tools.JavaFileObject$Kind
+-dontwarn javax.tools.JavaFileObject
+-dontwarn javax.tools.SimpleJavaFileObject
+-dontwarn javax.tools.StandardJavaFileManager
+-dontwarn javax.tools.StandardLocation
+-dontwarn javax.tools.ToolProvider
+-dontwarn javax.xml.bind.JAXBContext
+-dontwarn javax.xml.bind.Marshaller
+-dontwarn javax.xml.bind.Unmarshaller
+-dontwarn org.bouncycastle.asn1.ASN1Encodable
+-dontwarn org.bouncycastle.asn1.ASN1InputStream
+-dontwarn org.bouncycastle.asn1.ASN1Object
+-dontwarn org.bouncycastle.asn1.ASN1ObjectIdentifier
+-dontwarn org.bouncycastle.asn1.ASN1Primitive
+-dontwarn org.bouncycastle.asn1.ASN1Sequence
+-dontwarn org.bouncycastle.asn1.BERSequence
+-dontwarn org.bouncycastle.asn1.DERSequence
+-dontwarn org.bouncycastle.asn1.DLSequence
+-dontwarn org.bouncycastle.asn1.gm.GMNamedCurves
+-dontwarn org.bouncycastle.asn1.pkcs.PrivateKeyInfo
+-dontwarn org.bouncycastle.asn1.sec.ECPrivateKey
+-dontwarn org.bouncycastle.asn1.util.ASN1Dump
+-dontwarn org.bouncycastle.asn1.x509.AlgorithmIdentifier
+-dontwarn org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+-dontwarn org.bouncycastle.asn1.x9.X9ECParameters
+-dontwarn org.bouncycastle.asn1.x9.X9ObjectIdentifiers
+-dontwarn org.bouncycastle.cert.X509CertificateHolder
+-dontwarn org.bouncycastle.crypto.AlphabetMapper
+-dontwarn org.bouncycastle.crypto.BlockCipher
+-dontwarn org.bouncycastle.crypto.CipherParameters
+-dontwarn org.bouncycastle.crypto.CryptoException
+-dontwarn org.bouncycastle.crypto.Digest
+-dontwarn org.bouncycastle.crypto.InvalidCipherTextException
+-dontwarn org.bouncycastle.crypto.Mac
+-dontwarn org.bouncycastle.crypto.digests.SM3Digest
+-dontwarn org.bouncycastle.crypto.engines.SM2Engine$Mode
+-dontwarn org.bouncycastle.crypto.engines.SM2Engine
+-dontwarn org.bouncycastle.crypto.engines.SM4Engine
+-dontwarn org.bouncycastle.crypto.macs.CBCBlockCipherMac
+-dontwarn org.bouncycastle.crypto.macs.HMac
+-dontwarn org.bouncycastle.crypto.params.AsymmetricKeyParameter
+-dontwarn org.bouncycastle.crypto.params.ECDomainParameters
+-dontwarn org.bouncycastle.crypto.params.ECPrivateKeyParameters
+-dontwarn org.bouncycastle.crypto.params.ECPublicKeyParameters
+-dontwarn org.bouncycastle.crypto.params.KeyParameter
+-dontwarn org.bouncycastle.crypto.params.ParametersWithID
+-dontwarn org.bouncycastle.crypto.params.ParametersWithIV
+-dontwarn org.bouncycastle.crypto.params.ParametersWithRandom
+-dontwarn org.bouncycastle.crypto.signers.DSAEncoding
+-dontwarn org.bouncycastle.crypto.signers.PlainDSAEncoding
+-dontwarn org.bouncycastle.crypto.signers.SM2Signer
+-dontwarn org.bouncycastle.crypto.signers.StandardDSAEncoding
+-dontwarn org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
+-dontwarn org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
+-dontwarn org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util
+-dontwarn org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil
+-dontwarn org.bouncycastle.jcajce.spec.FPEParameterSpec
+-dontwarn org.bouncycastle.jcajce.spec.OpenSSHPrivateKeySpec
+-dontwarn org.bouncycastle.jcajce.spec.OpenSSHPublicKeySpec
+-dontwarn org.bouncycastle.jce.provider.BouncyCastleProvider
+-dontwarn org.bouncycastle.jce.spec.ECNamedCurveSpec
+-dontwarn org.bouncycastle.jce.spec.ECParameterSpec
+-dontwarn org.bouncycastle.jsse.BCSSLParameters
+-dontwarn org.bouncycastle.jsse.BCSSLSocket
+-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
+-dontwarn org.bouncycastle.math.ec.ECCurve
+-dontwarn org.bouncycastle.math.ec.ECPoint
+-dontwarn org.bouncycastle.math.ec.FixedPointCombMultiplier
+-dontwarn org.bouncycastle.openssl.PEMDecryptorProvider
+-dontwarn org.bouncycastle.openssl.PEMEncryptedKeyPair
+-dontwarn org.bouncycastle.openssl.PEMException
+-dontwarn org.bouncycastle.openssl.PEMKeyPair
+-dontwarn org.bouncycastle.openssl.PEMParser
+-dontwarn org.bouncycastle.openssl.X509TrustedCertificateBlock
+-dontwarn org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
+-dontwarn org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder
+-dontwarn org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder
+-dontwarn org.bouncycastle.operator.InputDecryptorProvider
+-dontwarn org.bouncycastle.operator.OperatorCreationException
+-dontwarn org.bouncycastle.pkcs.PKCS10CertificationRequest
+-dontwarn org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo
+-dontwarn org.bouncycastle.pkcs.PKCSException
+-dontwarn org.bouncycastle.util.Arrays
+-dontwarn org.bouncycastle.util.BigIntegers
+-dontwarn org.bouncycastle.util.encoders.Hex
+-dontwarn org.bouncycastle.util.io.pem.PemObject
+-dontwarn org.bouncycastle.util.io.pem.PemObjectGenerator
+-dontwarn org.bouncycastle.util.io.pem.PemReader
+-dontwarn org.bouncycastle.util.io.pem.PemWriter
+-dontwarn org.conscrypt.Conscrypt$Version
+-dontwarn org.conscrypt.Conscrypt
+-dontwarn org.conscrypt.ConscryptHostnameVerifier
+-dontwarn org.openjsse.javax.net.ssl.SSLParameters
+-dontwarn org.openjsse.javax.net.ssl.SSLSocket
+-dontwarn org.openjsse.net.ssl.OpenJSSE
+-dontwarn org.commonmark.ext.gfm.strikethrough.Strikethrough
+-dontwarn pl.droidsonroids.gif.GifDrawable
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/1.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/1.json
new file mode 100644
index 000000000..89435780f
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/1.json
@@ -0,0 +1,64 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "17b1a49245c8b41c456e06bbb7554b4c",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uiData` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `readAloudTarget` INTEGER NOT NULL, `msTtsProperty` TEXT, `httpTts` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uiData",
+ "columnName": "uiData",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "msTtsProperty",
+ "columnName": "msTtsProperty",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "httpTts",
+ "columnName": "httpTts",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '17b1a49245c8b41c456e06bbb7554b4c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/10.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/10.json
new file mode 100644
index 000000000..6fe062db1
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/10.json
@@ -0,0 +1,263 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 10,
+ "identityHash": "5744e6d22a05829e0ca3956e8a0e2a3a",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5744e6d22a05829e0ca3956e8a0e2a3a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/11.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/11.json
new file mode 100644
index 000000000..ee628bc40
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/11.json
@@ -0,0 +1,270 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 11,
+ "identityHash": "5bcca51c0d3dd68d497ac129c7108864",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bcca51c0d3dd68d497ac129c7108864')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/12.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/12.json
new file mode 100644
index 000000000..4fda9446d
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/12.json
@@ -0,0 +1,270 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 12,
+ "identityHash": "5bcca51c0d3dd68d497ac129c7108864",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bcca51c0d3dd68d497ac129c7108864')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/13.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/13.json
new file mode 100644
index 000000000..63164fbb8
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/13.json
@@ -0,0 +1,270 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 13,
+ "identityHash": "5bcca51c0d3dd68d497ac129c7108864",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bcca51c0d3dd68d497ac129c7108864')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/14.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/14.json
new file mode 100644
index 000000000..9b16f87cb
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/14.json
@@ -0,0 +1,277 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 14,
+ "identityHash": "37e1b6c08be1534864ab98afe56be74c",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '37e1b6c08be1534864ab98afe56be74c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/15.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/15.json
new file mode 100644
index 000000000..ab670236a
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/15.json
@@ -0,0 +1,284 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 15,
+ "identityHash": "49bad10542127ac0a5d951d7f0f721fa",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '49bad10542127ac0a5d951d7f0f721fa')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/16.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/16.json
new file mode 100644
index 000000000..e19d0c199
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/16.json
@@ -0,0 +1,297 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 16,
+ "identityHash": "a41d35ae3e4adcc466d0bdf126b300f1",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '')",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a41d35ae3e4adcc466d0bdf126b300f1')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/17.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/17.json
new file mode 100644
index 000000000..f0b2ef6b5
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/17.json
@@ -0,0 +1,367 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 17,
+ "identityHash": "d6b22ed6ea4951687d07aae4d63fe153",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '')",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6b22ed6ea4951687d07aae4d63fe153')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/18.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/18.json
new file mode 100644
index 000000000..344d87cb8
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/18.json
@@ -0,0 +1,388 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 18,
+ "identityHash": "acf6946fd71ad7ebe884e2477066a1cf",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '', `speechRule_tagData` TEXT NOT NULL DEFAULT '', `speechRule_configId` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagData",
+ "columnName": "speechRule_tagData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.configId",
+ "columnName": "speechRule_configId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `tagsData` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tagsData",
+ "columnName": "tagsData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'acf6946fd71ad7ebe884e2477066a1cf')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/19.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/19.json
new file mode 100644
index 000000000..e75ee4227
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/19.json
@@ -0,0 +1,395 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 19,
+ "identityHash": "53e08c5e51449226ae36af316654bd77",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '', `speechRule_tagName` TEXT NOT NULL DEFAULT '', `speechRule_tagData` TEXT NOT NULL DEFAULT '', `speechRule_configId` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagName",
+ "columnName": "speechRule_tagName",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagData",
+ "columnName": "speechRule_tagData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.configId",
+ "columnName": "speechRule_configId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `tagsData` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tagsData",
+ "columnName": "tagsData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '53e08c5e51449226ae36af316654bd77')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/2.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/2.json
new file mode 100644
index 000000000..ebfbd026c
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/2.json
@@ -0,0 +1,108 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "1af9d6ebe37f8debc406f7cfcb902e1a",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `displayName` TEXT, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1af9d6ebe37f8debc406f7cfcb902e1a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/20.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/20.json
new file mode 100644
index 000000000..45a1a83d1
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/20.json
@@ -0,0 +1,409 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 20,
+ "identityHash": "3b08d7569dae697e6efe84fd1413f2c5",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '', `speechRule_tagName` TEXT NOT NULL DEFAULT '', `speechRule_tagData` TEXT NOT NULL DEFAULT '', `speechRule_configId` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagName",
+ "columnName": "speechRule_tagName",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagData",
+ "columnName": "speechRule_tagData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.configId",
+ "columnName": "speechRule_configId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `defVars` TEXT NOT NULL DEFAULT '{}', `userVars` TEXT NOT NULL DEFAULT '{}', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defVars",
+ "columnName": "defVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "userVars",
+ "columnName": "userVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `tagsData` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tagsData",
+ "columnName": "tagsData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3b08d7569dae697e6efe84fd1413f2c5')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/21.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/21.json
new file mode 100644
index 000000000..2ba3990c6
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/21.json
@@ -0,0 +1,409 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 21,
+ "identityHash": "3b08d7569dae697e6efe84fd1413f2c5",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '', `speechRule_tagName` TEXT NOT NULL DEFAULT '', `speechRule_tagData` TEXT NOT NULL DEFAULT '', `speechRule_configId` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagName",
+ "columnName": "speechRule_tagName",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagData",
+ "columnName": "speechRule_tagData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.configId",
+ "columnName": "speechRule_configId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `defVars` TEXT NOT NULL DEFAULT '{}', `userVars` TEXT NOT NULL DEFAULT '{}', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defVars",
+ "columnName": "defVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "userVars",
+ "columnName": "userVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `tagsData` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tagsData",
+ "columnName": "tagsData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3b08d7569dae697e6efe84fd1413f2c5')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/22.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/22.json
new file mode 100644
index 000000000..452ee73e3
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/22.json
@@ -0,0 +1,430 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 22,
+ "identityHash": "b4c51dd25c6bf4f625aebb151e6977c6",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '', `speechRule_tagName` TEXT NOT NULL DEFAULT '', `speechRule_tagData` TEXT NOT NULL DEFAULT '', `speechRule_configId` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagName",
+ "columnName": "speechRule_tagName",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagData",
+ "columnName": "speechRule_tagData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.configId",
+ "columnName": "speechRule_configId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, `audioParams_speed` REAL NOT NULL DEFAULT 0.0, `audioParams_volume` REAL NOT NULL DEFAULT 0.0, `audioParams_pitch` REAL NOT NULL DEFAULT 0.0, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "audioParams.speed",
+ "columnName": "audioParams_speed",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "audioParams.volume",
+ "columnName": "audioParams_volume",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "audioParams.pitch",
+ "columnName": "audioParams_pitch",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `defVars` TEXT NOT NULL DEFAULT '{}', `userVars` TEXT NOT NULL DEFAULT '{}', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defVars",
+ "columnName": "defVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "userVars",
+ "columnName": "userVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `tagsData` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tagsData",
+ "columnName": "tagsData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4c51dd25c6bf4f625aebb151e6977c6')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/23.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/23.json
new file mode 100644
index 000000000..728b32587
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/23.json
@@ -0,0 +1,437 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 23,
+ "identityHash": "cdd66e8dd75632e2933b30c9a90f77d6",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '', `speechRule_tagName` TEXT NOT NULL DEFAULT '', `speechRule_tagData` TEXT NOT NULL DEFAULT '', `speechRule_configId` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagName",
+ "columnName": "speechRule_tagName",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagData",
+ "columnName": "speechRule_tagData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.configId",
+ "columnName": "speechRule_configId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, `audioParams_speed` REAL NOT NULL DEFAULT 0.0, `audioParams_volume` REAL NOT NULL DEFAULT 0.0, `audioParams_pitch` REAL NOT NULL DEFAULT 0.0, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "audioParams.speed",
+ "columnName": "audioParams_speed",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "audioParams.volume",
+ "columnName": "audioParams_volume",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "audioParams.pitch",
+ "columnName": "audioParams_pitch",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, `onExecution` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "onExecution",
+ "columnName": "onExecution",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `defVars` TEXT NOT NULL DEFAULT '{}', `userVars` TEXT NOT NULL DEFAULT '{}', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defVars",
+ "columnName": "defVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "userVars",
+ "columnName": "userVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `tagsData` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tagsData",
+ "columnName": "tagsData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cdd66e8dd75632e2933b30c9a90f77d6')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/24.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/24.json
new file mode 100644
index 000000000..48b9942ff
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/24.json
@@ -0,0 +1,444 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 24,
+ "identityHash": "78fcaa494796c10ba20d2f89ecc40f05",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `tts` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `speechRule_target` INTEGER NOT NULL, `speechRule_isStandby` INTEGER NOT NULL, `speechRule_tag` TEXT NOT NULL DEFAULT '', `speechRule_tagRuleId` TEXT NOT NULL DEFAULT '', `speechRule_tagName` TEXT NOT NULL DEFAULT '', `speechRule_tagData` TEXT NOT NULL DEFAULT '', `speechRule_configId` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "speechRule.target",
+ "columnName": "speechRule_target",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.isStandby",
+ "columnName": "speechRule_isStandby",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speechRule.tag",
+ "columnName": "speechRule_tag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagRuleId",
+ "columnName": "speechRule_tagRuleId",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagName",
+ "columnName": "speechRule_tagName",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.tagData",
+ "columnName": "speechRule_tagData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "speechRule.configId",
+ "columnName": "speechRule_configId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, `audioParams_speed` REAL NOT NULL DEFAULT 0.0, `audioParams_volume` REAL NOT NULL DEFAULT 0.0, `audioParams_pitch` REAL NOT NULL DEFAULT 0.0, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "audioParams.speed",
+ "columnName": "audioParams_speed",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "audioParams.volume",
+ "columnName": "audioParams_volume",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "audioParams.pitch",
+ "columnName": "audioParams_pitch",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `sampleText` TEXT NOT NULL DEFAULT '')",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "sampleText",
+ "columnName": "sampleText",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, `onExecution` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "onExecution",
+ "columnName": "onExecution",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Plugin",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `version` INTEGER NOT NULL DEFAULT 0, `name` TEXT NOT NULL, `pluginId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `defVars` TEXT NOT NULL DEFAULT '{}', `userVars` TEXT NOT NULL DEFAULT '{}', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pluginId",
+ "columnName": "pluginId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defVars",
+ "columnName": "defVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "userVars",
+ "columnName": "userVars",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{}'"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "speech_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `ruleId` TEXT NOT NULL, `author` TEXT NOT NULL, `code` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `tagsData` TEXT NOT NULL DEFAULT '', `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ruleId",
+ "columnName": "ruleId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tagsData",
+ "columnName": "tagsData",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '78fcaa494796c10ba20d2f89ecc40f05')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/3.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/3.json
new file mode 100644
index 000000000..996a79371
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/3.json
@@ -0,0 +1,108 @@
+{
+ "formatVersion": 2,
+ "database": {
+ "version": 3,
+ "identityHash": "1af9d6ebe37f8debc406f7cfcb902e1a",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isEnabled` INTEGER NOT NULL, `displayName` TEXT, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1af9d6ebe37f8debc406f7cfcb902e1a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/4.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/4.json
new file mode 100644
index 000000000..9e77492cc
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/4.json
@@ -0,0 +1,147 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "1ff434b373036cebb5cdc5d67ab18b7a",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isExpanded` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "groupId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ff434b373036cebb5cdc5d67ab18b7a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/5.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/5.json
new file mode 100644
index 000000000..46ba14ed1
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/5.json
@@ -0,0 +1,147 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 5,
+ "identityHash": "1ff434b373036cebb5cdc5d67ab18b7a",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isExpanded` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "groupId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ff434b373036cebb5cdc5d67ab18b7a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/6.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/6.json
new file mode 100644
index 000000000..59a920469
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/6.json
@@ -0,0 +1,154 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 6,
+ "identityHash": "499b92014ee3e6b853be6ab2729f75d7",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isExpanded` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "groupId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '499b92014ee3e6b853be6ab2729f75d7')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/7.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/7.json
new file mode 100644
index 000000000..e40a27f13
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/7.json
@@ -0,0 +1,161 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 7,
+ "identityHash": "b7c0690e81519f7f69dc49b5b442da2e",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "groupId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b7c0690e81519f7f69dc49b5b442da2e')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/8.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/8.json
new file mode 100644
index 000000000..233571aa5
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/8.json
@@ -0,0 +1,168 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 8,
+ "identityHash": "4424315f39a367dfb9b638642ede84ce",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "groupId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4424315f39a367dfb9b638642ede84ce')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/9.json b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/9.json
new file mode 100644
index 000000000..e1d8f81d7
--- /dev/null
+++ b/app/schemas/com.github.jing332.tts_server_android.data.AppDatabase/9.json
@@ -0,0 +1,213 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 9,
+ "identityHash": "30fa567a608c45f832845582ee80184e",
+ "entities": [
+ {
+ "tableName": "sysTts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `displayName` TEXT, `isEnabled` INTEGER NOT NULL, `isStandby` INTEGER NOT NULL DEFAULT 0, `readAloudTarget` INTEGER NOT NULL, `tts` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStandby",
+ "columnName": "isStandby",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readAloudTarget",
+ "columnName": "readAloudTarget",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tts",
+ "columnName": "tts",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SystemTtsGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL DEFAULT 1, `name` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "replaceRuleGroup",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `isExpanded` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isExpanded",
+ "columnName": "isExpanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '30fa567a608c45f832845582ee80184e')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/github/jing332/tts_server_android/DirectUploadEngineTest.kt b/app/src/androidTest/java/com/github/jing332/tts_server_android/DirectUploadEngineTest.kt
new file mode 100644
index 000000000..7276c8176
--- /dev/null
+++ b/app/src/androidTest/java/com/github/jing332/tts_server_android/DirectUploadEngineTest.kt
@@ -0,0 +1,59 @@
+package com.github.jing332.tts_server_android
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.jing332.tts_server_android.model.rhino.core.ext.JsExtensions
+import com.github.jing332.tts_server_android.model.rhino.direct_link_upload.DirectUploadEngine
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DirectUploadEngineTest {
+
+ @Test
+ fun catBox() {
+ val ext = JsExtensions(app, "")
+
+ val form = mutableMapOf()
+
+ form["reqtype"] = "fileupload"
+ form["fileToUpload"] = mutableMapOf().also {
+ it["fileName"] = "ccc.json"
+ it["body"] = """ {"1":"1", "2":"2"} """
+ it["contentType"] = "application/json"
+ }
+// form["file"] = mutableMapOf().apply {
+// put("file", mutableMapOf().apply {
+// put("fileToUpload", """ {"1":"1", "2":"2"} """)
+// })
+// put("fileName", "config.json")
+// put("contentType", "application/json")
+// }
+
+ val resp = ext.httpPostMultipart(
+ "https://catbox.moe/user/api.php",
+ form
+ )
+ println(resp.body?.string())
+ }
+
+ @Test
+ fun testJS() {
+ val code = """
+ let DirectUploadJS = {
+ "XX网盘(永久有效)": function(config){
+ println("from js: " + config)
+ return {'url':'https://xxx.com/111.json', 'summary':'永久有效'}
+ },
+ }
+ """.trimIndent()
+ val engine = DirectUploadEngine(context = app, code = code)
+ val list = engine.obtainFunctionList()
+ println(list)
+ list.forEach {
+ it.invoke("jsonsjosnsjkosnsojsn").apply {
+// println(keys)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/github/jing332/tts_server_android/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/github/jing332/tts_server_android/ExampleInstrumentedTest.kt
deleted file mode 100644
index e4a5cb5e0..000000000
--- a/app/src/androidTest/java/com/github/jing332/tts_server_android/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.jing332.tts_server_android
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.github.jing332.testgomobile", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/github/jing332/tts_server_android/GoLibTest.kt b/app/src/androidTest/java/com/github/jing332/tts_server_android/GoLibTest.kt
new file mode 100644
index 000000000..85f0c09a6
--- /dev/null
+++ b/app/src/androidTest/java/com/github/jing332/tts_server_android/GoLibTest.kt
@@ -0,0 +1,12 @@
+package com.github.jing332.tts_server_android
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class GoLibTest {
+ @Test
+ fun goLib() {
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/github/jing332/tts_server_android/HttpTtsUrlTest.kt b/app/src/androidTest/java/com/github/jing332/tts_server_android/HttpTtsUrlTest.kt
new file mode 100644
index 000000000..fa6cf05d5
--- /dev/null
+++ b/app/src/androidTest/java/com/github/jing332/tts_server_android/HttpTtsUrlTest.kt
@@ -0,0 +1,32 @@
+package com.github.jing332.tts_server_android
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.model.AnalyzeUrl
+import org.junit.Assert.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class HttpTtsUrlTest {
+ @Test
+ fun test() {
+ // Context of the app under test.
+// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+
+ // "http://tsn.baidu.com/text2audio,{\"method\": \"POST\", \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=220&vol=5&aue=6&pit=5&res_tag=audio\"}"
+ val url =
+ """ http://192.168.0.109:1233/api/ra ,{"method":"POST","body":"{{String(speakText).replace(/&/g, '&').replace(/\"/g, '"').replace(/'/g, ''').replace(//g, '>')}}"} """
+ Log.e("TAG", url)
+ val a = AnalyzeUrl(url, speakText = "t\\\\est\\测\\\\试")
+ Log.e("TAG", "baseUrl: " + a.eval().toString())
+
+ println(AppConst.SCRIPT_ENGINE.eval(""" String("Test\\\\测试") """))
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/github/jing332/tts_server_android/OkHttpTest.kt b/app/src/androidTest/java/com/github/jing332/tts_server_android/OkHttpTest.kt
new file mode 100644
index 000000000..93bb6ab76
--- /dev/null
+++ b/app/src/androidTest/java/com/github/jing332/tts_server_android/OkHttpTest.kt
@@ -0,0 +1,32 @@
+package com.github.jing332.tts_server_android
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.drake.net.Net
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class OkHttpTest {
+ @Test
+ fun postMultiPart() {
+ val json = """
+ {"11":"11", "22": "22"}
+ """.trimIndent()
+ val url = "http://v2.jt12.de/up-v2.php"
+ val resp: Response = Net.post(url) {
+ body = MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart(
+ "file",
+ "filename.json",
+ json.toRequestBody("text/javascript".toMediaType())
+ )
+ .build()
+ }.execute()
+ println("${resp.code} ${resp.message}: ${resp.body?.string()}")
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/github/jing332/tts_server_android/RhinoEngineTest.kt b/app/src/androidTest/java/com/github/jing332/tts_server_android/RhinoEngineTest.kt
new file mode 100644
index 000000000..0b1e417be
--- /dev/null
+++ b/app/src/androidTest/java/com/github/jing332/tts_server_android/RhinoEngineTest.kt
@@ -0,0 +1,28 @@
+package com.github.jing332.tts_server_android
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import cn.hutool.crypto.symmetric.SymmetricCrypto
+import com.script.javascript.RhinoScriptEngine
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.javascript.NativeObject
+
+@RunWith(AndroidJUnit4::class)
+class RhinoEngineTest {
+ @Test
+ fun script() {
+ val jsCode = """
+
+ """.trimIndent()
+
+ RhinoScriptEngine().apply {
+ val compiledScript = compile(jsCode)
+ compiledScript.eval()
+ println((get("tts") as NativeObject).get("name"))
+ }
+
+// PluginEngine().apply {
+// println(runScript(jsCode, "测试文本", 1))
+// }
+ }
+}
\ No newline at end of file
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 000000000..9e597a171
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DB·TTS Server
+
\ No newline at end of file
diff --git a/app/src/dev/res/values/strings.xml b/app/src/dev/res/values/strings.xml
new file mode 100644
index 000000000..f00a46260
--- /dev/null
+++ b/app/src/dev/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ D·TTS Server
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 96cdd9303..258dfcbf2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,50 +1,221 @@
+ xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+ android:theme="@style/Theme.TtsServer"
+ android:usesCleartextTraffic="true"
+ tools:ignore="UnusedAttribute">
+
+
+ android:label="@string/replace_rule_manager"
+ android:windowSoftInputMode="adjustResize" />
+
+
+
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
+ android:enabled="false"
+ android:exported="false">
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
+
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/defaultData/direct_link_upload.js b/app/src/main/assets/defaultData/direct_link_upload.js
new file mode 100644
index 000000000..cf8678ac1
--- /dev/null
+++ b/app/src/main/assets/defaultData/direct_link_upload.js
@@ -0,0 +1,52 @@
+let DirectUploadJS = {
+ "橘途网盘 (永久有效)": function(config) {
+ let resp = upload('http://v2.jt12.de/up-v2.php', config)
+ let str = resp.body().string()
+ let result = JSON.parse(str)
+ if (result['code'] !== 0) {
+ throw "error: " + result['msg']
+ }
+
+ return result['msg']
+ },
+
+ "喵公子 (有效期2天)": function(config) {
+ let url = 'https://sy.mgz6.cc/shuyuan'
+ let resp = upload(url, config)
+ let result = JSON.parse(resp.body().string())
+ if (result['msg'] !== 'success') {
+ throw "error: " + result['msg']
+ }
+
+ return url + '/' + result['data']
+ },
+
+ "Catbox (有效期未知)": function(config) {
+ let form = {
+ 'fileToUpload': {
+ 'body': config,
+ 'fileName': "config.json",
+ 'contentType': "application/json"
+ },
+ 'reqtype': 'fileupload',
+ }
+ let resp = ttsrv.httpPostMultipart('https://catbox.moe/user/api.php', form)
+ if (resp.code() !== 200) {
+ throw 'error: HTTP-' + resp.code()
+ }
+
+ return resp.body().string()
+ }
+}
+
+function upload(url, config, extra) {
+ let form = {
+ "file":{
+ 'body': config,
+ 'fileName': 'config.json',
+ 'contentType': 'application/json',
+ }
+ }
+
+ return ttsrv.httpPostMultipart(url, form)
+}
diff --git a/app/src/main/assets/defaultData/list.json b/app/src/main/assets/defaultData/list.json
new file mode 100644
index 000000000..6ad8d5895
--- /dev/null
+++ b/app/src/main/assets/defaultData/list.json
@@ -0,0 +1,56 @@
+[
+ {
+ "group": {
+ "id": 12333,
+ "name": "示例-旁白对话BGM",
+ "isExpanded": true
+ },
+ "list": [
+ {
+ "id": 1690331046541,
+ "displayName": "⚠️请在右上角打开多语音!晓晓(zh-CN-XiaoxiaoNeural)",
+ "groupId": "12333",
+ "isEnabled": true,
+ "speechRule": {
+ "target": 4,
+ "tag": "dialogue",
+ "tagRuleId": "ttsrv.multi_voice"
+ },
+ "tts": {
+ "#type": "internal"
+ }
+ },
+ {
+ "id": 1690331074092,
+ "displayName": "云健(zh-CN-YunjianNeural)",
+ "groupId": "12333",
+ "isEnabled": true,
+ "speechRule": {
+ "target": 4,
+ "tag": "narration",
+ "tagRuleId": "ttsrv.multi_voice"
+ },
+ "tts": {
+ "#type": "internal",
+ "voiceName": "zh-CN-YunjianNeural"
+ }
+ },
+ {
+ "id": 1681521093149,
+ "displayName": "bgm",
+ "groupId": "12333",
+ "isEnabled": true,
+ "speechRule": {
+ "target": 3
+ },
+ "tts": {
+ "#type": "bgm",
+ "volume": 50,
+ "audioFormat": {
+ }
+ },
+ "order": 3
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/app/src/main/assets/defaultData/plugin-azure.js b/app/src/main/assets/defaultData/plugin-azure.js
new file mode 100644
index 000000000..967976fa9
--- /dev/null
+++ b/app/src/main/assets/defaultData/plugin-azure.js
@@ -0,0 +1,354 @@
+// 请点击保存后在 更多选项按钮(垂直三个点) -> 设置变量 中设置密钥和区域
+// Please set the key and region in "More options" -> "Variables" after clicking save.
+
+let key = ttsrv.userVars['key'] || 'Default_KEY'
+let region = ttsrv.userVars['region'] || 'eastus'
+
+let format = "audio-24khz-48kbitrate-mono-mp3"
+let sampleRate = 24000 // 对应24khz. 格式后带有opus的实际采样率是其2倍
+let isNeedDecode = true // 是否需要解码,如 format 为 raw 请设为 false
+
+let PluginJS = {
+ "name": "Azure",
+ "id": "com.microsoft.azure",
+ "author": "TTS Server",
+ "description": "",
+ "version": 3,
+ "vars": { // 声明变量,再由用户设置。
+ key: {label: "密钥 Key"},
+ region: {label: "区域 Region", hint: "为空时使用默认'eastus'"},
+ },
+
+ "onLoad": function () {
+ checkKeyRegion()
+ },
+
+ "getAudio": function (text, locale, voice, rate, volume, pitch) {
+ rate = (rate * 2) - 100
+ pitch = pitch - 50
+
+ let styleDegree = ttsrv.tts.data['styleDegree']
+ if (!styleDegree || Number(styleDegree) < 0.01) {
+ styleDegree = '1.0'
+ }
+
+ let style = ttsrv.tts.data['style']
+ let role = ttsrv.tts.data['role']
+ if (!style || style === "") {
+ style = 'general'
+ }
+ if (!role || role === "") {
+ role = 'default'
+ }
+
+ let textSsml = ''
+ let langSkill = ttsrv.tts.data['languageSkill']
+ if (langSkill === "" || langSkill == null) {
+ textSsml = escapeXml(text)
+ } else {
+ textSsml = `${escapeXml(text)}`
+ }
+
+ let ssml = `
+
+
+
+ ${textSsml}
+
+
+
+ `
+
+ return getAudioInternal(ssml, format)
+ },
+}
+
+function escapeXml(s) {
+ return s.replace(/'/g, ''').replace(/"/g, '"').replace(//g, '>').replace(/&/g, '&').replace(/\//g, '').replace(/\\/g, '');
+}
+
+function checkKeyRegion() {
+ key = (key + '').trim()
+ region = (region + '').trim()
+ if (key === '' || region === '') {
+ throw "请设置变量: 密钥Key与区域Region。 Please set the key and region."
+ }
+}
+
+let ttsUrl = 'https://' + region + '.tts.speech.microsoft.com/cognitiveservices/v1'
+
+function getAudioInternal(ssml, format) {
+ let headers = {
+ 'Ocp-Apim-Subscription-Key': key,
+ "X-Microsoft-OutputFormat": format,
+ "Content-Type": "application/ssml+xml",
+ }
+ let resp = ttsrv.httpPost(ttsUrl, ssml, headers)
+ if (resp.code() !== 200) {
+ if (resp.code() === 401) {
+ throw "401 Unauthorized 未授权,请检查密钥与区域是否正确。"
+ }else if (resp.code() === 403) {
+ throw "403 Forbidden 被禁止,您的Azure账户可能已被禁用。"
+ }
+
+ throw "音频获取失败: HTTP-" + resp.code()
+ }
+
+ return resp.body().byteStream()
+}
+
+// 全部voice数据
+let voices = {}
+// 当前语言下的voice
+let currentVoices = new Map()
+
+// 语言技能 二级语言
+let skillSpinner
+
+let styleSpinner
+let roleSpinner
+let seekStyle
+
+let EditorJS = {
+ //音频的采样率 编辑TTS界面保存时调用
+ "getAudioSampleRate": function (locale, voice) {
+ return sampleRate
+ },
+
+ "isNeedDecode": function (locale, voice) {
+ return isNeedDecode
+ },
+
+ "getLocales": function () {
+ let locales = new Array()
+
+ voices.forEach(function (v) {
+ let loc = v["Locale"]
+ if (!locales.includes(loc)) {
+ locales.push(loc)
+ }
+ })
+
+ return locales
+ },
+
+ // 当语言变更时调用
+ "getVoices": function (locale) {
+ currentVoices = new Map()
+ voices.forEach(function (v) {
+ if (v['Locale'] === locale) {
+ currentVoices.set(v['ShortName'], v)
+ }
+ })
+
+ let mm = {}
+ for (let [key, value] of currentVoices.entries()) {
+ mm[key] = new java.lang.String(value['LocalName'] + ' (' + key + ')')
+ }
+ return mm
+ },
+
+ // 加载本地或网络数据,运行在IO线程。
+ "onLoadData": function () {
+ // 获取数据并缓存以便复用
+ let jsonStr = ''
+ if (ttsrv.fileExist('voices.json')) {
+ jsonStr = ttsrv.readTxtFile('voices.json')
+ } else {
+ checkKeyRegion()
+ let url = 'https://' + region + '.tts.speech.microsoft.com/cognitiveservices/voices/list'
+ let header = {
+ "Ocp-Apim-Subscription-Key": key,
+ "Content-Type": "application/json",
+ }
+ jsonStr = ttsrv.httpGetString(url, header)
+
+
+ ttsrv.writeTxtFile('voices.json', jsonStr)
+ }
+
+ voices = JSON.parse(jsonStr)
+ },
+
+ "onLoadUI": function (ctx, linerLayout) {
+ let layout = new LinearLayout(ctx)
+ layout.orientation = LinearLayout.HORIZONTAL // 水平布局
+ let params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1)
+
+ skillSpinner = JSpinner(ctx, "语言技能 (language skill)")
+ linerLayout.addView(skillSpinner)
+ ttsrv.setMargins(skillSpinner, 2, 4, 0, 0)
+ skillSpinner.setOnItemSelected(function (spinner, pos, item) {
+ ttsrv.tts.data['languageSkill'] = item.value + ''
+ })
+
+ styleSpinner = JSpinner(ctx, "风格 (style)")
+ styleSpinner.layoutParams = params
+ layout.addView(styleSpinner)
+ ttsrv.setMargins(styleSpinner, 2, 4, 0, 0)
+ styleSpinner.setOnItemSelected(function (spinner, pos, item) {
+ ttsrv.tts.data['style'] = item.value
+ // 默认 || value为空 || value空字符串
+ if (pos === 0 || !item.value || item.value === "") {
+ seekStyle.visibility = View.GONE // 移除风格强度
+ } else {
+ seekStyle.visibility = View.VISIBLE // 显示
+ }
+ })
+
+ roleSpinner = JSpinner(ctx, "角色 (role)")
+ roleSpinner.layoutParams = params
+ layout.addView(roleSpinner)
+ ttsrv.setMargins(roleSpinner, 0, 4, 2, 0)
+ roleSpinner.setOnItemSelected(function (spinner, pos, item) {
+ ttsrv.tts.data['role'] = item.value
+ })
+ linerLayout.addView(layout)
+
+ seekStyle = JSeekBar(ctx, "风格强度 (Style degree):")
+ linerLayout.addView(seekStyle)
+ ttsrv.setMargins(seekStyle, 0, 4, 0, -4)
+ seekStyle.setFloatType(2) // 二位小数
+ seekStyle.max = 200 //最大200个刻度
+
+ let styleDegree = Number(ttsrv.tts.data['styleDegree'])
+ if (!styleDegree || isNaN(styleDegree)) {
+ styleDegree = 1.0
+ }
+ seekStyle.value = new java.lang.Float(styleDegree)
+
+ seekStyle.setOnChangeListener(
+ {
+ // 开始时
+ onStartTrackingTouch: function (seek) {
+
+ },
+ // 进度滑动更改时
+ onProgressChanged: function (seek, progress, fromUser) {
+
+ },
+ // 停止时
+ onStopTrackingTouch: function (seek) {
+ ttsrv.tts.data['styleDegree'] = Number(seek.value).toFixed(2)
+ },
+ }
+ )
+ },
+
+ "onVoiceChanged": function (locale, voiceCode) {
+ let vic = currentVoices.get(voiceCode)
+
+ let locale2List = vic['SecondaryLocaleList']
+ let locale2Items = []
+ let locale2Pos = 0
+
+ if (locale2List) {
+ locale2Items.push(Item("默认 (default)", ""))
+ locale2List.map(function (v, i) {
+ let loc = java.util.Locale.forLanguageTag(v)
+ let name = loc.getDisplayName(loc)
+ locale2Items.push(Item(name, v))
+ if (v === ttsrv.tts.data['languageSkill'] + '') {
+ locale2Pos = i + 1
+ }
+ })
+ }
+ skillSpinner.items = locale2Items
+ skillSpinner.selectedPosition = locale2Pos
+
+ if (locale2Items.length === 0) {
+ skillSpinner.visibility = View.GONE
+ } else {
+ skillSpinner.visibility = View.VISIBLE
+ }
+
+ let styles = vic['StyleList']
+ let styleItems = []
+ let stylePos = 0
+ if (styles) {
+ styleItems.push(Item("默认 (general)", ""))
+ styles.map(function (v, i) {
+ styleItems.push(Item(getString(v), v))
+ if (v === ttsrv.tts.data['style'] + '') {
+ stylePos = i + 1 //算上默认的item 所以要 +1
+ }
+ })
+ } else {
+ seekStyle.visibility = View.GONE
+ }
+ styleSpinner.items = styleItems
+ styleSpinner.selectedPosition = stylePos
+
+ let roles = vic['RolePlayList']
+ let roleItems = []
+ let rolePos = 0
+ if (roles) {
+ roleItems.push(Item("默认 (default)", ""))
+ roles.map(function (v, i) {
+ roleItems.push(Item(getString(v), v))
+ if (v === ttsrv.tts.data['role'] + '') {
+ rolePos = i + 1 //算上默认的item 所以要 +1
+ }
+ })
+ }
+ roleSpinner.items = roleItems
+ roleSpinner.selectedPosition = rolePos
+ }
+}
+
+let cnLocales = {
+ "narrator": "旁白",
+ "girl": "女孩",
+ "boy": "男孩",
+ "youngadultfemale": "年轻女性",
+ "youngadultmale": "年轻男性",
+ "olderadultfemale": "年长女性",
+ "olderadultmale": "年长男性",
+ "seniorfemale": "年老女性",
+ "seniormale": "年老男性",
+
+ "advertisement_upbeat": "广告推销",
+ "affectionate": "亲切",
+ "angry": "生气",
+ "assistant": "数字助理",
+ "calm": "平静",
+ "chat": "闲聊",
+ "cheerful": "愉快",
+ "customerservice": "客户服务",
+ "depressed": "沮丧",
+ "disgruntled": "不满",
+ "documentary-narration": "纪录片",
+ "embarrassed": "尴尬",
+ "empathetic": "同情",
+ "envious": "嫉妒",
+ "excited": "兴奋",
+ "fearful": "恐惧",
+ "friendly": "友好",
+ "gentle": "温柔",
+ "hopeful": "希望",
+ "lyrical": "抒情",
+ "narration-professional": "专业",
+ "narration-relaxed": "轻松",
+ "newscast": "新闻",
+ "newscast-casual": "新闻-休闲",
+ "newscast-formal": "新闻-正式",
+ "poetry-reading": "诗歌朗诵",
+ "sad": "悲伤",
+ "serious": "严肃",
+ "shouting": "喊叫",
+ "sports_commentary": "体育",
+ "sports_commentary_excited": "体育-兴奋",
+ "whispering": "耳语",
+ "terrified": "恐惧",
+ "unfriendly": "不友好",
+}
+
+let isZh = java.util.Locale.getDefault().getLanguage() == 'zh'
+
+function getString(key) {
+ if (isZh) {
+ return cnLocales[key.toLowerCase()] || key
+ } else {
+ return key
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/assets/defaultData/speech_rule.js b/app/src/main/assets/defaultData/speech_rule.js
new file mode 100644
index 000000000..3fd088e27
--- /dev/null
+++ b/app/src/main/assets/defaultData/speech_rule.js
@@ -0,0 +1,52 @@
+let SpeechRuleJS = {
+ name: "旁白/对话",
+ id: "ttsrv.multi_voice",
+ author: "TTS Server",
+ version: 4,
+ tags: {narration: "旁白", dialogue: "对话"},
+
+ handleText(text) {
+ const list = [];
+ let tmpStr = "";
+ let endTag = "narration";
+
+ text.split("").forEach((char, index) => {
+ tmpStr += char;
+
+ if (char === '“') {
+ endTag = "dialogue";
+ list.push({text: tmpStr, tag: "narration"});
+ tmpStr = "";
+ } else if (char === '”') {
+ endTag = "narration";
+ tmpStr = tmpStr.slice(0, -1)
+ list.push({text: tmpStr, tag: "dialogue"});
+ tmpStr = "";
+ } else if (index === text.length - 1) {
+ list.push({text: tmpStr, tag: endTag});
+ }
+ });
+
+ return list;
+ },
+
+ splitText(text) {
+ let separatorStr = "。??!!;;"
+
+ let list = []
+ let tmpStr = ""
+ text.split("").forEach((char, index) => {
+ tmpStr += char
+
+ if (separatorStr.includes(char)) {
+ list.push(tmpStr)
+ tmpStr = ""
+ } else if (index === text.length - 1) {
+ list.push(tmpStr);
+ }
+ })
+
+ return list.filter(item => item.replace(/[“”]/g, '').trim().length > 0);
+ }
+
+};
diff --git a/app/src/main/assets/help/app.md b/app/src/main/assets/help/app.md
new file mode 100644
index 000000000..d50b7fe32
--- /dev/null
+++ b/app/src/main/assets/help/app.md
@@ -0,0 +1,64 @@
+version-1
+
+### 此帮助文档可在左侧滑菜单打开。
+
+[![Q群](https://img.shields.io/badge/Q%E7%BE%A4-124841768-blue.svg)](https://jq.qq.com/?_wv=1027&k=y7WCDjEA)
+[![Issue](https://img.shields.io/badge/Github-Issue-greeb.svg)](https://github.com/jing332/tts-server-android/issues)
+[![Dev](https://img.shields.io/github/actions/workflow/status/jing332/tts-server-android/test.yml?label=%E5%BC%80%E5%8F%91%E7%89%88)](https://github.com/jing332/tts-server-android/actions/workflows/test.yml)
+
+# TTS Server
+本应用有3个独立功能,通过左侧滑菜单进行切换。
+
+
+## 1️⃣ 系统TTS
+以下4个界面,在右上角更多选项中都有独立的导入、导出功能。您还可在设置中进行全部备份、恢复等操作。
+
+### 主界面
+配置列表,用于管理TTS配置,您可使用分组功能进行一键切换多个配置。
+- 可在设置中调换 `编辑`与`试听` 按钮的位置 ( 长按编辑按钮进行试听,反之,长按试听按钮进行编辑 )
+
+### 朗读规则
+用于处理朗读文本,根据用户配置的标签进行匹配TTS配置(如:旁白/对话)。
+程序已内置 基于中文的双引号的 `旁白对话` 朗读规则,您可直接进行使用。
+
+### 插件
+用于扩展TTS功能,使用JS脚本进行调用互联网的上的TTS接口,如:内置的`Azure插件`。
+
+### 替换规则
+用于替换朗读文本进行纠正发音等操作,如:将“你好”替换为“您好”
+
+高级示例:
+- 将字数5以内的对话的双引号替换为【】,以达到旁白朗读的目的。
+```
+(启用正则表达式)
+替换规则:(“)(.{1,5})(”)
+替换为:【$2】
+```
+
+## 👨🏫 系统TTS 常见问题
+### 1. 锁屏后一段时间朗读突然停止?
+> 在 `系统设置->应用->电池优化` 中将本APP与阅读APP加入电池优化白名单。
+>
+> 对于本APP,您可在左侧滑菜单中单击 `电池优化白名单` 进行快捷设置。
+>
+> PS: 对于国内系统,您可能还需对后台任务上锁,启用后台权限等操作。
+
+### 2. 段落间隔时间长?
+> 一般是由于网络延迟原因,因为 安卓系统TTS 服务的技术限制,导致无法预缓存音频,故每次只能同步获取。
+
+### 3. 启动朗读时提示 `⚠️ 缺少{朗读全部},...` ?
+> 添加一个`朗读全部`类型的TTS配置并启用。或尝试开启多语音选项使用 `标签`配置
+
+### 4. 启动朗读时提示 `⚠️无标签配置,...!`
+> 添加一个 `标签` 类型的TTS配置并启用。或尝试关闭多语音选项使用 `朗读全部` 。
+
+
+## 2️⃣ 系统TTS转发器
+用于将安卓系统TTS转为HTTP网络接口形式,便于在网页调用。
+
+## 3️⃣ 微软TTS转发器
+用于将Edge大声朗读接口简化为HTTP网络接口形式,便于`开源阅读`进行调用。
+
+这也是`TTS Server`名称的来源:
+
+早期,我将 tts-server-go 移植到安卓,即得名 tts-server-android (Github项目名)
\ No newline at end of file
diff --git a/app/src/main/assets/textmate/abyss.json b/app/src/main/assets/textmate/abyss.json
new file mode 100644
index 000000000..24ae2408e
--- /dev/null
+++ b/app/src/main/assets/textmate/abyss.json
@@ -0,0 +1,223 @@
+{
+ "name": "Abyss",
+ "settings": [{
+ "settings": {
+ "background": "#000c18",
+ "caret": "#ddbb88",
+ "foreground": "#6688cc",
+ "invisibles": "#002040",
+ "lineHighlight": "#082050",
+ "selection": "#770811",
+ "guide": "#002952"
+ }
+ }, {
+ "scope": ["meta.embedded", "source.groovy.embedded"],
+ "settings": {
+ "foreground": "#6688cc"
+ }
+ }, {
+ "name": "Comment",
+ "scope": "comment",
+ "settings": {
+ "foreground": "#384887"
+ }
+ }, {
+ "name": "String",
+ "scope": "string",
+ "settings": {
+ "foreground": "#22aa44"
+ }
+ }, {
+ "name": "Number",
+ "scope": "constant.numeric",
+ "settings": {
+ "foreground": "#f280d0"
+ }
+ }, {
+ "name": "Built-in constant",
+ "scope": "constant.language",
+ "settings": {
+ "foreground": "#f280d0"
+ }
+ }, {
+ "name": "User-defined constant",
+ "scope": ["constant.character", "constant.other"],
+ "settings": {
+ "foreground": "#f280d0"
+ }
+ }, {
+ "name": "Variable",
+ "scope": "variable",
+ "settings": {
+ "fontStyle": ""
+ }
+ }, {
+ "name": "Keyword",
+ "scope": "keyword",
+ "settings": {
+ "foreground": "#225588"
+ }
+ }, {
+ "name": "Storage",
+ "scope": "storage",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#225588"
+ }
+ }, {
+ "name": "Storage type",
+ "scope": "storage.type",
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#9966b8"
+ }
+ }, {
+ "name": "Class name",
+ "scope": ["entity.name.class", "entity.name.type", "entity.name.namespace", "entity.name.scope-resolution"],
+ "settings": {
+ "fontStyle": "underline",
+ "foreground": "#ffeebb"
+ }
+ }, {
+ "name": "Inherited class",
+ "scope": "entity.other.inherited-class",
+ "settings": {
+ "fontStyle": "italic underline",
+ "foreground": "#ddbb88"
+ }
+ }, {
+ "name": "Function name",
+ "scope": "entity.name.function",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#ddbb88"
+ }
+ }, {
+ "name": "Function argument",
+ "scope": "variable.parameter",
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#2277ff"
+ }
+ }, {
+ "name": "Tag name",
+ "scope": "entity.name.tag",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#225588"
+ }
+ }, {
+ "name": "Tag attribute",
+ "scope": "entity.other.attribute-name",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#ddbb88"
+ }
+ }, {
+ "name": "Library function",
+ "scope": "support.function",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#9966b8"
+ }
+ }, {
+ "name": "Library constant",
+ "scope": "support.constant",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#9966b8"
+ }
+ }, {
+ "name": "Library class/type",
+ "scope": ["support.type", "support.class"],
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#9966b8"
+ }
+ }, {
+ "name": "Library variable",
+ "scope": "support.other.variable",
+ "settings": {
+ "fontStyle": ""
+ }
+ }, {
+ "name": "Invalid",
+ "scope": "invalid",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#A22D44"
+ }
+ }, {
+ "name": "Invalid deprecated",
+ "scope": "invalid.deprecated",
+ "settings": {
+ "foreground": "#A22D44"
+ }
+ }, {
+ "name": "diff: header",
+ "scope": ["meta.diff", "meta.diff.header"],
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#E0EDDD"
+ }
+ }, {
+ "name": "diff: deleted",
+ "scope": "markup.deleted",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#dc322f"
+ }
+ }, {
+ "name": "diff: changed",
+ "scope": "markup.changed",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#cb4b16"
+ }
+ }, {
+ "name": "diff: inserted",
+ "scope": "markup.inserted",
+ "settings": {
+ "foreground": "#219186"
+ }
+ }, {
+ "name": "Markup Quote",
+ "scope": "markup.quote",
+ "settings": {
+ "foreground": "#22aa44"
+ }
+ }, {
+ "name": "Markup Styling",
+ "scope": ["markup.bold", "markup.italic"],
+ "settings": {
+ "foreground": "#22aa44"
+ }
+ }, {
+ "name": "Markup: Strong",
+ "scope": "markup.bold",
+ "settings": {
+ "fontStyle": "bold"
+ }
+ }, {
+ "name": "Markup: Emphasis",
+ "scope": "markup.italic",
+ "settings": {
+ "fontStyle": "italic"
+ }
+ }, {
+ "name": "Markup Inline",
+ "scope": "markup.inline.raw",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#9966b8"
+ }
+ }, {
+ "name": "Markup Headings",
+ "scope": ["markup.heading", "markup.heading.setext"],
+ "settings": {
+ "fontStyle": "bold",
+ "foreground": "#6688cc"
+ }
+ }]
+
+}
\ No newline at end of file
diff --git a/app/src/main/assets/textmate/darcula.json b/app/src/main/assets/textmate/darcula.json
new file mode 100644
index 000000000..7c07f8d1d
--- /dev/null
+++ b/app/src/main/assets/textmate/darcula.json
@@ -0,0 +1,465 @@
+{
+ "name": "darcula",
+ "settings": [{
+ "settings": {
+ "background": "#242424",
+ "foreground": "#cccccc",
+ "lineHighlight": "#2B2B2B",
+ "selection": "#214283",
+ "highlightedDelimetersForeground": "#57f6c0"
+ }
+ },
+ {
+ "name": "Comment",
+ "scope": "comment",
+ "settings": {
+ "foreground": "#707070"
+ }
+ },
+ {
+ "name": "Operator Keywords",
+ "scope": "keyword.operator,keyword.operator.logical,keyword.operator.relational,keyword.operator.assignment,keyword.operator.comparison,keyword.operator.ternary,keyword.operator.arithmetic,keyword.operator.spread",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Strings",
+ "scope": "string,string.character.escape,string.template.quoted,string.template.quoted.punctuation,string.template.quoted.punctuation.single,string.template.quoted.punctuation.double,string.type.declaration.annotation,string.template.quoted.punctuation.tag",
+ "settings": {
+ "foreground": "#6A8759"
+ }
+ },
+ {
+ "name": "String Interpolation Begin and End",
+ "scope": "punctuation.definition.template-expression.begin,punctuation.definition.template-expression.end",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "String Interpolation Body",
+ "scope": "expression.string,meta.template.expression",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Number",
+ "scope": "constant.numeric",
+ "settings": {
+ "foreground": "#7A9EC2"
+ }
+ },
+ {
+ "name": "Built-in constant",
+ "scope": "constant.language,variable.language",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "User-defined constant",
+ "scope": "constant.character, constant.other",
+ "settings": {
+ "foreground": "#9E7BB0"
+ }
+ },
+ {
+ "name": "Keyword",
+ "scope": "keyword,keyword.operator.new,keyword.operator.delete,keyword.operator.static,keyword.operator.this,keyword.operator.expression",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "Types, Class Types",
+ "scope": "entity.name.type,meta.return.type,meta.type.annotation,meta.type.parameters,support.type.primitive",
+ "settings": {
+ "foreground": "#7A9EC2"
+ }
+ },
+ {
+ "name": "Storage type",
+ "scope": "storage,storage.type,storage.modifier,storage.arrow",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "Class constructor",
+ "scope": "class.instance.constructor,new.expr entity.name.type",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "Function",
+ "scope": "support.function, entity.name.function",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "Function Types",
+ "scope": "annotation.meta.ts, annotation.meta.tsx",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Function Argument",
+ "scope": "variable.parameter, operator.rest.parameters",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Variable, Property",
+ "scope": "variable.property,variable.other.property,variable.other.object.property,variable.object.property,support.variable.property",
+ "settings": {
+ "foreground": "#9E7BB0"
+ }
+ },
+ {
+ "name": "Module Name",
+ "scope": "quote.module",
+ "settings": {
+ "foreground": "#6A8759"
+ }
+ },
+ {
+ "name": "Markup Headings",
+ "scope": "markup.heading",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "Tag name",
+ "scope": "punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end, entity.name.tag",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "Tag attribute",
+ "scope": "entity.other.attribute-name",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Object Keys",
+ "scope": "meta.object-literal.key",
+ "settings": {
+ "foreground": "#9E7BB0"
+ }
+ },
+ {
+ "name": "TypeScript Class Modifiers",
+ "scope": "storage.modifier.ts",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "TypeScript Type Casting",
+ "scope": "ts.cast.expr,ts.meta.entity.class.method.new.expr.cast,ts.meta.entity.type.name.new.expr.cast,ts.meta.entity.type.name.var-single-variable.annotation,tsx.cast.expr,tsx.meta.entity.class.method.new.expr.cast,tsx.meta.entity.type.name.new.expr.cast,tsx.meta.entity.type.name.var-single-variable.annotation",
+ "settings": {
+ "foreground": "#7A9EC2"
+ }
+ },
+ {
+ "name": "TypeScript Type Declaration",
+ "scope": "ts.meta.type.support,ts.meta.type.entity.name,ts.meta.class.inherited-class,tsx.meta.type.support,tsx.meta.type.entity.name,tsx.meta.class.inherited-class,type-declaration,enum-declaration",
+ "settings": {
+ "foreground": "#7A9EC2"
+ }
+ },
+ {
+ "name": "TypeScript Method Declaration",
+ "scope": "function-declaration,method-declaration,method-overload-declaration,type-fn-type-parameters",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "Documentation Block",
+ "scope": "comment.block.documentation",
+ "settings": {
+ "foreground": "#6A8759"
+ }
+ },
+ {
+ "name": "Documentation Highlight (JSDoc)",
+ "scope": "storage.type.class.jsdoc",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "Import-Export-All (*) Keyword",
+ "scope": "constant.language.import-export-all",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Object Key Seperator",
+ "scope": "objectliteral.key.separator, punctuation.separator.key-value",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Regex",
+ "scope": "regex",
+ "settings": {
+ "fontStyle": " italic"
+ }
+ },
+ {
+ "name": "Typescript Namespace",
+ "scope": "ts.meta.entity.name.namespace,tsx.meta.entity.name.namespace",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Regex Character-class",
+ "scope": "regex.character-class",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Class Name",
+ "scope": "entity.name.type.class",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Class Inheritances",
+ "scope": "entity.other.inherited-class",
+ "settings": {
+ "foreground": "#7A9EC2"
+ }
+ },
+ {
+ "name": "Documentation Entity",
+ "scope": "entity.name.type.instance.jsdoc",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "YAML entity",
+ "scope": "yaml.entity.name,yaml.string.entity.name",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "YAML string value",
+ "scope": "yaml.string.out",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Ignored (Exceptions Rules)",
+ "scope": "meta.brace.square.ts,block.support.module,block.support.type.module,block.support.function.variable,punctuation.definition.typeparameters.begin,punctuation.definition.typeparameters.end",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Regex",
+ "scope": "string.regexp",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "Regex Group/Set",
+ "scope": "punctuation.definition.group.regexp,punctuation.definition.character-class.regexp",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "Regex Character Class",
+ "scope": "constant.other.character-class.regexp, constant.character.escape.ts",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Regex Or Operator",
+ "scope": "expr.regex.or.operator",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Tag string",
+ "scope": "string.template.tag,string.template.punctuation.tag,string.quoted.punctuation.tag,string.quoted.embedded.tag, string.quoted.double.tag",
+ "settings": {
+ "foreground": "#6A8759"
+ }
+ },
+ {
+ "name": "Tag function parenthesis",
+ "scope": "tag.punctuation.begin.arrow.parameters.embedded,tag.punctuation.end.arrow.parameters.embedded",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "Object-literal key class",
+ "scope": "object-literal.object.member.key.field.other,object-literal.object.member.key.accessor,object-literal.object.member.key.array.brace.square",
+ "settings": {
+ "foreground": "#CCCCCC"
+ }
+ },
+ {
+ "name": "CSS Property-value",
+ "scope": "property-list.property-value,property-list.constant",
+ "settings": {
+ "foreground": "#A5C261"
+ }
+ },
+ {
+ "name": "CSS Property variable",
+ "scope": "support.type.property-name.variable.css,support.type.property-name.variable.scss,variable.scss",
+ "settings": {
+ "foreground": "#7A9EC2"
+ }
+ },
+ {
+ "name": "CSS Property entity",
+ "scope": "entity.other.attribute-name.class.css,entity.other.attribute-name.class.scss,entity.other.attribute-name.parent-selector-suffix.css,entity.other.attribute-name.parent-selector-suffix.scss",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "CSS Property-value",
+ "scope": "property-list.property-value.rgb-value, keyword.other.unit.css,keyword.other.unit.scss",
+ "settings": {
+ "foreground": "#7A9EC2"
+ }
+ },
+ {
+ "name": "CSS Property-value function",
+ "scope": "property-list.property-value.function",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "CSS constant variables",
+ "scope": "support.constant.property-value.css,support.constant.property-value.scss",
+ "settings": {
+ "foreground": "#A5C261"
+ }
+ },
+ {
+ "name": "CSS Tag",
+ "scope": "css.entity.name.tag,scss.entity.name.tag",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "CSS ID, Selector",
+ "scope": "meta.selector.css, entity.attribute-name.id, entity.other.attribute-name.pseudo-class.css,entity.other.attribute-name.pseudo-element.css",
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ },
+ {
+ "name": "CSS Keyword",
+ "scope": "keyword.scss,keyword.css",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "Triple-slash Directive Tag",
+ "scope": "triple-slash.tag",
+ "settings": {
+ "foreground": "#CCCCCC",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "scope": "token.info-token",
+ "settings": {
+ "foreground": "#6796e6"
+ }
+ },
+ {
+ "scope": "token.warn-token",
+ "settings": {
+ "foreground": "#cd9731"
+ }
+ },
+ {
+ "scope": "token.error-token",
+ "settings": {
+ "foreground": "#f44747"
+ }
+ },
+ {
+ "scope": "token.debug-token",
+ "settings": {
+ "foreground": "#b267e6"
+ }
+ },
+ {
+ "name": "Python operators",
+ "scope": "keyword.operator.logical.python",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "Dart class type",
+ "scope": "support.class.dart",
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "PHP variables",
+ "scope": ["variable.language.php", "variable.other.php"],
+ "settings": {
+ "foreground": "#9E7BB0"
+ }
+ },
+ {
+ "name": "Perl specific",
+ "scope": ["variable.other.readwrite.perl"],
+ "settings": {
+ "foreground": "#9E7BB0"
+ }
+ },
+ {
+ "name": "PHP variables",
+ "scope": ["variable.other.property.php"],
+ "settings": {
+ "foreground": "#CC8242"
+ }
+ },
+ {
+ "name": "PHP variables",
+ "scope": ["support.variable.property.php"],
+ "settings": {
+ "foreground": "#FFC66D"
+ }
+ }
+ ]
+}
diff --git a/app/src/main/assets/textmate/javascript/language-configuration.json b/app/src/main/assets/textmate/javascript/language-configuration.json
new file mode 100644
index 000000000..acccdad11
--- /dev/null
+++ b/app/src/main/assets/textmate/javascript/language-configuration.json
@@ -0,0 +1,188 @@
+{
+ "comments": {
+ "lineComment": "//",
+ "blockComment": [
+ "/*",
+ "*/"
+ ]
+ },
+ "brackets": [
+ [
+ "${",
+ "}"
+ ],
+ [
+ "{",
+ "}"
+ ],
+ [
+ "[",
+ "]"
+ ],
+ [
+ "(",
+ ")"
+ ]
+ ],
+ "autoClosingPairs": [
+ {
+ "open": "{",
+ "close": "}"
+ },
+ {
+ "open": "[",
+ "close": "]"
+ },
+ {
+ "open": "(",
+ "close": ")"
+ },
+ {
+ "open": "'",
+ "close": "'",
+ "notIn": [
+ "string",
+ "comment"
+ ]
+ },
+ {
+ "open": "\"",
+ "close": "\"",
+ "notIn": [
+ "string"
+ ]
+ },
+ {
+ "open": "`",
+ "close": "`",
+ "notIn": [
+ "string",
+ "comment"
+ ]
+ },
+ {
+ "open": "/**",
+ "close": " */",
+ "notIn": [
+ "string"
+ ]
+ }
+ ],
+ "surroundingPairs": [
+ [
+ "{",
+ "}"
+ ],
+ [
+ "[",
+ "]"
+ ],
+ [
+ "(",
+ ")"
+ ],
+ [
+ "'",
+ "'"
+ ],
+ [
+ "\"",
+ "\""
+ ],
+ [
+ "`",
+ "`"
+ ],
+ [
+ "<",
+ ">"
+ ]
+ ],
+ "autoCloseBefore": ";:.,=}])>` \n\t",
+ "folding": {
+ "markers": {
+ "start": "^\\s*//\\s*#?region\\b",
+ "end": "^\\s*//\\s*#?endregion\\b"
+ }
+ },
+ "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>/\\?\\s]+)",
+ "indentationRules": {
+ "decreaseIndentPattern": {
+ "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]].*$"
+ },
+ "increaseIndentPattern": {
+ "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$"
+ },
+
+ "unIndentedLinePattern": {
+ "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$"
+ }
+ },
+ "onEnterRules": [
+ {
+ "beforeText": {
+ "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$"
+ },
+ "afterText": {
+ "pattern": "^\\s*\\*/$"
+ },
+ "action": {
+ "indent": "indentOutdent",
+ "appendText": " * "
+ }
+ },
+ {
+
+ "beforeText": {
+ "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$"
+ },
+ "action": {
+ "indent": "none",
+ "appendText": " * "
+ }
+ },
+ {
+ "beforeText": {
+ "pattern": "^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$"
+ },
+ "previousLineText": {
+ "pattern": "(?=^(\\s*(/\\*\\*|\\*)).*)(?=(?!(\\s*\\*/)))"
+ },
+ "action": {
+ "indent": "none",
+ "appendText": "* "
+ }
+ },
+ {
+
+ "beforeText": {
+ "pattern": "^(\\t|[ ])*[ ]\\*/\\s*$"
+ },
+ "action": {
+ "indent": "none",
+ "removeText": 1
+ }
+ },
+ {
+
+ "beforeText": {
+ "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$"
+ },
+ "action": {
+ "indent": "none",
+ "removeText": 1
+ }
+ },
+ {
+ "beforeText": {
+ "pattern": "^\\s*(\\bcase\\s.+:|\\bdefault:)$"
+ },
+ "afterText": {
+ "pattern": "^(?!\\s*(\\bcase\\b|\\bdefault\\b))"
+ },
+ "action": {
+ "indent": "indent"
+ }
+ }
+ ]
+}
diff --git a/app/src/main/assets/textmate/javascript/syntaxes/JavaScript.tmLanguage.json b/app/src/main/assets/textmate/javascript/syntaxes/JavaScript.tmLanguage.json
new file mode 100644
index 000000000..c1070ccdd
--- /dev/null
+++ b/app/src/main/assets/textmate/javascript/syntaxes/JavaScript.tmLanguage.json
@@ -0,0 +1,5876 @@
+{
+ "information_for_contributors": [
+ "This file has been converted from https://github.com/microsoft/TypeScript-TmLanguage/blob/master/TypeScriptReact.tmLanguage",
+ "If you want to provide a fix or improvement, please create a pull request against the original repository.",
+ "Once accepted there, we are happy to receive an update request."
+ ],
+ "version": "https://github.com/microsoft/TypeScript-TmLanguage/commit/4d30ff834ec324f56291addd197aa1e423cedfdd",
+ "name": "JavaScript (with React support)",
+ "scopeName": "source.js",
+ "patterns": [
+ {
+ "include": "#directives"
+ },
+ {
+ "include": "#statements"
+ },
+ {
+ "include": "#shebang"
+ }
+ ],
+ "repository": {
+ "shebang": {
+ "name": "comment.line.shebang.js",
+ "match": "\\A(#!).*(?=$)",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.comment.js"
+ }
+ }
+ },
+ "statements": {
+ "patterns": [
+ {
+ "include": "#declaration"
+ },
+ {
+ "include": "#control-statement"
+ },
+ {
+ "include": "#after-operator-block-as-object-literal"
+ },
+ {
+ "include": "#decl-block"
+ },
+ {
+ "include": "#label"
+ },
+ {
+ "include": "#expression"
+ },
+ {
+ "include": "#punctuation-semicolon"
+ },
+ {
+ "include": "#string"
+ },
+ {
+ "include": "#comment"
+ }
+ ]
+ },
+ "declaration": {
+ "patterns": [
+ {
+ "include": "#decorator"
+ },
+ {
+ "include": "#var-expr"
+ },
+ {
+ "include": "#function-declaration"
+ },
+ {
+ "include": "#class-declaration"
+ },
+ {
+ "include": "#interface-declaration"
+ },
+ {
+ "include": "#enum-declaration"
+ },
+ {
+ "include": "#namespace-declaration"
+ },
+ {
+ "include": "#type-alias-declaration"
+ },
+ {
+ "include": "#import-equals-declaration"
+ },
+ {
+ "include": "#import-declaration"
+ },
+ {
+ "include": "#export-declaration"
+ },
+ {
+ "name": "storage.modifier.js",
+ "match": "(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*(?\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*))))))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))",
+ "beginCaptures": {
+ "1": {
+ "name": "meta.definition.variable.js entity.name.function.js"
+ },
+ "2": {
+ "name": "keyword.operator.definiteassignment.js"
+ }
+ },
+ "end": "(?=$|^|[;,=}]|((?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*(?\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*))))))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))",
+ "beginCaptures": {
+ "1": {
+ "name": "meta.definition.variable.js variable.other.constant.js entity.name.function.js"
+ }
+ },
+ "end": "(?=$|^|[;,=}]|((?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*(?\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*))))))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))",
+ "captures": {
+ "1": {
+ "name": "storage.modifier.js"
+ },
+ "2": {
+ "name": "keyword.operator.rest.js"
+ },
+ "3": {
+ "name": "entity.name.function.js variable.language.this.js"
+ },
+ "4": {
+ "name": "entity.name.function.js"
+ },
+ "5": {
+ "name": "keyword.operator.optional.js"
+ }
+ }
+ },
+ {
+ "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*(?\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*))))))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))",
+ "captures": {
+ "1": {
+ "name": "meta.definition.property.js entity.name.function.js"
+ },
+ "2": {
+ "name": "keyword.operator.optional.js"
+ },
+ "3": {
+ "name": "keyword.operator.definiteassignment.js"
+ }
+ }
+ },
+ {
+ "name": "meta.definition.property.js variable.object.property.js",
+ "match": "\\#?[_$[:alpha:]][_$[:alnum:]]*"
+ },
+ {
+ "name": "keyword.operator.optional.js",
+ "match": "\\?"
+ },
+ {
+ "name": "keyword.operator.definiteassignment.js",
+ "match": "\\!"
+ }
+ ]
+ },
+ "variable-initializer": {
+ "patterns": [
+ {
+ "begin": "(?\\s*$)",
+ "beginCaptures": {
+ "1": {
+ "name": "keyword.operator.assignment.js"
+ }
+ },
+ "end": "(?=$|^|[,);}\\]]|((?]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*))?[\\(])",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.js"
+ },
+ "2": {
+ "name": "storage.modifier.js"
+ },
+ "3": {
+ "name": "storage.modifier.js"
+ },
+ "4": {
+ "name": "storage.modifier.async.js"
+ },
+ "5": {
+ "name": "keyword.operator.new.js"
+ },
+ "6": {
+ "name": "keyword.generator.asterisk.js"
+ }
+ },
+ "end": "(?=\\}|;|,|$)|(?<=\\})",
+ "patterns": [
+ {
+ "include": "#method-declaration-name"
+ },
+ {
+ "include": "#function-body"
+ }
+ ]
+ },
+ {
+ "name": "meta.method.declaration.js",
+ "begin": "(?x)(?]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*))?[\\(])",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.js"
+ },
+ "2": {
+ "name": "storage.modifier.js"
+ },
+ "3": {
+ "name": "storage.modifier.js"
+ },
+ "4": {
+ "name": "storage.modifier.async.js"
+ },
+ "5": {
+ "name": "storage.type.property.js"
+ },
+ "6": {
+ "name": "keyword.generator.asterisk.js"
+ }
+ },
+ "end": "(?=\\}|;|,|$)|(?<=\\})",
+ "patterns": [
+ {
+ "include": "#method-declaration-name"
+ },
+ {
+ "include": "#function-body"
+ }
+ ]
+ }
+ ]
+ },
+ "object-literal-method-declaration": {
+ "name": "meta.method.declaration.js",
+ "begin": "(?x)(?]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*))?[\\(])",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ },
+ "2": {
+ "name": "storage.type.property.js"
+ },
+ "3": {
+ "name": "keyword.generator.asterisk.js"
+ }
+ },
+ "end": "(?=\\}|;|,)|(?<=\\})",
+ "patterns": [
+ {
+ "include": "#method-declaration-name"
+ },
+ {
+ "include": "#function-body"
+ },
+ {
+ "begin": "(?x)(?]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*))?[\\(])",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ },
+ "2": {
+ "name": "storage.type.property.js"
+ },
+ "3": {
+ "name": "keyword.generator.asterisk.js"
+ }
+ },
+ "end": "(?=\\(|\\<)",
+ "patterns": [
+ {
+ "include": "#method-declaration-name"
+ }
+ ]
+ }
+ ]
+ },
+ "method-declaration-name": {
+ "begin": "(?x)(?=((\\b(?)",
+ "captures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ },
+ "2": {
+ "name": "variable.parameter.js"
+ }
+ }
+ },
+ {
+ "name": "meta.arrow.js",
+ "begin": "(?x) (?:\n (? is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n )\n)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ }
+ },
+ "end": "(?==>|\\{|(^\\s*(export|function|class|interface|let|var|const|import|enum|namespace|module|type|abstract|declare)\\s+))",
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#type-parameters"
+ },
+ {
+ "include": "#function-parameters"
+ },
+ {
+ "include": "#arrow-return-type"
+ },
+ {
+ "include": "#possibly-arrow-return-type"
+ }
+ ]
+ },
+ {
+ "name": "meta.arrow.js",
+ "begin": "=>",
+ "beginCaptures": {
+ "0": {
+ "name": "storage.type.function.arrow.js"
+ }
+ },
+ "end": "((?<=\\}|\\S)(?)|((?!\\{)(?=\\S)))(?!\\/[\\/\\*])",
+ "patterns": [
+ {
+ "include": "#single-line-comment-consuming-line-ending"
+ },
+ {
+ "include": "#decl-block"
+ },
+ {
+ "include": "#expression"
+ }
+ ]
+ }
+ ]
+ },
+ "indexer-declaration": {
+ "name": "meta.indexer.declaration.js",
+ "begin": "(?:(?]|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^yield|[^\\._$[:alnum:]]yield|^throw|[^\\._$[:alnum:]]throw|^in|[^\\._$[:alnum:]]in|^of|[^\\._$[:alnum:]]of|^typeof|[^\\._$[:alnum:]]typeof|&&|\\|\\||\\*)\\s*(\\{)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.block.js"
+ }
+ },
+ "end": "\\}",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.block.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#object-member"
+ }
+ ]
+ },
+ "object-literal": {
+ "name": "meta.objectliteral.js",
+ "begin": "\\{",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.block.js"
+ }
+ },
+ "end": "\\}",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.block.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#object-member"
+ }
+ ]
+ },
+ "object-member": {
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#object-literal-method-declaration"
+ },
+ {
+ "name": "meta.object.member.js meta.object-literal.key.js",
+ "begin": "(?=\\[)",
+ "end": "(?=:)|((?<=[\\]])(?=\\s*[\\(\\<]))",
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#array-literal"
+ }
+ ]
+ },
+ {
+ "name": "meta.object.member.js meta.object-literal.key.js",
+ "begin": "(?=[\\'\\\"\\`])",
+ "end": "(?=:)|((?<=[\\'\\\"\\`])(?=((\\s*[\\(\\<,}])|(\\s+(as)\\s+))))",
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#string"
+ }
+ ]
+ },
+ {
+ "name": "meta.object.member.js meta.object-literal.key.js",
+ "begin": "(?x)(?=(\\b(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))",
+ "captures": {
+ "0": {
+ "name": "meta.object-literal.key.js"
+ },
+ "1": {
+ "name": "entity.name.function.js"
+ }
+ }
+ },
+ {
+ "name": "meta.object.member.js",
+ "match": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(?=(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*:)",
+ "captures": {
+ "0": {
+ "name": "meta.object-literal.key.js"
+ }
+ }
+ },
+ {
+ "name": "meta.object.member.js",
+ "begin": "\\.\\.\\.",
+ "beginCaptures": {
+ "0": {
+ "name": "keyword.operator.spread.js"
+ }
+ },
+ "end": "(?=,|\\})",
+ "patterns": [
+ {
+ "include": "#expression"
+ }
+ ]
+ },
+ {
+ "name": "meta.object.member.js",
+ "match": "([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=,|\\}|$|\\/\\/|\\/\\*)",
+ "captures": {
+ "1": {
+ "name": "variable.other.readwrite.js"
+ }
+ }
+ },
+ {
+ "name": "meta.object.member.js",
+ "match": "(?]|\\|\\||\\&\\&|\\!\\=\\=|$|^|((?]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)\\(\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ }
+ },
+ "end": "(?<=\\))",
+ "patterns": [
+ {
+ "include": "#type-parameters"
+ },
+ {
+ "begin": "\\(",
+ "beginCaptures": {
+ "0": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "end": "\\)",
+ "endCaptures": {
+ "0": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression-inside-possibly-arrow-parens"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "begin": "(?<=:)\\s*(async)?\\s*(\\()(?=\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ },
+ "2": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "end": "\\)",
+ "endCaptures": {
+ "0": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression-inside-possibly-arrow-parens"
+ }
+ ]
+ },
+ {
+ "begin": "(?<=:)\\s*(async)?\\s*(?=\\<\\s*$)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ }
+ },
+ "end": "(?<=\\>)",
+ "patterns": [
+ {
+ "include": "#type-parameters"
+ }
+ ]
+ },
+ {
+ "begin": "(?<=\\>)\\s*(\\()(?=\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))",
+ "beginCaptures": {
+ "1": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "end": "\\)",
+ "endCaptures": {
+ "0": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression-inside-possibly-arrow-parens"
+ }
+ ]
+ },
+ {
+ "include": "#possibly-arrow-return-type"
+ },
+ {
+ "include": "#expression"
+ }
+ ]
+ },
+ {
+ "include": "#punctuation-comma"
+ }
+ ]
+ },
+ "ternary-expression": {
+ "begin": "(?!\\?\\.\\s*[^[:digit:]])(\\?)(?!\\?)",
+ "beginCaptures": {
+ "1": {
+ "name": "keyword.operator.ternary.js"
+ }
+ },
+ "end": "\\s*(:)",
+ "endCaptures": {
+ "1": {
+ "name": "keyword.operator.ternary.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression"
+ }
+ ]
+ },
+ "function-call": {
+ "patterns": [
+ {
+ "begin": "(?=(((([_$[:alpha:]][_$[:alnum:]]*)(\\s*\\??\\.\\s*(\\#?[_$[:alpha:]][_$[:alnum:]]*))*)|(\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*))|(?<=[\\)]))\\s*(?:(\\?\\.\\s*)|(\\!))?((<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))(([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>)*(?))*(?)*(?\\s*)?\\())",
+ "end": "(?<=\\))(?!(((([_$[:alpha:]][_$[:alnum:]]*)(\\s*\\??\\.\\s*(\\#?[_$[:alpha:]][_$[:alnum:]]*))*)|(\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*))|(?<=[\\)]))\\s*(?:(\\?\\.\\s*)|(\\!))?((<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))(([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>)*(?))*(?)*(?\\s*)?\\())",
+ "patterns": [
+ {
+ "name": "meta.function-call.js",
+ "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)(\\s*\\??\\.\\s*(\\#?[_$[:alpha:]][_$[:alnum:]]*))*)|(\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*))",
+ "end": "(?=\\s*(?:(\\?\\.\\s*)|(\\!))?((<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))(([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>)*(?))*(?)*(?\\s*)?\\())",
+ "patterns": [
+ {
+ "include": "#function-call-target"
+ }
+ ]
+ },
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#function-call-optionals"
+ },
+ {
+ "include": "#type-arguments"
+ },
+ {
+ "include": "#paren-expression"
+ }
+ ]
+ },
+ {
+ "begin": "(?=(((([_$[:alpha:]][_$[:alnum:]]*)(\\s*\\??\\.\\s*(\\#?[_$[:alpha:]][_$[:alnum:]]*))*)|(\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*))|(?<=[\\)]))(<\\s*[\\{\\[\\(]\\s*$))",
+ "end": "(?<=\\>)(?!(((([_$[:alpha:]][_$[:alnum:]]*)(\\s*\\??\\.\\s*(\\#?[_$[:alpha:]][_$[:alnum:]]*))*)|(\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*))|(?<=[\\)]))(<\\s*[\\{\\[\\(]\\s*$))",
+ "patterns": [
+ {
+ "name": "meta.function-call.js",
+ "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)(\\s*\\??\\.\\s*(\\#?[_$[:alpha:]][_$[:alnum:]]*))*)|(\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*))",
+ "end": "(?=(<\\s*[\\{\\[\\(]\\s*$))",
+ "patterns": [
+ {
+ "include": "#function-call-target"
+ }
+ ]
+ },
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#function-call-optionals"
+ },
+ {
+ "include": "#type-arguments"
+ }
+ ]
+ }
+ ]
+ },
+ "function-call-target": {
+ "patterns": [
+ {
+ "include": "#support-function-call-identifiers"
+ },
+ {
+ "name": "entity.name.function.js",
+ "match": "(\\#?[_$[:alpha:]][_$[:alnum:]]*)"
+ }
+ ]
+ },
+ "function-call-optionals": {
+ "patterns": [
+ {
+ "name": "meta.function-call.js punctuation.accessor.optional.js",
+ "match": "\\?\\."
+ },
+ {
+ "name": "meta.function-call.js keyword.operator.definiteassignment.js",
+ "match": "\\!"
+ }
+ ]
+ },
+ "support-function-call-identifiers": {
+ "patterns": [
+ {
+ "include": "#literal"
+ },
+ {
+ "include": "#support-objects"
+ },
+ {
+ "include": "#object-identifiers"
+ },
+ {
+ "include": "#punctuation-accessor"
+ },
+ {
+ "name": "keyword.operator.expression.import.js",
+ "match": "(?:(?]|\\|\\||\\&\\&|\\!\\=\\=|$|((?]|\\|\\||\\&\\&|\\!\\=\\=|$|(([\\&\\~\\^\\|]\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s+instanceof(?![_$[:alnum:]])(?:(?=\\.\\.\\.)|(?!\\.)))|((?]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*))?\\(\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ }
+ },
+ "end": "(?<=\\))",
+ "patterns": [
+ {
+ "include": "#paren-expression-possibly-arrow-with-typeparameters"
+ }
+ ]
+ },
+ {
+ "begin": "(?<=[(=,]|=>|^return|[^\\._$[:alnum:]]return)\\s*(async)?(?=\\s*((((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*))?\\()|(<))\\s*$)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.modifier.async.js"
+ }
+ },
+ "end": "(?<=\\))",
+ "patterns": [
+ {
+ "include": "#paren-expression-possibly-arrow-with-typeparameters"
+ }
+ ]
+ },
+ {
+ "include": "#possibly-arrow-return-type"
+ }
+ ]
+ },
+ "paren-expression-possibly-arrow-with-typeparameters": {
+ "patterns": [
+ {
+ "include": "#type-parameters"
+ },
+ {
+ "begin": "\\(",
+ "beginCaptures": {
+ "0": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "end": "\\)",
+ "endCaptures": {
+ "0": {
+ "name": "meta.brace.round.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression-inside-possibly-arrow-parens"
+ }
+ ]
+ }
+ ]
+ },
+ "expression-inside-possibly-arrow-parens": {
+ "patterns": [
+ {
+ "include": "#expressionWithoutIdentifiers"
+ },
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#string"
+ },
+ {
+ "include": "#decorator"
+ },
+ {
+ "include": "#destructuring-parameter"
+ },
+ {
+ "match": "(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*(?\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*))))))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))",
+ "captures": {
+ "1": {
+ "name": "storage.modifier.js"
+ },
+ "2": {
+ "name": "keyword.operator.rest.js"
+ },
+ "3": {
+ "name": "entity.name.function.js variable.language.this.js"
+ },
+ "4": {
+ "name": "entity.name.function.js"
+ },
+ "5": {
+ "name": "keyword.operator.optional.js"
+ }
+ }
+ },
+ {
+ "match": "(?x)(?:(?]|\\|\\||\\&\\&|\\!\\=\\=|$|((?>=|>>>=|\\|="
+ },
+ {
+ "name": "keyword.operator.bitwise.shift.js",
+ "match": "<<|>>>|>>"
+ },
+ {
+ "name": "keyword.operator.comparison.js",
+ "match": "===|!==|==|!="
+ },
+ {
+ "name": "keyword.operator.relational.js",
+ "match": "<=|>=|<>|<|>"
+ },
+ {
+ "match": "(?<=[_$[:alnum:]])(\\!)\\s*(?:(/=)|(?:(/)(?![/*])))",
+ "captures": {
+ "1": {
+ "name": "keyword.operator.logical.js"
+ },
+ "2": {
+ "name": "keyword.operator.assignment.compound.js"
+ },
+ "3": {
+ "name": "keyword.operator.arithmetic.js"
+ }
+ }
+ },
+ {
+ "name": "keyword.operator.logical.js",
+ "match": "\\!|&&|\\|\\||\\?\\?"
+ },
+ {
+ "name": "keyword.operator.bitwise.js",
+ "match": "\\&|~|\\^|\\|"
+ },
+ {
+ "name": "keyword.operator.assignment.js",
+ "match": "\\="
+ },
+ {
+ "name": "keyword.operator.decrement.js",
+ "match": "--"
+ },
+ {
+ "name": "keyword.operator.increment.js",
+ "match": "\\+\\+"
+ },
+ {
+ "name": "keyword.operator.arithmetic.js",
+ "match": "%|\\*|/|-|\\+"
+ },
+ {
+ "begin": "(?<=[_$[:alnum:])\\]])\\s*(?=(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)+(?:(/=)|(?:(/)(?![/*]))))",
+ "end": "(?:(/=)|(?:(/)(?!\\*([^\\*]|(\\*[^\\/]))*\\*\\/)))",
+ "endCaptures": {
+ "1": {
+ "name": "keyword.operator.assignment.compound.js"
+ },
+ "2": {
+ "name": "keyword.operator.arithmetic.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#comment"
+ }
+ ]
+ },
+ {
+ "match": "(?<=[_$[:alnum:])\\]])\\s*(?:(/=)|(?:(/)(?![/*])))",
+ "captures": {
+ "1": {
+ "name": "keyword.operator.assignment.compound.js"
+ },
+ "2": {
+ "name": "keyword.operator.arithmetic.js"
+ }
+ }
+ }
+ ]
+ },
+ "typeof-operator": {
+ "begin": "(?:&|{\\?]|$|;|^\\s*$|(?:^\\s*(?:abstract|async|class|const|declare|enum|export|function|import|interface|let|module|namespace|return|type|var)\\b))",
+ "patterns": [
+ {
+ "include": "#expression"
+ }
+ ]
+ },
+ "literal": {
+ "patterns": [
+ {
+ "include": "#numeric-literal"
+ },
+ {
+ "include": "#boolean-literal"
+ },
+ {
+ "include": "#null-literal"
+ },
+ {
+ "include": "#undefined-literal"
+ },
+ {
+ "include": "#numericConstant-literal"
+ },
+ {
+ "include": "#array-literal"
+ },
+ {
+ "include": "#this-literal"
+ },
+ {
+ "include": "#super-literal"
+ }
+ ]
+ },
+ "array-literal": {
+ "name": "meta.array.literal.js",
+ "begin": "\\s*(\\[)",
+ "beginCaptures": {
+ "1": {
+ "name": "meta.brace.square.js"
+ }
+ },
+ "end": "\\]",
+ "endCaptures": {
+ "0": {
+ "name": "meta.brace.square.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression"
+ },
+ {
+ "include": "#punctuation-comma"
+ }
+ ]
+ },
+ "numeric-literal": {
+ "patterns": [
+ {
+ "name": "constant.numeric.hex.js",
+ "match": "\\b(?]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\\())\n |\n (?:(EPSILON|MAX_SAFE_INTEGER|MAX_VALUE|MIN_SAFE_INTEGER|MIN_VALUE|NEGATIVE_INFINITY|POSITIVE_INFINITY)\\b(?!\\$)))",
+ "captures": {
+ "1": {
+ "name": "punctuation.accessor.js"
+ },
+ "2": {
+ "name": "punctuation.accessor.optional.js"
+ },
+ "3": {
+ "name": "support.variable.property.js"
+ },
+ "4": {
+ "name": "support.constant.js"
+ }
+ }
+ },
+ {
+ "match": "(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*((([\\{\\[]\\s*)?$)|((\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})\\s*((:\\s*\\{?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))))) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)?\n [(]\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\]))([^=<>]|=[^<])*\\>)*\\>)*>\\s*)? # typeparameters\n \\(\\s*(\\/\\*([^\\*]|(\\*[^\\/]))*\\*\\/\\s*)*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()\\'\\\"\\`]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))",
+ "captures": {
+ "1": {
+ "name": "punctuation.accessor.js"
+ },
+ "2": {
+ "name": "punctuation.accessor.optional.js"
+ },
+ "3": {
+ "name": "entity.name.function.js"
+ }
+ }
+ },
+ {
+ "match": "(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*(\\#?[[:upper:]][_$[:digit:][:upper:]]*)(?![_$[:alnum:]])",
+ "captures": {
+ "1": {
+ "name": "punctuation.accessor.js"
+ },
+ "2": {
+ "name": "punctuation.accessor.optional.js"
+ },
+ "3": {
+ "name": "variable.other.constant.property.js"
+ }
+ }
+ },
+ {
+ "match": "(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*(\\#?[_$[:alpha:]][_$[:alnum:]]*)",
+ "captures": {
+ "1": {
+ "name": "punctuation.accessor.js"
+ },
+ "2": {
+ "name": "punctuation.accessor.optional.js"
+ },
+ "3": {
+ "name": "variable.other.property.js"
+ }
+ }
+ },
+ {
+ "name": "variable.other.constant.js",
+ "match": "([[:upper:]][_$[:digit:][:upper:]]*)(?![_$[:alnum:]])"
+ },
+ {
+ "name": "variable.other.readwrite.js",
+ "match": "[_$[:alpha:]][_$[:alnum:]]*"
+ }
+ ]
+ },
+ "object-identifiers": {
+ "patterns": [
+ {
+ "name": "support.class.js",
+ "match": "([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\\??\\.\\s*prototype\\b(?!\\$))"
+ },
+ {
+ "match": "(?x)(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*(?:\n (\\#?[[:upper:]][_$[:digit:][:upper:]]*) |\n (\\#?[_$[:alpha:]][_$[:alnum:]]*)\n)(?=\\s*\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*)",
+ "captures": {
+ "1": {
+ "name": "punctuation.accessor.js"
+ },
+ "2": {
+ "name": "punctuation.accessor.optional.js"
+ },
+ "3": {
+ "name": "variable.other.constant.object.property.js"
+ },
+ "4": {
+ "name": "variable.other.object.property.js"
+ }
+ }
+ },
+ {
+ "match": "(?x)(?:\n ([[:upper:]][_$[:digit:][:upper:]]*) |\n ([_$[:alpha:]][_$[:alnum:]]*)\n)(?=\\s*\\??\\.\\s*\\#?[_$[:alpha:]][_$[:alnum:]]*)",
+ "captures": {
+ "1": {
+ "name": "variable.other.constant.object.js"
+ },
+ "2": {
+ "name": "variable.other.object.js"
+ }
+ }
+ }
+ ]
+ },
+ "type-annotation": {
+ "patterns": [
+ {
+ "name": "meta.type.annotation.js",
+ "begin": "(:)(?=\\s*\\S)",
+ "beginCaptures": {
+ "1": {
+ "name": "keyword.operator.type.annotation.js"
+ }
+ },
+ "end": "(?])|((?<=[\\}>\\]\\)]|[_$[:alpha:]])\\s*(?=\\{)))",
+ "patterns": [
+ {
+ "include": "#type"
+ }
+ ]
+ },
+ {
+ "name": "meta.type.annotation.js",
+ "begin": "(:)",
+ "beginCaptures": {
+ "1": {
+ "name": "keyword.operator.type.annotation.js"
+ }
+ },
+ "end": "(?])|(?=^\\s*$)|((?<=\\S)(?=\\s*$))|((?<=[\\}>\\]\\)]|[_$[:alpha:]])\\s*(?=\\{)))",
+ "patterns": [
+ {
+ "include": "#type"
+ }
+ ]
+ }
+ ]
+ },
+ "parameter-type-annotation": {
+ "patterns": [
+ {
+ "name": "meta.type.annotation.js",
+ "begin": "(:)",
+ "beginCaptures": {
+ "1": {
+ "name": "keyword.operator.type.annotation.js"
+ }
+ },
+ "end": "(?=[,)])|(?==[^>])",
+ "patterns": [
+ {
+ "include": "#type"
+ }
+ ]
+ }
+ ]
+ },
+ "return-type": {
+ "patterns": [
+ {
+ "name": "meta.return.type.js",
+ "begin": "(?<=\\))\\s*(:)(?=\\s*\\S)",
+ "beginCaptures": {
+ "1": {
+ "name": "keyword.operator.type.annotation.js"
+ }
+ },
+ "end": "(?|\\{|(^\\s*(export|function|class|interface|let|var|const|import|enum|namespace|module|type|abstract|declare)\\s+))",
+ "patterns": [
+ {
+ "include": "#arrow-return-type-body"
+ }
+ ]
+ },
+ "possibly-arrow-return-type": {
+ "begin": "(?<=\\)|^)\\s*(:)(?=\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*=>)",
+ "beginCaptures": {
+ "1": {
+ "name": "meta.arrow.js meta.return.type.arrow.js keyword.operator.type.annotation.js"
+ }
+ },
+ "end": "(?==>|\\{|(^\\s*(export|function|class|interface|let|var|const|import|enum|namespace|module|type|abstract|declare)\\s+))",
+ "contentName": "meta.arrow.js meta.return.type.arrow.js",
+ "patterns": [
+ {
+ "include": "#arrow-return-type-body"
+ }
+ ]
+ },
+ "arrow-return-type-body": {
+ "patterns": [
+ {
+ "begin": "(?<=[:])(?=\\s*\\{)",
+ "end": "(?<=\\})",
+ "patterns": [
+ {
+ "include": "#type-object"
+ }
+ ]
+ },
+ {
+ "include": "#type-predicate-operator"
+ },
+ {
+ "include": "#type"
+ }
+ ]
+ },
+ "type-parameters": {
+ "name": "meta.type.parameters.js",
+ "begin": "(<)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.typeparameters.begin.js"
+ }
+ },
+ "end": "(>)",
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.typeparameters.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "name": "storage.modifier.js",
+ "match": "(?)"
+ }
+ ]
+ },
+ "type-arguments": {
+ "name": "meta.type.parameters.js",
+ "begin": "\\<",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.typeparameters.begin.js"
+ }
+ },
+ "end": "\\>",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.typeparameters.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#type-arguments-body"
+ }
+ ]
+ },
+ "type-arguments-body": {
+ "patterns": [
+ {
+ "match": "(?)\n ))\n ))\n)) |\n(:\\s*(?\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*)))|((\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])\\s*((:\\s*\\[?$)|((\\s*([^<>\\(\\)\\{\\}]|\\<([^<>]|\\<([^<>]|\\<[^<>]+\\>)+\\>)+\\>|\\([^\\(\\)]+\\)|\\{[^\\{\\}]+\\})+\\s*)?=\\s*))))))))",
+ "captures": {
+ "1": {
+ "name": "storage.modifier.js"
+ },
+ "2": {
+ "name": "keyword.operator.rest.js"
+ },
+ "3": {
+ "name": "entity.name.function.js variable.language.this.js"
+ },
+ "4": {
+ "name": "entity.name.function.js"
+ },
+ "5": {
+ "name": "keyword.operator.optional.js"
+ }
+ }
+ },
+ {
+ "match": "(?x)(?:(?)",
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#type-parameters"
+ }
+ ]
+ },
+ {
+ "name": "meta.type.constructor.js",
+ "begin": "(?)\n ))\n )\n )\n)",
+ "end": "(?<=\\))",
+ "patterns": [
+ {
+ "include": "#function-parameters"
+ }
+ ]
+ }
+ ]
+ },
+ "type-function-return-type": {
+ "patterns": [
+ {
+ "name": "meta.type.function.return.js",
+ "begin": "(=>)(?=\\s*\\S)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.type.function.arrow.js"
+ }
+ },
+ "end": "(?)(?:\\?]|//|$)",
+ "patterns": [
+ {
+ "include": "#type-function-return-type-core"
+ }
+ ]
+ },
+ {
+ "name": "meta.type.function.return.js",
+ "begin": "=>",
+ "beginCaptures": {
+ "0": {
+ "name": "storage.type.function.arrow.js"
+ }
+ },
+ "end": "(?)(?]|//|^\\s*$)|((?<=\\S)(?=\\s*$)))",
+ "patterns": [
+ {
+ "include": "#type-function-return-type-core"
+ }
+ ]
+ }
+ ]
+ },
+ "type-function-return-type-core": {
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "begin": "(?<==>)(?=\\s*\\{)",
+ "end": "(?<=\\})",
+ "patterns": [
+ {
+ "include": "#type-object"
+ }
+ ]
+ },
+ {
+ "include": "#type-predicate-operator"
+ },
+ {
+ "include": "#type"
+ }
+ ]
+ },
+ "type-operators": {
+ "patterns": [
+ {
+ "include": "#typeof-operator"
+ },
+ {
+ "include": "#type-infer"
+ },
+ {
+ "begin": "([&|])(?=\\s*\\{)",
+ "beginCaptures": {
+ "0": {
+ "name": "keyword.operator.type.js"
+ }
+ },
+ "end": "(?<=\\})",
+ "patterns": [
+ {
+ "include": "#type-object"
+ }
+ ]
+ },
+ {
+ "begin": "[&|]",
+ "beginCaptures": {
+ "0": {
+ "name": "keyword.operator.type.js"
+ }
+ },
+ "end": "(?=\\S)"
+ },
+ {
+ "name": "keyword.operator.expression.keyof.js",
+ "match": "(?)",
+ "endCaptures": {
+ "1": {
+ "name": "meta.type.parameters.js punctuation.definition.typeparameters.end.js"
+ }
+ },
+ "contentName": "meta.type.parameters.js",
+ "patterns": [
+ {
+ "include": "#type-arguments-body"
+ }
+ ]
+ },
+ {
+ "begin": "([_$[:alpha:]][_$[:alnum:]]*)\\s*(<)",
+ "beginCaptures": {
+ "1": {
+ "name": "entity.name.type.js"
+ },
+ "2": {
+ "name": "meta.type.parameters.js punctuation.definition.typeparameters.begin.js"
+ }
+ },
+ "end": "(>)",
+ "endCaptures": {
+ "1": {
+ "name": "meta.type.parameters.js punctuation.definition.typeparameters.end.js"
+ }
+ },
+ "contentName": "meta.type.parameters.js",
+ "patterns": [
+ {
+ "include": "#type-arguments-body"
+ }
+ ]
+ },
+ {
+ "match": "([_$[:alpha:]][_$[:alnum:]]*)\\s*(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))",
+ "captures": {
+ "1": {
+ "name": "entity.name.type.module.js"
+ },
+ "2": {
+ "name": "punctuation.accessor.js"
+ },
+ "3": {
+ "name": "punctuation.accessor.optional.js"
+ }
+ }
+ },
+ {
+ "name": "entity.name.type.js",
+ "match": "[_$[:alpha:]][_$[:alnum:]]*"
+ }
+ ]
+ },
+ "punctuation-comma": {
+ "name": "punctuation.separator.comma.js",
+ "match": ","
+ },
+ "punctuation-semicolon": {
+ "name": "punctuation.terminator.statement.js",
+ "match": ";"
+ },
+ "punctuation-accessor": {
+ "match": "(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))",
+ "captures": {
+ "1": {
+ "name": "punctuation.accessor.js"
+ },
+ "2": {
+ "name": "punctuation.accessor.optional.js"
+ }
+ }
+ },
+ "string": {
+ "patterns": [
+ {
+ "include": "#qstring-single"
+ },
+ {
+ "include": "#qstring-double"
+ },
+ {
+ "include": "#template"
+ }
+ ]
+ },
+ "qstring-double": {
+ "name": "string.quoted.double.js",
+ "begin": "\"",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.begin.js"
+ }
+ },
+ "end": "(\")|((?:[^\\\\\\n])$)",
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.string.end.js"
+ },
+ "2": {
+ "name": "invalid.illegal.newline.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#string-character-escape"
+ }
+ ]
+ },
+ "qstring-single": {
+ "name": "string.quoted.single.js",
+ "begin": "'",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.begin.js"
+ }
+ },
+ "end": "(\\')|((?:[^\\\\\\n])$)",
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.string.end.js"
+ },
+ "2": {
+ "name": "invalid.illegal.newline.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#string-character-escape"
+ }
+ ]
+ },
+ "string-character-escape": {
+ "name": "constant.character.escape.js",
+ "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)"
+ },
+ "template": {
+ "patterns": [
+ {
+ "include": "#template-call"
+ },
+ {
+ "name": "string.template.js",
+ "begin": "([_$[:alpha:]][_$[:alnum:]]*)?(`)",
+ "beginCaptures": {
+ "1": {
+ "name": "entity.name.function.tagged-template.js"
+ },
+ "2": {
+ "name": "punctuation.definition.string.template.begin.js"
+ }
+ },
+ "end": "`",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.template.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#template-substitution-element"
+ },
+ {
+ "include": "#string-character-escape"
+ }
+ ]
+ }
+ ]
+ },
+ "template-call": {
+ "patterns": [
+ {
+ "name": "string.template.js",
+ "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*\\s*\\??\\.\\s*)*|(\\??\\.\\s*)?)([_$[:alpha:]][_$[:alnum:]]*)(<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))(([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>)*(?))*(?)*(?\\s*)?`)",
+ "end": "(?=`)",
+ "patterns": [
+ {
+ "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*\\s*\\??\\.\\s*)*|(\\??\\.\\s*)?)([_$[:alpha:]][_$[:alnum:]]*))",
+ "end": "(?=(<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))(([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>)*(?))*(?)*(?\\s*)?`)",
+ "patterns": [
+ {
+ "include": "#support-function-call-identifiers"
+ },
+ {
+ "name": "entity.name.function.tagged-template.js",
+ "match": "([_$[:alpha:]][_$[:alnum:]]*)"
+ }
+ ]
+ },
+ {
+ "include": "#type-arguments"
+ }
+ ]
+ },
+ {
+ "name": "string.template.js",
+ "begin": "([_$[:alpha:]][_$[:alnum:]]*)?\\s*(?=(<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))(([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer|typeof|readonly)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{([^\\{\\}]|\\{[^\\{\\}]*\\})*\\}))*\\})|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(\\[([^\\[\\]]|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])*\\]))*\\])|(\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`))(?=\\s*([\\<\\>\\,\\.\\[]|=>|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\(([^\\(\\)]|\\([^\\(\\)]*\\))*\\)))*\\))|(?<==)\\>)*(?))*(?)*(?\\s*)`)",
+ "beginCaptures": {
+ "1": {
+ "name": "entity.name.function.tagged-template.js"
+ }
+ },
+ "end": "(?=`)",
+ "patterns": [
+ {
+ "include": "#type-arguments"
+ }
+ ]
+ }
+ ]
+ },
+ "template-substitution-element": {
+ "name": "meta.template.expression.js",
+ "begin": "\\$\\{",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.template-expression.begin.js"
+ }
+ },
+ "end": "\\}",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.template-expression.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression"
+ }
+ ],
+ "contentName": "meta.embedded.line.js"
+ },
+ "type-string": {
+ "patterns": [
+ {
+ "include": "#qstring-single"
+ },
+ {
+ "include": "#qstring-double"
+ },
+ {
+ "include": "#template-type"
+ }
+ ]
+ },
+ "template-type": {
+ "patterns": [
+ {
+ "include": "#template-call"
+ },
+ {
+ "name": "string.template.js",
+ "begin": "([_$[:alpha:]][_$[:alnum:]]*)?(`)",
+ "beginCaptures": {
+ "1": {
+ "name": "entity.name.function.tagged-template.js"
+ },
+ "2": {
+ "name": "punctuation.definition.string.template.begin.js"
+ }
+ },
+ "end": "`",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.template.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#template-type-substitution-element"
+ },
+ {
+ "include": "#string-character-escape"
+ }
+ ]
+ }
+ ]
+ },
+ "template-type-substitution-element": {
+ "name": "meta.template.expression.js",
+ "begin": "\\$\\{",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.template-expression.begin.js"
+ }
+ },
+ "end": "\\}",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.template-expression.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#type"
+ }
+ ],
+ "contentName": "meta.embedded.line.js"
+ },
+ "regex": {
+ "patterns": [
+ {
+ "name": "string.regexp.js",
+ "begin": "(?|&&|\\|\\||\\*\\/)\\s*(\\/)(?![\\/*])(?=(?:[^\\/\\\\\\[\\()]|\\\\.|\\[([^\\]\\\\]|\\\\.)+\\]|\\(([^\\)\\\\]|\\\\.)+\\))+\\/([dgimsuy]+|(?![\\/\\*])|(?=\\/\\*))(?!\\s*[a-zA-Z0-9_$]))",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.string.begin.js"
+ }
+ },
+ "end": "(/)([dgimsuy]*)",
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.string.end.js"
+ },
+ "2": {
+ "name": "keyword.other.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#regexp"
+ }
+ ]
+ },
+ {
+ "name": "string.regexp.js",
+ "begin": "((?",
+ "captures": {
+ "0": {
+ "name": "keyword.other.back-reference.regexp"
+ },
+ "1": {
+ "name": "variable.other.regexp"
+ }
+ }
+ },
+ {
+ "name": "keyword.operator.quantifier.regexp",
+ "match": "[?+*]|\\{(\\d+,\\d+|\\d+,|,\\d+|\\d+)\\}\\??"
+ },
+ {
+ "name": "keyword.operator.or.regexp",
+ "match": "\\|"
+ },
+ {
+ "name": "meta.group.assertion.regexp",
+ "begin": "(\\()((\\?=)|(\\?!)|(\\?<=)|(\\?))?",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.group.regexp"
+ },
+ "1": {
+ "name": "punctuation.definition.group.no-capture.regexp"
+ },
+ "2": {
+ "name": "variable.other.regexp"
+ }
+ },
+ "end": "\\)",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.group.regexp"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#regexp"
+ }
+ ]
+ },
+ {
+ "name": "constant.other.character-class.set.regexp",
+ "begin": "(\\[)(\\^)?",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.character-class.regexp"
+ },
+ "2": {
+ "name": "keyword.operator.negation.regexp"
+ }
+ },
+ "end": "(\\])",
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.character-class.regexp"
+ }
+ },
+ "patterns": [
+ {
+ "name": "constant.other.character-class.range.regexp",
+ "match": "(?:.|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))",
+ "captures": {
+ "1": {
+ "name": "constant.character.numeric.regexp"
+ },
+ "2": {
+ "name": "constant.character.control.regexp"
+ },
+ "3": {
+ "name": "constant.character.escape.backslash.regexp"
+ },
+ "4": {
+ "name": "constant.character.numeric.regexp"
+ },
+ "5": {
+ "name": "constant.character.control.regexp"
+ },
+ "6": {
+ "name": "constant.character.escape.backslash.regexp"
+ }
+ }
+ },
+ {
+ "include": "#regex-character-class"
+ }
+ ]
+ },
+ {
+ "include": "#regex-character-class"
+ }
+ ]
+ },
+ "regex-character-class": {
+ "patterns": [
+ {
+ "name": "constant.other.character-class.regexp",
+ "match": "\\\\[wWsSdDtrnvf]|\\."
+ },
+ {
+ "name": "constant.character.numeric.regexp",
+ "match": "\\\\([0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})"
+ },
+ {
+ "name": "constant.character.control.regexp",
+ "match": "\\\\c[A-Z]"
+ },
+ {
+ "name": "constant.character.escape.backslash.regexp",
+ "match": "\\\\."
+ }
+ ]
+ },
+ "comment": {
+ "patterns": [
+ {
+ "name": "comment.block.documentation.js",
+ "begin": "/\\*\\*(?!/)",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.comment.js"
+ }
+ },
+ "end": "\\*/",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.comment.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#docblock"
+ }
+ ]
+ },
+ {
+ "name": "comment.block.js",
+ "begin": "(/\\*)(?:\\s*((@)internal)(?=\\s|(\\*/)))?",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.comment.js"
+ },
+ "2": {
+ "name": "storage.type.internaldeclaration.js"
+ },
+ "3": {
+ "name": "punctuation.decorator.internaldeclaration.js"
+ }
+ },
+ "end": "\\*/",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.comment.js"
+ }
+ }
+ },
+ {
+ "begin": "(^[ \\t]+)?((//)(?:\\s*((@)internal)(?=\\s|$))?)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.whitespace.comment.leading.js"
+ },
+ "2": {
+ "name": "comment.line.double-slash.js"
+ },
+ "3": {
+ "name": "punctuation.definition.comment.js"
+ },
+ "4": {
+ "name": "storage.type.internaldeclaration.js"
+ },
+ "5": {
+ "name": "punctuation.decorator.internaldeclaration.js"
+ }
+ },
+ "end": "(?=$)",
+ "contentName": "comment.line.double-slash.js"
+ }
+ ]
+ },
+ "single-line-comment-consuming-line-ending": {
+ "begin": "(^[ \\t]+)?((//)(?:\\s*((@)internal)(?=\\s|$))?)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.whitespace.comment.leading.js"
+ },
+ "2": {
+ "name": "comment.line.double-slash.js"
+ },
+ "3": {
+ "name": "punctuation.definition.comment.js"
+ },
+ "4": {
+ "name": "storage.type.internaldeclaration.js"
+ },
+ "5": {
+ "name": "punctuation.decorator.internaldeclaration.js"
+ }
+ },
+ "end": "(?=^)",
+ "contentName": "comment.line.double-slash.js"
+ },
+ "directives": {
+ "name": "comment.line.triple-slash.directive.js",
+ "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\.)*\\')|(\\\"([^\\\"\\\\]|\\\\.)*\\\")|(\\`([^\\`\\\\]|\\\\.)*\\`)))+\\s*/>\\s*$)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.comment.js"
+ }
+ },
+ "end": "(?=$)",
+ "patterns": [
+ {
+ "name": "meta.tag.js",
+ "begin": "(<)(reference|amd-dependency|amd-module)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.directive.js"
+ },
+ "2": {
+ "name": "entity.name.tag.directive.js"
+ }
+ },
+ "end": "/>",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.tag.directive.js"
+ }
+ },
+ "patterns": [
+ {
+ "name": "entity.other.attribute-name.directive.js",
+ "match": "path|types|no-default-lib|lib|name"
+ },
+ {
+ "name": "keyword.operator.assignment.js",
+ "match": "="
+ },
+ {
+ "include": "#string"
+ }
+ ]
+ }
+ ]
+ },
+ "docblock": {
+ "patterns": [
+ {
+ "match": "(?x)\n((@)(?:access|api))\n\\s+\n(private|protected|public)\n\\b",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "constant.language.access-type.jsdoc"
+ }
+ }
+ },
+ {
+ "match": "(?x)\n((@)author)\n\\s+\n(\n [^@\\s<>*/]\n (?:[^@<>*/]|\\*[^/])*\n)\n(?:\n \\s*\n (<)\n ([^>\\s]+)\n (>)\n)?",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "entity.name.type.instance.jsdoc"
+ },
+ "4": {
+ "name": "punctuation.definition.bracket.angle.begin.jsdoc"
+ },
+ "5": {
+ "name": "constant.other.email.link.underline.jsdoc"
+ },
+ "6": {
+ "name": "punctuation.definition.bracket.angle.end.jsdoc"
+ }
+ }
+ },
+ {
+ "match": "(?x)\n((@)borrows) \\s+\n((?:[^@\\s*/]|\\*[^/])+) # \n\\s+ (as) \\s+ # as\n((?:[^@\\s*/]|\\*[^/])+) # ",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "entity.name.type.instance.jsdoc"
+ },
+ "4": {
+ "name": "keyword.operator.control.jsdoc"
+ },
+ "5": {
+ "name": "entity.name.type.instance.jsdoc"
+ }
+ }
+ },
+ {
+ "name": "meta.example.jsdoc",
+ "begin": "((@)example)\\s+",
+ "end": "(?=@|\\*/)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ }
+ },
+ "patterns": [
+ {
+ "match": "^\\s\\*\\s+"
+ },
+ {
+ "contentName": "constant.other.description.jsdoc",
+ "begin": "\\G(<)caption(>)",
+ "beginCaptures": {
+ "0": {
+ "name": "entity.name.tag.inline.jsdoc"
+ },
+ "1": {
+ "name": "punctuation.definition.bracket.angle.begin.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.bracket.angle.end.jsdoc"
+ }
+ },
+ "end": "()caption(>)|(?=\\*/)",
+ "endCaptures": {
+ "0": {
+ "name": "entity.name.tag.inline.jsdoc"
+ },
+ "1": {
+ "name": "punctuation.definition.bracket.angle.begin.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.bracket.angle.end.jsdoc"
+ }
+ }
+ },
+ {
+ "match": "[^\\s@*](?:[^*]|\\*[^/])*",
+ "captures": {
+ "0": {
+ "name": "source.embedded.js"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "match": "(?x) ((@)kind) \\s+ (class|constant|event|external|file|function|member|mixin|module|namespace|typedef) \\b",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "constant.language.symbol-type.jsdoc"
+ }
+ }
+ },
+ {
+ "match": "(?x)\n((@)see)\n\\s+\n(?:\n # URL\n (\n (?=https?://)\n (?:[^\\s*]|\\*[^/])+\n )\n |\n # JSDoc namepath\n (\n (?!\n # Avoid matching bare URIs (also acceptable as links)\n https?://\n |\n # Avoid matching {@inline tags}; we match those below\n (?:\\[[^\\[\\]]*\\])? # Possible description [preceding]{@tag}\n {@(?:link|linkcode|linkplain|tutorial)\\b\n )\n # Matched namepath\n (?:[^@\\s*/]|\\*[^/])+\n )\n)",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "variable.other.link.underline.jsdoc"
+ },
+ "4": {
+ "name": "entity.name.type.instance.jsdoc"
+ }
+ }
+ },
+ {
+ "match": "(?x)\n((@)template)\n\\s+\n# One or more valid identifiers\n(\n [A-Za-z_$] # First character: non-numeric word character\n [\\w$.\\[\\]]* # Rest of identifier\n (?: # Possible list of additional identifiers\n \\s* , \\s*\n [A-Za-z_$]\n [\\w$.\\[\\]]*\n )*\n)",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "variable.other.jsdoc"
+ }
+ }
+ },
+ {
+ "begin": "(?x)((@)template)\\s+(?={)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ }
+ },
+ "end": "(?=\\s|\\*/|[^{}\\[\\]A-Za-z_$])",
+ "patterns": [
+ {
+ "include": "#jsdoctype"
+ },
+ {
+ "name": "variable.other.jsdoc",
+ "match": "([A-Za-z_$][\\w$.\\[\\]]*)"
+ }
+ ]
+ },
+ {
+ "match": "(?x)\n(\n (@)\n (?:arg|argument|const|constant|member|namespace|param|var)\n)\n\\s+\n(\n [A-Za-z_$]\n [\\w$.\\[\\]]*\n)",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "variable.other.jsdoc"
+ }
+ }
+ },
+ {
+ "begin": "((@)typedef)\\s+(?={)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ }
+ },
+ "end": "(?=\\s|\\*/|[^{}\\[\\]A-Za-z_$])",
+ "patterns": [
+ {
+ "include": "#jsdoctype"
+ },
+ {
+ "name": "entity.name.type.instance.jsdoc",
+ "match": "(?:[^@\\s*/]|\\*[^/])+"
+ }
+ ]
+ },
+ {
+ "begin": "((@)(?:arg|argument|const|constant|member|namespace|param|prop|property|var))\\s+(?={)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ }
+ },
+ "end": "(?=\\s|\\*/|[^{}\\[\\]A-Za-z_$])",
+ "patterns": [
+ {
+ "include": "#jsdoctype"
+ },
+ {
+ "name": "variable.other.jsdoc",
+ "match": "([A-Za-z_$][\\w$.\\[\\]]*)"
+ },
+ {
+ "name": "variable.other.jsdoc",
+ "match": "(?x)\n(\\[)\\s*\n[\\w$]+\n(?:\n (?:\\[\\])? # Foo[ ].bar properties within an array\n \\. # Foo.Bar namespaced parameter\n [\\w$]+\n)*\n(?:\n \\s*\n (=) # [foo=bar] Default parameter value\n \\s*\n (\n # The inner regexes are to stop the match early at */ and to not stop at escaped quotes\n (?>\n \"(?:(?:\\*(?!/))|(?:\\\\(?!\"))|[^*\\\\])*?\" | # [foo=\"bar\"] Double-quoted\n '(?:(?:\\*(?!/))|(?:\\\\(?!'))|[^*\\\\])*?' | # [foo='bar'] Single-quoted\n \\[ (?:(?:\\*(?!/))|[^*])*? \\] | # [foo=[1,2]] Array literal\n (?:(?:\\*(?!/))|\\s(?!\\s*\\])|\\[.*?(?:\\]|(?=\\*/))|[^*\\s\\[\\]])* # Everything else\n )*\n )\n)?\n\\s*(?:(\\])((?:[^*\\s]|\\*[^\\s/])+)?|(?=\\*/))",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.optional-value.begin.bracket.square.jsdoc"
+ },
+ "2": {
+ "name": "keyword.operator.assignment.jsdoc"
+ },
+ "3": {
+ "name": "source.embedded.js"
+ },
+ "4": {
+ "name": "punctuation.definition.optional-value.end.bracket.square.jsdoc"
+ },
+ "5": {
+ "name": "invalid.illegal.syntax.jsdoc"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "begin": "(?x)\n(\n (@)\n (?:define|enum|exception|export|extends|lends|implements|modifies\n |namespace|private|protected|returns?|suppress|this|throws|type\n |yields?)\n)\n\\s+(?={)",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ }
+ },
+ "end": "(?=\\s|\\*/|[^{}\\[\\]A-Za-z_$])",
+ "patterns": [
+ {
+ "include": "#jsdoctype"
+ }
+ ]
+ },
+ {
+ "match": "(?x)\n(\n (@)\n (?:alias|augments|callback|constructs|emits|event|fires|exports?\n |extends|external|function|func|host|lends|listens|interface|memberof!?\n |method|module|mixes|mixin|name|requires|see|this|typedef|uses)\n)\n\\s+\n(\n (?:\n [^{}@\\s*] | \\*[^/]\n )+\n)",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "entity.name.type.instance.jsdoc"
+ }
+ }
+ },
+ {
+ "contentName": "variable.other.jsdoc",
+ "begin": "((@)(?:default(?:value)?|license|version))\\s+(([''\"]))",
+ "beginCaptures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "variable.other.jsdoc"
+ },
+ "4": {
+ "name": "punctuation.definition.string.begin.jsdoc"
+ }
+ },
+ "end": "(\\3)|(?=$|\\*/)",
+ "endCaptures": {
+ "0": {
+ "name": "variable.other.jsdoc"
+ },
+ "1": {
+ "name": "punctuation.definition.string.end.jsdoc"
+ }
+ }
+ },
+ {
+ "match": "((@)(?:default(?:value)?|license|tutorial|variation|version))\\s+([^\\s*]+)",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ },
+ "3": {
+ "name": "variable.other.jsdoc"
+ }
+ }
+ },
+ {
+ "name": "storage.type.class.jsdoc",
+ "match": "(?x) (@) (?:abstract|access|alias|api|arg|argument|async|attribute|augments|author|beta|borrows|bubbles |callback|chainable|class|classdesc|code|config|const|constant|constructor|constructs|copyright |default|defaultvalue|define|deprecated|desc|description|dict|emits|enum|event|example|exception |exports?|extends|extension(?:_?for)?|external|externs|file|fileoverview|final|fires|for|func |function|generator|global|hideconstructor|host|ignore|implements|implicitCast|inherit[Dd]oc |inner|instance|interface|internal|kind|lends|license|listens|main|member|memberof!?|method |mixes|mixins?|modifies|module|name|namespace|noalias|nocollapse|nocompile|nosideeffects |override|overview|package|param|polymer(?:Behavior)?|preserve|private|prop|property|protected |public|read[Oo]nly|record|require[ds]|returns?|see|since|static|struct|submodule|summary |suppress|template|this|throws|todo|tutorial|type|typedef|unrestricted|uses|var|variation |version|virtual|writeOnce|yields?) \\b",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ }
+ }
+ },
+ {
+ "include": "#inline-tags"
+ },
+ {
+ "match": "((@)(?:[_$[:alpha:]][_$[:alnum:]]*))(?=\\s+)",
+ "captures": {
+ "1": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.block.tag.jsdoc"
+ }
+ }
+ }
+ ]
+ },
+ "brackets": {
+ "patterns": [
+ {
+ "begin": "{",
+ "end": "}|(?=\\*/)",
+ "patterns": [
+ {
+ "include": "#brackets"
+ }
+ ]
+ },
+ {
+ "begin": "\\[",
+ "end": "\\]|(?=\\*/)",
+ "patterns": [
+ {
+ "include": "#brackets"
+ }
+ ]
+ }
+ ]
+ },
+ "inline-tags": {
+ "patterns": [
+ {
+ "name": "constant.other.description.jsdoc",
+ "match": "(\\[)[^\\]]+(\\])(?={@(?:link|linkcode|linkplain|tutorial))",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.bracket.square.begin.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.bracket.square.end.jsdoc"
+ }
+ }
+ },
+ {
+ "name": "entity.name.type.instance.jsdoc",
+ "begin": "({)((@)(?:link(?:code|plain)?|tutorial))\\s*",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.bracket.curly.begin.jsdoc"
+ },
+ "2": {
+ "name": "storage.type.class.jsdoc"
+ },
+ "3": {
+ "name": "punctuation.definition.inline.tag.jsdoc"
+ }
+ },
+ "end": "}|(?=\\*/)",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.bracket.curly.end.jsdoc"
+ }
+ },
+ "patterns": [
+ {
+ "match": "\\G((?=https?://)(?:[^|}\\s*]|\\*[/])+)(\\|)?",
+ "captures": {
+ "1": {
+ "name": "variable.other.link.underline.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.separator.pipe.jsdoc"
+ }
+ }
+ },
+ {
+ "match": "\\G((?:[^{}@\\s|*]|\\*[^/])+)(\\|)?",
+ "captures": {
+ "1": {
+ "name": "variable.other.description.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.separator.pipe.jsdoc"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "jsdoctype": {
+ "patterns": [
+ {
+ "contentName": "entity.name.type.instance.jsdoc",
+ "begin": "\\G({)",
+ "beginCaptures": {
+ "0": {
+ "name": "entity.name.type.instance.jsdoc"
+ },
+ "1": {
+ "name": "punctuation.definition.bracket.curly.begin.jsdoc"
+ }
+ },
+ "end": "((}))\\s*|(?=\\*/)",
+ "endCaptures": {
+ "1": {
+ "name": "entity.name.type.instance.jsdoc"
+ },
+ "2": {
+ "name": "punctuation.definition.bracket.curly.end.jsdoc"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#brackets"
+ }
+ ]
+ }
+ ]
+ },
+ "jsx": {
+ "patterns": [
+ {
+ "include": "#jsx-tag-without-attributes-in-expression"
+ },
+ {
+ "include": "#jsx-tag-in-expression"
+ }
+ ]
+ },
+ "jsx-tag-without-attributes-in-expression": {
+ "begin": "(?:*]|&&|\\|\\||\\?|\\*\\/|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\s*(?=(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?))",
+ "end": "(?!(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?))",
+ "patterns": [
+ {
+ "include": "#jsx-tag-without-attributes"
+ }
+ ]
+ },
+ "jsx-tag-without-attributes": {
+ "name": "meta.tag.without-attributes.js",
+ "begin": "(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?)",
+ "end": "()\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.begin.js"
+ },
+ "2": {
+ "name": "entity.name.tag.namespace.js"
+ },
+ "3": {
+ "name": "punctuation.separator.namespace.js"
+ },
+ "4": {
+ "name": "entity.name.tag.js"
+ },
+ "5": {
+ "name": "support.class.component.js"
+ },
+ "6": {
+ "name": "punctuation.definition.tag.end.js"
+ }
+ },
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.begin.js"
+ },
+ "2": {
+ "name": "entity.name.tag.namespace.js"
+ },
+ "3": {
+ "name": "punctuation.separator.namespace.js"
+ },
+ "4": {
+ "name": "entity.name.tag.js"
+ },
+ "5": {
+ "name": "support.class.component.js"
+ },
+ "6": {
+ "name": "punctuation.definition.tag.end.js"
+ }
+ },
+ "contentName": "meta.jsx.children.js",
+ "patterns": [
+ {
+ "include": "#jsx-children"
+ }
+ ]
+ },
+ "jsx-tag-in-expression": {
+ "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|\\*\\/|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?))",
+ "end": "(?!(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?))",
+ "patterns": [
+ {
+ "include": "#jsx-tag"
+ }
+ ]
+ },
+ "jsx-tag": {
+ "name": "meta.tag.js",
+ "begin": "(?=(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?))",
+ "end": "(/>)|(?:()\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?))",
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.end.js"
+ },
+ "2": {
+ "name": "punctuation.definition.tag.begin.js"
+ },
+ "3": {
+ "name": "entity.name.tag.namespace.js"
+ },
+ "4": {
+ "name": "punctuation.separator.namespace.js"
+ },
+ "5": {
+ "name": "entity.name.tag.js"
+ },
+ "6": {
+ "name": "support.class.component.js"
+ },
+ "7": {
+ "name": "punctuation.definition.tag.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "begin": "(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.begin.js"
+ },
+ "2": {
+ "name": "entity.name.tag.namespace.js"
+ },
+ "3": {
+ "name": "punctuation.separator.namespace.js"
+ },
+ "4": {
+ "name": "entity.name.tag.js"
+ },
+ "5": {
+ "name": "support.class.component.js"
+ }
+ },
+ "end": "(?=[/]?>)",
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#type-arguments"
+ },
+ {
+ "include": "#jsx-tag-attributes"
+ }
+ ]
+ },
+ {
+ "begin": "(>)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.end.js"
+ }
+ },
+ "end": "(?=)",
+ "contentName": "meta.jsx.children.js",
+ "patterns": [
+ {
+ "include": "#jsx-children"
+ }
+ ]
+ }
+ ]
+ },
+ "jsx-children": {
+ "patterns": [
+ {
+ "include": "#jsx-tag-without-attributes"
+ },
+ {
+ "include": "#jsx-tag"
+ },
+ {
+ "include": "#jsx-evaluated-code"
+ },
+ {
+ "include": "#jsx-entities"
+ }
+ ]
+ },
+ "jsx-evaluated-code": {
+ "contentName": "meta.embedded.expression.js",
+ "begin": "\\{",
+ "end": "\\}",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.section.embedded.begin.js"
+ }
+ },
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.section.embedded.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#expression"
+ }
+ ]
+ },
+ "jsx-entities": {
+ "patterns": [
+ {
+ "name": "constant.character.entity.js",
+ "match": "(&)([a-zA-Z0-9]+|#[0-9]+|#x[0-9a-fA-F]+)(;)",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.entity.js"
+ },
+ "3": {
+ "name": "punctuation.definition.entity.js"
+ }
+ }
+ },
+ {
+ "name": "invalid.illegal.bad-ampersand.js",
+ "match": "&"
+ }
+ ]
+ },
+ "jsx-tag-attributes": {
+ "name": "meta.tag.attributes.js",
+ "begin": "\\s+",
+ "end": "(?=[/]?>)",
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#jsx-tag-attribute-name"
+ },
+ {
+ "include": "#jsx-tag-attribute-assignment"
+ },
+ {
+ "include": "#jsx-string-double-quoted"
+ },
+ {
+ "include": "#jsx-string-single-quoted"
+ },
+ {
+ "include": "#jsx-evaluated-code"
+ },
+ {
+ "include": "#jsx-tag-attributes-illegal"
+ }
+ ]
+ },
+ "jsx-tag-attribute-name": {
+ "match": "(?x)\n \\s*\n (?:([_$[:alpha:]][-_$[:alnum:].]*)(:))?\n ([_$[:alpha:]][-_$[:alnum:]]*)\n (?=\\s|=|/?>|/\\*|//)",
+ "captures": {
+ "1": {
+ "name": "entity.other.attribute-name.namespace.js"
+ },
+ "2": {
+ "name": "punctuation.separator.namespace.js"
+ },
+ "3": {
+ "name": "entity.other.attribute-name.js"
+ }
+ }
+ },
+ "jsx-tag-attribute-assignment": {
+ "name": "keyword.operator.assignment.js",
+ "match": "=(?=\\s*(?:'|\"|{|/\\*|//|\\n))"
+ },
+ "jsx-string-double-quoted": {
+ "name": "string.quoted.double.js",
+ "begin": "\"",
+ "end": "\"",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.begin.js"
+ }
+ },
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#jsx-entities"
+ }
+ ]
+ },
+ "jsx-string-single-quoted": {
+ "name": "string.quoted.single.js",
+ "begin": "'",
+ "end": "'",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.begin.js"
+ }
+ },
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.end.js"
+ }
+ },
+ "patterns": [
+ {
+ "include": "#jsx-entities"
+ }
+ ]
+ },
+ "jsx-tag-attributes-illegal": {
+ "name": "invalid.illegal.attribute.js",
+ "match": "\\S+"
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/assets/textmate/quietlight.json b/app/src/main/assets/textmate/quietlight.json
new file mode 100644
index 000000000..9b8bec180
--- /dev/null
+++ b/app/src/main/assets/textmate/quietlight.json
@@ -0,0 +1,542 @@
+{
+ "name": "Quiet Light",
+ "tokenColors": [
+ {
+ "settings": {
+ "foreground": "#333333"
+ }
+ },
+ {
+ "scope": [
+ "meta.embedded",
+ "source.groovy.embedded"
+ ],
+ "settings": {
+ "foreground": "#333333"
+ }
+ },
+ {
+ "name": "Comments",
+ "scope": [
+ "comment",
+ "punctuation.definition.comment"
+ ],
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#AAAAAA"
+ }
+ },
+ {
+ "name": "Comments: Preprocessor",
+ "scope": "comment.block.preprocessor",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#AAAAAA"
+ }
+ },
+ {
+ "name": "Comments: Documentation",
+ "scope": [
+ "comment.documentation",
+ "comment.block.documentation",
+ "comment.block.documentation punctuation.definition.comment "
+ ],
+ "settings": {
+ "foreground": "#448C27"
+ }
+ },
+ {
+ "name": "Invalid",
+ "scope": "invalid",
+ "settings": {
+ "foreground": "#cd3131"
+ }
+ },
+ {
+ "name": "Invalid - Illegal",
+ "scope": "invalid.illegal",
+ "settings": {
+ "foreground": "#660000"
+ }
+ },
+ {
+ "name": "Operators",
+ "scope": "keyword.operator",
+ "settings": {
+ "foreground": "#777777"
+ }
+ },
+ {
+ "name": "Keywords",
+ "scope": [
+ "keyword",
+ "storage"
+ ],
+ "settings": {
+ "foreground": "#4B69C6"
+ }
+ },
+ {
+ "name": "Types",
+ "scope": [
+ "storage.type",
+ "support.type"
+ ],
+ "settings": {
+ "foreground": "#7A3E9D"
+ }
+ },
+ {
+ "name": "Language Constants",
+ "scope": [
+ "constant.language",
+ "support.constant",
+ "variable.language"
+ ],
+ "settings": {
+ "foreground": "#9C5D27"
+ }
+ },
+ {
+ "name": "Variables",
+ "scope": [
+ "variable",
+ "support.variable"
+ ],
+ "settings": {
+ "foreground": "#7A3E9D"
+ }
+ },
+ {
+ "name": "Functions",
+ "scope": [
+ "entity.name.function",
+ "support.function"
+ ],
+ "settings": {
+ "fontStyle": "bold",
+ "foreground": "#AA3731"
+ }
+ },
+ {
+ "name": "Classes",
+ "scope": [
+ "entity.name.type",
+ "entity.name.namespace",
+ "entity.name.scope-resolution",
+ "entity.other.inherited-class",
+ "support.class"
+ ],
+ "settings": {
+ "fontStyle": "bold",
+ "foreground": "#7A3E9D"
+ }
+ },
+ {
+ "name": "Exceptions",
+ "scope": "entity.name.exception",
+ "settings": {
+ "foreground": "#660000"
+ }
+ },
+ {
+ "name": "Sections",
+ "scope": "entity.name.section",
+ "settings": {
+ "fontStyle": "bold"
+ }
+ },
+ {
+ "name": "Numbers, Characters",
+ "scope": [
+ "constant.numeric",
+ "constant.character",
+ "constant"
+ ],
+ "settings": {
+ "foreground": "#9C5D27"
+ }
+ },
+ {
+ "name": "Strings",
+ "scope": "string",
+ "settings": {
+ "foreground": "#448C27"
+ }
+ },
+ {
+ "name": "Strings: Escape Sequences",
+ "scope": "constant.character.escape",
+ "settings": {
+ "foreground": "#777777"
+ }
+ },
+ {
+ "name": "Strings: Regular Expressions",
+ "scope": "string.regexp",
+ "settings": {
+ "foreground": "#4B69C6"
+ }
+ },
+ {
+ "name": "Strings: Symbols",
+ "scope": "constant.other.symbol",
+ "settings": {
+ "foreground": "#9C5D27"
+ }
+ },
+ {
+ "name": "Punctuation",
+ "scope": "punctuation",
+ "settings": {
+ "foreground": "#777777"
+ }
+ },
+ {
+ "name": "HTML: Doctype Declaration",
+ "scope": [
+ "meta.tag.sgml.doctype",
+ "meta.tag.sgml.doctype string",
+ "meta.tag.sgml.doctype entity.name.tag",
+ "meta.tag.sgml punctuation.definition.tag.html"
+ ],
+ "settings": {
+ "foreground": "#AAAAAA"
+ }
+ },
+ {
+ "name": "HTML: Tags",
+ "scope": [
+ "meta.tag",
+ "punctuation.definition.tag.html",
+ "punctuation.definition.tag.begin.html",
+ "punctuation.definition.tag.end.html"
+ ],
+ "settings": {
+ "foreground": "#91B3E0"
+ }
+ },
+ {
+ "name": "HTML: Tag Names",
+ "scope": "entity.name.tag",
+ "settings": {
+ "foreground": "#4B69C6"
+ }
+ },
+ {
+ "name": "HTML: Attribute Names",
+ "scope": [
+ "meta.tag entity.other.attribute-name",
+ "entity.other.attribute-name.html"
+ ],
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#8190A0"
+ }
+ },
+ {
+ "name": "HTML: Entities",
+ "scope": [
+ "constant.character.entity",
+ "punctuation.definition.entity"
+ ],
+ "settings": {
+ "foreground": "#9C5D27"
+ }
+ },
+ {
+ "name": "CSS: Selectors",
+ "scope": [
+ "meta.selector",
+ "meta.selector entity",
+ "meta.selector entity punctuation",
+ "entity.name.tag.css"
+ ],
+ "settings": {
+ "foreground": "#7A3E9D"
+ }
+ },
+ {
+ "name": "CSS: Property Names",
+ "scope": [
+ "meta.property-name",
+ "support.type.property-name"
+ ],
+ "settings": {
+ "foreground": "#9C5D27"
+ }
+ },
+ {
+ "name": "CSS: Property Values",
+ "scope": [
+ "meta.property-value",
+ "meta.property-value constant.other",
+ "support.constant.property-value"
+ ],
+ "settings": {
+ "foreground": "#448C27"
+ }
+ },
+ {
+ "name": "CSS: Important Keyword",
+ "scope": "keyword.other.important",
+ "settings": {
+ "fontStyle": "bold"
+ }
+ },
+ {
+ "name": "Markup: Changed",
+ "scope": "markup.changed",
+ "settings": {
+ "foreground": "#000000"
+ }
+ },
+ {
+ "name": "Markup: Deletion",
+ "scope": "markup.deleted",
+ "settings": {
+ "foreground": "#000000"
+ }
+ },
+ {
+ "name": "Markup: Emphasis",
+ "scope": "markup.italic",
+ "settings": {
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "scope": "markup.strikethrough",
+ "settings": {
+ "fontStyle": "strikethrough"
+ }
+ },
+ {
+ "name": "Markup: Error",
+ "scope": "markup.error",
+ "settings": {
+ "foreground": "#660000"
+ }
+ },
+ {
+ "name": "Markup: Insertion",
+ "scope": "markup.inserted",
+ "settings": {
+ "foreground": "#000000"
+ }
+ },
+ {
+ "name": "Markup: Link",
+ "scope": "meta.link",
+ "settings": {
+ "foreground": "#4B69C6"
+ }
+ },
+ {
+ "name": "Markup: Output",
+ "scope": [
+ "markup.output",
+ "markup.raw"
+ ],
+ "settings": {
+ "foreground": "#777777"
+ }
+ },
+ {
+ "name": "Markup: Prompt",
+ "scope": "markup.prompt",
+ "settings": {
+ "foreground": "#777777"
+ }
+ },
+ {
+ "name": "Markup: Heading",
+ "scope": "markup.heading",
+ "settings": {
+ "foreground": "#AA3731"
+ }
+ },
+ {
+ "name": "Markup: Strong",
+ "scope": "markup.bold",
+ "settings": {
+ "fontStyle": "bold"
+ }
+ },
+ {
+ "name": "Markup: Traceback",
+ "scope": "markup.traceback",
+ "settings": {
+ "foreground": "#660000"
+ }
+ },
+ {
+ "name": "Markup: Underline",
+ "scope": "markup.underline",
+ "settings": {
+ "fontStyle": "underline"
+ }
+ },
+ {
+ "name": "Markup Quote",
+ "scope": "markup.quote",
+ "settings": {
+ "foreground": "#7A3E9D"
+ }
+ },
+ {
+ "name": "Markup Lists",
+ "scope": "markup.list",
+ "settings": {
+ "foreground": "#4B69C6"
+ }
+ },
+ {
+ "name": "Markup Styling",
+ "scope": [
+ "markup.bold",
+ "markup.italic"
+ ],
+ "settings": {
+ "foreground": "#448C27"
+ }
+ },
+ {
+ "name": "Markup Inline",
+ "scope": "markup.inline.raw",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#9C5D27"
+ }
+ },
+ {
+ "name": "Extra: Diff Range",
+ "scope": [
+ "meta.diff.range",
+ "meta.diff.index",
+ "meta.separator"
+ ],
+ "settings": {
+ "foreground": "#434343"
+ }
+ },
+ {
+ "name": "Extra: Diff From",
+ "scope": [
+ "meta.diff.header.from-file",
+ "punctuation.definition.from-file.diff"
+ ],
+ "settings": {
+ "foreground": "#4B69C6"
+ }
+ },
+ {
+ "name": "Extra: Diff To",
+ "scope": [
+ "meta.diff.header.to-file",
+ "punctuation.definition.to-file.diff"
+ ],
+ "settings": {
+ "foreground": "#4B69C6"
+ }
+ },
+ {
+ "name": "diff: deleted",
+ "scope": "markup.deleted.diff",
+ "settings": {
+ "foreground": "#C73D20"
+ }
+ },
+ {
+ "name": "diff: changed",
+ "scope": "markup.changed.diff",
+ "settings": {
+ "foreground": "#9C5D27"
+ }
+ },
+ {
+ "name": "diff: inserted",
+ "scope": "markup.inserted.diff",
+ "settings": {
+ "foreground": "#448C27"
+ }
+ },
+ {
+ "name": "JSX: Tags",
+ "scope": [
+ "punctuation.definition.tag.js",
+ "punctuation.definition.tag.begin.js",
+ "punctuation.definition.tag.end.js"
+ ],
+ "settings": {
+ "foreground": "#91B3E0"
+ }
+ },
+ {
+ "name": "JSX: InnerText",
+ "scope": "meta.jsx.children.js",
+ "settings": {
+ "foreground": "#333333ff"
+ }
+ }
+ ],
+ "colors": {
+ "focusBorder": "#A6B39B",
+ "pickerGroup.foreground": "#A6B39B",
+ "pickerGroup.border": "#749351",
+ "list.activeSelectionForeground": "#6c6c6c",
+ "quickInputList.focusBackground": "#CADEB9",
+ "list.hoverBackground": "#e0e0e0",
+ "list.activeSelectionBackground": "#c4d9b1",
+ "list.inactiveSelectionBackground": "#d3dbcd",
+ "list.highlightForeground": "#9769dc",
+ "selection.background": "#C9D0D9",
+ "editor.background": "#F5F5F5",
+ "editorWhitespace.foreground": "#AAAAAA",
+ "editor.lineHighlightBackground": "#E4F6D4",
+ "editorLineNumber.activeForeground": "#9769dc",
+ "editor.selectionBackground": "#C9D0D9",
+ "minimap.selectionHighlight": "#C9D0D9",
+ "panel.background": "#F5F5F5",
+ "sideBar.background": "#F2F2F2",
+ "sideBarSectionHeader.background": "#ede8ef",
+ "editorLineNumber.foreground": "#6D705B",
+ "editorCursor.foreground": "#54494B",
+ "inputOption.activeBorder": "#adafb7",
+ "dropdown.background": "#F5F5F5",
+ "editor.findMatchBackground": "#BF9CAC",
+ "editor.findMatchHighlightBackground": "#edc9d8",
+ "peekViewEditor.matchHighlightBackground": "#C2DFE3",
+ "peekViewTitle.background": "#F2F8FC",
+ "peekViewEditor.background": "#F2F8FC",
+ "peekViewResult.background": "#F2F8FC",
+ "peekView.border": "#705697",
+ "peekViewResult.matchHighlightBackground": "#93C6D6",
+ "tab.lastPinnedBorder": "#c9d0d9",
+ "statusBar.background": "#705697",
+ "welcomePage.tileBackground": "#f0f0f7",
+ "statusBar.noFolderBackground": "#705697",
+ "statusBar.debuggingBackground": "#705697",
+ "statusBarItem.remoteBackground": "#4e3c69",
+ "ports.iconRunningProcessForeground": "#749351",
+ "activityBar.background": "#EDEDF5",
+ "activityBar.foreground": "#705697",
+ "activityBarBadge.background": "#705697",
+ "titleBar.activeBackground": "#c4b7d7",
+ "button.background": "#705697",
+ "editorGroup.dropBackground": "#C9D0D988",
+ "inputValidation.infoBorder": "#4ec1e5",
+ "inputValidation.infoBackground": "#f2fcff",
+ "inputValidation.warningBackground": "#fffee2",
+ "inputValidation.warningBorder": "#ffe055",
+ "inputValidation.errorBackground": "#ffeaea",
+ "inputValidation.errorBorder": "#f1897f",
+ "errorForeground": "#f1897f",
+ "badge.background": "#705697AA",
+ "progressBar.background": "#705697",
+ "walkThrough.embeddedEditorBackground": "#00000014",
+ "editorIndentGuide.background": "#aaaaaa60",
+ "editorIndentGuide.activeBackground": "#777777b0"
+ },
+ "semanticHighlighting": true
+}
\ No newline at end of file
diff --git a/app/src/main/assets/textmate/solarized_drak.json b/app/src/main/assets/textmate/solarized_drak.json
new file mode 100644
index 000000000..b0de1a5da
--- /dev/null
+++ b/app/src/main/assets/textmate/solarized_drak.json
@@ -0,0 +1,421 @@
+{
+ "name": "Solarized (dark)",
+ "tokenColors": [
+ {
+ "settings": {
+ "foreground": "#839496"
+ }
+ },
+ {
+ "scope": [
+ "meta.embedded",
+ "source.groovy.embedded"
+ ],
+ "settings": {
+ "foreground": "#839496"
+ }
+ },
+ {
+ "name": "Comment",
+ "scope": "comment",
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#586E75"
+ }
+ },
+ {
+ "name": "String",
+ "scope": "string",
+ "settings": {
+ "foreground": "#2AA198"
+ }
+ },
+ {
+ "name": "Regexp",
+ "scope": "string.regexp",
+ "settings": {
+ "foreground": "#DC322F"
+ }
+ },
+ {
+ "name": "Number",
+ "scope": "constant.numeric",
+ "settings": {
+ "foreground": "#D33682"
+ }
+ },
+ {
+ "name": "Variable",
+ "scope": [
+ "variable.language",
+ "variable.other"
+ ],
+ "settings": {
+ "foreground": "#268BD2"
+ }
+ },
+ {
+ "name": "Keyword",
+ "scope": "keyword",
+ "settings": {
+ "foreground": "#859900"
+ }
+ },
+ {
+ "name": "Storage",
+ "scope": "storage",
+ "settings": {
+ "fontStyle": "bold",
+ "foreground": "#93A1A1"
+ }
+ },
+ {
+ "name": "Class name",
+ "scope": [
+ "entity.name.class",
+ "entity.name.type",
+ "entity.name.namespace",
+ "entity.name.scope-resolution"
+ ],
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#CB4B16"
+ }
+ },
+ {
+ "name": "Function name",
+ "scope": "entity.name.function",
+ "settings": {
+ "foreground": "#268BD2"
+ }
+ },
+ {
+ "name": "Variable start",
+ "scope": "punctuation.definition.variable",
+ "settings": {
+ "foreground": "#859900"
+ }
+ },
+ {
+ "name": "Embedded code markers",
+ "scope": [
+ "punctuation.section.embedded.begin",
+ "punctuation.section.embedded.end"
+ ],
+ "settings": {
+ "foreground": "#DC322F"
+ }
+ },
+ {
+ "name": "Built-in constant",
+ "scope": [
+ "constant.language",
+ "meta.preprocessor"
+ ],
+ "settings": {
+ "foreground": "#B58900"
+ }
+ },
+ {
+ "name": "Support.construct",
+ "scope": [
+ "support.function.construct",
+ "keyword.other.new"
+ ],
+ "settings": {
+ "foreground": "#CB4B16"
+ }
+ },
+ {
+ "name": "User-defined constant",
+ "scope": [
+ "constant.character",
+ "constant.other"
+ ],
+ "settings": {
+ "foreground": "#CB4B16"
+ }
+ },
+ {
+ "name": "Inherited class",
+ "scope": "entity.other.inherited-class",
+ "settings": {
+ "foreground": "#6C71C4"
+ }
+ },
+ {
+ "name": "Function argument",
+ "scope": "variable.parameter",
+ "settings": {}
+ },
+ {
+ "name": "Tag name",
+ "scope": "entity.name.tag",
+ "settings": {
+ "foreground": "#268BD2"
+ }
+ },
+ {
+ "name": "Tag start/end",
+ "scope": "punctuation.definition.tag",
+ "settings": {
+ "foreground": "#586E75"
+ }
+ },
+ {
+ "name": "Tag attribute",
+ "scope": "entity.other.attribute-name",
+ "settings": {
+ "foreground": "#93A1A1"
+ }
+ },
+ {
+ "name": "Library function",
+ "scope": "support.function",
+ "settings": {
+ "foreground": "#268BD2"
+ }
+ },
+ {
+ "name": "Continuation",
+ "scope": "punctuation.separator.continuation",
+ "settings": {
+ "foreground": "#DC322F"
+ }
+ },
+ {
+ "name": "Library constant",
+ "scope": [
+ "support.constant",
+ "support.variable"
+ ],
+ "settings": {}
+ },
+ {
+ "name": "Library class/type",
+ "scope": [
+ "support.type",
+ "support.class"
+ ],
+ "settings": {
+ "foreground": "#859900"
+ }
+ },
+ {
+ "name": "Library Exception",
+ "scope": "support.type.exception",
+ "settings": {
+ "foreground": "#CB4B16"
+ }
+ },
+ {
+ "name": "Library variable",
+ "scope": "support.other.variable",
+ "settings": {}
+ },
+ {
+ "name": "Invalid",
+ "scope": "invalid",
+ "settings": {
+ "foreground": "#DC322F"
+ }
+ },
+ {
+ "name": "diff: header",
+ "scope": [
+ "meta.diff",
+ "meta.diff.header"
+ ],
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#268BD2"
+ }
+ },
+ {
+ "name": "diff: deleted",
+ "scope": "markup.deleted",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#DC322F"
+ }
+ },
+ {
+ "name": "diff: changed",
+ "scope": "markup.changed",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#CB4B16"
+ }
+ },
+ {
+ "name": "diff: inserted",
+ "scope": "markup.inserted",
+ "settings": {
+ "foreground": "#859900"
+ }
+ },
+ {
+ "name": "Markup Quote",
+ "scope": "markup.quote",
+ "settings": {
+ "foreground": "#859900"
+ }
+ },
+ {
+ "name": "Markup Lists",
+ "scope": "markup.list",
+ "settings": {
+ "foreground": "#B58900"
+ }
+ },
+ {
+ "name": "Markup Styling",
+ "scope": [
+ "markup.bold",
+ "markup.italic"
+ ],
+ "settings": {
+ "foreground": "#D33682"
+ }
+ },
+ {
+ "name": "Markup: Strong",
+ "scope": "markup.bold",
+ "settings": {
+ "fontStyle": "bold"
+ }
+ },
+ {
+ "name": "Markup: Emphasis",
+ "scope": "markup.italic",
+ "settings": {
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "scope": "markup.strikethrough",
+ "settings": {
+ "fontStyle": "strikethrough"
+ }
+ },
+ {
+ "name": "Markup Inline",
+ "scope": "markup.inline.raw",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#2AA198"
+ }
+ },
+ {
+ "name": "Markup Headings",
+ "scope": "markup.heading",
+ "settings": {
+ "fontStyle": "bold",
+ "foreground": "#268BD2"
+ }
+ },
+ {
+ "name": "Markup Setext Header",
+ "scope": "markup.heading.setext",
+ "settings": {
+ "fontStyle": "",
+ "foreground": "#268BD2"
+ }
+ }
+ ],
+ "colors": {
+ "focusBorder": "#2AA19899",
+ "selection.background": "#2AA19899",
+ "input.background": "#003847",
+ "input.foreground": "#93A1A1",
+ "input.placeholderForeground": "#93A1A1AA",
+ "inputOption.activeBorder": "#2AA19899",
+ "inputValidation.infoBorder": "#363b5f",
+ "inputValidation.infoBackground": "#052730",
+ "inputValidation.warningBackground": "#5d5938",
+ "inputValidation.warningBorder": "#9d8a5e",
+ "inputValidation.errorBackground": "#571b26",
+ "inputValidation.errorBorder": "#a92049",
+ "errorForeground": "#ffeaea",
+ "badge.background": "#047aa6",
+ "progressBar.background": "#047aa6",
+ "dropdown.background": "#00212B",
+ "dropdown.border": "#2AA19899",
+ "button.background": "#2AA19899",
+ "list.activeSelectionBackground": "#005A6F",
+ "quickInputList.focusBackground": "#005A6F",
+ "list.hoverBackground": "#004454AA",
+ "list.inactiveSelectionBackground": "#00445488",
+ "list.dropBackground": "#00445488",
+ "list.highlightForeground": "#1ebcc5",
+ "editor.background": "#002B36",
+ "editor.foreground": "#839496",
+ "editorWidget.background": "#00212B",
+ "editorCursor.foreground": "#D30102",
+ "editorWhitespace.foreground": "#93A1A180",
+ "editor.lineHighlightBackground": "#073642",
+ "editorLineNumber.activeForeground": "#949494",
+ "editor.selectionBackground": "#274642",
+ "minimap.selectionHighlight": "#274642",
+ "editorIndentGuide.background": "#93A1A180",
+ "editorIndentGuide.activeBackground": "#C3E1E180",
+ "editorHoverWidget.background": "#004052",
+ "editorMarkerNavigationError.background": "#AB395B",
+ "editorMarkerNavigationWarning.background": "#5B7E7A",
+ "editor.selectionHighlightBackground": "#005A6FAA",
+ "editor.wordHighlightBackground": "#004454AA",
+ "editor.wordHighlightStrongBackground": "#005A6FAA",
+ "editorBracketHighlight.foreground1": "#cdcdcdff",
+ "editorBracketHighlight.foreground2": "#b58900ff",
+ "editorBracketHighlight.foreground3": "#d33682ff",
+ "peekViewResult.background": "#00212B",
+ "peekViewEditor.background": "#10192c",
+ "peekViewTitle.background": "#00212B",
+ "peekView.border": "#2b2b4a",
+ "peekViewEditor.matchHighlightBackground": "#7744AA40",
+ "titleBar.activeBackground": "#002C39",
+ "editorGroup.border": "#00212B",
+ "editorGroup.dropBackground": "#2AA19844",
+ "editorGroupHeader.tabsBackground": "#004052",
+ "tab.activeForeground": "#d6dbdb",
+ "tab.activeBackground": "#002B37",
+ "tab.inactiveForeground": "#93A1A1",
+ "tab.inactiveBackground": "#004052",
+ "tab.border": "#003847",
+ "tab.lastPinnedBorder": "#2AA19844",
+ "activityBar.background": "#003847",
+ "panel.border": "#2b2b4a",
+ "sideBar.background": "#00212B",
+ "sideBarTitle.foreground": "#93A1A1",
+ "statusBar.foreground": "#93A1A1",
+ "statusBar.background": "#00212B",
+ "statusBar.debuggingBackground": "#00212B",
+ "statusBar.noFolderBackground": "#00212B",
+ "statusBarItem.remoteBackground": "#2AA19899",
+ "ports.iconRunningProcessForeground": "#369432",
+ "statusBarItem.prominentBackground": "#003847",
+ "statusBarItem.prominentHoverBackground": "#003847",
+ "debugToolBar.background": "#00212B",
+ "debugExceptionWidget.background": "#00212B",
+ "debugExceptionWidget.border": "#AB395B",
+ "pickerGroup.foreground": "#2AA19899",
+ "pickerGroup.border": "#2AA19899",
+ "terminal.ansiBlack": "#073642",
+ "terminal.ansiRed": "#dc322f",
+ "terminal.ansiGreen": "#859900",
+ "terminal.ansiYellow": "#b58900",
+ "terminal.ansiBlue": "#268bd2",
+ "terminal.ansiMagenta": "#d33682",
+ "terminal.ansiCyan": "#2aa198",
+ "terminal.ansiWhite": "#eee8d5",
+ "terminal.ansiBrightBlack": "#002b36",
+ "terminal.ansiBrightRed": "#cb4b16",
+ "terminal.ansiBrightGreen": "#586e75",
+ "terminal.ansiBrightYellow": "#657b83",
+ "terminal.ansiBrightBlue": "#839496",
+ "terminal.ansiBrightMagenta": "#6c71c4",
+ "terminal.ansiBrightCyan": "#93a1a1",
+ "terminal.ansiBrightWhite": "#fdf6e3"
+ },
+ "semanticHighlighting": true
+}
\ No newline at end of file
diff --git a/app/src/main/ic_app-playstore.png b/app/src/main/ic_app-playstore.png
deleted file mode 100644
index 7d4fa1f42..000000000
Binary files a/app/src/main/ic_app-playstore.png and /dev/null differ
diff --git a/app/src/main/ic_new_app-playstore.png b/app/src/main/ic_new_app-playstore.png
new file mode 100644
index 000000000..9ba224c01
Binary files /dev/null and b/app/src/main/ic_new_app-playstore.png differ
diff --git a/app/src/main/ic_new_app_launcher-playstore.png b/app/src/main/ic_new_app_launcher-playstore.png
new file mode 100644
index 000000000..ec2ba5cbb
Binary files /dev/null and b/app/src/main/ic_new_app_launcher-playstore.png differ
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/App.kt b/app/src/main/java/com/github/jing332/tts_server_android/App.kt
new file mode 100644
index 000000000..b313e13c3
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/App.kt
@@ -0,0 +1,54 @@
+package com.github.jing332.tts_server_android
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import android.os.Process
+import com.github.jing332.tts_server_android.model.hanlp.HanlpManager
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import java.util.*
+import kotlin.properties.Delegates
+
+
+val app: App
+ inline get() = App.instance
+
+@Suppress("DEPRECATION")
+class App : Application() {
+ companion object {
+ const val TAG = "App"
+ var instance: App by Delegates.notNull()
+ val context: Context by lazy { instance }
+ }
+
+ override fun attachBaseContext(base: Context) {
+ super.attachBaseContext(base.apply { AppLocale.setLocale(base) })
+ }
+
+ @SuppressLint("SdCardPath")
+ @OptIn(DelicateCoroutinesApi::class)
+ override fun onCreate() {
+ super.onCreate()
+ instance = this
+ CrashHandler(this)
+
+ GlobalScope.launch {
+ HanlpManager.initDir(
+ context.getExternalFilesDir("hanlp")?.absolutePath
+ ?: "/data/data/$packageName/files/hanlp"
+ )
+ }
+ }
+
+ @SuppressLint("UnspecifiedImmutableFlag")
+ fun restart() {
+ val intent = packageManager.getLaunchIntentForPackage(packageName)!!
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ //杀掉以前进程
+ Process.killProcess(Process.myPid());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/AppLocale.kt b/app/src/main/java/com/github/jing332/tts_server_android/AppLocale.kt
new file mode 100644
index 000000000..83ceeea42
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/AppLocale.kt
@@ -0,0 +1,108 @@
+package com.github.jing332.tts_server_android
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.os.LocaleList
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.os.LocaleListCompat
+import com.github.jing332.tts_server_android.utils.FileUtils
+import com.github.jing332.tts_server_android.utils.sysConfiguration
+import java.io.File
+import java.util.*
+
+
+@Suppress("DEPRECATION")
+object AppLocale {
+ val localeMap by lazy {
+ linkedMapOf().apply {
+ BuildConfig.TRANSLATION_ARRAY.sorted().forEach {
+ this[it] = Locale.forLanguageTag(it)
+ }
+ }
+ }
+
+ fun getLocaleCodeFromFile(context: Context): String {
+ kotlin.runCatching {
+ val file = File(context.filesDir.absolutePath + "/application_locale")
+ return file.readText()
+ }
+ return ""
+ }
+
+ fun saveLocaleCodeToFile(context: Context, lang: String) {
+ val file = File(context.filesDir.absolutePath + "/application_locale")
+ FileUtils.saveFile(file, lang.toByteArray())
+ }
+
+ fun setLocale(context: Context, locale: Locale = getLocaleFromFile(context)) {
+ val resources = context.resources
+ val metrics = resources.displayMetrics
+ val configuration = resources.configuration
+ val newLocale = getLocaleFromFile(context)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ configuration.setLocale(newLocale)
+ val localeList = LocaleList(newLocale)
+ LocaleList.setDefault(localeList)
+ configuration.setLocales(localeList)
+ } else {
+ Locale.setDefault(newLocale)
+ configuration.setLocale(newLocale)
+ }
+
+ resources.updateConfiguration(configuration, metrics)
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(locale))
+ }
+
+ /**
+ * 当前系统语言
+ */
+ @SuppressLint("ObsoleteSdkInt")
+ private fun getSystemLocale(): Locale {
+ val locale: Locale
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0有多语言设置获取顶部的语言
+ locale = sysConfiguration.locales.get(0)
+ } else {
+ @Suppress("DEPRECATION")
+ locale = sysConfiguration.locale
+ }
+ return locale
+ }
+
+ /**
+ * 当前App语言
+ */
+ @SuppressLint("ObsoleteSdkInt")
+ fun getAppLocale(context: Context): Locale {
+ val locale: Locale
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ locale = context.resources.configuration.locales[0]
+ } else {
+ @Suppress("DEPRECATION")
+ locale = context.resources.configuration.locale
+ }
+ return locale
+ }
+
+
+ /**
+ * 当前设置语言
+ */
+ fun getLocaleFromFile(context: Context): Locale {
+ return localeMap[getLocaleCodeFromFile(context)] ?: getSystemLocale()
+ }
+
+ /**
+ * 判断App语言和设置语言是否相同
+ */
+ fun isSameWithSetting(context: Context): Boolean {
+ val locale = getAppLocale(context)
+ val language = locale.language
+ val country = locale.country
+ val pfLocale = getLocaleFromFile(context)
+ val pfLanguage = pfLocale.language
+ val pfCountry = pfLocale.country
+ return language == pfLanguage && country == pfCountry
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/CrashHandler.kt b/app/src/main/java/com/github/jing332/tts_server_android/CrashHandler.kt
new file mode 100644
index 000000000..a80459d5e
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/CrashHandler.kt
@@ -0,0 +1,44 @@
+package com.github.jing332.tts_server_android
+
+import android.content.Context
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+import com.github.jing332.tts_server_android.utils.longToast
+import com.github.jing332.tts_server_android.utils.runOnUI
+import tts_server_lib.Tts_server_lib
+import java.time.LocalDateTime
+
+
+class CrashHandler(var context: Context) : Thread.UncaughtExceptionHandler {
+ private var mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
+
+ init {
+ Thread.setDefaultUncaughtExceptionHandler(this)
+ }
+
+ override fun uncaughtException(t: Thread, e: Throwable) {
+ handleException(e)
+ mDefaultHandler?.uncaughtException(t, e)
+ }
+
+ private fun handleException(e: Throwable) {
+ context.longToast("TTS Server已崩溃 上传日志中 稍后将会复制到剪贴板")
+ val log = "\n${LocalDateTime.now()}" +
+ "\n版本代码:${AppConst.appInfo.versionCode}, 版本名称:${AppConst.appInfo.versionName}\n" +
+ "崩溃详情:\n${e.stackTraceToString()}"
+ val copyContent: String = try {
+ if (BuildConfig.DEBUG)
+ log
+ else
+ Tts_server_lib.uploadLog(log)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ log
+ }
+
+ runOnUI {
+ ClipboardUtils.copyText("TTS-Server崩溃日志", copyContent)
+ context.longToast("已将日志复制到剪贴板")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/GoLog.kt b/app/src/main/java/com/github/jing332/tts_server_android/GoLog.kt
deleted file mode 100644
index 15f6e53bd..000000000
--- a/app/src/main/java/com/github/jing332/tts_server_android/GoLog.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.github.jing332.tts_server_android
-
-import android.graphics.Color
-import java.io.Serializable
-
-class GoLog(var level: Int, var msg: String) : Serializable {
- fun toText(): String {
- return GoLogLevel.toString(level)
- }
-
- fun toColor(): Int {
- return when {
- level == GoLogLevel.WarnLevel -> {
- Color.rgb(255, 215, 0) /* 金色 */
- }
- level <= GoLogLevel.ErrorLevel -> {
- Color.RED
- }
- else -> {
- Color.GRAY
- }
- }
- }
-
-}
-
-object GoLogLevel {
- const val PanicLevel = 0
- const val FatalLevel = 1
- const val ErrorLevel = 2
- const val WarnLevel = 3
- const val InfoLevel = 4
- const val DebugLevel = 5
- const val TraceLevel = 6
-
- fun toString(level: Int): String {
- when (level) {
- PanicLevel -> return "宕机"
- FatalLevel -> return "致命"
- ErrorLevel -> return "错误"
- WarnLevel -> return "警告"
- InfoLevel -> return "信息"
- DebugLevel -> return "调试"
- TraceLevel -> return "详细"
- }
- return level.toString()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/ShortCuts.kt b/app/src/main/java/com/github/jing332/tts_server_android/ShortCuts.kt
new file mode 100644
index 000000000..3aede614b
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/ShortCuts.kt
@@ -0,0 +1,85 @@
+package com.github.jing332.tts_server_android
+
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import com.github.jing332.tts_server_android.compose.systts.plugin.PluginManagerActivity
+import com.github.jing332.tts_server_android.compose.systts.replace.ReplaceManagerActivity
+import com.github.jing332.tts_server_android.compose.systts.speechrule.SpeechRuleManagerActivity
+import com.github.jing332.tts_server_android.ui.forwarder.MsForwarderSwitchActivity
+import com.github.jing332.tts_server_android.ui.forwarder.SystemForwarderSwitchActivity
+
+object ShortCuts {
+ private inline fun buildIntent(context: Context): Intent {
+ val intent = Intent(context, T::class.java)
+ intent.action = Intent.ACTION_VIEW
+ return intent
+ }
+
+ private fun buildSysSwitchShortCutInfo(context: Context): ShortcutInfoCompat {
+ val msSwitchIntent =
+ buildIntent(
+ context
+ )
+ return ShortcutInfoCompat.Builder(context, "forwarder_sys_switch")
+ .setShortLabel(context.getString(R.string.forwarder_systts))
+ .setLongLabel(context.getString(R.string.forwarder_systts))
+ .setIcon(IconCompat.createWithResource(context, R.drawable.ic_switch))
+ .setIntent(msSwitchIntent)
+ .build()
+ }
+
+
+ private fun buildMsSwitchShortCutInfo(context: Context): ShortcutInfoCompat {
+ val msSwitchIntent = buildIntent(context)
+ return ShortcutInfoCompat.Builder(context, "forwarder_ms_switch")
+ .setShortLabel(context.getString(R.string.forwarder_ms))
+ .setLongLabel(context.getString(R.string.forwarder_ms))
+ .setIcon(IconCompat.createWithResource(context, R.drawable.ic_switch))
+ .setIntent(msSwitchIntent)
+ .build()
+ }
+
+ private fun buildReplaceManagerShortCutInfo(context: Context): ShortcutInfoCompat {
+ return ShortcutInfoCompat.Builder(context, "replace_manager")
+ .setShortLabel(context.getString(R.string.replace_rule_manager))
+ .setLongLabel(context.getString(R.string.replace_rule_manager))
+ .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_replace))
+ .setIntent(buildIntent(context))
+ .build()
+ }
+
+ private fun buildSpeechManagerShortCutInfo(context: Context): ShortcutInfoCompat {
+ return ShortcutInfoCompat.Builder(context, "speech_rule_manager")
+ .setShortLabel(context.getString(R.string.speech_rule_manager))
+ .setLongLabel(context.getString(R.string.speech_rule_manager))
+ .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_speech_rule))
+ .setIntent(buildIntent(context))
+ .build()
+ }
+
+ private fun buildPluginManagerShortCutInfo(context: Context): ShortcutInfoCompat {
+ return ShortcutInfoCompat.Builder(context, "plugin_manager")
+ .setShortLabel(context.getString(R.string.plugin_manager))
+ .setLongLabel(context.getString(R.string.plugin_manager))
+ .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_plugin))
+ .setIntent(buildIntent(context))
+ .build()
+ }
+
+ fun buildShortCuts(context: Context) {
+ ShortcutManagerCompat.setDynamicShortcuts(
+ context, listOf(
+ buildMsSwitchShortCutInfo(context),
+ buildSysSwitchShortCutInfo(context),
+ buildReplaceManagerShortCutInfo(context),
+ buildSpeechManagerShortCutInfo(context),
+ buildPluginManagerShortCutInfo(context)
+ )
+ )
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/bean/EdgeVoiceBean.kt b/app/src/main/java/com/github/jing332/tts_server_android/bean/EdgeVoiceBean.kt
new file mode 100644
index 000000000..90bda67cb
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/bean/EdgeVoiceBean.kt
@@ -0,0 +1,23 @@
+package com.github.jing332.tts_server_android.bean
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class EdgeVoiceBean(
+ @SerialName("Gender")
+ val gender: String,
+ @SerialName("Locale")
+ val locale: String,
+ @SerialName("Name")
+ val name: String,
+ @SerialName("ShortName")
+ val shortName: String,
+ @SerialName("FriendlyName")
+ val friendlyName: String,
+// @SerialName("Status")
+// val status: String,
+// @SerialName("SuggestedCodec")
+// val suggestedCodec: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/bean/GithubReleaseApiBean.kt b/app/src/main/java/com/github/jing332/tts_server_android/bean/GithubReleaseApiBean.kt
new file mode 100644
index 000000000..df80cbdfd
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/bean/GithubReleaseApiBean.kt
@@ -0,0 +1,24 @@
+package com.github.jing332.tts_server_android.bean
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GithubReleaseApiBean(
+ @SerialName("assets")
+ val assets: List,
+ @SerialName("body")
+ val body: String,
+ @SerialName("tag_name")
+ val tagName: String,
+)
+
+@Serializable
+data class Asset(
+ @SerialName("browser_download_url")
+ val browserDownloadUrl: String,
+ @SerialName("name")
+ val name: String,
+ @SerialName("size")
+ val size: Int,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/bean/LegadoHttpTts.kt b/app/src/main/java/com/github/jing332/tts_server_android/bean/LegadoHttpTts.kt
new file mode 100644
index 000000000..d071661af
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/bean/LegadoHttpTts.kt
@@ -0,0 +1,28 @@
+package com.github.jing332.tts_server_android.bean
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+data class LegadoHttpTts(
+ @SerialName("header") val header: String = "",
+ @SerialName("id") val id: Long = 0,
+ @SerialName("name") val name: String = "",
+ @SerialName("url") val url: String = ""
+
+// @SerialName("concurrentRate")
+// val concurrentRate: String,
+// @SerialName("contentType")
+// val contentType: String,
+// @SerialName("enabledCookieJar")
+// val enabledCookieJar: Boolean,
+// @SerialName("lastUpdateTime")
+// val lastUpdateTime: Long,
+// @SerialName("loginCheckJs")
+// val loginCheckJs: String,
+// @SerialName("loginUi")
+// val loginUi: String,
+// @SerialName("loginUrl")
+// val loginUrl: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/AboutDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/AboutDialog.kt
new file mode 100644
index 000000000..207739b41
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/AboutDialog.kt
@@ -0,0 +1,93 @@
+package com.github.jing332.tts_server_android.compose
+
+import android.content.Intent
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextAlign.Companion
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import com.github.jing332.tts_server_android.BuildConfig
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.AppLauncherIcon
+
+@Composable
+fun AboutDialog(onDismissRequest: () -> Unit) {
+ val context = LocalContext.current
+
+ AppDialog(
+ onDismissRequest = onDismissRequest,
+ title = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ AppLauncherIcon(modifier = Modifier.size(64.dp))
+ Text(
+ stringResource(id = R.string.app_name),
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .padding(start = 8.dp)
+ )
+ }
+ },
+ content = {
+ fun openUrl(uri: String) {
+ context.startActivity(
+ Intent(Intent.ACTION_VIEW).apply {
+ data = uri.toUri()
+ }
+ )
+ }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ SelectionContainer {
+ Column(Modifier.padding(vertical = 4.dp)) {
+ Text("${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})")
+ }
+ }
+ HorizontalDivider(Modifier.padding(vertical = 8.dp))
+ Text(
+ "Github - TTS Server",
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.small)
+ .clickable {
+ openUrl("https://github.com/jing332/tts-server-android")
+ }
+ .padding(vertical = 8.dp)
+ .fillMaxWidth(),
+ textAlign = TextAlign.Center
+ )
+ }
+ },
+ buttons = {
+ TextButton(onClick = {
+ onDismissRequest()
+ context.startActivity(
+ Intent(context, LibrariesActivity::class.java).setAction(Intent.ACTION_VIEW)
+ )
+ }) {
+ Text(text = stringResource(id = R.string.open_source_license))
+ }
+
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.confirm))
+ }
+ })
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/AppUpdateDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/AppUpdateDialog.kt
new file mode 100644
index 000000000..881aaad18
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/AppUpdateDialog.kt
@@ -0,0 +1,148 @@
+package com.github.jing332.tts_server_android.compose
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.Markdown
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.model.updater.AppUpdateChecker
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+
+@Preview
+@Composable
+private fun PreviewAppUpdateDialog() {
+ var show by remember { androidx.compose.runtime.mutableStateOf(true) }
+ if (show)
+ AppUpdateDialog(
+ onDismissRequest = {
+ show = false
+ }, version = "1.0.0", content = "## 更新内容\n\n- 123", downloadUrl = "url"
+ )
+
+}
+
+@Composable
+fun AppUpdateActionDialog(onDismissRequest: () -> Unit, result: AppUpdateChecker.ActionResult) {
+ val context = LocalContext.current
+ fun openDownloadUrl(url: String) {
+ ClipboardUtils.copyText("TTS Server", url)
+ context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ }
+ AppDialog(onDismissRequest = onDismissRequest, title = {
+ Column {
+ Text(
+ stringResource(id = R.string.check_update) + " (Github Actions)",
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Text(
+ text = AppConst.dateFormatSec.format(result.time * 1000),
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center
+ )
+ }
+ }, content = {
+ Column(Modifier.padding(12.dp)) {
+ Text(
+ text = result.title,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ },
+ buttons = {
+ Row {
+ TextButton(onClick = {
+ onDismissRequest()
+ openDownloadUrl(result.url)
+ }) {
+ Text("Github")
+ }
+
+ TextButton(onClick = { onDismissRequest() }) {
+ Text(stringResource(id = R.string.cancel))
+ }
+ }
+ }
+ )
+}
+
+@Composable
+fun AppUpdateDialog(
+ onDismissRequest: () -> Unit,
+ version: String,
+ content: String,
+ downloadUrl: String
+) {
+ val context = LocalContext.current
+ fun openDownloadUrl(url: String) {
+ ClipboardUtils.copyText("TTS Server", url)
+ context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ }
+
+ AppDialog(onDismissRequest = onDismissRequest,
+ title = {
+ Text(
+ stringResource(id = R.string.check_update),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ },
+ content = {
+ Column {
+ Text(
+ text = version,
+ style = MaterialTheme.typography.titleSmall,
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ val scrollState = rememberScrollState()
+ Column(
+ Modifier
+ .padding(8.dp)
+ .verticalScroll(scrollState),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Markdown(
+ content = content,
+ modifier = Modifier
+ .padding(4.dp),
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ }
+ }
+ },
+ buttons = {
+ Row {
+ TextButton(onClick = { openDownloadUrl(downloadUrl) }) {
+ Text("下载(Github)")
+ }
+ TextButton(onClick = { openDownloadUrl("https://ghproxy.com/${downloadUrl}") }) {
+ Text("下载(ghproxy加速)")
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/AutoUpdateCheckerDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/AutoUpdateCheckerDialog.kt
new file mode 100644
index 000000000..60b75f0fe
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/AutoUpdateCheckerDialog.kt
@@ -0,0 +1,79 @@
+package com.github.jing332.tts_server_android.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import com.drake.net.utils.withIO
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.model.updater.AppUpdateChecker
+import com.github.jing332.tts_server_android.model.updater.UpdateResult
+import com.github.jing332.tts_server_android.utils.longToast
+import kotlinx.coroutines.CancellationException
+
+@Composable
+internal fun AutoUpdateCheckerDialog(
+ showUpdateToast: Boolean = true,
+ fromAction: Boolean = false,
+ dismiss: () -> Unit
+) {
+ val context = LocalContext.current
+ var showDialog by remember { mutableStateOf(null) }
+ if (showDialog != null) {
+ val ret = showDialog!!
+ LaunchedEffect(ret) {
+ if (showUpdateToast && ret.hasUpdate())
+ context.longToast(R.string.new_version_available, ret.version)
+ }
+ AppUpdateDialog(
+ onDismissRequest = {
+ showDialog = null
+ dismiss()
+ },
+ version = ret.version,
+ content = ret.content,
+ downloadUrl = ret.downloadUrl,
+ )
+ }
+
+ var showActionDialog by remember { mutableStateOf(null) }
+ if (showActionDialog != null) {
+ val ret = showActionDialog!!
+ AppUpdateActionDialog(
+ onDismissRequest = {
+ showActionDialog = null
+ dismiss()
+ },
+ result = ret
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ val result = try {
+ withIO {
+ if (fromAction) AppUpdateChecker.checkUpdateFromActions()
+ else AppUpdateChecker.checkUpdate()
+ }
+ } catch (_: CancellationException) {
+ null
+ } catch (e: Exception) {
+ e.printStackTrace()
+ context.longToast(context.getString(R.string.check_update_failed) + "\n$e")
+ null
+ }
+
+ if (result is AppUpdateChecker.ActionResult?) {
+ if (result == null) {
+ dismiss()
+ } else {
+ showActionDialog = result
+ }
+ } else if (result is UpdateResult?) {
+ if (result?.hasUpdate() == true) showDialog = result
+ else dismiss()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/LibrariesActivity.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/LibrariesActivity.kt
new file mode 100644
index 000000000..22c1e951e
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/LibrariesActivity.kt
@@ -0,0 +1,67 @@
+package com.github.jing332.tts_server_android.compose
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.theme.AppTheme
+import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
+import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
+
+class LibrariesActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ AppTheme {
+ Content()
+ }
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun Content() {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(text = stringResource(id = R.string.open_source_license)) },
+ navigationIcon = {
+ IconButton(onClick = { finish() }) {
+ Icon(
+ Icons.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.nav_back)
+ )
+ }
+ }
+ )
+ }
+ ) {
+ LibrariesContainer(
+ Modifier
+ .fillMaxSize()
+ .padding(it),
+ colors = LibraryDefaults.libraryColors(
+ backgroundColor = MaterialTheme.colorScheme.background,
+ contentColor = MaterialTheme.colorScheme.onBackground,
+ badgeBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
+ badgeContentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ ),
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/MainActivity.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/MainActivity.kt
new file mode 100644
index 000000000..b2d50d0cd
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/MainActivity.kt
@@ -0,0 +1,505 @@
+@file:Suppress("DEPRECATION")
+
+package com.github.jing332.tts_server_android.compose
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.SystemClock
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowCircleUp
+import androidx.compose.material.icons.filled.BatteryFull
+import androidx.compose.material.icons.filled.HelpOutline
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalNavigationDrawer
+import androidx.compose.material3.NavigationDrawerItem
+import androidx.compose.material3.NavigationDrawerItemDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import androidx.navigation.NavController
+import androidx.navigation.NavDeepLinkRequest
+import androidx.navigation.NavDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.github.jing332.tts_server_android.BuildConfig
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.ShortCuts
+import com.github.jing332.tts_server_android.compose.forwarder.ms.MsTtsForwarderScreen
+import com.github.jing332.tts_server_android.compose.forwarder.systts.SystemTtsForwarderScreen
+import com.github.jing332.tts_server_android.compose.nav.NavRoutes
+import com.github.jing332.tts_server_android.compose.settings.SettingsScreen
+import com.github.jing332.tts_server_android.compose.systts.SystemTtsScreen
+import com.github.jing332.tts_server_android.compose.systts.list.edit.TtsEditContainerScreen
+import com.github.jing332.tts_server_android.compose.theme.AppTheme
+import com.github.jing332.tts_server_android.compose.widgets.AppLauncherIcon
+import com.github.jing332.tts_server_android.conf.AppConfig
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTts
+import com.github.jing332.tts_server_android.model.speech.tts.ITextToSpeechEngine
+import com.github.jing332.tts_server_android.service.systts.SystemTtsService
+import com.github.jing332.tts_server_android.ui.AppHelpDocumentActivity
+import com.github.jing332.tts_server_android.utils.MyTools.killBattery
+import com.github.jing332.tts_server_android.utils.clone
+import com.github.jing332.tts_server_android.utils.longToast
+import com.github.jing332.tts_server_android.utils.performLongPress
+import com.github.jing332.tts_server_android.utils.toast
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
+import kotlinx.coroutines.launch
+
+
+val LocalNavController = compositionLocalOf { error("No nav controller") }
+val LocalDrawerState = compositionLocalOf { error("No drawer state") }
+
+fun Context.asAppCompatActivity(): AppCompatActivity {
+ return this as? AppCompatActivity ?: error("Context is not an AppCompatActivity")
+}
+
+fun Context.asActivity(): Activity {
+ return this as? Activity ?: error("Context is not an Activity")
+}
+
+private var updateCheckTrigger by mutableStateOf(false)
+
+class MainActivity : AppCompatActivity() {
+ @OptIn(ExperimentalPermissionsApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ ShortCuts.buildShortCuts(this)
+ setContent {
+ AppTheme {
+ var showAutoCheckUpdaterDialog by remember { mutableStateOf(false) }
+ if (showAutoCheckUpdaterDialog) {
+ val fromUser by remember { mutableStateOf(updateCheckTrigger) }
+ AutoUpdateCheckerDialog(fromUser, fromAction = true) {
+ showAutoCheckUpdaterDialog = false
+ updateCheckTrigger = false
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // A13
+ val notificationPermission =
+ rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
+ if (!notificationPermission.status.isGranted) {
+ LaunchedEffect(notificationPermission) {
+ notificationPermission.launchPermissionRequest()
+ }
+ }
+ }/* else {
+ val enabled = NotificationManagerCompat.from(this).areNotificationsEnabled()
+ if (!enabled) {
+ LaunchedEffect(Unit) {
+ gotoNotificationManager(this@MainActivity)
+ }
+ }
+ }*/
+
+ LaunchedEffect(Unit) {
+ showAutoCheckUpdaterDialog = AppConfig.isAutoCheckUpdateEnabled.value
+ }
+
+ LaunchedEffect(updateCheckTrigger) {
+ if (updateCheckTrigger) showAutoCheckUpdaterDialog = true
+ }
+
+ MainScreen { finish() }
+ }
+ }
+ }
+}
+
+//private fun gotoNotificationManager(context: Context) {
+// try {
+// val intent = Intent()
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //A8.0
+// intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+// intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
+// intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)
+// }
+// intent.putExtra("app_package", context.packageName)
+// intent.putExtra("app_uid", context.applicationInfo.uid)
+// context.startActivity(intent)
+// } catch (e: Exception) {
+// e.printStackTrace()
+// val intent = Intent()
+// intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+// val uri = Uri.fromParts("package", context.packageName, null)
+// intent.setData(uri)
+// context.startActivity(intent)
+// }
+//}
+
+@Composable
+private fun MainScreen(finish: () -> Unit) {
+ val context = LocalContext.current
+ val navController = rememberNavController()
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val entryState by navController.currentBackStackEntryAsState()
+
+ val gesturesEnabled = remember(entryState) {
+ NavRoutes.routes.find { it.id == entryState?.destination?.route } != null
+ }
+
+ var lastBackDownTime by remember { mutableLongStateOf(0L) }
+ BackHandler(enabled = drawerState.isClosed) {
+ val duration = 2000
+ SystemClock.elapsedRealtime().let {
+ if (it - lastBackDownTime <= duration) {
+ finish()
+ } else {
+ lastBackDownTime = it
+ context.toast(R.string.app_down_again_to_exit)
+ }
+ }
+ }
+ CompositionLocalProvider(
+ LocalNavController provides navController,
+ LocalDrawerState provides drawerState,
+ ) {
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ gesturesEnabled = gesturesEnabled,
+ drawerContent = {
+ NavDrawerContent(
+ Modifier
+ .fillMaxHeight()
+ .width(300.dp)
+ .clip(
+ MaterialTheme.shapes.large.copy(
+ topStart = CornerSize(0.dp),
+ bottomStart = CornerSize(0.dp)
+ )
+ )
+ .background(MaterialTheme.colorScheme.background)
+ .padding(12.dp),
+ navController,
+ drawerState,
+ )
+ }) {
+ NavHost(
+ navController = navController,
+ startDestination = NavRoutes.SystemTTS.id
+ ) {
+ composable(NavRoutes.SystemTTS.id) { SystemTtsScreen() }
+ composable(NavRoutes.SystemTtsForwarder.id) {
+ SystemTtsForwarderScreen()
+ }
+ composable(NavRoutes.MsTtsForwarder.id) { MsTtsForwarderScreen() }
+ composable(NavRoutes.Settings.id) { SettingsScreen(drawerState) }
+
+ composable(NavRoutes.TtsEdit.id) { stackEntry ->
+ val systts: SystemTts =
+ stackEntry.arguments?.getParcelable(NavRoutes.TtsEdit.DATA)
+ ?: return@composable
+ var stateSystts by rememberSaveable {
+ mutableStateOf(systts.run {
+ if (tts.locale.isBlank()) {
+ copy(
+ tts = tts.clone()!!
+ .apply { locale = AppConst.localeCode }
+ )
+ } else
+ this
+ })
+ }
+ TtsEditContainerScreen(
+ modifier = Modifier
+ .fillMaxSize(),
+ systts = stateSystts,
+ onSysttsChange = {
+ stateSystts = it
+ println("UpdateSystemTTS: $it")
+ },
+ onSave = {
+ navController.popBackStack()
+ appDb.systemTtsDao.insertTts(stateSystts)
+ if (stateSystts.isEnabled) SystemTtsService.notifyUpdateConfig()
+ },
+ onCancel = {
+ navController.popBackStack()
+ }
+ )
+ }
+ }
+ }
+
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun NavDrawerContent(
+ modifier: Modifier = Modifier,
+ navController: NavHostController,
+ drawerState: DrawerState,
+) {
+ val scope = rememberCoroutineScope()
+ BackHandler(enabled = drawerState.isOpen) {
+ scope.launch { drawerState.close() }
+ }
+
+ @Composable
+ fun DrawerItem(
+ selected: Boolean = false,
+ icon: @Composable () -> Unit,
+ label: @Composable () -> Unit,
+ onClick: () -> Unit
+ ) {
+ NavigationDrawerItem(
+ modifier = Modifier.padding(vertical = 2.dp),
+ icon = icon,
+ label = label,
+ selected = selected,
+ onClick = onClick,
+ colors = NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent)
+ )
+ }
+
+
+ @Composable
+ fun NavDrawerItem(
+ icon: @Composable () -> Unit,
+ targetScreen: NavRoutes,
+ onClick: () -> Unit = {
+ scope.launch { drawerState.close() }
+ navController.navigateSingleTop(targetScreen.id, popUpToMain = true)
+ }
+ ) {
+ val isSelected = navController.currentDestination?.route == targetScreen.id
+ DrawerItem(
+ icon = icon,
+ label = { Text(text = stringResource(id = targetScreen.strId)) },
+ selected = isSelected,
+ onClick = onClick,
+ )
+ }
+
+ Column(modifier = modifier.verticalScroll(rememberScrollState())) {
+ Spacer(modifier = Modifier.height(24.dp))
+ val context = LocalContext.current
+ val clipboardManager = LocalClipboardManager.current
+ val view = LocalView.current
+
+ var isBuildTimeExpanded by remember { mutableStateOf(false) }
+ val versionNameText = remember {
+ "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
+ }
+ Column(modifier = Modifier
+ .padding(end = 4.dp)
+ .clip(MaterialTheme.shapes.small)
+ .combinedClickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = true),
+ onClick = {
+ isBuildTimeExpanded = !isBuildTimeExpanded
+ },
+ onLongClick = {
+ view.performLongPress()
+ clipboardManager.setText(AnnotatedString(versionNameText))
+ context.longToast(R.string.copied)
+ }
+ )) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ AppLauncherIcon(Modifier.size(64.dp))
+ Column(
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .align(Alignment.CenterVertically)
+ ) {
+ Text(
+ text = stringResource(id = R.string.app_name),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Text(
+ text = versionNameText,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ AnimatedVisibility(visible = isBuildTimeExpanded) {
+ Text(
+ text = AppConst.dateFormatSec.format(BuildConfig.BUILD_TIME * 1000),
+ modifier = Modifier.padding(4.dp)
+ )
+ }
+ }
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp, horizontal = 4.dp)
+ )
+
+ for (route in NavRoutes.routes) {
+ NavDrawerItem(icon = route.icon, targetScreen = route)
+ }
+
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp, horizontal = 4.dp)
+ )
+
+ DrawerItem(
+ icon = { Icon(Icons.Default.BatteryFull, null) },
+ label = { Text(stringResource(id = R.string.battery_optimization_whitelist)) },
+ selected = false,
+ onClick = { context.killBattery() }
+ )
+
+ DrawerItem(
+ icon = { Icon(Icons.Default.ArrowCircleUp, null) },
+ label = { Text(stringResource(id = R.string.check_update)) },
+ selected = false,
+ onClick = {
+ scope.launch {
+ drawerState.close()
+ updateCheckTrigger = true
+ }
+ },
+ )
+
+ DrawerItem(
+ icon = { Icon(Icons.Default.HelpOutline, null) },
+ label = { Text(stringResource(id = R.string.app_help_document)) },
+ selected = false,
+ onClick = {
+ context.startActivity(Intent(context, AppHelpDocumentActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ })
+ },
+ )
+
+ var showAboutDialog by remember { mutableStateOf(false) }
+ if (showAboutDialog)
+ AboutDialog { showAboutDialog = false }
+
+ DrawerItem(
+ icon = { Icon(Icons.Default.Info, null) },
+ label = { Text(stringResource(id = R.string.about)) },
+ selected = false,
+ onClick = { showAboutDialog = true }
+ )
+ }
+}
+
+@SuppressLint("RestrictedApi")
+fun NavController.navigate(
+ route: String,
+ argsBuilder: Bundle.() -> Unit = {},
+ navOptions: NavOptions? = null,
+ navigatorExtras: Navigator.Extras? = null
+) {
+ navigate(route, Bundle().apply(argsBuilder), navOptions, navigatorExtras)
+}
+
+/*
+* 可传递 Bundle 到 Navigation
+* */
+@SuppressLint("RestrictedApi")
+fun NavController.navigate(
+ route: String,
+ args: Bundle,
+ navOptions: NavOptions? = null,
+ navigatorExtras: Navigator.Extras? = null
+) {
+ val routeLink = NavDeepLinkRequest
+ .Builder
+ .fromUri(NavDestination.createRoute(route).toUri())
+ .build()
+
+ val deepLinkMatch = graph.matchDeepLink(routeLink)
+ if (deepLinkMatch != null) {
+ val destination = deepLinkMatch.destination
+ val id = destination.id
+ navigate(id, args, navOptions, navigatorExtras)
+ } else {
+ navigate(route, navOptions, navigatorExtras)
+ }
+}
+
+/**
+ * 单例并清空其他栈
+ */
+fun NavHostController.navigateSingleTop(
+ route: String,
+ args: Bundle? = null,
+ popUpToMain: Boolean = false
+) {
+ val navController = this
+ val navOptions = NavOptions.Builder()
+ .setLaunchSingleTop(true)
+ .apply {
+ if (popUpToMain) setPopUpTo(
+ navController.graph.startDestinationId,
+ inclusive = false,
+ saveState = true
+ )
+ }
+ .setRestoreState(true)
+ .build()
+ if (args == null)
+ navController.navigate(route, navOptions)
+ else
+ navController.navigate(route, args, navOptions)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/ShadowReorderableItem.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/ShadowReorderableItem.kt
new file mode 100644
index 000000000..4d134ce49
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/ShadowReorderableItem.kt
@@ -0,0 +1,38 @@
+package com.github.jing332.tts_server_android.compose
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.dp
+import org.burnoutcrew.reorderable.ReorderableItem
+import org.burnoutcrew.reorderable.ReorderableState
+
+@Composable
+fun LazyItemScope.ShadowReorderableItem(
+ reorderableState: ReorderableState<*>,
+ key: Any,
+ content: @Composable LazyItemScope.(isDragging: Boolean) -> Unit
+) {
+ val view = LocalView.current
+ ReorderableItem(reorderableState, key) { isDragging ->
+ if (isDragging) {
+ view.isHapticFeedbackEnabled = true
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ val elevation =
+ animateDpAsState(if (isDragging) 24.dp else 0.dp, label = "")
+ Box(
+ modifier = Modifier
+ .shadow(elevation.value)
+ ) {
+ content(isDragging)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupDialog.kt
new file mode 100644
index 000000000..0083caaac
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupDialog.kt
@@ -0,0 +1,111 @@
+package com.github.jing332.tts_server_android.compose.backup
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.TextCheckBox
+import com.github.jing332.tts_server_android.ui.AppActivityResultContracts
+import com.github.jing332.tts_server_android.ui.FilePickerActivity
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import kotlinx.coroutines.launch
+
+@Composable
+internal fun BackupDialog(
+ onDismissRequest: () -> Unit,
+ vm: BackupRestoreViewModel = viewModel(),
+) {
+ val filePicker =
+ rememberLauncherForActivityResult(contract = AppActivityResultContracts.filePickerActivity())
+ {
+ }
+
+ var isLoading by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val checkedList = remember {
+ mutableStateListOf(
+ Type.Preference,
+ Type.List,
+ Type.ReplaceRule,
+ Type.SpeechRule,
+ Type.Plugin
+ )
+ }
+ AppDialog(onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.backup)) },
+ content = {
+ LazyColumn(Modifier.fillMaxWidth()) {
+ items(Type.typeList) {
+ TextCheckBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.CenterStart),
+ text = { Text(stringResource(id = it.nameStrId)) },
+ checked = checkedList.contains(it),
+ onCheckedChange = { check ->
+ if (check) {
+ if (it == Type.PluginVars) {
+ checkedList.contains(Type.Plugin) || checkedList.add(Type.Plugin)
+ }
+
+ checkedList.add(it)
+ } else {
+ if (it == Type.Plugin) {
+ checkedList.remove(Type.PluginVars)
+ }
+ checkedList.remove(it)
+ }
+ },
+ horizontalArrangement = Arrangement.Start
+ )
+ }
+ }
+ },
+ buttons = {
+ Row {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.cancel))
+ }
+
+ TextButton(onClick = {
+ scope.launch {
+ runCatching {
+ val data = vm.backup(checkedList)
+ filePicker.launch(
+ FilePickerActivity.RequestSaveFile(
+ fileName = "ttsrv-backup.zip",
+ fileMime = "application/zip",
+ fileBytes = data
+ )
+ )
+ }.onFailure {
+ context.displayErrorDialog(it, context.getString(R.string.backup))
+ }
+ onDismissRequest()
+ }
+ }) {
+ Text(stringResource(id = R.string.confirm))
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupRestoreActivity.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupRestoreActivity.kt
new file mode 100644
index 000000000..3ae3f690d
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupRestoreActivity.kt
@@ -0,0 +1,105 @@
+package com.github.jing332.tts_server_android.compose.backup
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.Input
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Input
+import androidx.compose.material.icons.filled.Output
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.settings.BasePreferenceWidget
+import com.github.jing332.tts_server_android.compose.theme.AppTheme
+import com.github.jing332.tts_server_android.utils.FileUtils.readBytes
+
+class BackupRestoreActivity : AppCompatActivity() {
+ companion object {
+ const val TAG = "BackupRestoreActivity"
+ }
+
+ private var showFromFileRestoreDialog = mutableStateOf(null)
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ AppTheme {
+ var showBackupDialog by remember { mutableStateOf(false) }
+ if (showBackupDialog) {
+ BackupDialog(onDismissRequest = { showBackupDialog = false })
+ }
+
+ var showRestoreDialog by remember { mutableStateOf(false) }
+ if (showRestoreDialog) {
+ RestoreDialog(onDismissRequest = { showRestoreDialog = false })
+ }
+
+ Scaffold(topBar = {
+ TopAppBar(
+ title = { Text(stringResource(id = R.string.backup_restore)) },
+ navigationIcon = {
+ IconButton(onClick = { finish() }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack,
+ stringResource(id = R.string.nav_back)
+ )
+ }
+ })
+ }) {
+ Column(Modifier.padding(it)) {
+ BasePreferenceWidget(
+ onClick = { showBackupDialog = true },
+ title = { Text(stringResource(id = R.string.backup)) },
+ icon = { Icon(Icons.Default.Output, null) }
+ )
+
+ BasePreferenceWidget(
+ onClick = { showRestoreDialog = true },
+ title = { Text(stringResource(id = R.string.restore)) },
+ icon = { Icon(Icons.AutoMirrored.Filled.Input, null) }
+ )
+ }
+ }
+ }
+ }
+ restoreFromIntent(intent)
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ restoreFromIntent(intent)
+ }
+
+ private fun restoreFromIntent(intent: Intent?) {
+ intent?.data?.let {
+ showFromFileRestoreDialog.value = it.readBytes(this)
+ intent.data = null
+// MaterialAlertDialogBuilder(this)
+// .setTitle(R.string.restore)
+// .setMessage(R.string.restore_confirm)
+// .setNegativeButton(R.string.cancel, null)
+// .setPositiveButton(R.string.restore) { _, _ ->
+// val bytes = it.readBytes(this)
+// fragment.restore(bytes)
+// }.setOnDismissListener { intent.data = null }
+// .show()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupRestoreViewModel.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupRestoreViewModel.kt
new file mode 100644
index 000000000..1c2cebfbe
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/BackupRestoreViewModel.kt
@@ -0,0 +1,152 @@
+package com.github.jing332.tts_server_android.compose.backup
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import com.drake.net.utils.withIO
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.SpeechRule
+import com.github.jing332.tts_server_android.data.entities.plugin.Plugin
+import com.github.jing332.tts_server_android.data.entities.replace.GroupWithReplaceRule
+import com.github.jing332.tts_server_android.data.entities.systts.GroupWithSystemTts
+import com.github.jing332.tts_server_android.utils.FileUtils
+import com.github.jing332.tts_server_android.utils.ZipUtils
+import kotlinx.serialization.encodeToString
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.util.zip.ZipInputStream
+
+class BackupRestoreViewModel(application: Application) : AndroidViewModel(application) {
+ // ... /cache/backupRestore
+ private val backupRestorePath by lazy {
+ application.externalCacheDir!!.absolutePath + File.separator + "backupRestore"
+ }
+
+ // /data/data/{package name}
+ private val internalDataFile by lazy {
+ application.filesDir!!.parentFile!!
+ }
+
+ // ... /cache/backupRestore/restore
+ private val restorePath by lazy {
+ backupRestorePath + File.separator + "restore"
+ }
+
+ // ... /cache/backupRestore/restore/shared_prefs
+ private val restorePrefsPath by lazy {
+ restorePath + File.separator + "shared_prefs"
+ }
+
+
+ suspend fun restore(bytes: ByteArray): Boolean {
+ var isRestart = false
+ val outFileDir = File(restorePath)
+ ZipUtils.unzipFile(ZipInputStream(ByteArrayInputStream(bytes)), outFileDir)
+ if (outFileDir.exists()) {
+ // shared_prefs
+ val restorePrefsFile = File(restorePrefsPath)
+ if (restorePrefsFile.exists()) {
+ FileUtils.copyFolder(restorePrefsFile, internalDataFile)
+ restorePrefsFile.deleteRecursively()
+ isRestart = true
+ }
+
+ // *.json
+ for (file in outFileDir.listFiles()!!) {
+ if (file.isFile) importFromJsonFile(file)
+ }
+ }
+
+ return isRestart
+ }
+
+ private fun importFromJsonFile(file: File) {
+ val jsonStr = file.readText()
+ if (file.name.endsWith("list.json")) {
+ val list: List = AppConst.jsonBuilder.decodeFromString(jsonStr)
+ appDb.systemTtsDao.insertGroupWithTts(*list.toTypedArray())
+ } else if (file.name.endsWith("speechRules.json")) {
+ val list: List = AppConst.jsonBuilder.decodeFromString(jsonStr)
+ appDb.speechRuleDao.insert(*list.toTypedArray())
+ } else if (file.name.endsWith("replaceRules.json")) {
+ val list: List =
+ AppConst.jsonBuilder.decodeFromString(jsonStr)
+ appDb.replaceRuleDao.insertRuleWithGroup(*list.toTypedArray())
+ } else if (file.name.endsWith("plugins.json")) {
+ val list: List = AppConst.jsonBuilder.decodeFromString(jsonStr)
+ appDb.pluginDao.insertOrUpdate(*list.toTypedArray())
+ }
+ }
+
+ suspend fun backup(_types: List): ByteArray = withIO {
+ File(tmpZipPath).deleteRecursively()
+ File(tmpZipPath).mkdirs()
+
+ val types = _types.toMutableList()
+ if (types.contains(Type.PluginVars)) types.remove(Type.Plugin)
+ types.forEach {
+ createConfigFile(it)
+ }
+
+ val zipFile = File(tmpZipFile)
+ ZipUtils.zipFolder(File(tmpZipPath), zipFile)
+ return@withIO zipFile.readBytes()
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ File(backupRestorePath).deleteRecursively()
+ }
+
+ // ... /cache/backupRestore/backup
+ private val tmpZipPath by lazy {
+ backupRestorePath + File.separator + "backup"
+ }
+
+ private val tmpZipFile by lazy {
+ backupRestorePath + File.separator + "backup.zip"
+ }
+
+ private fun createConfigFile(type: Type) {
+ when (type) {
+ is Type.Preference -> {
+ val folder = internalDataFile.absolutePath + File.separator + "shared_prefs"
+ FileUtils.copyFilesFromDir(
+ File(folder),
+ File(tmpZipPath + File.separator + "shared_prefs"),
+ )
+ }
+
+ is Type.List -> {
+ encodeJsonAndCopyToTmpZipPath(appDb.systemTtsDao.getSysTtsWithGroups(), "list")
+ }
+
+ is Type.SpeechRule -> {
+ encodeJsonAndCopyToTmpZipPath(appDb.speechRuleDao.all, "speechRules")
+ }
+
+ is Type.ReplaceRule -> {
+ encodeJsonAndCopyToTmpZipPath(
+ appDb.replaceRuleDao.allGroupWithReplaceRules(),
+ "replaceRules"
+ )
+ }
+
+ is Type.IPlugin -> {
+ if (type.includeVars) {
+ encodeJsonAndCopyToTmpZipPath(appDb.pluginDao.all, "plugins")
+ } else {
+ encodeJsonAndCopyToTmpZipPath(appDb.pluginDao.all.map {
+ it.userVars = mutableMapOf()
+ it
+ }, "plugins")
+ }
+ }
+ }
+ }
+
+ private inline fun encodeJsonAndCopyToTmpZipPath(v: T, name: String) {
+ val s = AppConst.jsonBuilder.encodeToString(v)
+ File(tmpZipPath + File.separator + name + ".json").writeText(s)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/RestoreDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/RestoreDialog.kt
new file mode 100644
index 000000000..dcc9caefd
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/RestoreDialog.kt
@@ -0,0 +1,95 @@
+package com.github.jing332.tts_server_android.compose.backup
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.app
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.LoadingContent
+import com.github.jing332.tts_server_android.ui.AppActivityResultContracts
+import com.github.jing332.tts_server_android.ui.FilePickerActivity
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import com.github.jing332.tts_server_android.utils.FileUtils.readBytes
+import kotlinx.coroutines.launch
+
+@Composable
+internal fun RestoreDialog(onDismissRequest: () -> Unit, vm: BackupRestoreViewModel = viewModel()) {
+ var isLoading by remember { mutableStateOf(true) }
+ var needRestart by remember { mutableStateOf(false) }
+
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+ val filePicker =
+ rememberLauncherForActivityResult(contract = AppActivityResultContracts.filePickerActivity())
+ {
+ if (it.second == null) {
+ onDismissRequest()
+ return@rememberLauncherForActivityResult
+ }
+ scope.launch {
+ runCatching {
+ needRestart = vm.restore(it.second!!.readBytes(context))
+ isLoading = false
+ }.onFailure {
+ context.displayErrorDialog(it)
+ }
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ filePicker.launch(FilePickerActivity.RequestSelectFile(listOf("application/zip")))
+ }
+
+ AppDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.restore)) },
+ content = {
+ LoadingContent(
+ Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ isLoading = isLoading
+ ) {
+ if (!isLoading)
+ if (needRestart)
+ Text(stringResource(id = R.string.restore_restart_msg))
+ else
+ Text(stringResource(id = R.string.restore_finished))
+ }
+ },
+ buttons = {
+ if (needRestart) {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.cancel))
+ }
+
+ TextButton(onClick = {
+ app.restart()
+ }) {
+ Text(stringResource(id = R.string.restart))
+ }
+ } else {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.confirm))
+ }
+ }
+
+
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/Type.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/Type.kt
new file mode 100644
index 000000000..ce13ef489
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/backup/Type.kt
@@ -0,0 +1,28 @@
+package com.github.jing332.tts_server_android.compose.backup
+
+import com.github.jing332.tts_server_android.R
+
+
+sealed class Type(val nameStrId: Int) {
+ companion object {
+ val typeList by lazy {
+ listOf(
+ Preference,
+ List,
+ SpeechRule,
+ ReplaceRule,
+ Plugin,
+ PluginVars
+ )
+ }
+ }
+
+ data object Preference : Type(R.string.preference_settings)
+ data object List : Type(R.string.config_list)
+ data object SpeechRule : Type(R.string.speech_rule)
+ data object ReplaceRule : Type(R.string.replace_rule)
+
+ abstract class IPlugin(val id: Int, val includeVars: Boolean) : Type(id)
+ object Plugin : IPlugin(R.string.plugin, false)
+ object PluginVars : IPlugin(R.string.plugin_vars, true)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditor.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditor.kt
new file mode 100644
index 000000000..1acf96dc6
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditor.kt
@@ -0,0 +1,29 @@
+package com.github.jing332.tts_server_android.compose.codeeditor
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.viewinterop.AndroidView
+import com.github.jing332.text_searcher.ui.plugin.CodeEditorHelper
+import com.github.jing332.tts_server_android.conf.CodeEditorConfig
+import io.github.rosemoe.sora.widget.CodeEditor
+
+@Composable
+fun CodeEditor(modifier: Modifier, onUpdate: (CodeEditor) -> Unit) {
+ val context = LocalContext.current
+
+ AndroidView(modifier = modifier, factory = {
+ CodeEditor(it).apply {
+ val helper = CodeEditorHelper(context, this)
+ helper.initEditor()
+ tag = helper
+ }
+ }, update = {
+// val helper = (it.tag as CodeEditorHelper)
+ onUpdate(it)
+ })
+}
+
+fun CodeEditor.helper(): CodeEditorHelper {
+ return tag as CodeEditorHelper
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorHelper.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorHelper.kt
new file mode 100644
index 000000000..e51e557d8
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorHelper.kt
@@ -0,0 +1,76 @@
+package com.github.jing332.text_searcher.ui.plugin
+
+import android.content.Context
+import android.content.res.Configuration
+import com.github.jing332.tts_server_android.constant.CodeEditorTheme
+import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme
+import io.github.rosemoe.sora.langs.textmate.TextMateLanguage
+import io.github.rosemoe.sora.langs.textmate.registry.FileProviderRegistry
+import io.github.rosemoe.sora.langs.textmate.registry.GrammarRegistry
+import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry
+import io.github.rosemoe.sora.langs.textmate.registry.dsl.languages
+import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel
+import io.github.rosemoe.sora.langs.textmate.registry.provider.AssetsFileResolver
+import io.github.rosemoe.sora.widget.CodeEditor
+import org.eclipse.tm4e.core.registry.IThemeSource
+
+
+class CodeEditorHelper(val context: Context, val editor: CodeEditor) {
+ fun initEditor() {
+ FileProviderRegistry.getInstance().addFileProvider(AssetsFileResolver(context.assets))
+
+ val themes = arrayOf(
+ "textmate/quietlight.json",
+ "textmate/solarized_drak.json",
+ "textmate/darcula.json",
+ "textmate/abyss.json"
+ )
+ val themeRegistry = ThemeRegistry.getInstance()
+ for (theme in themes) {
+ themeRegistry.loadTheme(
+ ThemeModel(
+ IThemeSource.fromInputStream(
+ FileProviderRegistry.getInstance().tryGetInputStream(theme), theme, null
+ )
+ )
+ )
+ }
+
+ GrammarRegistry.getInstance().loadGrammars(languages {
+ language("js") {
+ grammar = "textmate/javascript/syntaxes/JavaScript.tmLanguage.json"
+ defaultScopeName()
+ languageConfiguration = "textmate/javascript/language-configuration.json"
+ }
+ })
+ editor.setEditorLanguage(TextMateLanguage.create("source.js", true))
+
+ }
+
+ fun setTheme(theme: CodeEditorTheme) {
+ val themeRegistry = ThemeRegistry.getInstance()
+ when (theme) {
+ CodeEditorTheme.AUTO -> {
+ val isNight =
+ (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
+ setTheme(if (isNight) CodeEditorTheme.DARCULA else CodeEditorTheme.QUIET_LIGHT)
+ return
+ }
+
+ else-> {
+ themeRegistry.setTheme(theme.id)
+ ensureTextmateTheme()
+ return
+ }
+ }
+ }
+
+ private fun ensureTextmateTheme() {
+ var editorColorScheme = editor.colorScheme
+ if (editorColorScheme !is TextMateColorScheme) {
+ editorColorScheme = TextMateColorScheme.create(ThemeRegistry.getInstance())
+ editor.colorScheme = editorColorScheme
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorScreen.kt
new file mode 100644
index 000000000..6c7070811
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorScreen.kt
@@ -0,0 +1,273 @@
+package com.github.jing332.tts_server_android.compose.codeeditor
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.WrapText
+import androidx.compose.material.icons.filled.BugReport
+import androidx.compose.material.icons.filled.ColorLens
+import androidx.compose.material.icons.filled.InsertDriveFile
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Save
+import androidx.compose.material.icons.filled.SettingsRemote
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.CheckedMenuItem
+import com.github.jing332.tts_server_android.compose.widgets.LongClickIconButton
+import com.github.jing332.tts_server_android.conf.CodeEditorConfig
+import com.github.jing332.tts_server_android.ui.AppActivityResultContracts
+import com.github.jing332.tts_server_android.ui.FilePickerActivity
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import com.github.jing332.tts_server_android.utils.clickableRipple
+import io.github.rosemoe.sora.widget.CodeEditor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CodeEditorScreen(
+ title: @Composable () -> Unit,
+ onBack: () -> Unit,
+ onSave: () -> Unit,
+ onLongClickSave: () -> Unit = {},
+ onUpdate: (CodeEditor) -> Unit,
+ onSaveFile: (() -> Pair)?,
+
+ onDebug: () -> Unit,
+ onRemoteAction: (name: String, body: ByteArray?) -> Unit = { _, _ -> },
+
+ vm: CodeEditorViewModel = viewModel(),
+
+ debugIconContent: @Composable () -> Unit = {},
+ onLongClickMore: () -> Unit = {},
+ onLongClickMoreLabel: String? = null,
+ actions: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {},
+) {
+ var codeEditor by remember { mutableStateOf(null) }
+
+ var showThemeDialog by remember { mutableStateOf(false) }
+ if (showThemeDialog)
+ ThemeSettingsDialog { showThemeDialog = false }
+
+ var showRemoteSyncDialog by remember { mutableStateOf(false) }
+ if (showRemoteSyncDialog)
+ RemoteSyncSettings { showRemoteSyncDialog = false }
+
+ val fileSaver =
+ rememberLauncherForActivityResult(AppActivityResultContracts.filePickerActivity()) {
+ }
+
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ LaunchedEffect(vm) {
+ runCatching {
+ scope.launch(Dispatchers.IO) {
+ vm.startSyncServer(
+ port = CodeEditorConfig.remoteSyncPort.value,
+ onPush = { codeEditor?.setText(it) },
+ onPull = { codeEditor?.text.toString() },
+ onDebug = onDebug,
+ onAction = onRemoteAction
+ )
+ }
+ }.onFailure {
+ context.displayErrorDialog(it, context.getString(R.string.remote_sync_service))
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(title = title, navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.nav_back)
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = onDebug) {
+ Icon(
+ Icons.Filled.BugReport,
+ contentDescription = stringResource(id = R.string.nav_back)
+ )
+ debugIconContent()
+ }
+ LongClickIconButton(onClick = onSave, onLongClick = onLongClickSave) {
+ Icon(
+ Icons.Filled.Save,
+ contentDescription = stringResource(id = R.string.save)
+ )
+ }
+
+ var showOptions by remember { mutableStateOf(false) }
+
+ LongClickIconButton(
+ onClick = { showOptions = true },
+ onLongClick = onLongClickMore,
+ onLongClickLabel = onLongClickMoreLabel
+ ) {
+ Icon(Icons.Default.MoreVert, stringResource(id = R.string.more_options))
+
+ DropdownMenu(
+ expanded = showOptions,
+ onDismissRequest = { showOptions = false }) {
+ if (onSaveFile != null)
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.save_as_file)) },
+ onClick = {
+ onSaveFile.invoke().let {
+ fileSaver.launch(
+ FilePickerActivity.RequestSaveFile(
+ fileName = it.first,
+ fileBytes = it.second
+ )
+ )
+ }
+ },
+ leadingIcon = { Icon(Icons.Default.InsertDriveFile, null) }
+ )
+
+ var syncEnabled by remember { CodeEditorConfig.isRemoteSyncEnabled }
+ CheckedMenuItem(
+ text = { Text(stringResource(id = R.string.remote_sync_service)) },
+ checked = syncEnabled,
+ onClick = { showRemoteSyncDialog = true },
+ onClickCheckBox = { syncEnabled = it },
+ leadingIcon = {
+ Icon(Icons.Default.SettingsRemote, null)
+ }
+ )
+
+ HorizontalDivider()
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.theme)) },
+ onClick = { showThemeDialog = true },
+ leadingIcon = { Icon(Icons.Default.ColorLens, null) }
+ )
+
+ var wordWrap by remember { CodeEditorConfig.isWordWrapEnabled }
+ CheckedMenuItem(
+ text = { Text(stringResource(id = R.string.word_wrap)) },
+ checked = wordWrap,
+ onClick = { wordWrap = it },
+ leadingIcon = {
+ Icon(Icons.AutoMirrored.Default.WrapText, null)
+ }
+ )
+
+ actions { showOptions = false }
+ }
+
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ val theme by remember { CodeEditorConfig.theme }
+ LaunchedEffect(codeEditor, theme) {
+ codeEditor?.helper()?.setTheme(theme)
+ }
+
+ val wordWrap by remember { CodeEditorConfig.isWordWrapEnabled }
+ LaunchedEffect(codeEditor, wordWrap) {
+ codeEditor?.isWordwrap = wordWrap
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ CodeEditor(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(), onUpdate = {
+ codeEditor = it
+ onUpdate(it)
+ }
+ )
+
+ val symbolMap = remember {
+ linkedMapOf(
+ "\t" to "TAB",
+ "=" to "=",
+ ">" to ">",
+ "{" to "{",
+ "}" to "}",
+ "(" to "(",
+ ")" to ")",
+ "," to ",",
+ "." to ".",
+ ";" to ";",
+ "'" to "'",
+ "\"" to "\"",
+ "?" to "?",
+ "+" to "+",
+ "-" to "-",
+ "*" to "*",
+ "/" to "/",
+ )
+ }
+
+ HorizontalDivider(thickness = 1.dp)
+ LazyRow(Modifier.background(MaterialTheme.colorScheme.background)) {
+ items(symbolMap.toList()) {
+ Box(
+ Modifier
+ .clickableRipple {
+ codeEditor?.let { editor ->
+ val text = it.second
+ if (editor.isEditable)
+ if ("\t" == text && editor.snippetController.isInSnippet())
+ editor.snippetController.shiftToNextTabStop()
+ else
+ editor.insertText(text, 1)
+ }
+ }) {
+ Text(
+ text = it.second,
+ Modifier
+ .minimumInteractiveComponentSize()
+ .align(Alignment.Center)
+ )
+ }
+ }
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorViewModel.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorViewModel.kt
new file mode 100644
index 000000000..fd7c7d24b
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/CodeEditorViewModel.kt
@@ -0,0 +1,69 @@
+package com.github.jing332.tts_server_android.compose.codeeditor
+
+import android.util.Log
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import com.drake.net.utils.withMain
+import com.github.jing332.tts_server_android.utils.runOnUI
+import kotlinx.coroutines.runBlocking
+import tts_server_lib.ScriptCodeSyncServerCallback
+import tts_server_lib.ScriptSyncServer
+
+class CodeEditorViewModel : ViewModel() {
+ companion object {
+ const val TAG = "CodeEditorViewModel"
+
+ const val SYNC_ACTION_DEBUG = "debug"
+ }
+
+ private var server: ScriptSyncServer? = null
+
+ // 代码同步服务器
+ fun startSyncServer(
+ port: Int,
+ onPush: (code: String) -> Unit,
+ onPull: () -> String,
+ onDebug: () -> Unit,
+ onAction: (name: String, body: ByteArray?) -> Unit
+ ) {
+ if (server != null) return
+ server = ScriptSyncServer()
+ server?.init(object : ScriptCodeSyncServerCallback {
+ override fun log(level: Int, msg: String?) {
+ Log.i(TAG, "$level $msg")
+ }
+
+ override fun action(name: String, body: ByteArray?) {
+ runOnUI {
+ if (name == SYNC_ACTION_DEBUG) {
+ onDebug.invoke()
+ } else
+ onAction.invoke(name, body)
+ }
+ }
+
+ override fun pull(): String = runBlocking {
+ return@runBlocking withMain {
+ return@withMain onPull.invoke()
+ }
+ }
+
+ override fun push(code: String) {
+ runOnUI {
+ onPush.invoke(code)
+ }
+ }
+ })
+ server?.start(port.toLong())
+ }
+
+ private fun closeSyncServer() {
+ server?.close()
+ server = null
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ closeSyncServer()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/LoggerBottomSheet.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/LoggerBottomSheet.kt
new file mode 100644
index 000000000..b224ab7c6
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/LoggerBottomSheet.kt
@@ -0,0 +1,72 @@
+package com.github.jing332.tts_server_android.compose.codeeditor
+
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.compose.widgets.AppBottomSheet
+import com.github.jing332.tts_server_android.constant.LogLevel
+import com.github.jing332.tts_server_android.model.rhino.core.Logger
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LoggerBottomSheet(
+ logger: Logger,
+ onDismissRequest: () -> Unit,
+ onLaunched: () -> Unit
+) {
+ var logText by remember { mutableStateOf(AnnotatedString("")) }
+
+ val listener = remember {
+ Logger.LogListener { text, level ->
+ logText = buildAnnotatedString {
+ append(logText)
+ val color = LogLevel.toColor(level)
+ withStyle(SpanStyle(color = Color(color))) {
+ appendLine(text)
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(logger) {
+ logger.addListener(listener)
+ onLaunched()
+ }
+
+ DisposableEffect(logger) {
+ onDispose {
+ logger.removeListener(listener)
+ }
+ }
+
+ AppBottomSheet(onDismissRequest = onDismissRequest) {
+ SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ Text(
+ logText, modifier = Modifier
+ .fillMaxHeight()
+ .padding(horizontal = 8.dp)
+ .padding(bottom = 8.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/RemoteSyncSettings.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/RemoteSyncSettings.kt
new file mode 100644
index 000000000..c7c669150
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/RemoteSyncSettings.kt
@@ -0,0 +1,63 @@
+package com.github.jing332.tts_server_android.compose.codeeditor
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.DenseOutlinedField
+import com.github.jing332.tts_server_android.conf.CodeEditorConfig
+
+@Composable
+internal fun RemoteSyncSettings(onDismissRequest: () -> Unit) {
+ AppDialog(
+ title = { Text(stringResource(id = R.string.remote_sync_service)) },
+ content = {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ stringResource(id = R.string.remote_sync_service_description),
+ modifier = Modifier.padding(8.dp)
+ )
+
+ var port by remember { CodeEditorConfig.remoteSyncPort }
+ DenseOutlinedField(value = port.toString(), onValueChange = {
+ try {
+ port = it.toInt()
+ } catch (_: NumberFormatException) {
+ }
+ })
+
+ }
+ },
+ onDismissRequest = onDismissRequest,
+ buttons = {
+ val context = LocalContext.current
+ Row {
+ TextButton(onClick = {
+ context.startActivity(Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse("https://github.com/jing332/tts-server-psc")
+ })
+ }) {
+ Text(stringResource(id = R.string.learn_more))
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.close))
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/ThemeSettingsDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/ThemeSettingsDialog.kt
new file mode 100644
index 000000000..90c1984d5
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/codeeditor/ThemeSettingsDialog.kt
@@ -0,0 +1,40 @@
+package com.github.jing332.tts_server_android.compose.codeeditor
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppSelectionDialog
+import com.github.jing332.tts_server_android.conf.CodeEditorConfig
+import com.github.jing332.tts_server_android.constant.CodeEditorTheme
+
+@Composable
+internal fun ThemeSettingsDialog(onDismissRequest: () -> Unit) {
+ AppSelectionDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.theme)) },
+ value = CodeEditorConfig.theme.value,
+ values = CodeEditorTheme.values().toList(),
+ entries = CodeEditorTheme.values().map { it.id.ifBlank { stringResource(id = R.string.theme_default) } },
+ onClick = { value, _ ->
+ CodeEditorConfig.theme.value = value as CodeEditorTheme
+ onDismissRequest()
+ }
+ )
+}
+
+@Preview
+@Composable
+fun PreviewEditorThemeDialog() {
+ var show by remember { mutableStateOf(true) }
+ if (show) {
+ ThemeSettingsDialog {
+ show = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/BasicConfigScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/BasicConfigScreen.kt
new file mode 100644
index 000000000..4d463bb75
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/BasicConfigScreen.kt
@@ -0,0 +1,83 @@
+package com.github.jing332.tts_server_android.compose.forwarder
+
+import android.content.IntentFilter
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.systts.LogScreen
+import com.github.jing332.tts_server_android.compose.widgets.DenseOutlinedField
+import com.github.jing332.tts_server_android.compose.widgets.LocalBroadcastReceiver
+import com.github.jing332.tts_server_android.compose.widgets.SwitchFloatingButton
+import com.github.jing332.tts_server_android.constant.KeyConst
+import com.github.jing332.tts_server_android.constant.LogLevel
+import com.github.jing332.tts_server_android.ui.AppLog
+
+@Suppress("DEPRECATION")
+@Composable
+internal fun BasicConfigScreen(
+ modifier: Modifier,
+ vm: ConfigViewModel,
+ intentFilter: IntentFilter,
+ actionOnLog: String,
+ actionOnClosed: String,
+ actionOnStarting: String,
+ isRunning: Boolean,
+ onRunningChange: (Boolean) -> Unit,
+ switch: () -> Unit,
+ port: Int,
+ onPortChange: (Int) -> Unit
+) {
+ val context = LocalContext.current
+ LocalBroadcastReceiver(intentFilter = intentFilter) { intent ->
+ if (intent == null) return@LocalBroadcastReceiver
+ when (intent.action) {
+ actionOnLog -> {
+ intent.getParcelableExtra(KeyConst.KEY_DATA)?.let { log ->
+ vm.logs.add(log)
+ }
+ }
+
+ actionOnClosed -> {
+ onRunningChange(false)
+ vm.logs.add(AppLog(LogLevel.INFO, "服务已关闭"))
+ }
+
+ actionOnStarting -> {
+ onRunningChange(true)
+ vm.logs.add(AppLog(LogLevel.INFO, "服务已启动"))
+ }
+ }
+ }
+
+ Column(modifier) {
+ LogScreen(
+ modifier = Modifier.weight(1f), list = vm.logs, vm.logState
+ )
+
+ Row(Modifier.align(Alignment.CenterHorizontally)) {
+ DenseOutlinedField(
+ label = { Text(stringResource(id = R.string.listen_port)) },
+ modifier = Modifier.align(Alignment.CenterVertically),
+ value = port.toString(), onValueChange = {
+ kotlin.runCatching {
+ onPortChange(it.toInt())
+ }
+ }
+ )
+
+ SwitchFloatingButton(
+ modifier = Modifier.padding(8.dp),
+ switch = isRunning,
+ onSwitchChange = { switch() }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/BasicForwarderScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/BasicForwarderScreen.kt
new file mode 100644
index 000000000..c9c222413
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/BasicForwarderScreen.kt
@@ -0,0 +1,88 @@
+package com.github.jing332.tts_server_android.compose.forwarder
+
+import android.content.IntentFilter
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.TextSnippet
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.LocalBroadcastReceiver
+import com.github.jing332.tts_server_android.service.forwarder.system.SysTtsForwarderService
+import com.google.accompanist.web.rememberWebViewNavigator
+import com.google.accompanist.web.rememberWebViewState
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
+@Composable
+internal fun BasicForwarderScreen(
+ topBar: @Composable () -> Unit,
+ configScreen: @Composable () -> Unit,
+ onGetUrl: () -> String,
+) {
+ val pages = remember { listOf(R.string.log, R.string.web) }
+ val state = rememberPagerState { pages.size }
+ val scope = rememberCoroutineScope()
+ Scaffold(
+ topBar = topBar,
+ bottomBar = {
+ NavigationBar {
+ pages.forEachIndexed { index, strId ->
+ val selected = state.currentPage == index
+ NavigationBarItem(
+ selected = selected,
+ onClick = {
+ scope.launch {
+ state.animateScrollToPage(index)
+ }
+ },
+ icon = {
+ if (index == 0)
+ Icon(Icons.Default.TextSnippet, null)
+ else
+ Icon(painter = painterResource(R.drawable.ic_web), null)
+ },
+ label = { Text(stringResource(id = strId)) }
+ )
+ }
+ }
+ }) { paddingValues ->
+ HorizontalPager(
+ modifier = Modifier.padding(paddingValues).fillMaxSize(),
+ state = state,
+ userScrollEnabled = false
+ ) {
+ when (it) {
+ 0 -> configScreen()
+ 1 -> {
+ val webState = rememberWebViewState(url = onGetUrl())
+ val navigator = rememberWebViewNavigator()
+ LocalBroadcastReceiver(intentFilter = IntentFilter(SysTtsForwarderService.ACTION_ON_STARTING)) {
+ navigator.loadUrl(onGetUrl())
+ }
+
+ WebScreen(
+ modifier = Modifier.fillMaxSize(),
+ state = webState,
+ navigator = navigator
+ )
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ConfigViewModel.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ConfigViewModel.kt
new file mode 100644
index 000000000..b0d034f50
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ConfigViewModel.kt
@@ -0,0 +1,11 @@
+package com.github.jing332.tts_server_android.compose.forwarder
+
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.mutableStateListOf
+import androidx.lifecycle.ViewModel
+import com.github.jing332.tts_server_android.ui.AppLog
+
+class ConfigViewModel : ViewModel() {
+ val logs = mutableStateListOf()
+ val logState by lazy { LazyListState() }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ForwarderTopAppBar.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ForwarderTopAppBar.kt
new file mode 100644
index 000000000..a9bd22292
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ForwarderTopAppBar.kt
@@ -0,0 +1,118 @@
+package com.github.jing332.tts_server_android.compose.forwarder
+
+import android.content.Intent
+import android.webkit.CookieManager
+import android.webkit.WebStorage
+import android.webkit.WebView
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AddBusiness
+import androidx.compose.material.icons.filled.CleaningServices
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.core.net.toUri
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.nav.NavTopAppBar
+import com.github.jing332.tts_server_android.compose.widgets.CheckedMenuItem
+import com.github.jing332.tts_server_android.utils.toast
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun ForwarderTopAppBar(
+ title: @Composable () -> Unit,
+ wakeLockEnabled: Boolean,
+ onWakeLockEnabledChange: (Boolean) -> Unit,
+
+ actions: @Composable ColumnScope.(onDismissRequest: () -> Unit) -> Unit = { },
+ onClearWebData: (() -> Unit)? = null,
+ onOpenWeb: () -> String,
+ onAddDesktopShortCut: () -> Unit,
+) {
+ val context = LocalContext.current
+ NavTopAppBar(
+ title = title,
+ actions = {
+ IconButton(onClick = {
+ val url = onOpenWeb.invoke()
+ if (url.isNotEmpty()) {
+ context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
+ }
+ }) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_web),
+ contentDescription = stringResource(
+ id = R.string.open_web
+ )
+ )
+ }
+
+ var showOptions by remember { mutableStateOf(false) }
+
+ IconButton(onClick = { showOptions = true }) {
+ Icon(Icons.Default.MoreVert, stringResource(id = R.string.more_options))
+
+ DropdownMenu(expanded = showOptions, onDismissRequest = { showOptions = false }) {
+ CheckedMenuItem(
+ text = { Text(text = stringResource(id = R.string.wake_lock)) },
+ checked = wakeLockEnabled,
+ onClick = onWakeLockEnabledChange,
+ leadingIcon = {
+ Icon(Icons.Default.Lock, null)
+ }
+ )
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.clear_web_data)) },
+ onClick = {
+ showOptions = false
+ if (onClearWebData == null) {
+ WebView(context).apply {
+ clearCache(true)
+ clearFormData()
+ clearSslPreferences()
+ }
+ CookieManager.getInstance().apply {
+ removeAllCookies(null)
+ flush()
+ }
+ WebStorage.getInstance().deleteAllData()
+ context.toast(R.string.cleared)
+ } else
+ onClearWebData.invoke()
+ },
+ leadingIcon = {
+ Icon(Icons.Default.CleaningServices, null)
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.desktop_shortcut)) },
+ onClick = {
+ showOptions = false
+ onAddDesktopShortCut()
+ },
+ leadingIcon = {
+ Icon(Icons.Default.AddBusiness, null)
+ }
+ )
+
+ actions { showOptions = false }
+ }
+ }
+
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/WebScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/WebScreen.kt
new file mode 100644
index 000000000..4626d074d
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/WebScreen.kt
@@ -0,0 +1,168 @@
+package com.github.jing332.tts_server_android.compose.forwarder
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.webkit.JsResult
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.pullrefresh.PullRefreshIndicator
+import androidx.compose.material3.pullrefresh.pullRefresh
+import androidx.compose.material3.pullrefresh.rememberPullRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.utils.longToast
+import com.google.accompanist.web.AccompanistWebChromeClient
+import com.google.accompanist.web.AccompanistWebViewClient
+import com.google.accompanist.web.LoadingState
+import com.google.accompanist.web.WebView
+import com.google.accompanist.web.WebViewNavigator
+import com.google.accompanist.web.WebViewState
+import com.google.accompanist.web.rememberWebViewNavigator
+import com.google.accompanist.web.rememberWebViewState
+
+@SuppressLint("SetJavaScriptEnabled")
+@Composable
+internal fun WebScreen(
+ modifier: Modifier,
+ url: String = "",
+ state: WebViewState = rememberWebViewState(url),
+ navigator: WebViewNavigator = rememberWebViewNavigator(),
+) {
+ var showAlertDialog by remember { mutableStateOf?>(null) }
+ if (showAlertDialog != null) {
+ val webUrl = showAlertDialog!!.first
+ val msg = showAlertDialog!!.second
+ val result = showAlertDialog!!.third
+ AlertDialog(onDismissRequest = {
+ result.cancel()
+ showAlertDialog = null
+ },
+ title = { Text(webUrl) },
+ text = { Text(msg) },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ result.confirm()
+ showAlertDialog = null
+ }) {
+ Text(stringResource(id = android.R.string.ok))
+ }
+ }, dismissButton = {
+ result.cancel()
+ showAlertDialog = null
+ })
+ }
+
+ val context = LocalContext.current
+ val chromeClient = remember {
+ object : AccompanistWebChromeClient() {
+ override fun onJsConfirm(
+ view: WebView?,
+ url: String?,
+ message: String?,
+ result: JsResult?
+ ): Boolean {
+ if (result == null) return false
+ showAlertDialog = Triple(url ?: "", message ?: "", result)
+ return true
+ }
+
+ override fun onJsAlert(
+ view: WebView?,
+ url: String?,
+ message: String?,
+ result: JsResult?
+ ): Boolean {
+ if (result == null) return false
+ showAlertDialog = Triple(url ?: "", message ?: "", result)
+
+ return true
+ }
+ }
+ }
+
+ val client = remember {
+ object : AccompanistWebViewClient() {
+ override fun shouldOverrideUrlLoading(
+ view: WebView?,
+ request: WebResourceRequest?
+ ): Boolean {
+ kotlin.runCatching {
+ if (request?.url?.scheme?.startsWith("http") == false) {
+ val intent = Intent(Intent.ACTION_VIEW, request.url)
+ context.startActivity(Intent.createChooser(intent, request.url.toString()))
+ return true
+ }
+ }.onFailure {
+ context.longToast("跳转APP失败: ${request?.url}")
+ }
+
+ return super.shouldOverrideUrlLoading(view, request)
+ }
+ }
+ }
+
+ Column(modifier = modifier) {
+ val process =
+ if (state.loadingState is LoadingState.Loading) (state.loadingState as LoadingState.Loading).progress else 0f
+
+ if (process > 0)
+ LinearProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ progress = process
+ )
+
+ var lastTitle by remember { mutableStateOf("") }
+ val refreshState = rememberPullRefreshState(refreshing = state.isLoading, onRefresh = {
+ navigator.reload()
+ })
+ Text(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .pullRefresh(refreshState),
+ text = state.pageTitle?.apply { lastTitle = this } ?: lastTitle,
+ maxLines = 1,
+ style = MaterialTheme.typography.titleMedium,
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ WebView(
+ modifier = Modifier.fillMaxSize(),
+ state = state,
+ navigator = navigator,
+ onCreated = {
+ it.settings.javaScriptEnabled = true
+ },
+ client = client,
+ chromeClient = chromeClient,
+ )
+
+ Column(Modifier.fillMaxWidth()) {
+ PullRefreshIndicator(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ refreshing = refreshState.refreshing,
+ state = refreshState
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ms/MsTtsForwarderScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ms/MsTtsForwarderScreen.kt
new file mode 100644
index 000000000..249a833ff
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/ms/MsTtsForwarderScreen.kt
@@ -0,0 +1,102 @@
+package com.github.jing332.tts_server_android.compose.forwarder.ms
+
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Token
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.forwarder.BasicConfigScreen
+import com.github.jing332.tts_server_android.compose.forwarder.BasicForwarderScreen
+import com.github.jing332.tts_server_android.compose.forwarder.ConfigViewModel
+import com.github.jing332.tts_server_android.compose.forwarder.ForwarderTopAppBar
+import com.github.jing332.tts_server_android.compose.widgets.TextFieldDialog
+import com.github.jing332.tts_server_android.conf.MsForwarderConfig
+import com.github.jing332.tts_server_android.service.forwarder.ForwarderServiceManager.switchMsTtsForwarder
+import com.github.jing332.tts_server_android.service.forwarder.ms.MsTtsForwarderService
+import com.github.jing332.tts_server_android.ui.forwarder.MsForwarderSwitchActivity
+import com.github.jing332.tts_server_android.utils.MyTools
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MsTtsForwarderScreen(
+ cfgVM: ConfigViewModel = viewModel()
+) {
+ var showTokenDialog by remember { mutableStateOf(false) }
+ if (showTokenDialog) {
+ var text by remember { mutableStateOf(MsForwarderConfig.token.value) }
+ TextFieldDialog(
+ title = stringResource(id = R.string.server_set_token),
+ text = text,
+ onTextChange = { text = it },
+ onDismissRequest = { showTokenDialog = false }) {
+ MsForwarderConfig.token.value = text
+ }
+ }
+
+ var wakeLockEnabled by remember { MsForwarderConfig.isWakeLockEnabled }
+ var port by remember { MsForwarderConfig.port }
+ val context = LocalContext.current
+ BasicForwarderScreen(topBar = {
+ ForwarderTopAppBar(
+ title = { Text(stringResource(id = R.string.forwarder_ms)) },
+ wakeLockEnabled = wakeLockEnabled,
+ onWakeLockEnabledChange = { wakeLockEnabled = it },
+ onOpenWeb = { "http://localhost:${port}" },
+ onAddDesktopShortCut = {
+ MyTools.addShortcut(
+ ctx = context,
+ name = context.getString(R.string.forwarder_ms),
+ id = "switch_ms_forwarder",
+ iconResId = R.mipmap.ic_app_launcher_round,
+ launcherIntent = Intent(context, MsForwarderSwitchActivity::class.java)
+ )
+ },
+ actions = {
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.server_set_token)) },
+ onClick = { showTokenDialog = true },
+ leadingIcon = {
+ Icon(Icons.Default.Token, null)
+ }
+ )
+ }
+ )
+
+ },
+ configScreen = {
+ var isRunning by remember { mutableStateOf(MsTtsForwarderService.isRunning) }
+ BasicConfigScreen(modifier = Modifier.fillMaxSize(),
+ vm = cfgVM,
+ intentFilter = IntentFilter().apply {
+ addAction(MsTtsForwarderService.ACTION_ON_LOG)
+ addAction(MsTtsForwarderService.ACTION_ON_CLOSED)
+ addAction(MsTtsForwarderService.ACTION_ON_STARTING)
+ },
+ actionOnLog = MsTtsForwarderService.ACTION_ON_LOG,
+ actionOnClosed = MsTtsForwarderService.ACTION_ON_CLOSED,
+ actionOnStarting = MsTtsForwarderService.ACTION_ON_STARTING,
+ isRunning = isRunning,
+ onRunningChange = { isRunning = it },
+ switch = { context.switchMsTtsForwarder() },
+ port = port,
+ onPortChange = { port = it })
+ }) {
+
+ "http://localhost:${port}"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/systts/SystemTtsForwarderScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/systts/SystemTtsForwarderScreen.kt
new file mode 100644
index 000000000..8f8a15755
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/forwarder/systts/SystemTtsForwarderScreen.kt
@@ -0,0 +1,71 @@
+package com.github.jing332.tts_server_android.compose.forwarder.systts
+
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.forwarder.BasicConfigScreen
+import com.github.jing332.tts_server_android.compose.forwarder.BasicForwarderScreen
+import com.github.jing332.tts_server_android.compose.forwarder.ConfigViewModel
+import com.github.jing332.tts_server_android.compose.forwarder.ForwarderTopAppBar
+import com.github.jing332.tts_server_android.conf.SysttsForwarderConfig
+import com.github.jing332.tts_server_android.service.forwarder.ForwarderServiceManager.switchSysTtsForwarder
+import com.github.jing332.tts_server_android.service.forwarder.system.SysTtsForwarderService
+import com.github.jing332.tts_server_android.ui.forwarder.SystemForwarderSwitchActivity
+import com.github.jing332.tts_server_android.utils.MyTools
+
+@Composable
+fun SystemTtsForwarderScreen(cfgVM: ConfigViewModel = viewModel()) {
+ val context = LocalContext.current
+ var port by remember { SysttsForwarderConfig.port }
+ BasicForwarderScreen(
+ topBar = {
+ var wakeLockEnabled by remember { SysttsForwarderConfig.isWakeLockEnabled }
+ ForwarderTopAppBar(
+ title = { Text(text = stringResource(id = R.string.forwarder_systts)) },
+ wakeLockEnabled = wakeLockEnabled,
+ onWakeLockEnabledChange = { wakeLockEnabled = it },
+ onOpenWeb = { "http://localhost:${port}" }
+ ) {
+ MyTools.addShortcut(
+ ctx = context,
+ name = context.getString(R.string.forwarder_systts),
+ id = "switch_systts_forwarder",
+ iconResId = R.mipmap.ic_app_launcher_round,
+ launcherIntent = Intent(context, SystemForwarderSwitchActivity::class.java)
+ )
+ }
+ },
+ configScreen = {
+ var isRunning by remember { mutableStateOf(SysTtsForwarderService.isRunning) }
+ BasicConfigScreen(
+ modifier = Modifier.fillMaxSize(),
+ vm = cfgVM,
+ intentFilter = IntentFilter().apply {
+ addAction(SysTtsForwarderService.ACTION_ON_LOG)
+ addAction(SysTtsForwarderService.ACTION_ON_CLOSED)
+ addAction(SysTtsForwarderService.ACTION_ON_STARTING)
+ },
+ actionOnLog = SysTtsForwarderService.ACTION_ON_LOG,
+ actionOnClosed = SysTtsForwarderService.ACTION_ON_CLOSED,
+ actionOnStarting = SysTtsForwarderService.ACTION_ON_STARTING,
+ isRunning = isRunning,
+ onRunningChange = { isRunning = it },
+ switch = { context.switchSysTtsForwarder() },
+ port = port,
+ onPortChange = { port = it }
+ )
+ }) {
+ "http://localhost:${port}"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/nav/NavRoutes.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/nav/NavRoutes.kt
new file mode 100644
index 000000000..eb48f9f71
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/nav/NavRoutes.kt
@@ -0,0 +1,51 @@
+package com.github.jing332.tts_server_android.compose.nav
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+
+sealed class NavRoutes(
+ val id: String,
+ @StringRes val strId: Int,
+ val icon: @Composable () -> Unit = {},
+) {
+ companion object {
+ val routes by lazy {
+ listOf(
+ SystemTTS,
+ SystemTtsForwarder,
+ MsTtsForwarder,
+ Settings
+ )
+ }
+ }
+
+ data object SystemTTS : NavRoutes("system_tts", R.string.system_tts, icon = {
+ Icon(modifier = Modifier.size(24.dp), painter = painterResource(id = R.drawable.ic_tts), contentDescription = null)
+ })
+
+ data object SystemTtsForwarder :
+ NavRoutes("system_tts_forwarder", R.string.forwarder_systts, icon = {
+ Icon(modifier = Modifier.size(24.dp), painter = painterResource(id = R.drawable.ic_tts), contentDescription = null)
+ })
+
+ data object MsTtsForwarder : NavRoutes("ms_tts_forwarder", R.string.forwarder_ms, icon = {
+ Icon(painter = painterResource(id = R.drawable.ic_microsoft), null)
+ })
+
+ data object Settings : NavRoutes("settings", R.string.settings, icon = {
+ Icon(Icons.Default.Settings, null)
+ })
+
+ // =============
+ data object TtsEdit : NavRoutes("tts_edit", 0) {
+ const val DATA = "data"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/nav/NavTopAppBar.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/nav/NavTopAppBar.kt
new file mode 100644
index 000000000..a21bee24f
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/nav/NavTopAppBar.kt
@@ -0,0 +1,60 @@
+package com.github.jing332.tts_server_android.compose.nav
+
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.RichTooltip
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarColors
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.LocalDrawerState
+import com.github.jing332.tts_server_android.compose.widgets.AppTooltip
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NavTopAppBar(
+ modifier: Modifier = Modifier,
+ title: @Composable () -> Unit,
+ drawerState: DrawerState = LocalDrawerState.current,
+ navigationIcon: @Composable() (() -> Unit)? = null,
+ actions: @Composable RowScope.() -> Unit = {},
+ windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
+ colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
+ scrollBehavior: TopAppBarScrollBehavior? = null
+) {
+ val scope = rememberCoroutineScope()
+ TopAppBar(
+ title = title,
+ modifier = modifier,
+ navigationIcon = {
+ if (navigationIcon == null) {
+ AppTooltip(tooltip = stringResource(id = R.string.nav_app_bar_open_drawer_description)) {
+ IconButton(onClick = {
+ scope.launch { drawerState.open() }
+ }) {
+ Icon(Icons.Default.Menu, it)
+ }
+ }
+ } else
+ navigationIcon()
+ },
+ actions = actions,
+ windowInsets = windowInsets,
+ colors = colors,
+ scrollBehavior = scrollBehavior
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/SettingsScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/SettingsScreen.kt
new file mode 100644
index 000000000..7f5e1a0f3
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/SettingsScreen.kt
@@ -0,0 +1,393 @@
+package com.github.jing332.tts_server_android.compose.settings
+
+import android.content.Intent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.MenuOpen
+import androidx.compose.material.icons.automirrored.filled.TextSnippet
+import androidx.compose.material.icons.filled.AccessTime
+import androidx.compose.material.icons.filled.ArrowCircleUp
+import androidx.compose.material.icons.filled.Audiotrack
+import androidx.compose.material.icons.filled.ColorLens
+import androidx.compose.material.icons.filled.FileOpen
+import androidx.compose.material.icons.filled.Groups
+import androidx.compose.material.icons.filled.Headset
+import androidx.compose.material.icons.filled.Language
+import androidx.compose.material.icons.filled.Link
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.NotificationsNone
+import androidx.compose.material.icons.filled.PlayCircleOutline
+import androidx.compose.material.icons.filled.Repeat
+import androidx.compose.material.icons.filled.SelectAll
+import androidx.compose.material.icons.filled.SettingsBackupRestore
+import androidx.compose.material.icons.filled.Tag
+import androidx.compose.material.icons.filled.TextFields
+import androidx.compose.material.icons.filled.TextSnippet
+import androidx.compose.material.icons.filled.Waves
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.AppLocale
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.app
+import com.github.jing332.tts_server_android.compose.backup.BackupRestoreActivity
+import com.github.jing332.tts_server_android.compose.nav.NavTopAppBar
+import com.github.jing332.tts_server_android.compose.systts.directlink.LinkUploadRuleActivity
+import com.github.jing332.tts_server_android.compose.theme.getAppTheme
+import com.github.jing332.tts_server_android.compose.theme.setAppTheme
+import com.github.jing332.tts_server_android.conf.AppConfig
+import com.github.jing332.tts_server_android.conf.SystemTtsConfig
+import com.github.jing332.tts_server_android.constant.FilePickerMode
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsScreen(drawerState: DrawerState) {
+ var showThemeDialog by remember { mutableStateOf(false) }
+ if (showThemeDialog)
+ ThemeSelectionDialog(
+ onDismissRequest = { showThemeDialog = false },
+ currentTheme = getAppTheme(),
+ onChangeTheme = {
+ setAppTheme(it)
+ }
+ )
+
+ Scaffold(
+ topBar = {
+ NavTopAppBar(
+ title = { Text(stringResource(R.string.settings)) },
+ drawerState = drawerState
+ )
+ }
+ ) { paddingValues ->
+ val context = LocalContext.current
+ Column(
+ Modifier
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ ) {
+ DividerPreference { Text(stringResource(id = R.string.app_name)) }
+
+ BasePreferenceWidget(
+ icon = {
+ Icon(Icons.Default.SettingsBackupRestore, null)
+ },
+ onClick = {
+ context.startActivity(
+ Intent(
+ context,
+ BackupRestoreActivity::class.java
+ ).apply { action = Intent.ACTION_VIEW })
+ },
+ title = { Text(stringResource(id = R.string.backup_restore)) },
+ )
+
+ BasePreferenceWidget(
+ icon = {
+ Icon(Icons.Default.Link, null)
+ },
+ onClick = {
+ context.startActivity(
+ Intent(
+ context, LinkUploadRuleActivity::class.java
+ ).apply { action = Intent.ACTION_VIEW })
+ },
+ title = { Text(stringResource(id = R.string.direct_link_settings)) },
+ )
+
+ BasePreferenceWidget(
+ icon = { Icon(Icons.Default.ColorLens, null) },
+ onClick = { showThemeDialog = true },
+ title = { Text(stringResource(id = R.string.theme)) },
+ subTitle = { Text(stringResource(id = getAppTheme().stringResId)) },
+ )
+
+ val languageKeys = remember {
+ mutableListOf("").apply { addAll(AppLocale.localeMap.keys.toList()) }
+ }
+
+ val languageNames = remember {
+ AppLocale.localeMap.map { "${it.value.displayName} - ${it.value.getDisplayName(it.value)}" }
+ .toMutableList()
+ .apply { add(0, context.getString(R.string.follow_system)) }
+ }
+
+ var langMenu by remember { mutableStateOf(false) }
+ DropdownPreference(
+ Modifier.minimumInteractiveComponentSize(),
+ expanded = langMenu,
+ onExpandedChange = { langMenu = it },
+ icon = {
+ Icon(Icons.Default.Language, null)
+ },
+ title = { Text(stringResource(id = R.string.language)) },
+ subTitle = {
+ Text(
+ if (AppLocale.getLocaleCodeFromFile(context).isEmpty()) {
+ stringResource(id = R.string.follow_system)
+ } else {
+ AppLocale.getLocaleFromFile(context).displayName
+ }
+ )
+ }) {
+ languageNames.forEachIndexed { index, name ->
+ DropdownMenuItem(
+ text = {
+ Text(name)
+ }, onClick = {
+ langMenu = false
+
+ AppLocale.saveLocaleCodeToFile(context, languageKeys[index])
+ AppLocale.setLocale(app)
+ }
+ )
+ }
+ }
+
+ var filePickerMode by remember { AppConfig.filePickerMode }
+ var expanded by remember { mutableStateOf(false) }
+ DropdownPreference(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ icon = { Icon(Icons.Default.FileOpen, null) },
+ title = { Text(stringResource(id = R.string.file_picker_mode)) },
+ subTitle = {
+ Text(
+ when (filePickerMode) {
+ FilePickerMode.PROMPT -> stringResource(id = R.string.file_picker_mode_prompt)
+ FilePickerMode.BUILTIN -> stringResource(id = R.string.file_picker_mode_builtin)
+ else -> stringResource(id = R.string.file_picker_mode_system)
+ }
+ )
+ },
+ actions = {
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.file_picker_mode_prompt)) },
+ onClick = {
+ expanded = false
+ filePickerMode = FilePickerMode.PROMPT
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.file_picker_mode_builtin)) },
+ onClick = {
+ expanded = false
+ filePickerMode = FilePickerMode.BUILTIN
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.file_picker_mode_system)) },
+ onClick = {
+ expanded = false
+ filePickerMode = FilePickerMode.SYSTEM
+ }
+ )
+ }
+ )
+
+ var autoCheck by remember { AppConfig.isAutoCheckUpdateEnabled }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.auto_check_update)) },
+ subTitle = { Text(stringResource(id = R.string.check_update_summary)) },
+ checked = autoCheck,
+ onCheckedChange = { autoCheck = it },
+ icon = {
+ Icon(Icons.Default.ArrowCircleUp, contentDescription = null)
+ }
+ )
+
+ var maxDropdownCount by remember { AppConfig.spinnerMaxDropDownCount }
+ SliderPreference(
+ title = { Text(stringResource(id = R.string.spinner_drop_down_max_count)) },
+ subTitle = { Text(stringResource(id = R.string.spinner_drop_down_max_count_summary)) },
+ value = maxDropdownCount.toFloat(),
+ onValueChange = {
+ maxDropdownCount = it.toInt()
+ },
+ label = if (maxDropdownCount == 0) stringResource(id = R.string.unlimited) else maxDropdownCount.toString(),
+ valueRange = 0f..50f,
+ icon = { Icon(Icons.AutoMirrored.Filled.MenuOpen, null) }
+ )
+
+ DividerPreference {
+ Text(stringResource(id = R.string.system_tts))
+ }
+
+ var useExoDecoder by remember { SystemTtsConfig.isExoDecoderEnabled }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.use_exo_decoder)) },
+ subTitle = { Text(stringResource(id = R.string.use_exo_decoder_summary)) },
+ checked = useExoDecoder,
+ onCheckedChange = { useExoDecoder = it },
+ icon = { Icon(Icons.Default.PlayCircleOutline, null) }
+ )
+
+ var streamPlay by remember { SystemTtsConfig.isStreamPlayModeEnabled }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.stream_audio_mode)) },
+ subTitle = { Text(stringResource(id = R.string.stream_audio_mode_summary)) },
+ checked = streamPlay,
+ onCheckedChange = { streamPlay = it },
+ icon = { Icon(Icons.Default.Waves, null) }
+ )
+
+ var skipSilentText by remember { SystemTtsConfig.isSkipSilentText }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.skip_request_silent_text)) },
+ subTitle = { Text(stringResource(id = R.string.skip_request_silent_text_summary)) },
+ checked = skipSilentText,
+ onCheckedChange = { skipSilentText = it },
+ icon = { Icon(Icons.AutoMirrored.Filled.TextSnippet, null) }
+ )
+
+ var foregroundService by remember { SystemTtsConfig.isForegroundServiceEnabled }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.foreground_service_and_notification)) },
+ subTitle = { Text(stringResource(id = R.string.foreground_service_and_notification_summary)) },
+ checked = foregroundService,
+ onCheckedChange = { foregroundService = it },
+ icon = { Icon(Icons.Default.NotificationsNone, null) }
+ )
+
+ var wakeLock by remember { SystemTtsConfig.isWakeLockEnabled }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.wake_lock)) },
+ subTitle = { Text(stringResource(id = R.string.wake_lock_summary)) },
+ checked = wakeLock,
+ onCheckedChange = { wakeLock = it },
+ icon = { Icon(Icons.Default.Lock, null) }
+ )
+
+ var maxRetry by remember { SystemTtsConfig.maxRetryCount }
+ val maxRetryValue =
+ if (maxRetry == 0) stringResource(id = R.string.no_retries) else maxRetry.toString()
+ SliderPreference(
+ title = { Text(stringResource(id = R.string.max_retry_count)) },
+ subTitle = { Text(stringResource(id = R.string.max_retry_count_summary)) },
+ value = maxRetry.toFloat(),
+ onValueChange = { maxRetry = it.toInt() },
+ valueRange = 0f..10f,
+ icon = { Icon(Icons.Default.Repeat, null) },
+ label = maxRetryValue,
+ )
+
+ var emptyAudioCount by remember { SystemTtsConfig.maxEmptyAudioRetryCount }
+ val emptyAudioCountValue =
+ if (emptyAudioCount == 0) stringResource(id = R.string.no_retries) else emptyAudioCount.toString()
+ SliderPreference(
+ title = { Text(stringResource(id = R.string.retry_count_when_audio_empty)) },
+ subTitle = { Text(stringResource(id = R.string.retry_count_when_audio_empty_summary)) },
+ value = emptyAudioCount.toFloat(),
+ onValueChange = { emptyAudioCount = it.toInt() },
+ valueRange = 0f..10f,
+ icon = { Icon(Icons.Default.Audiotrack, null) },
+ label = emptyAudioCountValue
+ )
+
+ var standbyTriggeredIndex by remember { SystemTtsConfig.standbyTriggeredRetryIndex }
+ val standbyTriggeredIndexValue = standbyTriggeredIndex.toString()
+ SliderPreference(
+ title = { Text(stringResource(id = R.string.systts_standby_triggered_retry_index)) },
+ subTitle = { Text(stringResource(id = R.string.systts_standby_triggered_retry_index_summary)) },
+ value = standbyTriggeredIndex.toFloat(),
+ onValueChange = { standbyTriggeredIndex = it.toInt() },
+ valueRange = 0f..10f,
+ icon = { Icon(Icons.Default.Repeat, null) },
+ label = standbyTriggeredIndexValue
+ )
+
+
+ var requestTimeout by remember { SystemTtsConfig.requestTimeout }
+ val requestTimeoutValue = "${requestTimeout / 1000}s"
+ SliderPreference(
+ title = { Text(stringResource(id = R.string.request_timeout)) },
+ subTitle = { Text(stringResource(id = R.string.request_timeout_summary)) },
+ value = (requestTimeout / 1000).toFloat(),
+ onValueChange = { requestTimeout = it.toInt() * 1000 },
+ valueRange = 1f..30f,
+ icon = { Icon(Icons.Default.AccessTime, null) },
+ label = requestTimeoutValue
+ )
+
+ DividerPreference {
+ Text(stringResource(id = R.string.systts_interface_preference))
+ }
+
+ var limitTagLen by remember { AppConfig.limitTagLength }
+ val limitTagLenString =
+ if (limitTagLen == 0) stringResource(id = R.string.unlimited) else limitTagLen.toString()
+ SliderPreference(
+ title = { Text(stringResource(id = R.string.limit_tag_length)) },
+ subTitle = { Text(stringResource(id = R.string.limit_tag_length_summary)) },
+ value = limitTagLen.toFloat(),
+ onValueChange = { limitTagLen = it.toInt() },
+ valueRange = 0f..50f,
+ icon = { Icon(Icons.Default.Tag, null) },
+ label = limitTagLenString
+ )
+
+ var limitNameLen by remember { AppConfig.limitNameLength }
+ val limitNameLenString =
+ if (limitNameLen == 0) stringResource(id = R.string.unlimited) else limitNameLen.toString()
+ SliderPreference(
+ title = { Text(stringResource(id = R.string.limit_name_length)) },
+ subTitle = { Text(stringResource(id = R.string.limit_name_length_summary)) },
+ value = limitNameLen.toFloat(),
+ onValueChange = { limitNameLen = it.toInt() },
+ valueRange = 0f..50f,
+ icon = { Icon(Icons.Default.TextFields, null) },
+ label = limitNameLenString
+ )
+
+ var wrapButton by remember { AppConfig.isSwapListenAndEditButton }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.pref_swap_listen_and_edit_button)) },
+ subTitle = {},
+ checked = wrapButton,
+ onCheckedChange = { wrapButton = it },
+ icon = {
+ Icon(Icons.Default.Headset, contentDescription = null)
+ }
+ )
+
+ var targetMultiple by remember { SystemTtsConfig.isVoiceMultipleEnabled }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.voice_multiple_option)) },
+ subTitle = { Text(stringResource(id = R.string.voice_multiple_summary)) },
+ checked = targetMultiple,
+ onCheckedChange = { targetMultiple = it },
+ icon = {
+ Icon(Icons.Default.SelectAll, contentDescription = null)
+ }
+ )
+
+ var groupMultiple by remember { SystemTtsConfig.isGroupMultipleEnabled }
+ SwitchPreference(
+ title = { Text(stringResource(id = R.string.groups_multiple)) },
+ subTitle = { Text(stringResource(id = R.string.groups_multiple_summary)) },
+ checked = groupMultiple,
+ onCheckedChange = { groupMultiple = it },
+ icon = {
+ Icon(Icons.Default.Groups, contentDescription = null)
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/SettingsWidgets.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/SettingsWidgets.kt
new file mode 100644
index 000000000..ab143eb39
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/SettingsWidgets.kt
@@ -0,0 +1,226 @@
+package com.github.jing332.tts_server_android.compose.settings
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.LabelSlider
+
+@Composable
+internal fun DropdownPreference(
+ modifier: Modifier = Modifier,
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+ icon: @Composable () -> Unit,
+ title: @Composable () -> Unit,
+ subTitle: @Composable () -> Unit,
+ actions: @Composable ColumnScope. () -> Unit = {}
+) {
+ BasePreferenceWidget(modifier = modifier, icon = icon, onClick = {
+ onExpandedChange(true)
+ }, title = title, subTitle = subTitle) {
+ DropdownMenu(
+ modifier = Modifier.align(Alignment.Top),
+ expanded = expanded,
+ onDismissRequest = { onExpandedChange(false) }) {
+ actions()
+ }
+ }
+}
+
+@Composable
+internal fun DividerPreference(title: @Composable () -> Unit) {
+ Column(Modifier.padding(top = 4.dp)) {
+ HorizontalDivider(thickness = 0.5.dp)
+ Row(
+ Modifier
+ .padding(vertical = 8.dp)
+ .align(Alignment.CenterHorizontally)
+ ) {
+ CompositionLocalProvider(
+ LocalTextStyle provides MaterialTheme.typography.titleMedium.copy(
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold
+ ),
+ ) {
+ title()
+ }
+ }
+ }
+
+}
+
+@Composable
+internal fun SwitchPreference(
+ modifier: Modifier = Modifier,
+ title: @Composable () -> Unit,
+ subTitle: @Composable () -> Unit,
+ icon: @Composable () -> Unit = {},
+
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit
+) {
+ BasePreferenceWidget(
+ modifier = modifier.semantics(mergeDescendants = true) {
+ role = Role.Switch
+ },
+ onClick = { onCheckedChange(!checked) },
+ title = title,
+ subTitle = subTitle,
+ icon = icon,
+ content = {
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
+ )
+}
+
+@Composable
+internal fun BasePreferenceWidget(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ title: @Composable () -> Unit,
+ subTitle: @Composable () -> Unit = {},
+ icon: @Composable () -> Unit = {},
+ content: @Composable RowScope.() -> Unit = {},
+) {
+ Row(modifier = modifier
+ .minimumInteractiveComponentSize()
+ .defaultMinSize(minHeight = 64.dp)
+ .clip(MaterialTheme.shapes.extraSmall)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple()
+ ) {
+ onClick()
+ }
+ .padding(8.dp)
+ ) {
+ Column(
+ Modifier.align(Alignment.CenterVertically)
+ ) {
+ icon()
+ }
+
+ Column(
+ Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ .padding(horizontal = 8.dp)
+ ) {
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) {
+ title()
+ }
+
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {
+ subTitle()
+ }
+ }
+
+ Row(Modifier.align(Alignment.CenterVertically)) {
+ content()
+ }
+ }
+}
+
+
+@Composable
+internal fun SliderPreference(
+ title: @Composable () -> Unit,
+ subTitle: @Composable () -> Unit,
+ icon: @Composable () -> Unit = {},
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ steps: Int = 0,
+ label: String,
+) {
+ val view = LocalView.current
+ LaunchedEffect(value) {
+ view.announceForAccessibility(value.toString())
+ }
+
+ PreferenceDialog(
+ title = title,
+ subTitle = subTitle,
+ dialogContent = {
+ LabelSlider(
+ modifier = Modifier.padding(vertical = 16.dp),
+ value = value,
+ onValueChange = onValueChange,
+ valueRange = valueRange,
+ steps = steps,
+ buttonSteps = 1f,
+ buttonLongSteps = 2f,
+ text = label
+ )
+ },
+ icon = icon,
+ endContent = { Text(label) }
+ )
+}
+
+@Composable
+internal fun PreferenceDialog(
+ modifier: Modifier = Modifier,
+ title: @Composable () -> Unit,
+ subTitle: @Composable () -> Unit,
+ icon: @Composable () -> Unit,
+
+ dialogContent: @Composable ColumnScope.() -> Unit,
+ endContent: @Composable RowScope.() -> Unit = {},
+) {
+ var showDialog by remember { mutableStateOf(false) }
+ if (showDialog) {
+ AppDialog(title = title, content = {
+ Column {
+ dialogContent()
+ }
+ }, buttons = {
+ TextButton(onClick = { showDialog = false }) {
+ Text(stringResource(id = R.string.close))
+ }
+ }, onDismissRequest = { showDialog = false })
+ }
+ BasePreferenceWidget(modifier, onClick = {
+ showDialog = true
+ }, title = title, icon = icon, subTitle = subTitle) {
+ endContent()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/ThemeSelectionDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/ThemeSelectionDialog.kt
new file mode 100644
index 000000000..717e2ccb9
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/settings/ThemeSelectionDialog.kt
@@ -0,0 +1,98 @@
+package com.github.jing332.tts_server_android.compose.settings
+
+import android.os.Build
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.theme.AppTheme
+import kotlinx.coroutines.delay
+
+
+@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun ThemeSelectionDialog(
+ onDismissRequest: () -> Unit,
+ currentTheme: AppTheme,
+ onChangeTheme: (AppTheme) -> Unit
+) {
+ AlertDialog(onDismissRequest = onDismissRequest,
+ title = {
+ Text(text = stringResource(id = R.string.theme))
+ },
+ confirmButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.close))
+ }
+ }, text = {
+ val view = LocalView.current
+ val context = LocalContext.current
+ Column {
+ var showWarn by remember { androidx.compose.runtime.mutableStateOf(false) }
+ AnimatedVisibility(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ visible = showWarn
+ ) {
+ LaunchedEffect(showWarn) {
+ view.announceForAccessibility(context.getString(R.string.dynamic_color_not_support))
+ delay(2000)
+ showWarn = false
+ }
+
+ Text(
+ text = stringResource(id = R.string.dynamic_color_not_support),
+ modifier = Modifier.padding(bottom = 8.dp),
+ color = MaterialTheme.colorScheme.error,
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+
+ FlowRow {
+ AppTheme.values().forEach {
+ val leadingIcon: @Composable () -> Unit =
+ { Icon(Icons.Default.Check, null) }
+// var selected by remember { mutableStateOf(it == AppTheme.DEFAULT) }
+ val selected = currentTheme.id == it.id
+ FilterChip(
+ selected,
+ modifier = Modifier.padding(horizontal = 2.dp),
+ leadingIcon = if (selected) leadingIcon else null,
+ onClick = {
+ if (it == AppTheme.DYNAMIC_COLOR && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ showWarn = true
+ return@FilterChip
+ }
+
+ onChangeTheme(it)
+ },
+ label = { Text(stringResource(id = it.stringResId), color = it.color) }
+ )
+ }
+ }
+ }
+ })
+}
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/AuditionDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/AuditionDialog.kt
new file mode 100644
index 000000000..0741fb6c3
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/AuditionDialog.kt
@@ -0,0 +1,125 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.drake.net.utils.withMain
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.LoadingContent
+import com.github.jing332.tts_server_android.conf.AppConfig
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTts
+import com.github.jing332.tts_server_android.help.audio.AudioDecoder
+import com.github.jing332.tts_server_android.help.audio.AudioPlayer
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+import com.github.jing332.tts_server_android.utils.StringUtils.sizeToReadable
+import com.github.jing332.tts_server_android.utils.clickableRipple
+import com.github.jing332.tts_server_android.utils.toast
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@Composable
+fun AuditionDialog(
+ systts: SystemTts,
+ text: String = AppConfig.testSampleText.value,
+ onDismissRequest: () -> Unit
+) {
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+ val audioPlayer = remember { AudioPlayer(context) }
+
+ var error by remember { mutableStateOf("") }
+
+ var audioInfo by remember { mutableStateOf?>(null) }
+
+ LaunchedEffect(systts.id) {
+ scope.launch(Dispatchers.IO) {
+ kotlin.runCatching {
+ systts.tts.onLoad()
+ systts.tts.getAudioWithSystemParams(text)
+ ?.use { ins ->
+ val audio = ins.readBytes()
+ val info = AudioDecoder.getSampleRateAndMime(audio)
+ if (audio.isEmpty()) {
+ error = context.getString(R.string.systts_log_audio_empty, "")
+ return@launch
+ }
+ audioInfo = Triple(audio.size, info.first, info.second)
+
+ if (systts.tts.audioFormat.isNeedDecode)
+ audioPlayer.play(audio)
+ else
+ audioPlayer.play(audio, systts.tts.audioFormat.sampleRate)
+ }
+ }.onFailure {
+ withMain { error = it.stackTraceToString() }
+
+ return@launch
+ }
+ withMain { onDismissRequest() }
+ }
+ }
+
+ DisposableEffect(systts.id) {
+ onDispose {
+ audioPlayer.release()
+ systts.tts.onDestroy()
+ }
+ }
+
+ AppDialog(onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.audition)) },
+ content = {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Text(
+ error.ifEmpty { text },
+ color = if (error.isEmpty()) Color.Unspecified else MaterialTheme.colorScheme.error,
+// maxLines = if (error.isEmpty()) Int.MAX_VALUE else 1,
+ style = MaterialTheme.typography.bodySmall
+ )
+
+ val infoStr = stringResource(
+ id = R.string.systts_test_success_info,
+ audioInfo?.first?.toLong()?.sizeToReadable() ?: 0,
+ audioInfo?.second ?: 0,
+ audioInfo?.third ?: ""
+ )
+ if (error.isEmpty())
+ LoadingContent(
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .fillMaxWidth()
+ .clickableRipple {
+ ClipboardUtils.copyText("TTS Server", infoStr)
+ context.toast(R.string.copied)
+ }, isLoading = audioInfo == null
+ ) {
+ Text(infoStr, style = MaterialTheme.typography.bodyMedium)
+ }
+
+ }
+ },
+ buttons = {
+ TextButton(onClick = onDismissRequest) { Text(stringResource(id = R.string.cancel)) }
+ }
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigDeleteDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigDeleteDialog.kt
new file mode 100644
index 000000000..4e3f2b005
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigDeleteDialog.kt
@@ -0,0 +1,51 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DeleteForever
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import com.github.jing332.tts_server_android.R
+
+@Composable
+fun ConfigDeleteDialog(onDismissRequest: () -> Unit, name: String, onConfirm: () -> Unit) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.is_confirm_delete)) },
+ text = {
+ Text(
+ name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ onConfirm()
+ }) {
+ Text(
+ stringResource(id = R.string.delete),
+ color = MaterialTheme.colorScheme.error,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.cancel))
+ }
+ },
+ icon = {
+ Icon(
+ Icons.Default.DeleteForever,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.error
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigExportBottomSheet.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigExportBottomSheet.kt
new file mode 100644
index 000000000..d86b1ab2a
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigExportBottomSheet.kt
@@ -0,0 +1,120 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.systts.directlink.LinkUploadSelectionDialog
+import com.github.jing332.tts_server_android.compose.widgets.AppBottomSheet
+import com.github.jing332.tts_server_android.ui.AppActivityResultContracts
+import com.github.jing332.tts_server_android.ui.FilePickerActivity
+import com.github.jing332.tts_server_android.ui.view.BigTextView
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+import com.github.jing332.tts_server_android.utils.toast
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigExportBottomSheet(
+ json: String,
+ fileName: String = "config.json",
+ content: @Composable ColumnScope.() -> Unit = {},
+ onDismissRequest: () -> Unit,
+) {
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ val fileSaver =
+ rememberLauncherForActivityResult(AppActivityResultContracts.filePickerActivity()) {
+ }
+
+ var showSelectUploadTargetDialog by remember { mutableStateOf(false) }
+ if (showSelectUploadTargetDialog)
+ LinkUploadSelectionDialog(
+ onDismissRequest = { showSelectUploadTargetDialog = false },
+ json = json
+ )
+
+ AppBottomSheet(onDismissRequest = onDismissRequest) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ ) {
+ content()
+ Row(Modifier.align(Alignment.CenterHorizontally)) {
+ TextButton(
+ onClick = {
+ ClipboardUtils.copyText(json)
+ context.toast(R.string.copied)
+ }
+ ) {
+ Text(stringResource(id = R.string.copy))
+ }
+
+ TextButton(
+ onClick = {
+ showSelectUploadTargetDialog = true
+ }
+ ) {
+ Text(stringResource(id = R.string.upload_to_url))
+ }
+
+ TextButton(
+ onClick = {
+ fileSaver.launch(
+ FilePickerActivity.RequestSaveFile(
+ fileName = fileName,
+ fileMime = "application/json",
+ fileBytes = json.toByteArray()
+ )
+ )
+ }) {
+ Text(stringResource(id = R.string.save_as_file))
+ }
+ }
+
+ var tv by remember {
+ mutableStateOf(null)
+ }
+
+ AndroidView(modifier = Modifier.verticalScroll(rememberScrollState()), factory = {
+ tv = BigTextView(it)
+
+ tv!!
+ }, update = {
+ it.setText(json)
+ })
+
+ LaunchedEffect(key1 = json) {
+ tv?.setText(json)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigImportBottomSheet.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigImportBottomSheet.kt
new file mode 100644
index 000000000..f6c5cf2d0
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ConfigImportBottomSheet.kt
@@ -0,0 +1,309 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import android.net.Uri
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.FileOpen
+import androidx.compose.material.icons.filled.Input
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import com.drake.net.Net
+import com.drake.net.okhttp.trustSSLCertificate
+import com.drake.net.utils.withMain
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppBottomSheet
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.RowToggleButtonGroup
+import com.github.jing332.tts_server_android.ui.AppActivityResultContracts
+import com.github.jing332.tts_server_android.ui.FilePickerActivity
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+import com.github.jing332.tts_server_android.utils.FileUtils.readAllText
+import com.github.jing332.tts_server_android.utils.longToast
+import com.github.jing332.tts_server_android.utils.toJsonListString
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.Response
+
+class ImportSource {
+ companion object {
+ const val CLIPBOARD = 0
+ const val FILE = 1
+ const val URL = 2
+ }
+}
+
+val LocalImportRemoteUrl = compositionLocalOf { mutableStateOf("") }
+val LocalImportFilePath = compositionLocalOf { mutableStateOf("") }
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigImportBottomSheet(
+ content: @Composable ColumnScope.() -> Unit = {},
+ onDismissRequest: () -> Unit,
+ onImport: (json: String) -> Unit,
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ suspend fun getConfig(
+ src: Int,
+ url: String? = null,
+ uri: Uri? = null
+ ): String {
+ return when (src) {
+ ImportSource.URL -> withContext(Dispatchers.IO) {
+ val resp: Response = Net.get(url.toString()) {
+ setClient { trustSSLCertificate() }
+ }.execute()
+ val str = resp.body?.string()
+ if (resp.isSuccessful && !str.isNullOrBlank()) {
+ return@withContext str
+ } else {
+ throw Exception("GET $url failed: code=${resp.code}, message=${resp.message}, body=${str}")
+ }
+ }
+
+ ImportSource.FILE -> withContext(Dispatchers.IO) {
+ uri?.readAllText(context) ?: throw Exception("file uri is null!")
+ }
+
+ ImportSource.CLIPBOARD -> withMain { ClipboardUtils.text.toString() } // CLIPBOARD
+
+ else -> throw IllegalArgumentException("unknown source: $src")
+ }
+ }
+
+ var source by remember { mutableIntStateOf(0) }
+ var path by remember { mutableStateOf("") }
+ var url by remember { mutableStateOf("") }
+
+ AppBottomSheet(
+ onDismissRequest = onDismissRequest
+ ) {
+ Column(Modifier.padding(horizontal = 8.dp)) {
+ Column(
+ Modifier
+ .weight(weight = 1f, fill = false)
+ .align(Alignment.Start)
+ ) {
+ Text(
+ stringResource(id = R.string.import_config),
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ style = MaterialTheme.typography.displayMedium
+ )
+
+ Column(Modifier.fillMaxWidth()) {
+ content()
+
+ Text(
+ stringResource(id = R.string.source),
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ RowToggleButtonGroup(
+ selectionIndex = source,
+ buttonCount = 3,
+ onButtonClick = { source = it },
+ buttonTexts = arrayOf(
+ R.string.clipboard, R.string.file, R.string.url_net
+ ).map { stringResource(id = it) }.toTypedArray(),
+ buttonIcons = arrayOf(
+ R.drawable.ic_baseline_select_all_24,
+ R.drawable.ic_baseline_insert_drive_file_24,
+ R.drawable.ic_web
+ ).map { painterResource(id = it) }.toTypedArray(),
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ if (LocalImportRemoteUrl.current.value.isNotBlank()) {
+ source = ImportSource.URL
+ url = LocalImportRemoteUrl.current.value
+ LocalImportRemoteUrl.current.value = ""
+ } else if (LocalImportFilePath.current.value.isNotBlank()) {
+ source = ImportSource.FILE
+ path = LocalImportFilePath.current.value
+ LocalImportFilePath.current.value = ""
+ }
+
+ AnimatedVisibility(
+ modifier = Modifier.animateContentSize(),
+ visible = source != ImportSource.CLIPBOARD
+ ) {
+ when (source) {
+ ImportSource.URL -> OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = url,
+ onValueChange = { url = it },
+ label = {
+ Text(stringResource(id = R.string.url_net))
+ },
+ )
+
+ ImportSource.FILE -> {
+ val filePicker =
+ rememberLauncherForActivityResult(contract = AppActivityResultContracts.filePickerActivity()) {
+ it.second?.let { uri ->
+ path = uri.toString()
+ }
+ }
+
+ OutlinedTextField(
+ readOnly = true,
+ modifier = Modifier.fillMaxWidth(),
+ value = path,
+ onValueChange = { path = it },
+ label = {
+ Text(stringResource(id = R.string.file))
+ },
+ trailingIcon = {
+ IconButton(onClick = {
+ filePicker.launch(
+ FilePickerActivity.RequestSelectFile(
+ listOf("application/json", "text/*")
+ )
+ )
+ }) {
+ Icon(
+ Icons.Default.FileOpen,
+ stringResource(id = R.string.select_file)
+ )
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+
+
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .align(Alignment.End)
+ .padding(top = 8.dp)
+ ) {
+ TextButton(
+ modifier = Modifier.align(Alignment.CenterEnd),
+ onClick = {
+ scope.launch {
+ runCatching {
+ val jsonStr =
+ getConfig(src = source, url = url, uri = Uri.parse(path))
+ onImport(jsonStr.toJsonListString())
+ }.onFailure {
+ context.displayErrorDialog(it)
+ }
+ }
+ }) {
+ Row {
+ Icon(Icons.Default.Input, null)
+ Text(stringResource(id = R.string.import_config))
+ }
+ }
+ }
+ }
+ }
+}
+
+data class ConfigModel(
+ val isSelected: Boolean,
+ val title: String,
+ val subtitle: String,
+ val data: Any
+)
+
+@Composable
+fun SelectImportConfigDialog(
+ onDismissRequest: () -> Unit,
+ models: List,
+ onSelectedList: (list: List) -> Int
+) {
+ val context = LocalContext.current
+ val modelsState = remember { mutableStateListOf(*models.toTypedArray()) }
+ AppDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.select_import)) },
+ content = {
+ LazyColumn {
+ itemsIndexed(modelsState, key = { i, _ -> i }) { index, item ->
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .minimumInteractiveComponentSize()
+ .clip(MaterialTheme.shapes.small)
+ .clickable(role = Role.Checkbox) {
+ modelsState[index] = item.copy(isSelected = !item.isSelected)
+ }
+ .padding(vertical = 4.dp)
+ ) {
+ Checkbox(
+ checked = item.isSelected,
+ onCheckedChange = null,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ Column(Modifier.padding(start = 4.dp)) {
+ Text(item.title, style = MaterialTheme.typography.titleMedium)
+ Text(item.subtitle, style = MaterialTheme.typography.bodyMedium)
+ }
+ }
+ }
+ }
+ },
+ buttons = {
+ TextButton(onClick = {
+ val count =
+ onSelectedList.invoke(modelsState.filter { it.isSelected }.map { it.data })
+ if (count > 0) {
+ onDismissRequest()
+ context.longToast(R.string.config_import_success_msg, count)
+ }
+ }) {
+ Text(stringResource(id = R.string.import_config))
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/GroupItem.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/GroupItem.kt
new file mode 100644
index 000000000..6243a9a54
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/GroupItem.kt
@@ -0,0 +1,191 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DeleteForever
+import androidx.compose.material.icons.filled.ExpandCircleDown
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Output
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TriStateCheckbox
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.state.ToggleableState
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+
+fun Int.sizeToToggleableState(total: Int): ToggleableState = when (this) {
+ 0 -> ToggleableState.Off
+ total -> ToggleableState.On
+ else -> ToggleableState.Indeterminate
+}
+
+@Composable
+fun GroupItem(
+ modifier: Modifier,
+ isExpanded: Boolean,
+ name: String,
+ toggleableState: ToggleableState,
+ onToggleableStateChange: (Boolean) -> Unit,
+ onClick: () -> Unit,
+ onExport: () -> Unit,
+ onDelete: () -> Unit,
+ actions: @Composable ColumnScope.(() -> Unit) -> Unit,
+) {
+ val view = LocalView.current
+ val context = LocalContext.current
+
+ var expandedFirst by remember { mutableStateOf(true) }
+ LaunchedEffect(isExpanded) {
+ if (expandedFirst) expandedFirst = false
+ else {
+ val msg =
+ if (isExpanded) context.getString(
+ R.string.group_expanded,
+ name
+ ) else context.getString(R.string.group_collapsed, name)
+ view.announceForAccessibility(msg)
+ }
+ }
+
+ var checkFirst by remember { mutableStateOf(true) }
+ LaunchedEffect(toggleableState) {
+ if (checkFirst) checkFirst = false
+ else {
+ val msg = when (toggleableState) {
+ ToggleableState.On -> context.getString(R.string.group_all_enabled, name)
+ ToggleableState.Off -> context.getString(R.string.group_all_disabled, name)
+ else -> context.getString(R.string.group_part_enabled, name)
+ }
+ view.announceForAccessibility(msg)
+ }
+ }
+
+ var showDeleteDialog by remember { mutableStateOf(false) }
+ if (showDeleteDialog)
+ ConfigDeleteDialog(
+ onDismissRequest = { showDeleteDialog = false }, name = name, onConfirm = onDelete
+ )
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surface)
+ .semantics(true) {
+ contentDescription = context.getString(
+ if (isExpanded) R.string.group_expanded
+ else R.string.group_collapsed, " "
+ )
+ }
+ .clickable { onClick() }
+ .padding(vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val rotationAngle by animateFloatAsState(
+ targetValue = if (isExpanded) 0f else -45f,
+ label = ""
+ )
+ Icon(
+ Icons.Default.ExpandCircleDown,
+ contentDescription = null,
+ modifier = Modifier
+ .rotate(rotationAngle)
+ .graphicsLayer { rotationZ = rotationAngle }
+ )
+
+ Text(
+ name,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .weight(1f)
+ )
+ Row {
+ TriStateCheckbox(
+ state = toggleableState,
+ onClick = {
+ onToggleableStateChange(toggleableState != ToggleableState.On)
+ },
+ modifier = Modifier.semantics {
+ contentDescription = context.getString(
+ when (toggleableState) {
+ ToggleableState.On -> R.string.group_all_enabled
+ ToggleableState.Off -> R.string.group_all_disabled
+ else -> R.string.group_part_enabled
+ }, name
+ )
+ }
+ )
+
+ var showOptions by remember { mutableStateOf(false) }
+ IconButton(onClick = { showOptions = true }) {
+ Icon(
+ Icons.Default.MoreVert,
+ contentDescription = stringResource(id = R.string.more_options_desc, name)
+ )
+
+ DropdownMenu(expanded = showOptions, onDismissRequest = { showOptions = false }) {
+ actions { showOptions = false }
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.export_config)) },
+ onClick = {
+ showOptions = false
+ onExport()
+ },
+ leadingIcon = {
+ Icon(Icons.Default.Output, null)
+ }
+ )
+
+ HorizontalDivider()
+
+ DropdownMenuItem(text = {
+ Text(
+ stringResource(id = R.string.delete),
+ color = MaterialTheme.colorScheme.error
+ )
+ },
+ leadingIcon = {
+ Icon(
+ Icons.Default.DeleteForever,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.error
+ )
+ },
+ onClick = {
+ showOptions = false
+ showDeleteDialog = true
+ }
+ )
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ListSortSettingsDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ListSortSettingsDialog.kt
new file mode 100644
index 000000000..1a6422f69
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/ListSortSettingsDialog.kt
@@ -0,0 +1,111 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.LoadingContent
+import com.github.jing332.tts_server_android.compose.widgets.TextCheckBox
+import com.github.jing332.tts_server_android.utils.toast
+import kotlinx.coroutines.launch
+import kotlin.system.measureTimeMillis
+
+@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun ListSortSettingsDialog(
+ name: String,
+ onDismissRequest: () -> Unit,
+ index: Int,
+ onIndexChange: (Int) -> Unit,
+ entries: List,
+ onConfirm: suspend (index: Int, descending: Boolean) -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ var descending by remember { mutableStateOf(false) }
+ var sorting by remember { mutableStateOf(false) }
+ AppDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.sort)) },
+ content = {
+ LoadingContent(Modifier.padding(vertical = 8.dp), isLoading = sorting) {
+ Column(Modifier.fillMaxWidth()) {
+ Text(
+ name,
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(vertical = 4.dp),
+ style = MaterialTheme.typography.titleMedium
+ )
+ FlowRow(Modifier.align(Alignment.CenterHorizontally)) {
+ entries.forEachIndexed { i, s ->
+ val selected = i == index
+ FilterChip(
+ selected,
+ modifier = Modifier.padding(horizontal = 4.dp),
+ onClick = { onIndexChange(i) },
+ label = {
+ Text(
+ s,
+ fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+ },
+ buttons = {
+ Row(Modifier.fillMaxWidth()) {
+ TextCheckBox(
+ text = {
+ Text(stringResource(id = R.string.descending), modifier = Modifier.padding(end = 8.dp))
+ }, checked = descending, onCheckedChange = { descending = it }
+ )
+
+ Row(Modifier.weight(1f)) {
+ Spacer(modifier = Modifier.weight(1f))
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.cancel))
+ }
+
+ TextButton(onClick = {
+ scope.launch {
+ sorting = true
+ val cost = measureTimeMillis { onConfirm(index, descending) }
+ context.toast(R.string.sorting_complete_msg, cost)
+ sorting = false
+ }
+ }) {
+ Text(stringResource(id = R.string.start))
+ }
+ }
+ }
+ }
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/LogScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/LogScreen.kt
new file mode 100644
index 000000000..7aa796b92
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/LogScreen.kt
@@ -0,0 +1,122 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SmallFloatingActionButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.core.text.HtmlCompat
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.ui.AppLog
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+import com.github.jing332.tts_server_android.utils.toAnnotatedString
+import com.github.jing332.tts_server_android.utils.toast
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun LogScreen(
+ modifier: Modifier,
+ list: List,
+ lazyListState: LazyListState = rememberLazyListState()
+) {
+ val scope = rememberCoroutineScope()
+ val view = LocalView.current
+ val context = LocalContext.current
+ Box(modifier) {
+ val isAtBottom by remember {
+ derivedStateOf {
+ val layoutInfo = lazyListState.layoutInfo
+ val visibleItemsInfo = layoutInfo.visibleItemsInfo
+ if (layoutInfo.totalItemsCount <= 0) {
+ true
+ } else {
+ val lastVisibleItem = visibleItemsInfo.last()
+ lastVisibleItem.index > layoutInfo.totalItemsCount - 5
+ }
+ }
+ }
+
+ LaunchedEffect(list.size) {
+ if (isAtBottom && list.isNotEmpty())
+ scope.launch {
+ lazyListState.animateScrollToItem(list.size - 1)
+ }
+ }
+
+ LazyColumn(Modifier.fillMaxSize(), state = lazyListState) {
+ itemsIndexed(list, key = { index, _ -> index }) { _, item ->
+ val style = MaterialTheme.typography.bodyMedium
+ val spanned = remember {
+ HtmlCompat.fromHtml(item.msg, HtmlCompat.FROM_HTML_MODE_COMPACT)
+ .toAnnotatedString()
+ }
+
+ Text(
+ text = spanned,
+ style = style,
+ lineHeight = style.lineHeight * 0.75f,
+ modifier = Modifier
+ .combinedClickable(
+ onClick = {
+ println("onClick")
+ },
+ onLongClick = {
+ view.isHapticFeedbackEnabled = true
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ ClipboardUtils.copyText("tts-server-log", spanned.text)
+ context.toast(R.string.copied)
+ }
+ )
+ .padding(horizontal = 4.dp)
+ )
+ }
+ }
+
+ AnimatedVisibility(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(12.dp), visible = !isAtBottom
+ ) {
+ SmallFloatingActionButton(
+ shape = CircleShape,
+ onClick = {
+ scope.launch {
+ kotlin.runCatching {
+ lazyListState.scrollToItem(list.size - 1)
+ }
+ }
+ }) {
+ Icon(
+ Icons.Default.KeyboardDoubleArrowDown,
+ stringResource(id = R.string.move_to_bottom)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/SystemTtsScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/SystemTtsScreen.kt
new file mode 100644
index 000000000..bde1e9cb5
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/SystemTtsScreen.kt
@@ -0,0 +1,96 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.TextSnippet
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.systts.list.ListManagerScreen
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun SystemTtsScreen( vm: SystemTtsViewModel = viewModel()) {
+ val pagerState = rememberPagerState { 2 }
+ val scope = rememberCoroutineScope()
+
+ Scaffold(
+ bottomBar = {
+ NavigationBar {
+ (0 until 2).forEach {
+ when (it) {
+ 0 -> {
+ val isSelected = pagerState.currentPage == it
+ NavigationBarItem(
+ selected = isSelected,
+ onClick = {
+ scope.launch {
+ pagerState.animateScrollToPage(it)
+ }
+ },
+ icon = {
+ Icon(
+ painterResource(id = R.drawable.ic_config),
+ null,
+ Modifier.size(24.dp)
+ )
+ },
+ label = {
+ Text(stringResource(id = R.string.config))
+ }
+ )
+ }
+
+ 1 -> {
+ val isSelected = pagerState.currentPage == it
+ NavigationBarItem(
+ selected = isSelected,
+ onClick = {
+ scope.launch {
+ pagerState.animateScrollToPage(it)
+ }
+ },
+ icon = {
+ Icon(Icons.Default.TextSnippet, null)
+ },
+ label = {
+ Text(stringResource(id = R.string.log))
+ }
+ )
+ }
+ }
+
+ }
+ }
+ }
+ ) { paddingValues ->
+ HorizontalPager(
+ modifier = Modifier
+ .padding(bottom = paddingValues.calculateBottomPadding())
+ .fillMaxSize(),
+ state = pagerState,
+ userScrollEnabled = false
+ ) { index ->
+ when (index) {
+ 0 -> ListManagerScreen()
+ 1 -> TtsLogScreen()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/SystemTtsViewModel.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/SystemTtsViewModel.kt
new file mode 100644
index 000000000..bd6995b3f
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/SystemTtsViewModel.kt
@@ -0,0 +1,8 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.lifecycle.ViewModel
+
+class SystemTtsViewModel : ViewModel() {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/TtsLogScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/TtsLogScreen.kt
new file mode 100644
index 000000000..16f0d524a
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/TtsLogScreen.kt
@@ -0,0 +1,52 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import android.content.IntentFilter
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DeleteOutline
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.nav.NavTopAppBar
+import com.github.jing332.tts_server_android.compose.widgets.LocalBroadcastReceiver
+import com.github.jing332.tts_server_android.constant.KeyConst
+import com.github.jing332.tts_server_android.service.systts.SystemTtsService
+import com.github.jing332.tts_server_android.ui.AppLog
+
+@Suppress("DEPRECATION")
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun TtsLogScreen(vm: TtsLogViewModel = viewModel()) {
+ LocalBroadcastReceiver(intentFilter = IntentFilter(SystemTtsService.ACTION_ON_LOG)) {
+ if (it?.action == SystemTtsService.ACTION_ON_LOG) {
+ it.getParcelableExtra(KeyConst.KEY_DATA)?.let { log ->
+ println("ACTION_ON_LOG ${log.msg}")
+ vm.logs.add(log)
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ NavTopAppBar(title = { Text(stringResource(id = R.string.log)) }, actions = {
+ IconButton(onClick = { vm.logs.clear() }) {
+ Icon(Icons.Default.DeleteOutline, stringResource(id = R.string.clear_log))
+ }
+ })
+ }
+ ) { paddingValues ->
+ LogScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = paddingValues.calculateTopPadding()), list = vm.logs
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/TtsLogViewModel.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/TtsLogViewModel.kt
new file mode 100644
index 000000000..2c31d95a9
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/TtsLogViewModel.kt
@@ -0,0 +1,9 @@
+package com.github.jing332.tts_server_android.compose.systts
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.lifecycle.ViewModel
+import com.github.jing332.tts_server_android.ui.AppLog
+
+class TtsLogViewModel : ViewModel() {
+ val logs = mutableStateListOf()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/directlink/LinkUploadRuleActivity.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/directlink/LinkUploadRuleActivity.kt
new file mode 100644
index 000000000..110142660
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/directlink/LinkUploadRuleActivity.kt
@@ -0,0 +1,123 @@
+package com.github.jing332.tts_server_android.compose.systts.directlink
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.codeeditor.CodeEditorScreen
+import com.github.jing332.tts_server_android.compose.codeeditor.LoggerBottomSheet
+import com.github.jing332.tts_server_android.compose.theme.AppTheme
+import com.github.jing332.tts_server_android.conf.DirectUploadConfig
+import com.github.jing332.tts_server_android.model.rhino.core.Logger
+import com.github.jing332.tts_server_android.model.rhino.direct_link_upload.DirectUploadEngine
+import com.github.jing332.tts_server_android.model.rhino.direct_link_upload.DirectUploadFunction
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import io.github.rosemoe.sora.widget.CodeEditor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class LinkUploadRuleActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ AppTheme {
+ LinkUploadRuleScreen()
+ }
+ }
+ }
+
+
+ @Composable
+ private fun LinkUploadRuleScreen() {
+ var editor by remember { mutableStateOf(null) }
+ val logger by remember { mutableStateOf(Logger()) }
+ var targets by remember { mutableStateOf?>(null) }
+ val scope = rememberCoroutineScope()
+
+ LaunchedEffect(editor) {
+ editor?.setText(DirectUploadConfig.code.value)
+ }
+
+ var showDebugLogger by remember { mutableStateOf("") }
+ if (showDebugLogger.isNotEmpty())
+ LoggerBottomSheet(
+ logger = logger,
+ onDismissRequest = { showDebugLogger = "" }) {
+ scope.launch(Dispatchers.IO) {
+ runCatching {
+ val engine = DirectUploadEngine(
+ context = this@LinkUploadRuleActivity,
+ logger = logger,
+ code = editor!!.text.toString()
+ )
+ val func =
+ engine.obtainFunctionList().find { it.funcName == showDebugLogger }
+
+ val url = func?.invoke(""" {"test":"test"} """)
+ logger.i("url: $url")
+ }.onFailure {
+ this@LinkUploadRuleActivity.displayErrorDialog(it)
+ }
+ }
+ }
+
+ fun obtainFunctionList(): List {
+ val engine = DirectUploadEngine(
+ context = this,
+ logger = logger,
+ code = editor!!.text.toString()
+ )
+ return engine.obtainFunctionList()
+ }
+
+
+ CodeEditorScreen(
+ title = { Text(stringResource(id = R.string.direct_link_settings)) },
+ onBack = { finishAfterTransition() },
+ onSave = {
+ runCatching {
+ obtainFunctionList()
+ DirectUploadConfig.code.value = editor!!.text.toString()
+
+ finishAfterTransition()
+ }.onFailure {
+ this.displayErrorDialog(it)
+ }
+ },
+ onUpdate = { editor = it },
+ onDebug = {
+ kotlin.runCatching {
+ targets = obtainFunctionList()
+ }.onFailure {
+ this.displayErrorDialog(it)
+ }
+ },
+ debugIconContent = {
+ DropdownMenu(expanded = targets != null, onDismissRequest = { targets = null }) {
+ targets?.forEach {
+ DropdownMenuItem(text = { Text(it.funcName) }, onClick = {
+ targets = null
+ showDebugLogger = it.funcName
+ })
+ }
+ }
+ },
+ onSaveFile = {
+ "ttsrv-directLink.js" to editor!!.text.toString().toByteArray()
+ }
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/directlink/LinkUploadSelectionDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/directlink/LinkUploadSelectionDialog.kt
new file mode 100644
index 000000000..b01f3744c
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/directlink/LinkUploadSelectionDialog.kt
@@ -0,0 +1,72 @@
+package com.github.jing332.tts_server_android.compose.systts.directlink
+
+
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.drake.net.utils.withIO
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppSelectionDialog
+import com.github.jing332.tts_server_android.model.rhino.direct_link_upload.DirectUploadEngine
+import com.github.jing332.tts_server_android.model.rhino.direct_link_upload.DirectUploadFunction
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+import com.github.jing332.tts_server_android.utils.longToast
+import kotlinx.coroutines.launch
+
+@Composable
+fun LinkUploadSelectionDialog(onDismissRequest: () -> Unit, json: String) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val targetList = remember {
+ try {
+ DirectUploadEngine(context = context).obtainFunctionList()
+ } catch (e: Exception) {
+ context.displayErrorDialog(e, context.getString(R.string.upload_to_url))
+ null
+ }
+ }
+
+ var loading by remember { mutableStateOf(false) }
+ if (targetList != null)
+ AppSelectionDialog(
+ isLoading = loading,
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.choose_an_upload_target)) },
+ value = Any(),
+ values = targetList,
+ entries = targetList.map { it.funcName },
+ onClick = { value, _ ->
+ scope.launch {
+ runCatching {
+ loading = true
+ val url =
+ withIO { (value as DirectUploadFunction).invoke(json) }
+ ?: throw Exception("url is null")
+ ClipboardUtils.copyText("TTS Server", url)
+ context.longToast(R.string.copied_url)
+ loading = false
+ }.onFailure {
+ loading = false
+ context.displayErrorDialog(it)
+ return@launch
+ }
+
+ onDismissRequest()
+ }
+ },
+ buttons = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.cancel))
+ }
+ },
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/BasicAudioParamsDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/BasicAudioParamsDialog.kt
new file mode 100644
index 000000000..132d36f9f
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/BasicAudioParamsDialog.kt
@@ -0,0 +1,97 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.LabelSlider
+import com.github.jing332.tts_server_android.utils.toScale
+
+@Composable
+fun BasicAudioParamsDialog(
+ title: @Composable () -> Unit = { Text(stringResource(id = R.string.audio_params)) },
+ onDismissRequest: () -> Unit,
+
+ resetValue: Float = 0f,
+ onReset: () -> Unit,
+
+ defaultSpeed: Float = 0f,
+ speedRange: ClosedFloatingPointRange = 0f..3f,
+ speed: Float,
+ onSpeedChange: (Float) -> Unit,
+
+ defaultVolume : Float = 0f,
+ volumeRange: ClosedFloatingPointRange = 0f..3f,
+ volume: Float,
+ onVolumeChange: (Float) -> Unit,
+
+ defaultPitch: Float = 0f,
+ pitchRange: ClosedFloatingPointRange = 0f..3f,
+ pitch: Float,
+ onPitchChange: (Float) -> Unit,
+
+ buttons: @Composable (BoxScope.() -> Unit) = {
+ Row {
+ TextButton(
+ enabled = speed != resetValue || volume != resetValue || pitch != resetValue,
+ onClick = {
+ onReset()
+ }) {
+ Text(stringResource(id = R.string.reset))
+ }
+
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.close))
+ }
+ }
+ },
+) {
+ AppDialog(
+ title = title,
+ content = {
+ Column {
+ val str = stringResource(
+ id = R.string.label_speech_rate,
+ if (speed == defaultSpeed) stringResource(R.string.follow) else speed.toString()
+ )
+ LabelSlider(
+ value = speed,
+ onValueChange = { onSpeedChange(it.toScale(2)) },
+ valueRange = speedRange,
+ text = str
+ )
+
+ val volStr =
+ stringResource(
+ id = R.string.label_speech_volume,
+ if (volume == defaultVolume) stringResource(R.string.follow) else volume.toString()
+ )
+ LabelSlider(
+ value = volume,
+ onValueChange = { onVolumeChange(it.toScale(2)) },
+ valueRange = volumeRange,
+ text = volStr
+ )
+
+ val pitchStr =
+ stringResource(
+ id = R.string.label_speech_pitch,
+ if (pitch == defaultPitch) stringResource(R.string.follow) else pitch.toString()
+ )
+ LabelSlider(
+ value = pitch,
+ onValueChange = { onPitchChange(it.toScale(2)) },
+ valueRange = pitchRange,
+ text = pitchStr
+ )
+
+ }
+ },
+ buttons = buttons, onDismissRequest = onDismissRequest
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/BgmSettingsDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/BgmSettingsDialog.kt
new file mode 100644
index 000000000..512a40ff7
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/BgmSettingsDialog.kt
@@ -0,0 +1,59 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.conf.SystemTtsConfig
+import com.github.jing332.tts_server_android.utils.clickableRipple
+
+@Composable
+fun BgmSettingsDialog(onDismissRequest: () -> Unit) {
+ AppDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.bgm_settings)) },
+ content = {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ var shuffle by remember { SystemTtsConfig.isBgmShuffleEnabled }
+ Row(
+ Modifier
+ .height(48.dp)
+ .clip(MaterialTheme.shapes.medium)
+ .clickableRipple { shuffle = !shuffle }
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Checkbox(checked = shuffle, onCheckedChange = null)
+ Text(stringResource(id = R.string.shuffle))
+ }
+
+ var volume by remember { SystemTtsConfig.bgmVolume }
+ val volumeStr = stringResource(id = R.string.label_speech_volume, (volume * 1000f).toInt().toString())
+ IntSlider(
+ label = volumeStr,
+ value = volume * 1000f,
+ onValueChange = { volume = it / 1000f },
+ valueRange = 1f..1000f
+ )
+
+ Text(
+ stringResource(id = R.string.bgm_settings_tip),
+ modifier = Modifier.padding(4.dp)
+ )
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/GlobalAudioParamsDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/GlobalAudioParamsDialog.kt
new file mode 100644
index 000000000..d8cb3c74e
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/GlobalAudioParamsDialog.kt
@@ -0,0 +1,38 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.conf.SystemTtsConfig
+
+@Composable
+fun GlobalAudioParamsDialog(onDismissRequest: () -> Unit) {
+ var speed by remember { SystemTtsConfig.audioParamsSpeed }
+ var volume by remember { SystemTtsConfig.audioParamsVolume }
+ var pitch by remember { SystemTtsConfig.audioParamsPitch }
+ BasicAudioParamsDialog(
+ title = { Text(stringResource(id = R.string.audio_params_settings)) },
+ onDismissRequest = onDismissRequest,
+
+ speedRange = 0.1f..3f,
+ speed = speed,
+ onSpeedChange = { speed = it },
+
+ volumeRange = 0.1f..3f,
+ volume = volume,
+ onVolumeChange = { volume = it },
+
+ pitchRange = 0.1f..3f,
+ pitch = pitch,
+ onPitchChange = { pitch = it },
+
+ onReset = {
+ speed = 1f
+ volume = 1f
+ pitch = 1f
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/Group.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/Group.kt
new file mode 100644
index 000000000..bf0f859e6
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/Group.kt
@@ -0,0 +1,120 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material.icons.filled.DriveFileRenameOutline
+import androidx.compose.material.icons.filled.Sort
+import androidx.compose.material.icons.filled.Speed
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.state.ToggleableState
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.systts.GroupItem
+import com.github.jing332.tts_server_android.compose.widgets.TextFieldDialog
+
+@Composable
+fun Group(
+ modifier: Modifier,
+ name: String,
+ isExpanded: Boolean,
+ toggleableState: ToggleableState,
+ onToggleableStateChange: (Boolean) -> Unit,
+ onClick: () -> Unit,
+ onExport: () -> Unit,
+ onDelete: () -> Unit,
+ onRename: (newName: String) -> Unit,
+ onCopy: (newName: String) -> Unit,
+ onEditAudioParams: () -> Unit,
+ onSort: () -> Unit,
+) {
+
+ var showRenameDialog by remember { mutableStateOf(false) }
+ if (showRenameDialog) {
+ var nameValue by remember { mutableStateOf(name) }
+ TextFieldDialog(
+ title = stringResource(id = R.string.rename),
+ text = nameValue,
+ onTextChange = { nameValue = it },
+ onDismissRequest = { showRenameDialog = false }) {
+ showRenameDialog = false
+ onRename(nameValue)
+ }
+ }
+
+ var showCopyDialog by remember { mutableStateOf(false) }
+ if (showCopyDialog) {
+ var nameValue by remember { mutableStateOf(name) }
+ TextFieldDialog(
+ title = stringResource(id = R.string.copy),
+ text = nameValue,
+ onTextChange = { nameValue = it },
+ onDismissRequest = { showCopyDialog = false }) {
+ showCopyDialog = false
+ onCopy(nameValue)
+ }
+ }
+
+ GroupItem(
+ modifier = modifier,
+ isExpanded = isExpanded,
+ name = name,
+ toggleableState = toggleableState,
+ onToggleableStateChange = onToggleableStateChange,
+ onClick = onClick,
+ onExport = onExport,
+ onDelete = onDelete,
+ actions = { dismiss ->
+ DropdownMenuItem(text = { Text(stringResource(id = R.string.rename)) },
+ onClick = {
+ dismiss()
+ showRenameDialog = true
+ },
+ leadingIcon = {
+ Icon(Icons.Default.DriveFileRenameOutline, null)
+ }
+ )
+
+ DropdownMenuItem(text = { Text(stringResource(id = R.string.copy)) },
+ onClick = {
+ dismiss()
+ showCopyDialog = true
+ },
+ leadingIcon = {
+ Icon(Icons.Default.ContentCopy, null)
+ }
+ )
+
+ DropdownMenuItem(text = { Text(stringResource(id = R.string.audio_params)) },
+ onClick = {
+ dismiss()
+ onEditAudioParams()
+ },
+ leadingIcon = {
+ Icon(Icons.Default.Speed, null)
+ }
+ )
+
+ DropdownMenuItem(text = { Text(stringResource(id = R.string.sort)) },
+ onClick = {
+ dismiss()
+ onSort()
+ },
+ leadingIcon = {
+ Icon(Icons.Default.Sort, null)
+ }
+ )
+ }
+ )
+
+}
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/GroupAudioParamsDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/GroupAudioParamsDialog.kt
new file mode 100644
index 000000000..e2687155d
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/GroupAudioParamsDialog.kt
@@ -0,0 +1,63 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.data.entities.systts.AudioParams
+
+@Composable
+fun GroupAudioParamsDialog(
+ onDismissRequest: () -> Unit,
+ params: AudioParams,
+ onConfirm: (AudioParams) -> Unit
+) {
+ var speed by remember { mutableFloatStateOf(params.speed) }
+ var volume by remember { mutableFloatStateOf(params.volume) }
+ var pitch by remember { mutableFloatStateOf(params.pitch) }
+
+ BasicAudioParamsDialog(
+ onDismissRequest = onDismissRequest,
+ onReset = {
+ speed = AudioParams.FOLLOW_GLOBAL_VALUE
+ volume = AudioParams.FOLLOW_GLOBAL_VALUE
+ pitch = AudioParams.FOLLOW_GLOBAL_VALUE
+ },
+ speed = speed,
+ onSpeedChange = { speed = it },
+ volume = volume,
+ onVolumeChange = { volume = it },
+ pitch = pitch,
+ onPitchChange = { pitch = it },
+ buttons = {
+ Row {
+ TextButton(
+ enabled = speed != AudioParams.FOLLOW_GLOBAL_VALUE || volume != AudioParams.FOLLOW_GLOBAL_VALUE || pitch != AudioParams.FOLLOW_GLOBAL_VALUE,
+ onClick = {
+ speed = AudioParams.FOLLOW_GLOBAL_VALUE
+ volume = AudioParams.FOLLOW_GLOBAL_VALUE
+ pitch = AudioParams.FOLLOW_GLOBAL_VALUE
+ }) {
+ Text(stringResource(id = R.string.reset))
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ Row {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.cancel))
+ }
+ TextButton(onClick = { onConfirm(AudioParams(speed, volume, pitch)) }) {
+ Text(stringResource(id = R.string.confirm))
+ }
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/IntSlider.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/IntSlider.kt
new file mode 100644
index 000000000..78ccea521
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/IntSlider.kt
@@ -0,0 +1,24 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.github.jing332.tts_server_android.compose.widgets.LabelSlider
+
+@Composable
+fun IntSlider(
+ modifier: Modifier = Modifier,
+ label: String,
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ valueRange: ClosedFloatingPointRange
+) {
+ LabelSlider(
+ modifier = modifier,
+ value = value,
+ onValueChange = onValueChange,
+ valueRange = valueRange,
+ text = label,
+ buttonSteps = 1f,
+ buttonLongSteps = 10f
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/InternalPlayerDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/InternalPlayerDialog.kt
new file mode 100644
index 000000000..91592498c
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/InternalPlayerDialog.kt
@@ -0,0 +1,73 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.LabelSlider
+import com.github.jing332.tts_server_android.conf.SystemTtsConfig
+import com.github.jing332.tts_server_android.utils.toScale
+
+@Composable
+fun InternalPlayerDialog(onDismissRequest: () -> Unit) {
+ var speed by remember { SystemTtsConfig.inAppPlaySpeed }
+ var volume by remember { SystemTtsConfig.inAppPlayVolume }
+ var pitch by remember { SystemTtsConfig.inAppPlayPitch }
+ AppDialog(
+ title = { Text(stringResource(id = R.string.systts_use_internal_audio_player)) },
+ content = {
+ Column {
+ LabelSlider(
+ value = speed,
+ onValueChange = {
+ speed = it.toScale(2)
+ },
+ valueRange = 0.1f..3.0f,
+ text = stringResource(id = R.string.label_speed) + speed
+ )
+ LabelSlider(
+ value = volume,
+ onValueChange = {
+ volume = it.toScale(2)
+ },
+ valueRange = 0.1f..1.0f,
+ text = stringResource(id = R.string.label_volume) + volume
+ )
+
+ LabelSlider(
+ value = pitch,
+ onValueChange = {
+ pitch = it.toScale(2)
+ },
+ valueRange = 0.1f..3.0f,
+ text = stringResource(id = R.string.label_pitch) + pitch
+ )
+
+ }
+ },
+ buttons = {
+ Row {
+ TextButton(
+ enabled = speed != 1f || volume != 1f || pitch != 1f,
+ onClick = {
+ speed = 1f
+ volume = 1f
+ pitch = 1f
+ }) {
+ Text(stringResource(id = R.string.reset))
+ }
+
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.close))
+ }
+ }
+ }, onDismissRequest = onDismissRequest
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/Item.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/Item.kt
new file mode 100644
index 000000000..789893ae0
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/Item.kt
@@ -0,0 +1,326 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CopyAll
+import androidx.compose.material.icons.filled.DeleteForever
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Headphones
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Output
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.widgets.HtmlText
+import com.github.jing332.tts_server_android.compose.widgets.LongClickIconButton
+import com.github.jing332.tts_server_android.conf.AppConfig
+import com.github.jing332.tts_server_android.utils.StringUtils.limitLength
+import com.github.jing332.tts_server_android.utils.clickableRipple
+import com.github.jing332.tts_server_android.utils.performLongPress
+import org.burnoutcrew.reorderable.ReorderableLazyListState
+import org.burnoutcrew.reorderable.detectReorder
+
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun Item(
+ modifier: Modifier,
+ name: String,
+ tagName: String,
+ type: String,
+ desc: String,
+ params: String,
+ reorderState: ReorderableLazyListState,
+
+ standby: Boolean,
+ enabled: Boolean,
+ onEnabledChange: (Boolean) -> Unit,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+
+ onCopy: () -> Unit,
+ onDelete: () -> Unit,
+ onEdit: () -> Unit,
+ onAudition: () -> Unit,
+ onExport: () -> Unit,
+) {
+ val view = LocalView.current
+ val context = LocalContext.current
+
+ val limitNameLen by remember { AppConfig.limitNameLength }
+ val limitedName = remember(name, limitNameLen) {
+ if (limitNameLen == 0) name else name.limitLength(limitNameLen)
+ }
+
+ ElevatedCard(modifier) {
+ ConstraintLayout(
+ Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = {
+ view.performLongPress()
+ onLongClick()
+ }
+ )
+ .padding(vertical = 4.dp)
+ ) {
+ val (checkRef,
+ nameRef,
+ contentRef,
+ targetRef,
+ typeRef,
+ buttonsRef) = createRefs()
+ Row(
+ Modifier
+ .constrainAs(checkRef) {
+ start.linkTo(parent.start)
+ top.linkTo(nameRef.top)
+ bottom.linkTo(contentRef.bottom)
+
+ height = Dimension.fillToConstraints
+ }
+ .detectReorder(reorderState)) {
+ Checkbox(
+ modifier = Modifier
+ .fillMaxHeight()
+ .semantics {
+ role = Role.Switch
+ context
+ .getString(
+ if (enabled) R.string.config_enabled_desc else R.string.config_disabled_desc,
+ limitedName
+ )
+ .let {
+ contentDescription = it
+ stateDescription = it
+ }
+ },
+ checked = enabled,
+ onCheckedChange = onEnabledChange,
+ )
+ }
+ Text(
+ limitedName,
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ textAlign = TextAlign.Start,
+ fontWeight = FontWeight.Bold,
+ overflow = TextOverflow.Clip,
+ modifier = Modifier
+ .constrainAs(nameRef) {
+ start.linkTo(checkRef.end)
+ top.linkTo(parent.top)
+ }
+ .padding(bottom = 4.dp)
+ )
+
+ Column(
+ Modifier
+ .constrainAs(contentRef) {
+ start.linkTo(checkRef.end)
+ top.linkTo(nameRef.bottom)
+ bottom.linkTo(parent.bottom)
+ }
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.Start
+ ) {
+ HtmlText(
+ text = desc,
+ style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
+ )
+
+ HtmlText(
+ text = params,
+ style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onBackground),
+ )
+ }
+
+ val limitLen by remember { AppConfig.limitTagLength }
+ val limitedTagName = remember(tagName, limitLen) {
+ if (limitLen == 0) tagName else tagName.limitLength(limitLen)
+ }
+ if (limitedTagName.isNotEmpty())
+ TagScreen(
+ Modifier
+ .constrainAs(targetRef) {
+ top.linkTo(nameRef.top)
+ end.linkTo(parent.end)
+ }
+ .padding(end = 4.dp),
+ tag = limitedTagName,
+ )
+
+ Row(modifier = Modifier.constrainAs(buttonsRef) {
+ top.linkTo(parent.top)
+ bottom.linkTo(parent.bottom)
+ end.linkTo(parent.end)
+ }) {
+ val swapButton = AppConfig.isSwapListenAndEditButton.value
+ IconButton(
+ modifier = Modifier,
+ onClick = { if (swapButton) onAudition() else onEdit() }
+ ) {
+ if (swapButton)
+ Icon(Icons.Default.Headphones, stringResource(id = R.string.audition))
+ else
+ Icon(Icons.Default.Edit, stringResource(id = R.string.edit_desc, name))
+ }
+
+ var showOptions by remember { mutableStateOf(false) }
+ LongClickIconButton(
+ onClick = { showOptions = true },
+ onLongClick = { if (swapButton) onEdit() else onAudition() },
+ onLongClickLabel = stringResource(id = if (swapButton) R.string.edit else R.string.audition)
+ ) {
+ Icon(
+ Icons.Default.MoreVert,
+ stringResource(id = R.string.more_options_desc, name)
+ )
+
+ DropdownMenu(
+ expanded = showOptions,
+ onDismissRequest = { showOptions = false }) {
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = if (swapButton) R.string.edit else R.string.audition)) },
+ onClick = {
+ showOptions = false
+ if (swapButton)
+ onEdit()
+ else
+ onAudition()
+ },
+ leadingIcon = {
+ Icon(
+ if (swapButton) Icons.Default.Edit else Icons.Default.Headphones,
+ null
+ )
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.copy)) },
+ onClick = {
+ showOptions = false
+ onCopy()
+ },
+ leadingIcon = {
+ Icon(Icons.Default.CopyAll, null)
+ }
+ )
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.export_config)) },
+ onClick = {
+ showOptions = false
+ onExport()
+ },
+ leadingIcon = {
+ Icon(Icons.Default.Output, null)
+ }
+ )
+ HorizontalDivider()
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.delete)) },
+ onClick = {
+ showOptions = false
+ onDelete()
+ },
+ leadingIcon = {
+ Icon(
+ Icons.Default.DeleteForever,
+ null,
+ tint = MaterialTheme.colorScheme.error
+ )
+ }
+ )
+ }
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .constrainAs(typeRef) {
+ end.linkTo(parent.end)
+// top.linkTo(buttonsRef.bottom)
+ bottom.linkTo(parent.bottom)
+ }
+ .padding(end = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (standby) {
+ Text(
+ modifier = Modifier.padding(end = 4.dp),
+ text = stringResource(id = R.string.systts_standby),
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.tertiary,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+
+ Text(
+ text = type,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.tertiary,
+ )
+ }
+
+ }
+
+ }
+}
+
+@Composable
+private fun TagScreen(modifier: Modifier = Modifier, tag: String) {
+ OutlinedCard(shape = MaterialTheme.shapes.extraSmall, modifier = modifier) {
+ Text(
+ text = tag,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
+ overflow = TextOverflow.Clip,
+ textAlign = TextAlign.Center,
+ maxLines = 1,
+ )
+ }
+}
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListExportBottomSheet.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListExportBottomSheet.kt
new file mode 100644
index 000000000..4f669fc39
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListExportBottomSheet.kt
@@ -0,0 +1,16 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.runtime.Composable
+import com.github.jing332.tts_server_android.compose.systts.ConfigExportBottomSheet
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.data.entities.systts.GroupWithSystemTts
+import kotlinx.serialization.encodeToString
+
+@Composable
+fun ListExportBottomSheet(onDismissRequest: () -> Unit, list: List) {
+ ConfigExportBottomSheet(
+ json = AppConst.jsonBuilder.encodeToString(list),
+ onDismissRequest = onDismissRequest,
+ fileName = "ttsrv-list.json"
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListImportBottomSheet.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListImportBottomSheet.kt
new file mode 100644
index 000000000..711f63a45
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListImportBottomSheet.kt
@@ -0,0 +1,113 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.github.jing332.tts_server_android.bean.LegadoHttpTts
+import com.github.jing332.tts_server_android.compose.systts.ConfigImportBottomSheet
+import com.github.jing332.tts_server_android.compose.systts.ConfigModel
+import com.github.jing332.tts_server_android.compose.systts.SelectImportConfigDialog
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.systts.CompatSystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.GroupWithSystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTtsGroup
+import com.github.jing332.tts_server_android.model.speech.tts.BaseAudioFormat
+import com.github.jing332.tts_server_android.model.speech.tts.HttpTTS
+import com.github.jing332.tts_server_android.utils.StringUtils
+
+@Composable
+fun ListImportBottomSheet(onDismissRequest: () -> Unit) {
+ var selectDialog by remember { mutableStateOf?>(null) }
+ if (selectDialog != null) {
+ SelectImportConfigDialog(
+ onDismissRequest = { selectDialog = null },
+ models = selectDialog!!,
+ onSelectedList = { list ->
+ list.map {
+ @Suppress("UNCHECKED_CAST")
+ it as Pair
+ }
+ .forEach {
+ val group = it.first
+ val tts = it.second
+ appDb.systemTtsDao.insertGroup(group)
+ appDb.systemTtsDao.insertTts(tts)
+ }
+
+ list.size
+ }
+ )
+ }
+
+ ConfigImportBottomSheet(onDismissRequest = onDismissRequest,
+ onImport = { json ->
+ val allList = mutableListOf()
+ getImportList(json, false)?.forEach { groupWithTts ->
+ val group = groupWithTts.group
+ groupWithTts.list.forEach { sysTts ->
+ allList.add(
+ ConfigModel(
+ true, sysTts.displayName.toString(),
+ group.name, group to sysTts
+ )
+ )
+ }
+ }
+ selectDialog = allList
+ }
+ )
+}
+
+private fun getImportList(json: String, fromLegado: Boolean): List? {
+ val groupName = StringUtils.formattedDate()
+ val groupId = System.currentTimeMillis()
+ val groupCount = appDb.systemTtsDao.groupCount
+ if (fromLegado) {
+ AppConst.jsonBuilder.decodeFromString>(json).ifEmpty { return null }
+ .let { list ->
+ return listOf(GroupWithSystemTts(
+ group = SystemTtsGroup(
+ id = groupId,
+ name = groupName,
+ order = groupCount
+ ),
+ list = list.map {
+ SystemTts(
+ groupId = groupId,
+ id = it.id,
+ displayName = it.name,
+ tts = HttpTTS(
+ url = it.url,
+ header = it.header,
+ audioFormat = BaseAudioFormat(isNeedDecode = true)
+ )
+ )
+ }
+
+ ))
+ }
+
+ } else {
+ return if (json.contains("\"group\"")) { // 新版数据结构
+ AppConst.jsonBuilder.decodeFromString>(json)
+ } else {
+ val list = AppConst.jsonBuilder.decodeFromString>(json)
+ listOf(
+ GroupWithSystemTts(
+ group = appDb.systemTtsDao.getGroup()!!,
+ list = list.mapIndexed { index, value ->
+ SystemTts(
+ id = System.currentTimeMillis() + index,
+ displayName = value.displayName,
+ tts = value.tts
+ )
+ }
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListManagerScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListManagerScreen.kt
new file mode 100644
index 000000000..6c69c52fc
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListManagerScreen.kt
@@ -0,0 +1,456 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import android.os.Bundle
+import androidx.annotation.StringRes
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.AddCard
+import androidx.compose.material.icons.filled.Audiotrack
+import androidx.compose.material.icons.filled.Javascript
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.PhoneAndroid
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.drake.net.utils.withIO
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.LocalDrawerState
+import com.github.jing332.tts_server_android.compose.LocalNavController
+import com.github.jing332.tts_server_android.compose.ShadowReorderableItem
+import com.github.jing332.tts_server_android.compose.nav.NavRoutes
+import com.github.jing332.tts_server_android.compose.nav.NavTopAppBar
+import com.github.jing332.tts_server_android.compose.navigate
+import com.github.jing332.tts_server_android.compose.systts.AuditionDialog
+import com.github.jing332.tts_server_android.compose.systts.ConfigDeleteDialog
+import com.github.jing332.tts_server_android.compose.systts.ConfigExportBottomSheet
+import com.github.jing332.tts_server_android.compose.systts.list.edit.QuickEditBottomSheet
+import com.github.jing332.tts_server_android.compose.systts.list.edit.TagDataClearConfirmDialog
+import com.github.jing332.tts_server_android.compose.systts.sizeToToggleableState
+import com.github.jing332.tts_server_android.compose.widgets.LazyListIndexStateSaver
+import com.github.jing332.tts_server_android.compose.widgets.TextFieldDialog
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.constant.SpeechTarget
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.AbstractListGroup
+import com.github.jing332.tts_server_android.data.entities.systts.GroupWithSystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTtsGroup
+import com.github.jing332.tts_server_android.model.rhino.speech_rule.SpeechRuleEngine
+import com.github.jing332.tts_server_android.model.speech.tts.BgmTTS
+import com.github.jing332.tts_server_android.model.speech.tts.LocalTTS
+import com.github.jing332.tts_server_android.model.speech.tts.MsTTS
+import com.github.jing332.tts_server_android.model.speech.tts.PluginTTS
+import com.github.jing332.tts_server_android.service.systts.SystemTtsService
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import com.github.jing332.tts_server_android.utils.longToast
+import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
+import org.burnoutcrew.reorderable.detectReorderAfterLongPress
+import org.burnoutcrew.reorderable.rememberReorderableLazyListState
+import org.burnoutcrew.reorderable.reorderable
+
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@Composable
+internal fun ListManagerScreen(vm: ListManagerViewModel = viewModel()) {
+ val navController = LocalNavController.current
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+ val drawerState = LocalDrawerState.current
+
+ var showSortDialog by remember { mutableStateOf?>(null) }
+ if (showSortDialog != null) SortDialog(
+ onDismissRequest = { showSortDialog = null },
+ list = showSortDialog!!
+ )
+
+ var showQuickEdit by remember { mutableStateOf(null) }
+ if (showQuickEdit != null) {
+ QuickEditBottomSheet(onDismissRequest = {
+ appDb.systemTtsDao.insertTts(showQuickEdit!!)
+ if (showQuickEdit?.isEnabled == true) SystemTtsService.notifyUpdateConfig()
+ showQuickEdit = null
+ }, systts = showQuickEdit!!, onSysttsChange = {
+ showQuickEdit = it
+ })
+ }
+
+ fun navigateToEdit(systts: SystemTts) {
+ navController.navigate(NavRoutes.TtsEdit.id, Bundle().apply {
+ putParcelable(NavRoutes.TtsEdit.DATA, systts)
+ })
+ }
+
+ // 长按Item拖拽提示
+ var hasShownTip by rememberSaveable { mutableStateOf(false) }
+
+ var showTagClearDialog by remember { mutableStateOf(null) }
+ if (showTagClearDialog != null) {
+ val systts = showTagClearDialog!!
+ TagDataClearConfirmDialog(
+ tagData = systts.speechRule.tagData.toString(),
+ onDismissRequest = { showTagClearDialog = null },
+ onConfirm = {
+ systts.speechRule.target = SpeechTarget.ALL
+ systts.speechRule.resetTag()
+ appDb.systemTtsDao.updateTts(systts)
+ if (systts.isEnabled) SystemTtsService.notifyUpdateConfig()
+ showTagClearDialog = null
+ }
+ )
+ }
+
+ fun switchSpeechTarget(systts: SystemTts) {
+ if (!hasShownTip) {
+ hasShownTip = true
+ context.longToast(R.string.systts_drag_tip_msg)
+ }
+
+ val model = systts.copy()
+ if (model.speechRule.target == SpeechTarget.BGM) return
+
+ if (model.speechRule.target == SpeechTarget.CUSTOM_TAG) appDb.speechRuleDao.getByRuleId(
+ model.speechRule.tagRuleId
+ )?.let { speechRule ->
+ val keys = speechRule.tags.keys.toList()
+ val idx = keys.indexOf(model.speechRule.tag)
+
+ val nextIndex = (idx + 1)
+ val newTag = keys.getOrNull(nextIndex)
+ if (newTag == null) {
+ if (model.speechRule.isTagDataEmpty()) {
+ model.speechRule.target = SpeechTarget.ALL
+ model.speechRule.resetTag()
+ } else {
+ showTagClearDialog = model
+ return
+ }
+ } else {
+ model.speechRule.tag = newTag
+ runCatching {
+ model.speechRule.tagName =
+ SpeechRuleEngine.getTagName(context, speechRule, info = model.speechRule)
+ }.onFailure {
+ model.speechRule.tagName = ""
+ context.displayErrorDialog(it)
+ }
+
+ }
+ }
+ else {
+ appDb.speechRuleDao.getByRuleId(model.speechRule.tagRuleId)?.let {
+ model.speechRule.target = SpeechTarget.CUSTOM_TAG
+ model.speechRule.tag = it.tags.keys.first()
+ }
+ }
+
+ appDb.systemTtsDao.updateTts(model)
+ if (model.isEnabled) SystemTtsService.notifyUpdateConfig()
+ }
+
+ var deleteTts by remember { mutableStateOf(null) }
+ if (deleteTts != null) {
+ ConfigDeleteDialog(
+ onDismissRequest = { deleteTts = null }, name = deleteTts?.displayName ?: ""
+ ) {
+ appDb.systemTtsDao.deleteTts(deleteTts!!)
+ deleteTts = null
+ }
+ }
+
+ var groupAudioParamsDialog by remember { mutableStateOf(null) }
+ if (groupAudioParamsDialog != null) {
+ GroupAudioParamsDialog(onDismissRequest = { groupAudioParamsDialog = null },
+ params = groupAudioParamsDialog!!.audioParams,
+ onConfirm = {
+ appDb.systemTtsDao.updateGroup(
+ groupAudioParamsDialog!!.copy(audioParams = it)
+ )
+
+ groupAudioParamsDialog = null
+ })
+ }
+
+ val models by vm.list.collectAsStateWithLifecycle()
+ val listState = rememberLazyListState()
+ LazyListIndexStateSaver(models = models, listState = listState)
+
+ val reorderState = rememberReorderableLazyListState(
+ listState = listState, onMove = vm::reorder
+ )
+
+ LaunchedEffect(models) {
+ println("update models: ${models.size}")
+ }
+
+ var addGroupDialog by remember { mutableStateOf(false) }
+ if (addGroupDialog) {
+ var name by remember { mutableStateOf("") }
+ TextFieldDialog(title = stringResource(id = R.string.add_group),
+ text = name,
+ onTextChange = { name = it },
+ onDismissRequest = { addGroupDialog = false }) {
+ addGroupDialog = false
+ appDb.systemTtsDao.insertGroup(SystemTtsGroup(name = name))
+ }
+ }
+
+ var showGroupExportSheet by remember { mutableStateOf?>(null) }
+ if (showGroupExportSheet != null) {
+ val list = showGroupExportSheet!!
+ ListExportBottomSheet(onDismissRequest = { showGroupExportSheet = null }, list = list)
+ }
+
+ var showExportSheet by remember { mutableStateOf?>(null) }
+ if (showExportSheet != null) {
+ val jStr = remember { AppConst.jsonBuilder.encodeToString(showExportSheet!!) }
+ ConfigExportBottomSheet(json = jStr) { showExportSheet = null }
+ }
+
+ var addPluginDialog by remember { mutableStateOf(false) }
+ if (addPluginDialog) {
+ PluginSelectionDialog(onDismissRequest = { addPluginDialog = false }) {
+ navigateToEdit(SystemTts(tts = PluginTTS(pluginId = it.pluginId)))
+ }
+ }
+
+ var showAuditionDialog by remember { mutableStateOf(null) }
+ if (showAuditionDialog != null) AuditionDialog(systts = showAuditionDialog!!) {
+ showAuditionDialog = null
+ }
+
+ var showOptions by rememberSaveable { mutableStateOf(false) }
+
+ Scaffold(
+ topBar = {
+ NavTopAppBar(drawerState = drawerState, title = {
+ Text(stringResource(id = R.string.system_tts))
+ }, actions = {
+ var showAddMenu by remember { mutableStateOf(false) }
+ IconButton(onClick = { showAddMenu = true }) {
+ Icon(Icons.Default.Add, stringResource(id = R.string.add_config))
+
+ DropdownMenu(expanded = showAddMenu,
+ onDismissRequest = { showAddMenu = false }) {
+
+ @Composable
+ fun MenuItem(
+ icon: @Composable () -> Unit,
+ @StringRes title: Int,
+ onClick: () -> Unit
+ ) {
+ DropdownMenuItem(text = {
+ Text(stringResource(id = title))
+ }, onClick = {
+ showAddMenu = false
+ onClick()
+ }, leadingIcon = icon)
+ }
+
+ MenuItem(
+ icon = { Icon(Icons.AutoMirrored.Default.PlaylistAdd, null) },
+ title = R.string.systts_add_internal_tts
+ ) {
+ navigateToEdit(SystemTts(tts = MsTTS()))
+ }
+
+ MenuItem(
+ icon = { Icon(Icons.Default.PhoneAndroid, null) },
+ title = R.string.add_local_tts
+ ) {
+ navigateToEdit(SystemTts(tts = LocalTTS()))
+ }
+
+// MenuItem(
+// icon = { Icon(Icons.Default.Http, null) },
+// title = R.string.systts_add_custom_tts
+// ) {
+//// startTtsEditor(HttpTtsEditActivity::class.java)
+// }
+
+ MenuItem(
+ icon = { Icon(Icons.Default.Javascript, null) },
+ title = R.string.systts_add_plugin_tts
+ ) {
+ addPluginDialog = true
+ }
+
+ MenuItem(
+ icon = { Icon(Icons.Default.Audiotrack, null) },
+ title = R.string.add_bgm_tts
+ ) {
+ navigateToEdit(SystemTts(tts = BgmTTS()))
+ }
+
+ MenuItem(
+ icon = { Icon(Icons.Default.AddCard, null) },
+ title = R.string.add_group
+ ) {
+ addGroupDialog = true
+ }
+ }
+ }
+
+ IconButton(onClick = { showOptions = true }) {
+ Icon(Icons.Default.MoreVert, stringResource(id = R.string.more_options))
+ MenuMoreOptions(
+ expanded = showOptions,
+ onDismissRequest = { showOptions = false },
+ onExportAll = { showGroupExportSheet = models },
+ )
+ }
+ })
+ },
+ ) { paddingValues ->
+ Box(Modifier.padding(top = paddingValues.calculateTopPadding())) {
+ LazyColumn(
+ Modifier
+ .fillMaxSize()
+ .reorderable(state = reorderState),
+ state = listState
+ ) {
+ models.forEachIndexed { _, groupWithSystemTts ->
+ val g = groupWithSystemTts.group
+ val checkState =
+ groupWithSystemTts.list.filter { it.isEnabled }.size.sizeToToggleableState(
+ groupWithSystemTts.list.size
+ )
+ val key = "g_${g.id}"
+ stickyHeader(key = key) {
+ ShadowReorderableItem(reorderableState = reorderState, key = key) {
+ Group(modifier = Modifier.detectReorderAfterLongPress(reorderState),
+ name = g.name,
+ isExpanded = g.isExpanded,
+ toggleableState = checkState,
+ onToggleableStateChange = {
+ vm.updateGroupEnable(groupWithSystemTts, it)
+ },
+ onClick = {
+ appDb.systemTtsDao.updateGroup(g.copy(isExpanded = !g.isExpanded))
+ },
+ onDelete = {
+ appDb.systemTtsDao.deleteTts(*groupWithSystemTts.list.toTypedArray())
+ appDb.systemTtsDao.deleteGroup(g)
+ },
+ onRename = {
+ appDb.systemTtsDao.updateGroup(g.copy(name = it))
+ },
+ onCopy = {
+ scope.launch {
+ val group = g.copy(id = System.currentTimeMillis(),
+ name = it.ifBlank { context.getString(R.string.unnamed) })
+ appDb.systemTtsDao.insertGroup(group)
+ appDb.systemTtsDao.getTtsByGroup(g.id)
+ .forEachIndexed { index, tts ->
+ appDb.systemTtsDao.insertTts(
+ tts.copy(
+ id = System.currentTimeMillis() + index,
+ groupId = group.id
+ )
+ )
+ }
+ }
+ },
+ onEditAudioParams = {
+ groupAudioParamsDialog = g
+ },
+ onExport = {
+ showGroupExportSheet = listOf(groupWithSystemTts)
+ },
+ onSort = {
+ showSortDialog = groupWithSystemTts.list
+ }
+ )
+ }
+ }
+
+ if (g.isExpanded) {
+ itemsIndexed(groupWithSystemTts.list.sortedBy { it.order },
+ key = { _, v -> "${g.id}_${v.id}" }) { _, item ->
+ if (g.id == 1L) println(item.displayName + ", " + item.order)
+
+ ShadowReorderableItem(
+ reorderableState = reorderState,
+ key = "${g.id}_${item.id}"
+ ) {
+ Item(reorderState = reorderState,
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
+ name = item.displayName ?: "",
+ tagName = item.speechRule.tagName,
+ type = item.tts.getType(),
+ standby = item.speechRule.isStandby,
+ enabled = item.isEnabled,
+ onEnabledChange = {
+ vm.updateTtsEnabled(item, it)
+ if (it) SystemTtsService.notifyUpdateConfig()
+ },
+ desc = item.tts.getDescription(),
+ params = item.tts.getBottomContent(),
+ onClick = { showQuickEdit = item },
+ onLongClick = { switchSpeechTarget(item) },
+ onCopy = {
+ navigateToEdit(item.copy(id = System.currentTimeMillis()))
+ },
+ onDelete = { deleteTts = item },
+ onEdit = {
+ navController.navigate(
+ NavRoutes.TtsEdit.id,
+ Bundle().apply {
+ putParcelable(NavRoutes.TtsEdit.DATA, item)
+ }
+ )
+ },
+ onAudition = { showAuditionDialog = item },
+ onExport = {
+ showExportSheet =
+ listOf(item.copy(groupId = AbstractListGroup.DEFAULT_GROUP_ID))
+ }
+ )
+ }
+ }
+ }
+ }
+
+ item {
+ Spacer(Modifier.height(60.dp))
+ }
+ }
+
+
+ LaunchedEffect(key1 = Unit) {
+ withIO {
+ vm.checkListData(context)
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListManagerViewModel.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListManagerViewModel.kt
new file mode 100644
index 000000000..560eafb8d
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/ListManagerViewModel.kt
@@ -0,0 +1,159 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import android.content.Context
+import android.util.Log
+import androidx.compose.ui.graphics.vector.DefaultGroupName
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.conf.SystemTtsConfig
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.constant.SpeechTarget
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.AbstractListGroup.Companion.DEFAULT_GROUP_ID
+import com.github.jing332.tts_server_android.data.entities.systts.GroupWithSystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTtsGroup
+import com.github.jing332.tts_server_android.utils.FileUtils.readAllText
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.launch
+import org.burnoutcrew.reorderable.ItemPosition
+import java.util.Collections
+
+class ListManagerViewModel : ViewModel() {
+ companion object {
+ const val TAG = "ListManagerViewModel"
+ }
+
+ private val _list = MutableStateFlow>(emptyList())
+ val list: StateFlow> get() = _list
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ appDb.systemTtsDao.updateAllOrder()
+ appDb.systemTtsDao.getFlowAllGroupWithTts().conflate().collectLatest {
+ _list.value = it
+ }
+ }
+ }
+
+ fun updateTtsEnabled(item: SystemTts, enabled: Boolean) {
+ if (!SystemTtsConfig.isVoiceMultipleEnabled.value && enabled)
+ appDb.systemTtsDao.allEnabledTts.forEach { systts ->
+ if (systts.speechRule.target == SpeechTarget.BGM || systts.speechRule.isStandby)
+ return@forEach
+
+ if (systts.speechRule.target == item.speechRule.target) {
+ if (systts.speechRule.tagRuleId == item.speechRule.tagRuleId
+ && systts.speechRule.tag == item.speechRule.tag
+ && systts.speechRule.tagName == item.speechRule.tagName
+ && systts.speechRule.isStandby == item.speechRule.isStandby
+ )
+ appDb.systemTtsDao.updateTts(systts.copy(isEnabled = false))
+ }
+ }
+
+ appDb.systemTtsDao.updateTts(item.copy(isEnabled = enabled))
+ }
+
+ fun updateGroupEnable(
+ item: GroupWithSystemTts,
+ enabled: Boolean
+ ) {
+ if (!SystemTtsConfig.isGroupMultipleEnabled.value && enabled) {
+ list.value.forEach {
+ it.list.forEach { systts ->
+ if (systts.isEnabled)
+ appDb.systemTtsDao.updateTts(systts.copy(isEnabled = false))
+ }
+ }
+ }
+
+ appDb.systemTtsDao.updateTts(
+ *item.list.filter { it.isEnabled != enabled }.map { it.copy(isEnabled = enabled) }
+ .toTypedArray()
+ )
+ }
+
+ fun reorder(from: ItemPosition, to: ItemPosition) {
+ if (from.key is String && to.key is String) {
+ val fromKey = from.key as String
+ val toKey = to.key as String
+
+ if (fromKey.startsWith("g") && toKey.startsWith("g")) {
+ val mList = list.value.map { it.group }.toMutableList()
+
+ val fromId = fromKey.substring(2).toLong()
+ val fromIndex = mList.indexOfFirst { it.id == fromId }
+
+ val toId = toKey.substring(2).toLong()
+ val toIndex = mList.indexOfFirst { it.id == toId }
+
+ try {
+ Collections.swap(mList, fromIndex, toIndex)
+ } catch (_: IndexOutOfBoundsException) {
+ return
+ }
+ mList.forEachIndexed { index, systemTtsGroup ->
+ if (systemTtsGroup.order != index)
+ appDb.systemTtsDao.updateGroup(systemTtsGroup.copy(order = index))
+ }
+ } else if (!fromKey.startsWith("g") && !toKey.startsWith("g")) {
+ val (fromGId, fromId) = fromKey.split("_").map { it.toLong() }
+ val (toGId, toId) = toKey.split("_").map { it.toLong() }
+ if (fromGId != toGId) return
+
+ val listInGroup = findListInGroup(fromGId).toMutableList()
+ val fromIndex = listInGroup.indexOfFirst { it.id == fromId }
+ val toIndex = listInGroup.indexOfFirst { it.id == toId }
+ Log.d(TAG, "fromIndex: $fromIndex, toIndex: $toIndex")
+
+ try {
+ Collections.swap(listInGroup, fromIndex, toIndex)
+ } catch (_: IndexOutOfBoundsException) {
+ return
+ }
+
+ listInGroup.forEachIndexed { index, systts ->
+ Log.d(TAG, "$index ${systts.displayName}")
+ if (systts.order != index)
+ appDb.systemTtsDao.updateTts(systts.copy(order = index))
+ }
+ }
+
+ }
+ }
+
+ private fun findListInGroup(groupId: Long): List {
+ return list.value.find { it.group.id == groupId }?.list?.sortedBy { it.order }
+ ?: emptyList()
+ }
+
+ fun checkListData(context: Context) {
+ appDb.systemTtsDao.getGroup(DEFAULT_GROUP_ID) ?: kotlin.run {
+ appDb.systemTtsDao.insertGroup(
+ SystemTtsGroup(
+ DEFAULT_GROUP_ID,
+ context.getString(R.string.default_group),
+ appDb.systemTtsDao.groupCount
+ )
+ )
+ }
+
+ if (appDb.systemTtsDao.ttsCount == 0)
+ importDefaultListData(context)
+ }
+
+ private fun importDefaultListData(context: Context) {
+ val json = context.assets.open("defaultData/list.json").readAllText()
+ val list = AppConst.jsonBuilder.decodeFromString>(json)
+ viewModelScope.launch(Dispatchers.IO) {
+ appDb.systemTtsDao.insertGroupWithTts(*list.toTypedArray())
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/MenuMoreOptions.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/MenuMoreOptions.kt
new file mode 100644
index 000000000..3a1758ea6
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/MenuMoreOptions.kt
@@ -0,0 +1,178 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Audiotrack
+import androidx.compose.material.icons.filled.ContentCut
+import androidx.compose.material.icons.filled.Group
+import androidx.compose.material.icons.filled.Input
+import androidx.compose.material.icons.filled.ManageSearch
+import androidx.compose.material.icons.filled.MenuBook
+import androidx.compose.material.icons.filled.Output
+import androidx.compose.material.icons.filled.SmartDisplay
+import androidx.compose.material.icons.filled.Speed
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.asAppCompatActivity
+import com.github.jing332.tts_server_android.compose.systts.plugin.PluginManagerActivity
+import com.github.jing332.tts_server_android.compose.systts.replace.ReplaceManagerActivity
+import com.github.jing332.tts_server_android.compose.systts.speechrule.SpeechRuleManagerActivity
+import com.github.jing332.tts_server_android.compose.widgets.CheckedMenuItem
+import com.github.jing332.tts_server_android.conf.SystemTtsConfig
+import com.github.jing332.tts_server_android.utils.startActivity
+
+@Composable
+internal fun MenuMoreOptions(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ onExportAll: () -> Unit,
+) {
+ var showBgmSettingsDialog by remember { mutableStateOf(false) }
+ if (showBgmSettingsDialog)
+ BgmSettingsDialog { showBgmSettingsDialog = false }
+
+ var showImportSheet by remember { mutableStateOf(false) }
+ if (showImportSheet)
+ ListImportBottomSheet(onDismissRequest = { showImportSheet = false })
+
+ var showInternalPlayerDialog by remember { mutableStateOf(false) }
+ if (showInternalPlayerDialog)
+ InternalPlayerDialog {
+ showInternalPlayerDialog = false
+ }
+
+ var showAudioParamsDialog by remember { mutableStateOf(false) }
+ if (showAudioParamsDialog)
+ GlobalAudioParamsDialog {
+ showAudioParamsDialog = false
+ }
+
+ val context = LocalContext.current
+ val activity = remember { context.asAppCompatActivity() }
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest
+ ) {
+
+ var isSplit by remember { SystemTtsConfig.isSplitEnabled }
+ CheckedMenuItem(
+ text = { Text(stringResource(id = R.string.systts_split_long_sentences)) },
+ checked = isSplit,
+ onClick = {
+ isSplit = it
+ },
+ leadingIcon = {
+ Icon(Icons.Default.ContentCut, null)
+ }
+ )
+
+ var isMultiVoice by remember { SystemTtsConfig.isMultiVoiceEnabled }
+ CheckedMenuItem(
+ text = { Text(stringResource(id = R.string.systts_multi_voice_option)) },
+ checked = isMultiVoice,
+ onClick = {
+ isMultiVoice = it
+ },
+ leadingIcon = {
+ Icon(Icons.Default.Group, null)
+ },
+ )
+ HorizontalDivider()
+
+ var isInternalPlayer by remember { SystemTtsConfig.isInternalPlayerEnabled }
+ CheckedMenuItem(
+ text = { Text(stringResource(id = R.string.systts_use_internal_audio_player)) },
+ checked = isInternalPlayer,
+ onClick = { showInternalPlayerDialog = true },
+ onClickCheckBox = { isInternalPlayer = it },
+ leadingIcon = {
+ Icon(Icons.Default.SmartDisplay, null)
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.audio_params)) },
+ onClick = { showAudioParamsDialog = true },
+ leadingIcon = {
+ Icon(Icons.Default.Speed, null)
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.bgm_settings)) },
+ onClick = { showBgmSettingsDialog = true },
+ leadingIcon = {
+ Icon(Icons.Default.Audiotrack, null)
+ }
+ )
+
+ HorizontalDivider()
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.speech_rule_manager)) },
+ onClick = {
+ onDismissRequest()
+ context.startActivity(SpeechRuleManagerActivity::class.java)
+ },
+ leadingIcon = {
+ Icon(Icons.Default.MenuBook, null)
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.plugin_manager)) },
+ onClick = {
+ onDismissRequest()
+ context.startActivity(PluginManagerActivity::class.java)
+ },
+ leadingIcon = {
+ Icon(painterResource(id = R.drawable.ic_shortcut_plugin), null)
+ }
+ )
+
+ CheckedMenuItem(
+ text = { Text(stringResource(id = R.string.replace_rule_manager)) },
+ checked = SystemTtsConfig.isReplaceEnabled.value,
+ onClick = {
+ onDismissRequest()
+ context.startActivity(ReplaceManagerActivity::class.java)
+ },
+ onClickCheckBox = {
+ SystemTtsConfig.isReplaceEnabled.value = it
+ },
+ leadingIcon = {
+ Icon(Icons.Default.ManageSearch, null)
+ }
+ )
+
+ HorizontalDivider()
+ DropdownMenuItem(text = {
+ Text(stringResource(id = R.string.import_config))
+ }, onClick = {
+ onDismissRequest()
+ showImportSheet = true },
+ leadingIcon = {
+ Icon(Icons.Default.Input, null)
+ }
+ )
+
+ DropdownMenuItem(text = {
+ Text(stringResource(id = R.string.export_config))
+ }, onClick = {
+ onDismissRequest()
+ onExportAll()
+ }, leadingIcon = {
+ Icon(Icons.Default.Output, null)
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/PluginSelectionDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/PluginSelectionDialog.kt
new file mode 100644
index 000000000..ae0fa5fc1
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/PluginSelectionDialog.kt
@@ -0,0 +1,81 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.plugin.Plugin
+
+@Composable
+fun PluginSelectionDialog(onDismissRequest: () -> Unit, onSelect: (Plugin) -> Unit) {
+ AlertDialog(onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(id = R.string.select_plugin)) },
+ text = {
+ val plugins = appDb.pluginDao.allEnabled
+ if (plugins.isEmpty())
+ Text(
+ stringResource(id = R.string.no_plugins),
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth(),
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.error
+ )
+
+ LazyColumn {
+ items(plugins, { it.id }) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .minimumInteractiveComponentSize()
+ .clip(MaterialTheme.shapes.small)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple()
+ ) { onSelect(it) }
+ .padding(vertical = 4.dp)
+ ) {
+ Text(
+ text = it.name,
+ modifier = Modifier
+ .padding(horizontal = 8.dp),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Text(
+ text = it.pluginId,
+ modifier = Modifier
+ .padding(horizontal = 8.dp),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = R.string.cancel))
+ }
+ }
+ )
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/SortDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/SortDialog.kt
new file mode 100644
index 000000000..8c08d6e17
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/SortDialog.kt
@@ -0,0 +1,50 @@
+package com.github.jing332.tts_server_android.compose.systts.list
+
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import com.drake.net.utils.withIO
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.systts.ListSortSettingsDialog
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTts
+
+internal enum class SortFields(@StringRes val strResId: Int) {
+ NAME(R.string.name),
+ TAG_NAME(R.string.tag),
+ TYPE(R.string.type),
+ ENABLE(R.string.enabled),
+ ID(R.string.created_time_id)
+}
+
+@Composable
+internal fun SortDialog(onDismissRequest: () -> Unit, list: List) {
+ var index by remember { mutableIntStateOf(0) }
+ ListSortSettingsDialog(
+ name = list.size.toString(),
+ index = index,
+ onIndexChange = { index = it },
+ onDismissRequest = onDismissRequest,
+ entries = SortFields.values().map { stringResource(id = it.strResId) },
+ onConfirm = { _, descending ->
+ withIO {
+ val sortedList = when (SortFields.values()[index]) {
+ SortFields.NAME -> list.sortedBy { it.displayName }
+ SortFields.TAG_NAME -> list.sortedBy { it.speechRule.tagName }
+ SortFields.TYPE -> list.sortedBy { it.tts.getType() }
+ SortFields.ENABLE -> list.sortedBy { it.isEnabled }
+ SortFields.ID -> list.sortedBy { it.id }
+ }.run {
+ if (descending) this.reversed() else this
+ }
+ sortedList.forEachIndexed { i, systemTts ->
+ appDb.systemTtsDao.updateTts(systemTts.copy(order = i))
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/edit/BasicInfoEditScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/edit/BasicInfoEditScreen.kt
new file mode 100644
index 000000000..51dda0d2b
--- /dev/null
+++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/list/edit/BasicInfoEditScreen.kt
@@ -0,0 +1,507 @@
+package com.github.jing332.tts_server_android.compose.systts.list.edit
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.HelpOutline
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.SmartDisplay
+import androidx.compose.material.icons.filled.Speed
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import com.github.jing332.tts_server_android.R
+import com.github.jing332.tts_server_android.compose.systts.list.BasicAudioParamsDialog
+import com.github.jing332.tts_server_android.compose.systts.list.edit.ui.SaveActionHandler
+import com.github.jing332.tts_server_android.compose.systts.list.edit.ui.widgets.InternalPlayerDialog
+import com.github.jing332.tts_server_android.compose.widgets.AppDialog
+import com.github.jing332.tts_server_android.compose.widgets.AppSpinner
+import com.github.jing332.tts_server_android.compose.widgets.RowToggleButtonGroup
+import com.github.jing332.tts_server_android.constant.AppConst
+import com.github.jing332.tts_server_android.constant.SpeechTarget
+import com.github.jing332.tts_server_android.data.appDb
+import com.github.jing332.tts_server_android.data.entities.AbstractListGroup.Companion.DEFAULT_GROUP_ID
+import com.github.jing332.tts_server_android.data.entities.SpeechRule
+import com.github.jing332.tts_server_android.data.entities.systts.AudioParams
+import com.github.jing332.tts_server_android.data.entities.systts.SpeechRuleInfo
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTts
+import com.github.jing332.tts_server_android.data.entities.systts.SystemTtsGroup
+import com.github.jing332.tts_server_android.model.rhino.speech_rule.SpeechRuleEngine
+import com.github.jing332.tts_server_android.model.speech.tts.ITextToSpeechEngine
+import com.github.jing332.tts_server_android.ui.view.AppDialogs.displayErrorDialog
+import com.github.jing332.tts_server_android.utils.ClipboardUtils
+import com.github.jing332.tts_server_android.utils.clone
+import com.github.jing332.tts_server_android.utils.longToast
+import com.github.jing332.tts_server_android.utils.toast
+import kotlinx.serialization.encodeToString
+
+@Composable
+fun BasicInfoEditScreen(
+ modifier: Modifier,
+ systts: SystemTts,
+ onSysttsChange: (SystemTts) -> Unit,
+
+ showSpeechTarget: Boolean = true,
+ group: SystemTtsGroup = rememberUpdatedState(
+ newValue = appDb.systemTtsDao.getGroup(systts.groupId)
+ ?: SystemTtsGroup(id = DEFAULT_GROUP_ID, name = "")
+ ).value,
+ groups: List = remember { appDb.systemTtsDao.allGroup },
+
+ speechRules: List = remember { appDb.speechRuleDao.allEnabled },
+) {
+ val context = LocalContext.current
+ val speechRule by rememberUpdatedState(newValue = speechRules.find { it.ruleId == systts.speechRule.tagRuleId })
+
+ // 确保在 SaveActionHandler 中始终引用最新的obj
+ @Suppress("NAME_SHADOWING")
+ val systts by rememberUpdatedState(newValue = systts)
+
+ SaveActionHandler {
+ var tagName = ""
+ if (speechRule != null) {
+ runCatching {
+ tagName =
+ SpeechRuleEngine.getTagName(context, speechRule!!, info = systts.speechRule)
+ }.onFailure {
+ context.displayErrorDialog(it, "获取标签名失败")
+ }
+ }
+
+ tagName = tagName.ifBlank {
+ speechRule?.tags?.getOrDefault(systts.speechRule.tag, "") ?: ""
+ }
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(tagName = tagName)
+ )
+ )
+
+ true
+ }
+
+ var showStandbyHelpDialog by remember { mutableStateOf(false) }
+ if (showStandbyHelpDialog)
+ AppDialog(
+ title = { Text(stringResource(id = R.string.systts_as_standby_help)) },
+ content = {
+ Text(
+ stringResource(id = R.string.systts_standby_help_msg)
+ )
+ },
+ buttons = {
+ TextButton(onClick = { showStandbyHelpDialog = false }) {
+ Text(stringResource(id = R.string.confirm))
+ }
+ },
+ onDismissRequest = { showStandbyHelpDialog = false }
+ )
+
+
+ var showPlayerParamsDialog by remember { mutableStateOf(false) }
+ if (showPlayerParamsDialog)
+ InternalPlayerDialog(
+ onDismissRequest = { showPlayerParamsDialog = false },
+ params = systts.tts.audioPlayer,
+ onParamsChange = {
+ onSysttsChange(
+ systts.copy(
+ tts = systts.tts.clone()!!.apply { audioPlayer = it }
+ )
+ )
+ }
+ )
+
+ var showParamsDialog by remember { mutableStateOf(false) }
+ if (showParamsDialog) {
+ val params = systts.tts.audioParams
+ fun changeParams(
+ speed: Float = params.speed,
+ volume: Float = params.volume,
+ pitch: Float = params.pitch
+ ) {
+ onSysttsChange(
+ systts.copy(
+ tts = systts.tts.clone()!!.apply {
+ audioParams = AudioParams(speed, volume, pitch)
+ }
+ )
+ )
+ }
+ BasicAudioParamsDialog(
+ onDismissRequest = { showParamsDialog = false },
+ speed = params.speed,
+ volume = params.volume,
+ pitch = params.pitch,
+
+ onSpeedChange = { changeParams(speed = it) },
+ onVolumeChange = { changeParams(volume = it) },
+ onPitchChange = { changeParams(pitch = it) },
+
+ onReset = { changeParams(0f, 0f, 0f) }
+ )
+ }
+
+ Column(modifier) {
+ if (showSpeechTarget)
+ Column(Modifier.fillMaxWidth()) {
+ Row(
+ Modifier
+ .align(Alignment.CenterHorizontally)
+ .horizontalScroll(rememberScrollState())
+ ) {
+ TextButton(onClick = { showParamsDialog = true }) {
+ Row {
+ Icon(Icons.Default.Speed, null)
+ Text(stringResource(id = R.string.audio_params))
+ }
+ }
+
+ TextButton(onClick = { showPlayerParamsDialog = true }) {
+ Row {
+ Icon(Icons.Default.SmartDisplay, null)
+ Text(stringResource(id = R.string.internal_player))
+ }
+ }
+
+ Row(
+ Modifier
+ .minimumInteractiveComponentSize()
+ .clip(MaterialTheme.shapes.medium)
+ .clickable(role = Role.Checkbox) {
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(
+ isStandby = !systts.speechRule.isStandby
+ )
+ )
+ )
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Checkbox(checked = systts.speechRule.isStandby, onCheckedChange = null)
+ Text(stringResource(id = R.string.as_standby))
+ IconButton(onClick = { showStandbyHelpDialog = true }) {
+ Icon(
+ Icons.AutoMirrored.Filled.HelpOutline,
+ stringResource(id = R.string.systts_as_standby_help)
+ )
+ }
+ }
+ }
+
+ var showTagClearDialog by remember { mutableStateOf(false) }
+ if (showTagClearDialog) {
+ TagDataClearConfirmDialog(
+ systts.speechRule.tagData.toString(),
+ onDismissRequest = { showTagClearDialog = false },
+ onConfirm = {
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(
+ tagName = "",
+ target = SpeechTarget.ALL
+ ).apply { resetTag() }
+ )
+ )
+ showTagClearDialog = false
+ })
+ }
+ Column(
+ modifier = Modifier
+ .wrapContentWidth()
+ .align(Alignment.CenterHorizontally),
+ ) {
+
+ var showTagOptions by remember { mutableStateOf(false) }
+ RowToggleButtonGroup(
+ selectionIndex = if (systts.speechRule.target == SpeechTarget.ALL) 0 else 1,
+ buttonCount = 2,
+ buttonIcons = arrayOf(
+ painterResource(id = R.drawable.ic_baseline_select_all_24),
+ painterResource(id = R.drawable.baseline_tag_24)
+ ),
+ buttonTexts = arrayOf(
+ stringResource(id = R.string.ra_all),
+ stringResource(id = R.string.tag)
+ ),
+ onButtonClick = { index ->
+ if (index == 1) {
+ if (systts.speechRule.target == SpeechTarget.CUSTOM_TAG)
+ showTagOptions = true
+ else
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(target = SpeechTarget.CUSTOM_TAG)
+ )
+ )
+ } else { // 朗读全部
+ if (systts.speechRule.isTagDataEmpty())
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(
+ tagName = "",
+ target = SpeechTarget.ALL
+ ).apply { resetTag() }
+ )
+ )
+ else
+ showTagClearDialog = true
+ }
+ },
+ )
+
+ DropdownMenu(
+ expanded = showTagOptions,
+ onDismissRequest = { showTagOptions = false }) {
+ Text(
+ text = stringResource(R.string.tag_data),
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ HorizontalDivider()
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.copy)) },
+ onClick = {
+ showTagOptions = false
+ val info = systts.speechRule
+ val jStr = AppConst.jsonBuilder.encodeToString(info)
+ ClipboardUtils.copyText(jStr)
+ context.toast(R.string.copied)
+ })
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.paste)) },
+ onClick = {
+ showTagOptions = false
+ val jStr = ClipboardUtils.text.toString()
+ if (jStr.isBlank()) {
+ context.toast(R.string.format_error)
+ return@DropdownMenuItem
+ }
+
+ runCatching {
+ val info =
+ AppConst.jsonBuilder.decodeFromString(jStr)
+ onSysttsChange(systts.copy(speechRule = info))
+ }.onSuccess {
+ context.longToast(R.string.save_success)
+ }.onFailure {
+ context.displayErrorDialog(
+ it,
+ context.getString(R.string.format_error)
+ )
+ }
+ })
+ }
+ }
+
+ AnimatedVisibility(visible = systts.speechRule.target == SpeechTarget.CUSTOM_TAG) {
+ Row(Modifier.padding(top = 4.dp)) {
+ AppSpinner(
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 4.dp),
+ label = { Text(stringResource(id = R.string.speech_rule_script)) },
+ value = systts.speechRule.tagRuleId,
+ values = speechRules.map { it.ruleId },
+ entries = speechRules.map { it.name },
+ onSelectedChange = { k, v ->
+ if (systts.speechRule.target != SpeechTarget.CUSTOM_TAG) return@AppSpinner
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(
+ tagRuleId = k as String
+ )
+ )
+ )
+ }
+ )
+
+ speechRule?.let { speechRule ->
+ AppSpinner(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 4.dp),
+ label = { Text(stringResource(id = R.string.tag)) },
+ value = systts.speechRule.tag,
+ values = speechRule.tags.keys.toList(),
+ entries = speechRule.tags.values.toList(),
+ onSelectedChange = { k, _ ->
+ if (systts.speechRule.target != SpeechTarget.CUSTOM_TAG) return@AppSpinner
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(tag = k as String)
+ )
+ )
+ }
+ )
+ }
+ }
+ }
+
+ speechRule?.let {
+ CustomTagScreen(
+ systts = systts,
+ onSysttsChange = {
+ if (systts.speechRule.target == SpeechTarget.CUSTOM_TAG)
+ onSysttsChange(it)
+ },
+ speechRule = it
+ )
+ }
+ }
+
+ AppSpinner(
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text(stringResource(id = R.string.group)) },
+ value = group,
+ values = groups,
+ onValueSame = { current, new -> (current as SystemTtsGroup).id == (new as SystemTtsGroup).id },
+ entries = groups.map { it.name },
+ onSelectedChange = { k, _ ->
+ onSysttsChange(systts.copy(groupId = (k as SystemTtsGroup).id))
+ }
+ )
+ OutlinedTextField(
+ label = { Text(stringResource(id = R.string.display_name)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp),
+ value = systts.displayName ?: "", onValueChange = {
+ onSysttsChange(systts.copy(displayName = it))
+ },
+ trailingIcon = {
+ if (systts.displayName?.isNotEmpty() == true)
+ IconButton(onClick = {
+ onSysttsChange(systts.copy(displayName = ""))
+ }) {
+ Icon(Icons.Default.Clear, stringResource(id = R.string.clear_text_content))
+ }
+ }
+ )
+ }
+}
+
+@Composable
+private fun CustomTagScreen(
+ systts: SystemTts,
+ onSysttsChange: (SystemTts) -> Unit,
+ speechRule: SpeechRule
+) {
+ var showHelpDialog by remember { mutableStateOf("" to "") }
+ if (showHelpDialog.first.isNotEmpty()) {
+ AppDialog(title = { Text(showHelpDialog.first) }, content = {
+ Text(showHelpDialog.second)
+ }, buttons = {
+ TextButton(onClick = { showHelpDialog = "" to "" }) {
+ Text(stringResource(id = R.string.confirm))
+ }
+ }, onDismissRequest = { showHelpDialog = "" to "" })
+ }
+
+ Column(Modifier.padding(vertical = 4.dp)) {
+ speechRule.tagsData[systts.speechRule.tag]?.forEach { defTag ->
+ val key = defTag.key
+ val label = defTag.value["label"] ?: ""
+ val hint = defTag.value["hint"] ?: ""
+
+ val items = defTag.value["items"]
+ val value by rememberUpdatedState(newValue = systts.speechRule.tagData[key] ?: "")
+ if (items.isNullOrEmpty()) {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp),
+ leadingIcon = {
+ if (hint.isNotEmpty())
+ IconButton(onClick = { showHelpDialog = label to hint }) {
+ Icon(
+ Icons.AutoMirrored.Filled.HelpOutline,
+ stringResource(id = R.string.help)
+ )
+ }
+ },
+ label = { Text(label) },
+ value = value,
+ onValueChange = {
+ onSysttsChange(
+ systts.copy(
+ speechRule = systts.speechRule.copy(
+ tagData = systts.speechRule.tagData.toMutableMap().apply {
+ this[key] = it
+ }
+ )
+ )
+ )
+ }
+ )
+ } else {
+ val itemsMap by rememberUpdatedState(
+ newValue = AppConst.jsonBuilder.decodeFromString