Merge pull request #18 from AkiChase/dev

Scrcpy Mask v0.1.9
This commit is contained in:
如初 2024-05-11 17:06:06 +08:00 committed by GitHub
commit 61a9355354
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 945 additions and 324 deletions

65
README-zh.md Normal file
View File

@ -0,0 +1,65 @@
# Scrcpy-mask
为了实现电脑控制安卓设备,本人使用 Tarui + Vue 3 + Rust 开发了一款跨平台桌面客户端。该客户端能够提供可视化的鼠标和键盘按键映射配置。通过按键映射实现了实现类似安卓模拟器的多点触控操作,具有毫秒级响应速度。该工具可广泛用于电脑控制安卓设备玩手游等等,提供流畅的触控体验。
本项目仅实现了 Scrcpy 控制协议,**不提供投屏功能**。因为投屏会存在延迟和模糊问题,本项目另辟蹊径,直接放弃投屏,而使用透明的蒙版显示窗口背后的内容(可以使用 AVD 、手机厂商提供的低延迟投屏等),从根本上杜绝了 Scrcpy 的投屏体验差的问题。
## 特性
- [x] 有线、无线连接安卓设备
- [x] 启动并连接 Scrcpy 服务端
- [x] 实现 Scrcpy 控制协议
- [x] 鼠标和键盘按键映射
- [x] 可视化编辑按键映射配置
- [x] 按键映射配置的导入与导出
- [x] 更新检查
- [x] 在按键映射和插入文本之间切换
- [x] 国际化
- [ ] 手柄按键映射
- [ ] 更好的宏
- [ ] 通过 WebSocket 提供外部接口
- [ ] 帮助文档
## 视频演示
- [M 系列 Mac 电脑玩王者,暃排位实录,使用 Android Stuido 模拟器和开源 Scrcpy Mask 按键映射工具-哔哩哔哩](https://b23.tv/q6iDW1w)
- [自制跨平台开源项目 Scrcpy Mask ,像模拟器一样用键鼠控制任意安卓设备!以 M 系列芯片 MacBook 打王者为例-哔哩哔哩](https://b23.tv/gqmriXr)
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
- [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5)
## 截图
- 设备控制
![](https://pic.superbed.cc/item/6637190cf989f2fb975b6162.png)
- 可视化编辑按键映射配置
![](https://pic.superbed.cc/item/66371911f989f2fb975b62a3.png)
- 游戏控制
![](https://pic.superbed.cc/item/66373c8cf989f2fb97679dfd.png)
## 基本使用
1. 从 [releases](https://github.com/AkiChase/scrcpy-mask/releases) 中安装适合你系统平台的软件包
2. 确认你的安卓设备类型
1. 对于手机或平板电脑等物理设备
1. 你需要自己解决投屏的问题。推荐使用设备品牌的官方投屏方式,这样一般延迟最小。
2. 通过 USB 或无线方式在设备上启用 ADB 调试,然后将其连接到电脑。
2. 对于模拟器,不仅不需要投屏,而且模拟器通常默认启用 ADB 有线调试。所以几乎不用操作就能获得最好的体验。
3. 启动软件并导航到设备页面。
1. 在可用的设备中查找你的设备(如果未找到,请自行搜索如何为安装设备启用 ADB 调试)。
2. 右击你的设备并选择“获取屏幕大小”。根据获得的屏幕尺寸为参考,正确输入设备的宽度和高度。注意:如果宽度或高度不正确 (例如,在纵向和横向模式下这两个参数是颠倒的),所有触摸操作将被忽略,但是不会有任何错误消息。
3. 再次右击设备并选择“控制此设备”。
4. 导航到设置页面->蒙版设置,将蒙版的宽度和高度设置为设备大小的一定倍数,以确保蒙版大小合适。
5. 导航到蒙版页面,你可以在其中看到一个完全透明的蒙版区域。接下来,调整并移动模拟器窗口或投屏窗口,让其内容区域与透明蒙版区域完全对齐。
6. 导航到键映射页面,切换或编辑键映射配置。
7. 返回到蒙版界面,开始使用吧!
## 贡献
如果你对这个项目感兴趣,欢迎提 PR 或 Issue。但我的时间和精力有限所以可能无法全部及时处理。
[![Star History Chart](https://api.star-history.com/svg?repos=AkiChase/scrcpy-mask&type=Date)](https://star-history.com/#AkiChase/scrcpy-mask&Date)

View File

@ -1,8 +1,8 @@
# Scrcpy-mask
A Scrcpy client in Rust & Tarui aimed at providing mouse and key mapping to control Android device.
To achieve computer control of Android devices, I developed a cross-platform desktop client using Tarui + Vue 3 + Rust. This client provides visual mouse and keyboard mapping configuration, enabling multi-touch operations similar to Android emulators through key mapping, with millisecond-level response time. This tool can be widely used for controlling Android devices from computers to play mobile games, providing a smooth touch experience.
Due to the delay and blurred image quality of the mirror screen. This project found another way, directly abandoned the mirror screen, and instead used a transparent mask to display the screen content behind the window, which fundamentally put an end to the delay in casting the screen.
This project only implements the Scrcpy control protocol and **does not provide Screen mirroring**. Because screen mirroring may involve latency and blurriness issues, this project takes a different approach by directly abandoning screen mirroring and instead using a transparent mask to display the content behind the window (which can be AVD, low-latency screen mirroring provided by your phone manufacturers, etc.), Completely eliminates the problem of poor screen casting experience inherent in Scrcpy.
## Features
@ -14,7 +14,7 @@ Due to the delay and blurred image quality of the mirror screen. This project fo
- [x] Key mapping config import and export
- [x] Update check
- [x] Switch between key mapping and input-text box
- [ ] Internationalization (i18n)
- [x] Internationalization (i18n)
- [ ] Gamepad key mapping
- [ ] Better macro support
- [ ] Provide external interface through websocket
@ -45,14 +45,14 @@ Due to the delay and blurred image quality of the mirror screen. This project fo
1. Install software suitable for your system platform from [releases](https://github.com/AkiChase/scrcpy-mask/releases)
2. Identify your Android device type
1. For physical devices like phones or tablets
1. You need to solve the problem of screen casting on your own. Recommend using the official screen mirror method of your device brand to achieve the minimum delay
2. Enable ADB debugging on your device via USB or wirelessly, then connect it to your computer.
2. For emulator, you don't need screen mirror, and emulator generally default to enabling ADB wired debugging. So this is the best way for game, I think.
1. For physical devices like phones or tablets
1. You need to solve the problem of screen casting on your own. Recommend using the official screen mirror method of your device brand to achieve the minimum delay
2. Enable ADB debugging on your device via USB or wirelessly, then connect it to your computer.
2. For emulator, you don't need screen mirror, and emulator generally default to enabling ADB wired debugging. So this is the best way for game, I think.
3. Launch the software and navigate to the Device page.
1. Find your device among the available devices (if not found, please search for how to enable ADB debugging for your device).
2. Right-click on your device and choose "Get Screen Size". Use the obtained screen size as a reference and enter the device's width and height correctly. Note: If the width or height is incorrect (for example, they are reversed in portrait and landscape modes), all touch operations will be ignored, but no error message will appear.
3. Right-click on your device again and choose "Control this device".
1. Find your device among the available devices (if not found, please search for how to enable ADB debugging for your device).
2. Right-click on your device and choose "Get Screen Size". Use the obtained screen size as a reference and enter the device's width and height correctly. Note: If the width or height is incorrect (for example, they are reversed in portrait and landscape modes), all touch operations will be ignored, but no error message will appear.
3. Right-click on your device again and choose "Control this device".
4. Navigate to the Settings page -> Mask Settings, and set the width and height of the mask to a certain multiple of the device's size to ensure the mask size is appropriate.
5. Navigate to the Mask page where you can see a transparent mask. Next, adjust and move your emulator window or screen mirroring window to align the displayed content area with the transparent mask area.
6. Navigate to the Key mapping page and switch or edit the key mapping configs.
@ -62,4 +62,4 @@ Due to the delay and blurred image quality of the mirror screen. This project fo
If you are interested in this project, you are welcome to submit pull request or issue. But my time and energy is limited, so I may not be able to deal with it all.
[![Star History Chart](https://api.star-history.com/svg?repos=AkiChase/scrcpy-mask&type=Date)](https://star-history.com/#AkiChase/scrcpy-mask&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=AkiChase/scrcpy-mask&type=Date)](https://star-history.com/#AkiChase/scrcpy-mask&Date)

View File

@ -1,7 +1,7 @@
{
"name": "scrcpy-mask",
"private": true,
"version": "0.1.8",
"version": "0.1.9",
"type": "module",
"scripts": {
"dev": "vite",
@ -18,6 +18,7 @@
"@tauri-apps/plugin-store": "2.0.0-beta.2",
"pinia": "^2.1.7",
"vue": "^3.3.4",
"vue-i18n": "^9.13.1",
"vue-router": "4"
},
"devDependencies": {

View File

@ -1,6 +1,6 @@
[package]
name = "scrcpy-mask"
version = "0.1.8"
version = "0.1.9"
description = "A Tauri App"
authors = ["AkiChase"]
edition = "2021"
@ -11,12 +11,12 @@ edition = "2021"
tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies]
tauri = { version = "2.0.0-beta.15", features = ["macos-private-api", "devtools"] }
tauri-plugin-store = "2.0.0-beta.4"
tauri = { version = "2.0.0-beta.18", features = ["macos-private-api", "devtools"] }
tauri-plugin-store = "2.0.0-beta"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1.0"
tokio = { version = "1.36.0", features = ["rt-multi-thread", "net", "macros", "io-util", "time", "sync"] }
tauri-plugin-process = "2.0.0-beta.3"
tauri-plugin-shell = "2.0.0-beta.4"
tauri-plugin-http = "2.0.0-beta.7"
tauri-plugin-process = "2.0.0-beta"
tauri-plugin-shell = "2.0.0-beta"
tauri-plugin-http = "2.0.0-beta"

View File

@ -161,7 +161,7 @@ async fn main() {
let size_h = value["sizeH"].as_i64().unwrap_or(600);
let main_window: tauri::WebviewWindow =
app.get_webview_window("main").unwrap();
main_window.set_zoom(1.).unwrap();
main_window.set_zoom(1.).unwrap_or(());
main_window
.set_position(tauri::Position::Logical(tauri::LogicalPosition {
x: (pos_x - 70) as f64,

View File

@ -1,6 +1,6 @@
{
"productName": "scrcpy-mask",
"version": "0.1.8",
"version": "0.1.9",
"identifier": "com.akichase.mask",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@ -7,84 +7,13 @@ import {
NMessageProvider,
NDialogProvider,
} from "naive-ui";
import { Store } from "@tauri-apps/plugin-store";
import { KeyMappingConfig } from "./keyMappingConfig";
import { onMounted } from "vue";
import { useGlobalStore } from "./store/global";
import { useRouter } from "vue-router";
const store = useGlobalStore();
const router = useRouter();
onMounted(async () => {
router.replace({ name: "mask" });
const localStore = new Store("store.bin");
// loading screenSize from local store
const screenSize = await localStore.get<{ sizeW: number; sizeH: number }>(
"screenSize"
);
if (screenSize !== null) {
store.screenSizeW = screenSize.sizeW;
store.screenSizeH = screenSize.sizeH;
}
// loading keyMappingConfigList from local store
let keyMappingConfigList = await localStore.get<KeyMappingConfig[]>(
"keyMappingConfigList"
);
if (keyMappingConfigList === null || keyMappingConfigList.length === 0) {
// add empty key mapping config
// unable to get mask element when app is not ready
// so we use the stored mask area to get relative size
const maskArea = await localStore.get<{
posX: number;
posY: number;
sizeW: number;
sizeH: number;
}>("maskArea");
let relativeSize = { w: 800, h: 600 };
if (maskArea !== null) {
relativeSize = {
w: maskArea.sizeW,
h: maskArea.sizeH,
};
}
keyMappingConfigList = [
{
relativeSize,
title: "空白方案",
list: [],
},
];
await localStore.set("keyMappingConfigList", keyMappingConfigList);
}
store.keyMappingConfigList = keyMappingConfigList;
// loading curKeyMappingIndex from local store
let curKeyMappingIndex = await localStore.get<number>("curKeyMappingIndex");
if (
curKeyMappingIndex === null ||
curKeyMappingIndex >= keyMappingConfigList.length
) {
curKeyMappingIndex = 0;
localStore.set("curKeyMappingIndex", curKeyMappingIndex);
}
store.curKeyMappingIndex = curKeyMappingIndex;
// loading maskButton from local store
let maskButton = await localStore.get<{
show: boolean;
transparency: number;
}>("maskButton");
if (maskButton === null) {
maskButton = {
show: true,
transparency: 0.5,
};
await localStore.set("maskButton", maskButton);
}
store.maskButton = maskButton;
});
</script>

View File

@ -45,7 +45,9 @@ import { UnlistenFn, listen } from "@tauri-apps/api/event";
import { Store } from "@tauri-apps/plugin-store";
import { shutdown } from "../frontcommand/scrcpyMaskCmd";
import { useGlobalStore } from "../store/global";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const dialog = useDialog();
const store = useGlobalStore();
const message = useMessage();
@ -105,7 +107,7 @@ const tableCols: DataTableColumns = [
key: "id",
},
{
title: "Status",
title: t("pages.Device.status"),
key: "status",
},
];
@ -144,10 +146,10 @@ const tableRowProps = (_: any, index: number) => {
async function shutdownSC() {
dialog.warning({
title: "Warning",
content: "确定关闭Scrcpy控制服务?",
positiveText: "确定",
negativeText: "取消",
title: t("pages.Device.shutdown.title"),
content: t("pages.Device.shutdown.content"),
positiveText: t("pages.Device.shutdown.positiveText"),
negativeText: t("pages.Device.shutdown.negativeText"),
onPositiveClick: async () => {
await shutdown();
store.controledDevice = null;
@ -162,11 +164,11 @@ const menuY = ref(0);
const showMenu = ref(false);
const menuOptions: DropdownOption[] = [
{
label: () => h("span", "控制此设备"),
label: () => h("span", t("pages.Device.menu.control")),
key: "control",
},
{
label: () => h("span", "获取屏幕尺寸"),
label: () => h("span", t("pages.Device.menu.screen")),
key: "screen",
},
];
@ -181,7 +183,7 @@ async function deviceControl() {
}
if (!(store.screenSizeW > 0) || !(store.screenSizeH > 0)) {
message.error("请正确输入当前控制设备的屏幕尺寸");
message.error(t("pages.Device.deviceControl.inputScreenSize"));
store.screenSizeW = 0;
store.screenSizeH = 0;
store.hideLoading();
@ -189,7 +191,7 @@ async function deviceControl() {
}
if (store.controledDevice) {
message.error("请先关闭当前控制设备");
message.error(t("pages.Device.deviceControl.closeCurDevice"));
store.hideLoading();
return;
}
@ -198,7 +200,7 @@ async function deviceControl() {
sizeW: store.screenSizeW,
sizeH: store.screenSizeH,
});
message.info("屏幕尺寸已保存,正在启动控制服务,请保持设备亮屏");
message.info(t("pages.Device.deviceControl.controlInfo"));
const device = devices.value[rowIndex];
@ -216,7 +218,7 @@ async function deviceControl() {
await shutdown();
store.controledDevice = null;
store.hideLoading();
message.error("设备连接超时");
message.error(t("pages.Device.deviceControl.connectTimeout"));
}
}, 6000);
@ -239,7 +241,9 @@ async function deviceGetScreenSize() {
let id = devices.value[rowIndex].id;
const size = await getDeviceScreenSize(id);
store.hideLoading();
message.success(`设备屏幕尺寸为: ${size[0]} x ${size[1]}`);
message.success(
t("pages.Device.deviceGetScreenSize") + `${size[0]} x ${size[1]}`
);
}
async function onMenuSelect(key: string) {
@ -264,7 +268,7 @@ async function refreshDevices() {
async function connectDevice() {
if (!address.value) {
message.error("请输入无线调试地址");
message.error(t("pages.Device.inputWirelessAddress"));
return;
}
@ -278,51 +282,51 @@ async function connectDevice() {
<NScrollbar>
<div class="device">
<NSpin :show="store.showLoadingRef">
<NH4 prefix="bar">本地端口</NH4>
<NH4 prefix="bar">{{ $t("pages.Device.localPort") }}</NH4>
<NInputNumber
v-model:value="port"
:show-button="false"
:min="16384"
:max="49151"
placeholder="Scrcpy 本地端口"
:placeholder="$t('pages.Device.localPortPlaceholder')"
style="max-width: 300px"
/>
<NH4 prefix="bar">无线连接</NH4>
<NH4 prefix="bar">{{ $t("pages.Device.wireless") }}</NH4>
<NInputGroup style="max-width: 300px">
<NInput
v-model:value="address"
clearable
placeholder="无线调试地址"
:placeholder="$t('pages.Device.wirelessPlaceholder')"
/>
<NButton type="primary" @click="connectDevice">连接</NButton>
<NButton type="primary" @click="connectDevice">{{
$t("pages.Device.connect")
}}</NButton>
</NInputGroup>
<NH4 prefix="bar">设备尺寸</NH4>
<NH4 prefix="bar">{{ $t("pages.Device.deviceSize.title") }}</NH4>
<NFlex justify="left" align="center">
<NFormItem label="宽度">
<NFormItem :label="$t('pages.Device.deviceSize.width')">
<NInputNumber
v-model:value="store.screenSizeW"
placeholder="屏幕宽度"
:placeholder="$t('pages.Device.deviceSize.widthPlaceholder')"
:min="0"
:disabled="store.controledDevice !== null"
/>
</NFormItem>
<NFormItem label="高度">
<NFormItem :label="$t('pages.Device.deviceSize.height')">
<NInputNumber
v-model:value="store.screenSizeH"
placeholder="屏幕高度"
:placeholder="$t('pages.Device.deviceSize.heightPlaceholder')"
:min="0"
:disabled="store.controledDevice !== null"
/>
</NFormItem>
</NFlex>
<NP
>提示请正确输入当前控制设备的屏幕尺寸这是成功发送触摸事件的必要参数</NP
>
<NH4 prefix="bar">受控设备</NH4>
<NP>{{ $t("pages.Device.deviceSize.tip") }}</NP>
<NH4 prefix="bar">{{ $t("pages.Device.controledDevice") }}</NH4>
<div class="controled-device-list">
<NEmpty
size="small"
description="No Controled Device"
:description="$t('pages.Device.noControledDevice')"
v-if="!store.controledDevice"
/>
<div class="controled-device" v-if="store.controledDevice">
@ -352,7 +356,9 @@ async function connectDevice() {
</div>
</div>
<NFlex justify="space-between" align="center">
<NH4 style="margin: 20px 0" prefix="bar">可用设备</NH4>
<NH4 style="margin: 20px 0" prefix="bar">{{
$t("pages.Device.availableDevice")
}}</NH4>
<NButton
tertiary
circle

View File

@ -10,7 +10,7 @@ import {
unlistenToEvent,
updateScreenSizeAndMaskArea,
} from "../hotkey";
import { KeySteeringWheel } from "../keyMappingConfig";
import { KeyMappingConfig, KeySteeringWheel } from "../keyMappingConfig";
import { getVersion } from "@tauri-apps/api/app";
import { fetch } from "@tauri-apps/plugin-http";
import { open } from "@tauri-apps/plugin-shell";
@ -24,7 +24,10 @@ import {
AndroidKeycode,
AndroidMetastate,
} from "../frontcommand/android";
import { Store } from "@tauri-apps/plugin-store";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const store = useGlobalStore();
const router = useRouter();
const message = useMessage();
@ -59,17 +62,87 @@ onActivated(async () => {
) {
listenToEvent();
} else {
message.error("按键方案异常,请删除此方案");
message.error(t("pages.Mask.keyconfigException"));
}
}
});
onMounted(() => {
onMounted(async () => {
await loadLocalStore();
store.checkUpdate = checkUpdate;
checkUpdate();
store.showInputBox = showInputBox;
if (store.checkUpdateAtStart) checkUpdate();
});
async function loadLocalStore() {
const localStore = new Store("store.bin");
// loading screenSize from local store
const screenSize = await localStore.get<{ sizeW: number; sizeH: number }>(
"screenSize"
);
if (screenSize !== null) {
store.screenSizeW = screenSize.sizeW;
store.screenSizeH = screenSize.sizeH;
}
// loading keyMappingConfigList from local store
let keyMappingConfigList = await localStore.get<KeyMappingConfig[]>(
"keyMappingConfigList"
);
if (keyMappingConfigList === null || keyMappingConfigList.length === 0) {
// add empty key mapping config
// unable to get mask element when app is not ready
// so we use the stored mask area to get relative size
const maskArea = await localStore.get<{
posX: number;
posY: number;
sizeW: number;
sizeH: number;
}>("maskArea");
let relativeSize = { w: 800, h: 600 };
if (maskArea !== null) {
relativeSize = {
w: maskArea.sizeW,
h: maskArea.sizeH,
};
}
keyMappingConfigList = [
{
relativeSize,
title: t("pages.Mask.blankConfig"),
list: [],
},
];
await localStore.set("keyMappingConfigList", keyMappingConfigList);
}
store.keyMappingConfigList = keyMappingConfigList;
// loading curKeyMappingIndex from local store
let curKeyMappingIndex = await localStore.get<number>("curKeyMappingIndex");
if (
curKeyMappingIndex === null ||
curKeyMappingIndex >= keyMappingConfigList.length
) {
curKeyMappingIndex = 0;
localStore.set("curKeyMappingIndex", curKeyMappingIndex);
}
store.curKeyMappingIndex = curKeyMappingIndex;
// loading maskButton from local store
let maskButton = await localStore.get<{
show: boolean;
transparency: number;
}>("maskButton");
store.maskButton = maskButton ?? {
show: true,
transparency: 0.5,
};
// loading checkUpdateAtStart from local store
let checkUpdateAtStart = await localStore.get<boolean>("checkUpdateAtStart");
store.checkUpdateAtStart = checkUpdateAtStart ?? true;
}
async function cleanAfterimage() {
const appWindow = getCurrent();
const oldSize = await appWindow.outerSize();
@ -164,20 +237,20 @@ async function checkUpdate() {
}
);
if (res.status !== 200) {
message.error("检查更新失败");
message.error(t("pages.Mask.checkUpdate.failed"));
} else {
const data = await res.json();
const latestVersion = (data.tag_name as string).slice(1);
if (latestVersion <= curVersion) {
message.success(`最新版本: ${latestVersion},当前已是最新版本`);
message.success(t("pages.Mask.checkUpdate.isLatest", [latestVersion]));
return;
}
const body = data.body as string;
dialog.info({
title: `最新版本:${data.tag_name}`,
title: t("pages.Mask.checkUpdate.notLatest.title", [latestVersion]),
content: () => renderUpdateInfo(body),
positiveText: "前往发布页",
negativeText: "取消",
positiveText: t("pages.Mask.checkUpdate.notLatest.positiveText"),
negativeText: t("pages.Mask.checkUpdate.notLatest.negativeText"),
onPositiveClick: () => {
open(data.html_url);
},
@ -185,7 +258,7 @@ async function checkUpdate() {
}
} catch (e) {
console.error(e);
message.error("检查更新失败");
message.error(t("pages.Mask.checkUpdate.failed"));
}
}
</script>
@ -195,9 +268,9 @@ async function checkUpdate() {
<div class="content">
<NDialog
:closable="false"
title="未找到受控设备"
content="请启动服务端并控制任意设备"
positive-text="去启动"
:title="$t('pages.Mask.noControledDevice.title')"
:content="$t('pages.Mask.noControledDevice.content')"
:positive-text="$t('pages.Mask.noControledDevice.positiveText')"
type="warning"
@positive-click="toStartServer"
/>
@ -215,7 +288,7 @@ async function checkUpdate() {
ref="inputInstRef"
v-model:value="inputBoxVal"
type="text"
placeholder="Input text and then press enter/esc"
:placeholder="$t('pages.Mask.inputBoxPlaceholder')"
/>
</div>
<div

View File

@ -25,7 +25,9 @@ import {
AndroidMetastate,
} from "../frontcommand/android";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const store = useGlobalStore();
@ -61,7 +63,7 @@ async function sendKeyCodeToDevice(code: AndroidKeycode) {
metastate: AndroidMetastate.AMETA_NONE,
});
} else {
message.error("未连接设备");
message.error(t("sidebar.noControledDevice"));
}
}
@ -70,7 +72,7 @@ async function changeScreenPowerMode() {
sendSetScreenPowerMode({ mode: nextScreenPowerMode.value });
nextScreenPowerMode.value = nextScreenPowerMode.value ? 0 : 2;
} else {
message.error("未连接设备");
message.error(t("sidebar.noControledDevice"));
}
}
</script>

View File

@ -18,7 +18,9 @@ import { useGlobalStore } from "../../store/global";
import { DropdownOption, NDropdown, useDialog, useMessage } from "naive-ui";
import { onBeforeRouteLeave } from "vue-router";
import { useKeyboardStore } from "../../store/keyboard";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const store = useGlobalStore();
const keyboardStore = useKeyboardStore();
const dialog = useDialog();
@ -27,27 +29,27 @@ const message = useMessage();
const addButtonPos = ref({ x: 0, y: 0 });
const addButtonOptions: DropdownOption[] = [
{
label: "普通点击",
label: () => t("pages.KeyBoard.addButton.Tap"),
key: "Tap",
},
{
label: "键盘行走",
label: () => t("pages.KeyBoard.addButton.SteeringWheel"),
key: "SteeringWheel",
},
{
label: "技能",
label: () => t("pages.KeyBoard.addButton.Skill"),
key: "DirectionalSkill",
},
{
label: "技能取消",
label: () => t("pages.KeyBoard.addButton.CancelSkill"),
key: "CancelSkill",
},
{
label: "观察视角",
label: () => t("pages.KeyBoard.addButton.Observation"),
key: "Observation",
},
{
label: "宏",
label: () => t("pages.KeyBoard.addButton.Macro"),
key: "Macro",
},
];
@ -83,7 +85,7 @@ function onAddButtonSelect(
} else if (type === "DirectionalSkill") {
(keyMapping as unknown as KeyDirectionalSkill).range = 30;
} else if (type === "CancelSkill") {
keyMapping.note = "取消技能";
keyMapping.note = t("pages.KeyBoard.addButton.CancelSkill");
} else if (type === "Observation") {
(keyMapping as unknown as KeyMappingObservation).scale = 0.6;
} else if (type === "Macro") {
@ -141,7 +143,7 @@ function setCurButtonKey(curKey: string) {
return;
if (!isKeyUnique(curKey)) {
message.error("按键重复:" + curKey);
message.error(t("pages.KeyBoard.buttonKeyRepeat", [curKey]));
return;
}
@ -250,16 +252,16 @@ onBeforeRouteLeave(() => {
document.removeEventListener("wheel", handleMouseWheel);
if (keyboardStore.edited) {
dialog.warning({
title: "Warning",
content: "当前方案尚未保存,是否保存?",
positiveText: "保存",
negativeText: "取消",
title: t("pages.KeyBoard.noSaveDialog.title"),
content: t("pages.KeyBoard.noSaveDialog.content"),
positiveText: t("pages.KeyBoard.noSaveDialog.positiveText"),
negativeText: t("pages.KeyBoard.noSaveDialog.negativeText"),
onPositiveClick: () => {
if (store.applyEditKeyMappingList()) {
keyboardStore.edited = false;
resolve(true);
} else {
message.error("存在重复按键,无法保存");
message.error(t("pages.KeyBoard.noSaveDialog.keyRepeat"));
resolve(false);
}
},

View File

@ -16,6 +16,7 @@ import {
import { CloseCircle, Settings } from "@vicons/ionicons5";
import { KeyCommon, KeyMacro, KeyMacroList } from "../../keyMappingConfig";
import { useKeyboardStore } from "../../store/keyboard";
import { useI18n } from "vue-i18n";
const props = defineProps<{
index: number;
@ -23,6 +24,7 @@ const props = defineProps<{
const keyboardStore = useKeyboardStore();
const { t } = useI18n();
const store = useGlobalStore();
const message = useMessage();
const elementRef = ref<HTMLElement | null>(null);
@ -139,10 +141,10 @@ function saveMacro() {
(keyMapping.value as KeyMacro).macro = macro;
showMacroModal.value = false;
keyboardStore.edited = true;
message.success("宏代码解析成功,但不保证代码正确性,请自行测试");
message.success(t("pages.KeyBoard.KeyCommon.macroParseSuccess"));
} catch (e) {
console.error(e);
message.error("宏代码保存失败,请检查代码格式是否正确");
message.error(t("pages.KeyBoard.KeyCommon.macroParseFailed"));
}
}
@ -213,29 +215,40 @@ function showSetting() {
? "普通点击"
: "宏"
}}</NH4>
<NFormItem v-if="keyMapping.type === 'Macro'" label="宏代码">
<NButton type="success" @click="editMacro"> 编辑代码 </NButton>
<NFormItem
v-if="keyMapping.type === 'Macro'"
:label="$t('pages.KeyBoard.KeyCommon.macro')"
>
<NButton type="success" @click="editMacro">
{{ $t("pages.KeyBoard.KeyCommon.editMacro") }}
</NButton>
</NFormItem>
<NFormItem v-if="keyMapping.type === 'Tap'" label="触摸时长">
<NFormItem
v-if="keyMapping.type === 'Tap'"
:label="$t('pages.KeyBoard.setting.touchTime')"
>
<NInputNumber
v-model:value="keyMapping.time"
:min="0"
placeholder="请输入触摸时长(ms)"
:placeholder="$t('pages.KeyBoard.setting.touchTimePlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem v-if="keyMapping.type !== 'Macro'" label="触点ID">
<NFormItem
v-if="keyMapping.type !== 'Macro'"
:label="$t('pages.KeyBoard.setting.pointerID')"
>
<NInputNumber
v-model:value="keyMapping.pointerId"
:min="0"
placeholder="请输入触点ID"
:placeholder="$t('pages.KeyBoard.setting.pointerIDPlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem label="备注">
<NFormItem :label="$t('pages.KeyBoard.setting.note')">
<NInput
v-model:value="keyMapping.note"
placeholder="请输入备注"
:placeholder="$t('pages.KeyBoard.setting.notePlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
@ -245,33 +258,36 @@ function showSetting() {
v-model:show="showMacroModal"
@before-leave="saveMacro"
>
<NCard style="width: 50%; height: 80%" title="宏编辑">
<NCard
style="width: 50%; height: 80%"
:title="$t('pages.KeyBoard.KeyCommon.macroModal.title')"
>
<NFlex vertical style="height: 100%">
<div>按下按键执行</div>
<div>{{ $t("pages.KeyBoard.KeyCommon.macroModal.down") }}</div>
<NInput
type="textarea"
style="flex-grow: 1"
placeholder="JSON宏代码, 可为空"
placeholder="$t('pages.KeyBoard.KeyCommon.macroModal.placeholder')"
v-model:value="editedMacroRaw.down"
@update:value="macroEditedFlag = true"
round
clearable
/>
<div>按住执行</div>
<div>{{ $t("pages.KeyBoard.KeyCommon.macroModal.loop") }}</div>
<NInput
type="textarea"
style="flex-grow: 1"
placeholder="JSON宏代码, 可为空"
:placeholder="$t('pages.KeyBoard.KeyCommon.macroModal.placeholder')"
v-model:value="editedMacroRaw.loop"
@update:value="macroEditedFlag = true"
round
clearable
/>
<div>抬起执行</div>
<div>{{ $t("pages.KeyBoard.KeyCommon.macroModal.up") }}</div>
<NInput
type="textarea"
style="flex-grow: 1"
placeholder="JSON宏代码, 可为空"
:placeholder="$t('pages.KeyBoard.KeyCommon.macroModal.placeholder')"
v-model:value="editedMacroRaw.up"
@update:value="macroEditedFlag = true"
round

View File

@ -109,7 +109,7 @@ function dragHandler(downEvent: MouseEvent) {
<template>
<div v-show="keyboardStore.showKeyInfoFlag" class="key-info" @contextmenu.prevent>
<div class="key-info-header" @mousedown="dragHandler">
Key Info
{{ $t('pages.KeyBoard.KeyInfo.title') }}
<div
class="key-info-close"
@click="keyboardStore.showKeyInfoFlag = false"
@ -121,7 +121,7 @@ function dragHandler(downEvent: MouseEvent) {
<div style="border-bottom: 1px solid var(--light-color)">
{{ mouseX }}, {{ mouseY }}
</div>
<div v-if="keyboardCodeList.length === 0">Press any key</div>
<div v-if="keyboardCodeList.length === 0">{{ $t('pages.KeyBoard.KeyInfo.note') }}</div>
<div v-for="code in keyboardCodeList">
{{ code }}
</div>

View File

@ -124,27 +124,27 @@ function showSetting() {
top: `${settingPosY}px`,
}"
>
<NH4 prefix="bar">观察视角</NH4>
<NFormItem label="灵敏度">
<NH4 prefix="bar">{{ $t("pages.KeyBoard.Observation.observation") }}</NH4>
<NFormItem :label="$t('pages.KeyBoard.Observation.scale')">
<NInputNumber
v-model:value="keyMapping.scale"
placeholder="请输入灵敏度"
:placeholder="$t('pages.KeyBoard.Observation.scalePlaceholder')"
:step="0.1"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem label="触点ID">
<NFormItem :label="$t('pages.KeyBoard.setting.pointerID')">
<NInputNumber
v-model:value="keyMapping.pointerId"
:min="0"
placeholder="请输入触点ID"
:placeholder="$t('pages.KeyBoard.setting.pointerIDPlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem label="备注">
<NFormItem :label="$t('pages.KeyBoard.setting.note')">
<NInput
v-model:value="keyMapping.note"
placeholder="请输入备注"
:placeholder="$t('pages.KeyBoard.setting.notePlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>

View File

@ -18,7 +18,9 @@ import { Store } from "@tauri-apps/plugin-store";
import { loadDefaultKeyconfig } from "../../invoke";
import { KeyMappingConfig } from "../../keyMappingConfig";
import { useKeyboardStore } from "../../store/keyboard";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const store = useGlobalStore();
const keyboardStore = useKeyboardStore();
const localStore = new Store("store.bin");
@ -126,7 +128,7 @@ function dragHandler(downEvent: MouseEvent) {
keyboardStore.showSettingFlag &&
store.keyMappingConfigList.length === 1
) {
message.info("当前仅有一个按键方案,点击导入默认,可导入预设方案");
message.info(t("pages.KeyBoard.KeySetting.onlyOneConfig"));
}
}
};
@ -139,14 +141,14 @@ function importKeyMappingConfig() {
keyMappingConfig = JSON.parse(importModalInputValue.value);
} catch (e) {
console.error(e);
message.error("导入失败");
message.error(t("pages.KeyBoard.KeySetting.importFailed"));
return;
}
store.keyMappingConfigList.push(keyMappingConfig);
store.setKeyMappingIndex(store.keyMappingConfigList.length - 1);
showImportModal.value = false;
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
message.success("按键方案已导入");
message.success(t("pages.KeyBoard.KeySetting.importSuccess"));
}
async function importDefaultKeyMappingConfig() {
@ -161,17 +163,17 @@ async function importDefaultKeyMappingConfig() {
}
} catch (e) {
console.error(e);
message.error("默认按键方案导入失败");
message.error(t("pages.KeyBoard.KeySetting.importDefaultFailed"));
return;
}
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
message.success(`已导入${count}个默认方案`);
message.success(t("pages.KeyBoard.KeySetting.importDefaultSuccess", [count]));
}
function createKeyMappingConfig() {
if (keyboardStore.edited) {
message.error("请先保存或还原当前方案");
message.error(t("pages.KeyBoard.KeySetting.configEdited"));
return;
}
@ -179,7 +181,7 @@ function createKeyMappingConfig() {
"keyboardElement"
) as HTMLElement;
const newConfig: KeyMappingConfig = {
title: "新方案",
title: t("pages.KeyBoard.KeySetting.newConfig"),
relativeSize: {
w: keyboardElement.clientWidth,
h: keyboardElement.clientHeight,
@ -189,18 +191,21 @@ function createKeyMappingConfig() {
store.keyMappingConfigList.push(newConfig);
store.setKeyMappingIndex(store.keyMappingConfigList.length - 1);
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
message.success("新方案已创建");
message.success(t("pages.KeyBoard.KeySetting.newConfigSuccess"));
}
function copyCurKeyMappingConfig() {
if (keyboardStore.edited) {
message.error("请先保存或还原当前方案");
message.error(t("pages.KeyBoard.KeySetting.configEdited"));
return;
}
const curConfig = store.keyMappingConfigList[store.curKeyMappingIndex];
const newTitle = t("pages.KeyBoard.KeySetting.copyConfigTitle", [
curConfig.title,
]);
const newConfig: KeyMappingConfig = {
title: curConfig.title + "-副本",
title: newTitle,
relativeSize: curConfig.relativeSize,
list: curConfig.list,
};
@ -209,12 +214,12 @@ function copyCurKeyMappingConfig() {
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
store.setKeyMappingIndex(store.keyMappingConfigList.length - 1);
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
message.success("方案已复制为:" + curConfig.title + "-副本");
message.success(t("pages.KeyBoard.KeySetting.copyConfigSuccess", [newTitle]));
}
function delCurKeyMappingConfig() {
if (store.keyMappingConfigList.length <= 1) {
message.error("至少保留一个方案");
message.error(t("pages.KeyBoard.KeySetting.delConfigLeast"));
return;
}
const title = store.keyMappingConfigList[store.curKeyMappingIndex].title;
@ -228,7 +233,7 @@ function delCurKeyMappingConfig() {
store.curKeyMappingIndex > 0 ? store.curKeyMappingIndex - 1 : 0
);
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
message.success("方案已删除:" + title);
message.success(t("pages.KeyBoard.KeySetting.delSuccess", [title]));
}
function renameKeyMappingConfig() {
@ -237,9 +242,9 @@ function renameKeyMappingConfig() {
if (newTitle !== "") {
store.keyMappingConfigList[store.curKeyMappingIndex].title = newTitle;
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
message.success("方案已重命名为:" + newTitle);
message.success(t("pages.KeyBoard.KeySetting.renameSuccess", [newTitle]));
} else {
message.error("方案名不能为空");
message.error(t("pages.KeyBoard.KeySetting.renameEmpty"));
}
}
@ -249,11 +254,11 @@ function exportKeyMappingConfig() {
navigator.clipboard
.writeText(data)
.then(() => {
message.success("当前按键方案已导出到剪切板");
message.success(t("pages.KeyBoard.KeySetting.exportSuccess"));
})
.catch((e) => {
console.error(e);
message.error("按键方案导出失败");
message.error(t("pages.KeyBoard.KeySetting.exportFailed"));
});
}
@ -261,7 +266,7 @@ function saveKeyMappingConfig() {
if (store.applyEditKeyMappingList()) {
keyboardStore.edited = false;
} else {
message.error("存在重复按键,无法保存");
message.error(t("pages.KeyBoard.KeySetting.saveKeyRepeat"));
}
}
@ -278,14 +283,16 @@ function checkConfigSize() {
keyboardElement.clientHeight !== relativeSize.h
) {
message.warning(
`请注意当前按键方案"${curKeyMappingConfig.title}"与蒙版尺寸不一致,若有需要可进行迁移`
t("pages.KeyBoard.KeySetting.checkConfigSizeWarning", [
curKeyMappingConfig.title,
])
);
}
}
function migrateKeyMappingConfig() {
if (keyboardStore.edited) {
message.error("请先保存或还原当前按键方案");
message.error(t("pages.KeyBoard.KeySetting.configEdited"));
return;
}
@ -313,25 +320,29 @@ function migrateKeyMappingConfig() {
keyMapping.posY = Math.round((keyMapping.posY / relativeSize.h) * sizeH);
}
// migrate title
newConfig.title += "-迁移";
newConfig.title = t("pages.KeyBoard.KeySetting.migrateConfigTitle", [
newConfig.title,
]);
store.keyMappingConfigList.splice(
store.curKeyMappingIndex + 1,
0,
newConfig
);
message.success("已迁移到新方案:" + newConfig.title);
message.success(
t("pages.KeyBoard.KeySetting.migrateConfigSuccess", [newConfig.title])
);
keyboardStore.activeButtonIndex = -1;
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
store.setKeyMappingIndex(store.curKeyMappingIndex + 1);
} else {
message.info("当前方案符合蒙版尺寸,无需迁移");
message.info(t("pages.KeyBoard.KeySetting.migrateConfigNeedless"));
}
}
function selectKeyMappingConfig(index: number) {
if (keyboardStore.edited) {
message.error("请先保存或还原当前按键方案");
message.error(t("pages.KeyBoard.KeySetting.configEdited"));
return;
}
@ -355,7 +366,7 @@ function resetKeyMappingConfig() {
size="large"
class="key-setting-btn"
id="keySettingBtn"
title="长按可拖动"
:title="$t('pages.KeyBoard.KeySetting.buttonDrag')"
@mousedown="dragHandler"
:style="{
left: keySettingPos.x + 'px',
@ -381,50 +392,73 @@ function resetKeyMappingConfig() {
>
<NIcon><CloseCircle></CloseCircle></NIcon>
</NButton>
<NH4 prefix="bar">按键方案</NH4>
<NH4 prefix="bar">{{ $t("pages.KeyBoard.KeySetting.config") }}</NH4>
<NSelect
:value="store.curKeyMappingIndex"
@update:value="selectKeyMappingConfig"
:options="keyMappingNameOptions"
/>
<NP style="margin-top: 20px">
Relative Size:{{ curRelativeSize.w }}x{{ curRelativeSize.h }}
{{
$t("pages.KeyBoard.KeySetting.configRelativeSize", [
curRelativeSize.w,
curRelativeSize.h,
])
}}
</NP>
<NFlex style="margin-top: 20px">
<template v-if="keyboardStore.edited">
<NButton type="success" @click="saveKeyMappingConfig">保存方案</NButton>
<NButton type="error" @click="resetKeyMappingConfig">还原方案</NButton>
<NButton type="success" @click="saveKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.saveConfig")
}}</NButton>
<NButton type="error" @click="resetKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.resetConfig")
}}</NButton>
</template>
<NButton @click="createKeyMappingConfig">新建方案</NButton>
<NButton @click="copyCurKeyMappingConfig">复制方案</NButton>
<NButton @click="migrateKeyMappingConfig">迁移方案</NButton>
<NButton @click="delCurKeyMappingConfig">删除方案</NButton>
<NButton @click="createKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.createConfig")
}}</NButton>
<NButton @click="copyCurKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.copyConfig")
}}</NButton>
<NButton @click="migrateKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.migrateConfig")
}}</NButton>
<NButton @click="delCurKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.delConfig")
}}</NButton>
<NButton
@click="
showRenameModal = true;
renameModalInputValue =
store.keyMappingConfigList[store.curKeyMappingIndex].title;
"
>重命名</NButton
>{{ $t("pages.KeyBoard.KeySetting.renameConfig") }}</NButton
>
</NFlex>
<NH4 prefix="bar">其他</NH4>
<NH4 prefix="bar">{{ $t("pages.KeyBoard.KeySetting.others") }}</NH4>
<NFlex>
<NButton
@click="
showImportModal = true;
importModalInputValue = '';
"
>导入方案</NButton
>{{ $t("pages.KeyBoard.KeySetting.importConfig") }}</NButton
>
<NButton @click="exportKeyMappingConfig">导出方案</NButton>
<NButton @click="importDefaultKeyMappingConfig">导入默认</NButton>
<NButton @click="exportKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.exportConfig")
}}</NButton>
<NButton @click="importDefaultKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.importDefaultConfig")
}}</NButton>
<NButton
@click="keyboardStore.showKeyInfoFlag = !keyboardStore.showKeyInfoFlag"
>按键信息</NButton
>{{ $t("pages.KeyBoard.KeySetting.keyInfo") }}</NButton
>
</NFlex>
<NP style="margin-top: 40px">提示右键空白区域可添加按键</NP>
<NP style="margin-top: 40px">{{
$t("pages.KeyBoard.KeySetting.addButtonTip")
}}</NP>
</div>
<NModal v-model:show="showImportModal">
<NCard style="width: 40%; height: 50%">
@ -432,24 +466,27 @@ function resetKeyMappingConfig() {
<NInput
type="textarea"
style="flex-grow: 1"
placeholder="粘贴单个按键方案的JSON文本 (此处无法对按键方案的合法性进行判断, 请确保JSON内容正确)"
:placeholder="$t('pages.KeyBoard.KeySetting.importPlaceholder')"
v-model:value="importModalInputValue"
round
clearable
/>
<NButton type="success" round @click="importKeyMappingConfig"
>导入</NButton
>
<NButton type="success" round @click="importKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.import")
}}</NButton>
</NFlex>
</NCard>
</NModal>
<NModal v-model:show="showRenameModal">
<NCard style="width: 40%" title="重命名按键方案">
<NCard
style="width: 40%"
:title="$t('pages.KeyBoard.KeySetting.renameTitle')"
>
<NFlex vertical>
<NInput v-model:value="renameModalInputValue" clearable />
<NButton type="success" round @click="renameKeyMappingConfig"
>重命名</NButton
>
<NButton type="success" round @click="renameKeyMappingConfig">{{
$t("pages.KeyBoard.KeySetting.renameConfig")
}}</NButton>
</NFlex>
</NCard>
</NModal>

View File

@ -260,27 +260,30 @@ function updateRangeIndicator(element?: HTMLElement) {
top: `${settingPosY}px`,
}"
>
<NH4 prefix="bar">技能</NH4>
<NFormItem label="选项">
<NH4 prefix="bar">{{ $t("pages.KeyBoard.KeySkill.skill") }}</NH4>
<NFormItem :label="$t('pages.KeyBoard.KeySkill.options')">
<NFlex vertical>
<NCheckbox
@click="changeSkillType('trigger-double')"
:checked="isTriggerWhenDoublePressed"
>双击施放</NCheckbox
>{{ $t("pages.KeyBoard.KeySkill.double") }}</NCheckbox
>
<NCheckbox
@click="changeSkillType('direction')"
:checked="isDirectionless"
>无方向技能</NCheckbox
>{{ $t("pages.KeyBoard.KeySkill.directionless") }}</NCheckbox
>
<NCheckbox
@click="changeSkillType('trigger')"
:checked="isTriggerWhenPressed"
>按下时触发</NCheckbox
>{{ $t("pages.KeyBoard.KeySkill.triggerWhenPressed") }}</NCheckbox
>
</NFlex>
</NFormItem>
<NFormItem v-if="!isDirectionless" label="范围">
<NFormItem
v-if="!isDirectionless"
:label="$t('pages.KeyBoard.KeySkill.range')"
>
<NInputNumber
v-if="keyMapping.type === 'DirectionalSkill'"
v-model:value="keyMapping.range"
@ -317,27 +320,27 @@ function updateRangeIndicator(element?: HTMLElement) {
</NFormItem>
<NFormItem
v-if="(keyMapping.type==='TriggerWhenPressedSkill'&&!(keyMapping as KeyTriggerWhenPressedSkill).directional)"
label="触摸时长"
:label="$t('pages.KeyBoard.setting.touchTime')"
>
<NInputNumber
v-model:value="keyMapping.rangeOrTime"
:min="0"
placeholder="请输入触摸时长(ms)"
:placeholder="$t('pages.KeyBoard.setting.touchTimePlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem label="触点ID">
<NFormItem :label="$t('pages.KeyBoard.setting.pointerID')">
<NInputNumber
v-model:value="keyMapping.pointerId"
:min="0"
placeholder="请输入触点ID"
:placeholder="$t('pages.KeyBoard.setting.pointerIDPlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem label="备注">
<NFormItem :label="$t('pages.KeyBoard.setting.note')">
<NInput
v-model:value="keyMapping.note"
placeholder="请输入备注"
:placeholder="$t('pages.KeyBoard.setting.notePlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>

View File

@ -184,26 +184,28 @@ function showSetting() {
top: `${settingPosY}px`,
}"
>
<NH4 prefix="bar">键盘行走</NH4>
<NFormItem label="偏移">
<NH4 prefix="bar">{{
$t("pages.KeyBoard.SteeringWheel.steeringWheel")
}}</NH4>
<NFormItem :label="$t('pages.KeyBoard.SteeringWheel.offset')">
<NInputNumber
v-model:value="keyMapping.offset"
:min="1"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem label="触点ID">
<NFormItem :label="$t('pages.KeyBoard.setting.pointerID')">
<NInputNumber
v-model:value="keyMapping.pointerId"
:min="0"
placeholder="请输入触点ID"
:placeholder="$t('pages.KeyBoard.setting.pointerIDPlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>
<NFormItem label="备注">
<NFormItem :label="$t('pages.KeyBoard.setting.note')">
<NInput
v-model:value="keyMapping.note"
placeholder="请输入备注"
:placeholder="$t('pages.KeyBoard.setting.notePlaceholder')"
@update:value="keyboardStore.edited = true"
/>
</NFormItem>

View File

@ -1,12 +1,14 @@
<script setup lang="ts">
import { NFlex, NH4, NButton, NIcon, NP } from "naive-ui";
import { NFlex, NH4, NButton, NIcon, NP, NCheckbox } from "naive-ui";
import { LogoGithub, Planet } from "@vicons/ionicons5";
import { open } from "@tauri-apps/plugin-shell";
import { getVersion } from "@tauri-apps/api/app";
import { onMounted, ref } from "vue";
import { useGlobalStore } from "../../store/global";
import { Store } from "@tauri-apps/plugin-store";
const store = useGlobalStore();
const localStore = new Store("store.bin");
const appVersion = ref("");
onMounted(async () => {
@ -26,11 +28,8 @@ async function checkUpdate() {
<template>
<div class="setting-page">
<NH4 prefix="bar">关于</NH4>
<NP
>A Scrcpy client in Rust & Tarui aimed at providing mouse and key mapping
to control Android device.</NP
>
<NH4 prefix="bar">{{ $t("pages.Setting.About.about") }}</NH4>
<NP>{{ $t("pages.Setting.About.introduction") }}</NP>
<NFlex class="website">
<NButton
text
@ -39,7 +38,7 @@ async function checkUpdate() {
<template #icon>
<NIcon><LogoGithub /> </NIcon>
</template>
Github repo
{{ $t("pages.Setting.About.github") }}
</NButton>
<NButton
text
@ -67,12 +66,21 @@ async function checkUpdate() {
<template #icon>
<NIcon><Planet /> </NIcon>
</template>
AkiChase's Blog
{{ $t("pages.Setting.About.blog") }}
</NButton>
</NFlex>
<NH4 prefix="bar">更新</NH4>
<NP>当前版本{{ appVersion }}</NP>
<NButton @click="checkUpdate">检查更新</NButton>
<NH4 prefix="bar">{{ $t("pages.Setting.About.update") }}</NH4>
<NCheckbox
v-model:checked="store.checkUpdateAtStart"
@update:checked="
localStore.set('checkUpdateAtStart', store.checkUpdateAtStart)
"
>{{ $t("pages.Setting.About.checkUpdateOnStartup") }}</NCheckbox
>
<NP>{{ $t("pages.Setting.About.curVersion", [appVersion]) }}</NP>
<NButton @click="checkUpdate">{{
$t("pages.Setting.About.checkUpdate")
}}</NButton>
</div>
</template>

View File

@ -13,10 +13,14 @@ import {
useDialog,
NCard,
NIcon,
NSelect,
} from "naive-ui";
import { relaunch } from "@tauri-apps/plugin-process";
import { onMounted, ref } from "vue";
import i18n from "../../i18n";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const localStore = new Store("store.bin");
const dialog = useDialog();
@ -25,8 +29,16 @@ const showDataModal = ref(false);
const dataModalInputVal = ref("");
let curDataIndex = -1;
onMounted(() => {
const languageOptions = [
{ label: "简体中文", value: "zh-CN" },
{ label: "English", value: "en-US" },
];
const curLanguage = ref("en-US");
onMounted(async () => {
refreshLocalData();
curLanguage.value = (await localStore.get<string>("language")) ?? "en-US";
});
async function refreshLocalData() {
@ -46,10 +58,10 @@ function showLocalStore(index: number) {
function delLocalStore(key?: string) {
if (key) {
dialog.warning({
title: "Warning",
content: `即将删除数据"${key}",删除操作不可撤回,是否继续?`,
positiveText: "删除",
negativeText: "取消",
title: t("pages.Setting.Basic.delLocalStore.dialog.title"),
content: t("pages.Setting.Basic.delLocalStore.dialog.delKey", [key]),
positiveText: t("pages.Setting.Basic.delLocalStore.dialog.positiveText"),
negativeText: t("pages.Setting.Basic.delLocalStore.dialog.negativeText"),
onPositiveClick: () => {
localStore.delete(key);
localStoreEntries.value.splice(curDataIndex, 1);
@ -58,23 +70,37 @@ function delLocalStore(key?: string) {
});
} else {
dialog.warning({
title: "Warning",
content: "即将清空数据,操作不可撤回,且清空后将重启软件,是否继续?",
positiveText: "删除",
negativeText: "取消",
title: t("pages.Setting.Basic.delLocalStore.dialog.title"),
content: t("pages.Setting.Basic.delLocalStore.dialog.delAll"),
positiveText: t("pages.Setting.Basic.delLocalStore.dialog.positiveText"),
negativeText: t("pages.Setting.Basic.delLocalStore.dialog.negativeText"),
onPositiveClick: () => {
// localStore.clear();
localStore.clear();
relaunch();
},
});
}
}
function changeLanguage(language: "zh-CN" | "en-US") {
if (language === curLanguage.value) return;
curLanguage.value = language;
localStore.set("language", language);
i18n.global.locale = language;
}
</script>
<template>
<div class="setting-page">
<NH4 prefix="bar">{{ $t("pages.Setting.Basic.language") }}</NH4>
<NSelect
:value="curLanguage"
@update:value="changeLanguage"
:options="languageOptions"
style="max-width: 300px; margin: 20px 0"
/>
<NFlex justify="space-between">
<NH4 prefix="bar">本地数据</NH4>
<NH4 prefix="bar">{{ $t("pages.Setting.Basic.localStore") }}</NH4>
<NFlex>
<NButton
tertiary
@ -100,9 +126,7 @@ function delLocalStore(key?: string) {
</NButton>
</NFlex>
</NFlex>
<NP
>删除数据可能导致无法预料的后果请慎重操作若出现异常请尝试清空数据并重启软件</NP
>
<NP>{{ $t("pages.Setting.Basic.delLocalStore.warning") }}</NP>
<NList class="data-list" hoverable clickable>
<NListItem v-for="(entrie, index) in localStoreEntries">
<div @click="showLocalStore(index)">
@ -112,7 +136,10 @@ function delLocalStore(key?: string) {
</NList>
</div>
<NModal v-model:show="showDataModal">
<NCard style="width: 50%; height: 80%" title="卡片">
<NCard
style="width: 50%; height: 80%"
:title="localStoreEntries[curDataIndex][0]"
>
<NFlex vertical style="height: 100%">
<NInput
type="textarea"
@ -125,7 +152,7 @@ function delLocalStore(key?: string) {
type="success"
round
@click="delLocalStore(localStoreEntries[curDataIndex][0])"
>删除当前数据</NButton
>{{ $t("pages.Setting.Basic.delCurData") }}</NButton
>
</NFlex>
</NCard>

View File

@ -28,11 +28,13 @@ import { Store } from "@tauri-apps/plugin-store";
import { SettingsOutline } from "@vicons/ionicons5";
import { UnlistenFn } from "@tauri-apps/api/event";
import { useGlobalStore } from "../../store/global";
import { useI18n } from "vue-i18n";
let unlistenResize: UnlistenFn = () => {};
let unlistenMove: UnlistenFn = () => {};
let factor = 1;
const { t } = useI18n();
const localStore = new Store("store.bin");
const store = useGlobalStore();
const message = useMessage();
@ -57,25 +59,25 @@ const areaFormRules: FormRules = {
type: "number",
required: true,
trigger: ["blur", "input"],
message: "请输入左上角X坐标",
message: () => t("pages.Setting.Mask.areaFormMissing.x"),
},
posY: {
type: "number",
required: true,
trigger: ["blur", "input"],
message: "请输入左上角Y坐标",
message: () => t("pages.Setting.Mask.areaFormMissing.y"),
},
sizeW: {
type: "number",
required: true,
trigger: ["blur", "input"],
message: "请输入蒙版宽度",
message: () => t("pages.Setting.Mask.areaFormMissing.w"),
},
sizeH: {
type: "number",
required: true,
trigger: ["blur", "input"],
message: "请输入蒙版高度",
message: () => t("pages.Setting.Mask.areaFormMissing.h"),
},
};
@ -104,10 +106,10 @@ function handleAdjustClick(e: MouseEvent) {
if (!errors) {
adjustMaskArea().then(() => {
localStore.set("maskArea", areaModel.value);
message.success("蒙版区域已保存");
message.success(t("pages.Setting.Mask.areaSaved"));
});
} else {
message.error("请正确输入蒙版的坐标和尺寸");
message.error(t("pages.Setting.Mask.incorrectArea"));
}
});
}
@ -163,14 +165,17 @@ onUnmounted(() => {
<template>
<div class="setting-page">
<NH4 prefix="bar">按键提示</NH4>
<NFormItem label="是否显示" label-placement="left">
<NH4 prefix="bar">{{ $t("pages.Setting.Mask.buttonPrompts") }}</NH4>
<NFormItem
:label="$t('pages.Setting.Mask.ifButtonPrompts')"
label-placement="left"
>
<NCheckbox
v-model:checked="store.maskButton.show"
@update:checked="localStore.set('maskButton', store.maskButton)"
/>
</NFormItem>
<NFormItem label="不透明度" label-placement="left">
<NFormItem :label="$t('pages.Setting.Mask.opacity')" label-placement="left">
<NSlider
v-model:value="store.maskButton.transparency"
@update:checked="localStore.set('maskButton', store.maskButton)"
@ -190,7 +195,7 @@ onUnmounted(() => {
require-mark-placement="right-hanging"
>
<NFlex justify="space-between" align="center">
<NH4 prefix="bar">蒙版调整</NH4>
<NH4 prefix="bar">{{ $t("pages.Setting.Mask.areaAdjust") }}</NH4>
<NButton
tertiary
circle
@ -207,29 +212,29 @@ onUnmounted(() => {
<NFormItemGi label="X" path="posX">
<NInputNumber
v-model:value="areaModel.posX"
placeholder="左上角X坐标"
:placeholder="$t('pages.Setting.Mask.areaPlaceholder.x')"
/>
</NFormItemGi>
<NFormItemGi label="Y" path="posY">
<NInputNumber
v-model:value="areaModel.posY"
placeholder="左上角Y坐标"
:placeholder="$t('pages.Setting.Mask.areaFormPlaceholder.y')"
/>
</NFormItemGi>
<NFormItemGi label="W" path="sizeW">
<NInputNumber
v-model:value="areaModel.sizeW"
placeholder="蒙版宽度"
:placeholder="$t('pages.Setting.Mask.areaFormPlaceholder.w')"
/>
</NFormItemGi>
<NFormItemGi label="H" path="sizeH">
<NInputNumber
v-model:value="areaModel.sizeH"
placeholder="蒙版高度"
:placeholder="$t('pages.Setting.Mask.areaFormPlaceholder.h')"
/>
</NFormItemGi>
</NGrid>
<NP>提示蒙版尺寸与设备尺寸将用于坐标转换请保证尺寸的准确性</NP>
<NP>{{ $t("pages.Setting.Mask.areaTip") }}</NP>
</NForm>
</div>
</template>

View File

@ -1,15 +0,0 @@
<script setup lang="ts">
import { NH4 } from "naive-ui";
</script>
<template>
<div class="setting-page">
<NH4 prefix="bar">敬请期待</NH4>
</div>
</template>
<style scoped>
.setting-page {
padding: 10px 25px;
}
</style>

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import Basic from "./Basic.vue";
import Script from "./Script.vue";
import Mask from "./Mask.vue";
import About from "./About.vue";
import { NTabs, NTabPane, NScrollbar, NSpin } from "naive-ui";
@ -13,22 +12,17 @@ const store = useGlobalStore();
<div class="setting">
<NSpin :show="store.showLoadingRef">
<NTabs type="line" animated placement="left" default-value="basic">
<NTabPane tab="基本设置" name="basic">
<NTabPane :tab="$t('pages.Setting.tabs.basic')" name="basic">
<NScrollbar>
<Basic />
</NScrollbar>
</NTabPane>
<NTabPane tab="蒙版设置" name="mask">
<NTabPane :tab="$t('pages.Setting.tabs.mask')" name="mask">
<NScrollbar>
<Mask />
</NScrollbar>
</NTabPane>
<NTabPane tab="脚本设置" name="script">
<NScrollbar>
<Script />
</NScrollbar>
</NTabPane>
<NTabPane tab="关于" name="about">
<NTabPane :tab="$t('pages.Setting.tabs.about')" name="about">
<NScrollbar>
<About />
</NScrollbar>
@ -46,7 +40,7 @@ const store = useGlobalStore();
overflow-y: auto;
display: flex;
.n-tabs{
.n-tabs {
height: 100%;
}

220
src/i18n/en-US.json Normal file
View File

@ -0,0 +1,220 @@
{
"pages": {
"Device": {
"localPort": "Local port",
"status": "Status",
"shutdown": {
"title": "Warning",
"content": "Are you sure to turn off the Scrcpy control service?",
"positiveText": "Confirm",
"negativeText": "Cancel"
},
"menu": {
"control": "Control this device",
"screen": "Get screen size"
},
"deviceControl": {
"inputScreenSize": "Please enter the screen size of the current control device correctly",
"closeCurDevice": "Please close the current control device first",
"controlInfo": "The screen size has been saved and the control service is starting. Please keep the device screen on.",
"connectTimeout": "Device connection timeout"
},
"deviceGetScreenSize": "Device screen size: ",
"inputWirelessAddress": "Please enter the wireless debugging address",
"localPortPlaceholder": "Scrcpy local port",
"wireless": "Wireless connection",
"wirelessPlaceholder": "Wireless connection address",
"connect": "Connect",
"deviceSize": {
"title": "Device screen size",
"width": "Width",
"widthPlaceholder": "Screen width",
"height": "Height",
"heightPlaceholder": "Screen height",
"tip": "Tip: Please enter the screen size of the current control device correctly. This is a necessary parameter to successfully send touch events."
},
"controledDevice": "Controlled device",
"availableDevice": "Available devices",
"noControledDevice": "No Controled Device"
},
"Mask": {
"inputBoxPlaceholder": "Input text and then press enter/esc",
"keyconfigException": "The key mapping config is abnormal, please delete this config",
"blankConfig": "Blank config",
"checkUpdate": {
"failed": "Check for updates failed",
"isLatest": "Latest version: {0}, currently the latest version",
"notLatest": {
"title": "Latest version: {0}",
"positiveText": "Release page",
"negativeText": "Cancel"
}
},
"noControledDevice": {
"title": "Controlled device not found",
"content": "Please go to the device page to control any device",
"positiveText": "To control"
}
},
"Setting": {
"tabs": {
"basic": "Basic settings",
"mask": "Mask setting",
"about": "About"
},
"Mask": {
"areaFormMissing": {
"x": "Enter the X coordinate of the mask upper left corner",
"y": "Enter the Y coordinate of the mask upper left corner",
"w": "Enter mask width",
"h": "Enter the mask height"
},
"areaSaved": "Mask area saved",
"incorrectArea": "Please enter the coordinates and size of the mask correctly",
"buttonPrompts": "Button prompts",
"ifButtonPrompts": "Whether to display",
"opacity": "Opacity",
"areaAdjust": "Mask adjustment",
"areaPlaceholder": {
"x": "X coordinate of upper left corner"
},
"areaFormPlaceholder": {
"y": "Y coordinate of upper left corner",
"w": "Mask width",
"h": "Mask height"
},
"areaTip": "Tip: The mask size and device size will be used for coordinate conversion, please ensure the accuracy of the size"
},
"Basic": {
"delLocalStore": {
"dialog": {
"title": "Warning",
"delKey": "Data \"{0}\" is about to be deleted. The deletion operation is irreversible. Do you want to continue?",
"positiveText": "Delete",
"negativeText": "Cancel",
"delAll": "The data is about to be cleared. The operation is irreversible and the software will be restarted after clearing. Do you want to continue?"
},
"warning": "Deleting data may lead to unpredictable consequences, so please operate with caution. \nIf an exception occurs, please try clearing the data and restarting the software."
},
"language": "Language",
"localStore": "Local data",
"delCurData": "Delete current data"
},
"About": {
"introduction": "A Scrcpy client in Rust & Tarui aimed at providing mouse and key mapping to control Android device.",
"github": "Github repo",
"blog": "AkiChase's Blog",
"about": "About",
"update": "Update",
"checkUpdateOnStartup": "Check for software updates on startup",
"curVersion": "Current version: {0}",
"checkUpdate": "Check for updates"
}
},
"KeyBoard": {
"noSaveDialog": {
"title": "Warning",
"content": "The current plan has not been saved. Do you want to save it?",
"positiveText": "Save",
"negativeText": "Cancel",
"keyRepeat": "There are duplicate keystrokes and cannot be saved."
},
"addButton": {
"SteeringWheel": "SteeringWheel",
"Tap": "Tap",
"Skill": "Skill",
"CancelSkill": "CancelSkill",
"Observation": "Observation",
"Macro": "Macro"
},
"buttonKeyRepeat": "Key repeat: {0}",
"KeyCommon": {
"macroParseSuccess": "The macro code is parsed successfully, but the correctness of the code is not guaranteed. Please test by yourself.",
"macroParseFailed": "Macro code failed to save, please check whether the code format is correct.",
"macro": "Macro code",
"editMacro": "Edit macro",
"macroModal": {
"title": "Macro editor",
"down": "Macro executed on key press",
"placeholder": "JSON macro code, can be empty",
"loop": "Macro executed on key press and hold",
"up": "Macro executed on key up"
}
},
"setting": {
"touchTime": "Touch duration",
"touchTimePlaceholder": "Touch duration (ms)",
"pointerID": "Pointer ID",
"pointerIDPlaceholder": "Please enter Pointer ID",
"note": "Note",
"notePlaceholder": "Please enter note"
},
"KeyInfo": {
"title": "按键信息",
"note": "按下任意键"
},
"Observation": {
"observation": "Observation",
"scale": "Sensitivity",
"scalePlaceholder": "Please enter sensitivity"
},
"KeySetting": {
"onlyOneConfig": "There is currently only one config. Click Import Default to import the preset configs.",
"importFailed": "Import failed",
"importSuccess": "Key config has been imported",
"importDefaultFailed": "Import of default key config failed",
"importDefaultSuccess": "{0} default configs have been imported",
"configEdited": "Please save or reset the current config first",
"newConfig": "New Config",
"newConfigSuccess": "New config has been created",
"copyConfigTitle": "{0}-Copy",
"copyConfigSuccess": "The config has been copied as: {0}",
"delConfigLeast": "Keep at least one config",
"delSuccess": "Config deleted: {0}",
"renameSuccess": "Config has been renamed: {0}",
"renameEmpty": "Config name cannot be empty",
"exportSuccess": "The current key config has been exported to the clipboard",
"exportFailed": "Key config export failed",
"saveKeyRepeat": "There are duplicate key and cannot be saved.",
"checkConfigSizeWarning": "Please note that the current key config \"{0}\" is inconsistent with the mask size. You can migrate it if necessary.",
"migrateConfigTitle": "{0}-Migrate",
"migrateConfigSuccess": "Migrated to new config: {0}",
"migrateConfigNeedless": "The current config conforms to the mask size and does not need to be migrated",
"buttonDrag": "Long press to drag",
"config": "Key mapping config",
"configRelativeSize": "Relative Mask Size: {0}x{1}",
"saveConfig": "Save config",
"resetConfig": "Reset config",
"renameConfig": "Rename",
"renameTitle": "Rename key config",
"import": "import",
"importPlaceholder": "Paste the JSON text of a key mapping config (the legality of the key mapping config cannot be judged here, please ensure that the JSON content is correct)",
"addButtonTip": "Tip: Right-click on the blank area to add buttons",
"keyInfo": "Key Info",
"importDefaultConfig": "Import default",
"exportConfig": "Export config",
"importConfig": "Import config",
"others": "Others",
"delConfig": "Delete config",
"migrateConfig": "Migration config",
"copyConfig": "Copy config",
"createConfig": "Create config"
},
"KeySkill": {
"skill": "Skill",
"options": "Options",
"double": "Double click to cast",
"directionless": "Directionless skills",
"triggerWhenPressed": "Trigger when pressed",
"range": "Range"
},
"SteeringWheel": {
"steeringWheel": "SteeringWheel",
"offset": "Offset"
}
}
},
"sidebar": {
"noControledDevice": "No devices are controlled"
}
}

21
src/i18n/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { createI18n } from "vue-i18n";
import { Store } from "@tauri-apps/plugin-store";
import enUS from "./en-US.json";
import zhCN from "./zh-CN.json";
const localStore = new Store("store.bin");
const i18n = createI18n({
allowComposition: true,
messages: {
"en-US": enUS,
"zh-CN": zhCN,
},
});
localStore.get<"en-US" | "zh-CN">("language").then((language) => {
i18n.global.locale = language ?? "en-US";
});
export default i18n;

220
src/i18n/zh-CN.json Normal file
View File

@ -0,0 +1,220 @@
{
"pages": {
"Device": {
"status": "状态",
"shutdown": {
"title": "警告",
"content": "确定关闭Scrcpy控制服务?",
"positiveText": "确定",
"negativeText": "取消"
},
"menu": {
"control": "控制此设备",
"screen": "获取屏幕尺寸"
},
"deviceControl": {
"inputScreenSize": "请正确输入当前控制设备的屏幕尺寸",
"closeCurDevice": "请先关闭当前控制设备",
"controlInfo": "屏幕尺寸已保存,正在启动控制服务,请保持设备亮屏",
"connectTimeout": "设备连接超时"
},
"deviceGetScreenSize": "设备屏幕尺寸为:",
"inputWirelessAddress": "请输入无线调试地址",
"localPort": "本地端口",
"localPortPlaceholder": "Scrcpy 本地端口",
"wireless": "无线连接",
"wirelessPlaceholder": "无线连接地址",
"connect": "连接",
"deviceSize": {
"title": "设备屏幕尺寸",
"width": "宽度",
"widthPlaceholder": "屏幕宽度",
"heightPlaceholder": "屏幕高度",
"height": "高度",
"tip": "提示:请正确输入当前控制设备的屏幕尺寸,这是成功发送触摸事件的必要参数"
},
"controledDevice": "受控设备",
"noControledDevice": "无受控设备",
"availableDevice": "可用设备"
},
"Mask": {
"keyconfigException": "按键方案异常,请删除此方案",
"blankConfig": "空白方案",
"checkUpdate": {
"failed": "检查更新失败",
"isLatest": "最新版本: {0},当前已是最新版本",
"notLatest": {
"title": "最新版本:{0}",
"positiveText": "前往发布页",
"negativeText": "取消"
}
},
"noControledDevice": {
"title": "未找到受控设备",
"content": "请前往设备页面,控制任意设备",
"positiveText": "去控制"
},
"inputBoxPlaceholder": "输入文本后按Enter/Esc"
},
"Setting": {
"tabs": {
"basic": "基本设置",
"mask": "蒙版设置",
"about": "关于"
},
"Mask": {
"incorrectArea": "请正确输入蒙版的坐标和尺寸",
"areaSaved": "蒙版区域已保存",
"buttonPrompts": "按键提示",
"ifButtonPrompts": "是否显示",
"opacity": "不透明度",
"areaAdjust": "蒙版调整",
"areaPlaceholder": {
"x": "左上角X坐标"
},
"areaFormPlaceholder": {
"y": "左上角Y坐标",
"w": "蒙版宽度",
"h": "蒙版高度"
},
"areaTip": "提示:蒙版尺寸与设备尺寸将用于坐标转换,请保证尺寸的准确性",
"areaFormMissing": {
"x": "请输入蒙版左上角X坐标",
"y": "请输入蒙版左上角Y坐标",
"w": "请输入蒙版宽度",
"h": "请输入蒙版高度"
}
},
"Basic": {
"delLocalStore": {
"dialog": {
"title": "警告",
"positiveText": "删除",
"negativeText": "取消",
"delKey": "即将删除数据\"{0}\",删除操作不可撤回,是否继续?",
"delAll": "即将清空数据,操作不可撤回,且清空后将重启软件,是否继续?"
},
"warning": "删除数据可能导致无法预料的后果,请慎重操作。若出现异常请尝试清空数据并重启软件。"
},
"language": "语言",
"localStore": "本地数据",
"delCurData": "删除当前数据"
},
"About": {
"about": "关于",
"introduction": "一个基于 Rust & Tarui 的 Scrcpy 客户端,旨在提供鼠标键盘按键映射来控制安卓设备。",
"github": "Github 仓库",
"blog": "AkiChase 博客",
"update": "更新",
"curVersion": "当前版本:{0}",
"checkUpdate": "检查更新",
"checkUpdateOnStartup": "启动时检查软件更新"
}
},
"KeyBoard": {
"addButton": {
"Tap": "普通点击",
"SteeringWheel": "键盘行走",
"Skill": "技能",
"CancelSkill": "技能取消",
"Observation": "观察视角",
"Macro": "宏"
},
"buttonKeyRepeat": "按键重复: {0}",
"noSaveDialog": {
"title": "警告",
"content": "当前方案尚未保存,是否保存?",
"positiveText": "保存",
"negativeText": "取消",
"keyRepeat": "存在重复按键,无法保存"
},
"KeyCommon": {
"macroParseSuccess": "宏代码解析成功,但不保证代码正确性,请自行测试",
"macroParseFailed": "宏代码保存失败,请检查代码格式是否正确",
"macro": "宏代码",
"editMacro": "编辑宏代码",
"macroModal": {
"title": "宏编辑",
"down": "按下按键执行的宏",
"loop": "按住执行的宏",
"placeholder": "JSON宏代码, 可为空",
"up": "抬起执行的宏"
}
},
"setting": {
"touchTime": "触摸时长",
"touchTimePlaceholder": "请输入触摸时长(ms)",
"pointerID": "触点ID",
"pointerIDPlaceholder": "请输入触点ID",
"note": "备注",
"notePlaceholder": "请输入备注"
},
"KeyInfo": {
"title": "Key Info",
"note": "Press any key"
},
"Observation": {
"observation": "观察视角",
"scale": "灵敏度",
"scalePlaceholder": "请输入灵敏度"
},
"KeySetting": {
"onlyOneConfig": "当前仅有一个按键方案,点击导入默认,可导入预设方案",
"importFailed": "导入失败",
"importSuccess": "按键方案已导入",
"importDefaultFailed": "默认按键方案导入失败",
"importDefaultSuccess": "已导入{0}个默认方案",
"configEdited": "请先保存或还原当前方案",
"newConfig": "新方案",
"newConfigSuccess": "新方案已创建",
"copyConfigTitle": "{0}-副本",
"copyConfigSuccess": "方案已复制为:{0}",
"exportSuccess": "当前按键方案已导出到剪切板",
"exportFailed": "按键方案导出失败",
"checkConfigSizeWarning": "请注意当前按键方案\"{0}\"与蒙版尺寸不一致,若有需要可进行迁移",
"migrateConfigTitle": "{0}-迁移",
"migrateConfigSuccess": "已迁移到新方案:{0}",
"migrateConfigNeedless": "当前方案符合蒙版尺寸,无需迁移",
"buttonDrag": "长按可拖动",
"config": "按键映射方案",
"configRelativeSize": "相对蒙版尺寸: {0}x{1}",
"saveConfig": "保存方案",
"resetConfig": "还原方案",
"createConfig": "新建方案",
"copyConfig": "复制方案",
"migrateConfig": "迁移方案",
"delConfig": "删除方案",
"renameConfig": "重命名",
"others": "其他",
"importConfig": "导入方案",
"exportConfig": "导出方案",
"importDefaultConfig": "导入默认",
"keyInfo": "按键信息",
"addButtonTip": "提示:右键空白区域可添加按键",
"importPlaceholder": "粘贴单个按键方案的JSON文本 (此处无法对按键方案的合法性进行判断, 请确保JSON内容正确)",
"import": "导入",
"renameTitle": "重命名按键方案",
"delConfigLeast": "至少保留一个方案",
"delSuccess": "方案已删除:{0}",
"renameSuccess": "方案已重命名为:{0}",
"renameEmpty": "方案名不能为空",
"saveKeyRepeat": "存在重复按键,无法保存"
},
"KeySkill": {
"skill": "技能",
"options": "选项",
"double": "双击施放",
"directionless": "无方向技能",
"triggerWhenPressed": "按下时触发",
"range": "范围"
},
"SteeringWheel": {
"steeringWheel": "键盘行走",
"offset": "偏移"
}
}
},
"sidebar": {
"noControledDevice": "未控制任何设备"
}
}

View File

@ -3,10 +3,12 @@ import { createPinia } from "pinia";
import "./styles.css";
import App from "./App.vue";
import router from "./router";
import i18n from "./i18n";
const pinia = createPinia();
const app = createApp(App);
app.use(router);
app.use(pinia);
app.use(i18n);
app.mount("#app");

View File

@ -25,20 +25,9 @@ export const useGlobalStore = defineStore("global", () => {
device: Device;
}
const screenSizeW: Ref<number> = ref(0);
const screenSizeH: Ref<number> = ref(0);
const controledDevice: Ref<ControledDevice | null> = ref(null);
const keyMappingConfigList: Ref<KeyMappingConfig[]> = ref([]);
const curKeyMappingIndex = ref(0);
const editKeyMappingList: Ref<KeyMapping[]> = ref([]);
const maskButton = ref({
transparency: 0.5,
show: true,
});
const showInputBox: (_: boolean) => void = (_: boolean) => {};
let checkUpdate: () => Promise<void> = async () => {};
@ -81,17 +70,31 @@ export const useGlobalStore = defineStore("global", () => {
localStore.set("curKeyMappingIndex", index);
}
// persistent storage
const screenSizeW: Ref<number> = ref(0);
const screenSizeH: Ref<number> = ref(0);
const keyMappingConfigList: Ref<KeyMappingConfig[]> = ref([]);
const curKeyMappingIndex = ref(0);
const maskButton = ref({
transparency: 0.5,
show: true,
});
const checkUpdateAtStart = ref(true);
return {
showLoading,
hideLoading,
showLoadingRef,
controledDevice,
// persistent storage
screenSizeW,
screenSizeH,
keyMappingConfigList,
curKeyMappingIndex,
editKeyMappingList,
maskButton,
checkUpdateAtStart,
// in-memory storage
showLoading,
hideLoading,
showLoadingRef,
controledDevice,
editKeyMappingList,
showInputBox,
applyEditKeyMappingList,
resetEditKeyMappingList,