├── .github
└── workflows
│ ├── release.yml
│ └── test-build.yml
├── .gitignore
├── README.md
├── UPDATE_LOG.md
├── app-icon.png
├── auto-imports.d.ts
├── components.d.ts
├── demo
└── index.html
├── index.html
├── live2d.html
├── package-lock.json
├── package.json
├── public
├── assets
│ ├── live2d.min.js
│ └── live2dcubismcore.min.js
├── tauri.svg
└── vite.svg
├── scripts
├── release.mjs
├── tauriversion.mjs
├── updatelog.mjs
└── updater.mjs
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── src
│ ├── app
│ │ ├── commands.rs
│ │ ├── config.rs
│ │ ├── menu.rs
│ │ ├── mod.rs
│ │ └── mstruct.rs
│ ├── main.rs
│ ├── plugins
│ │ ├── autostart.rs
│ │ ├── checkupdate.rs
│ │ └── mod.rs
│ └── utils.rs
├── tauri.conf.json
└── web_server
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── src
├── App.vue
├── assets
│ ├── autoload.js
│ ├── flat-ui-icons-regular.eot
│ ├── flat-ui-icons-regular.svg
│ ├── flat-ui-icons-regular.ttf
│ ├── flat-ui-icons-regular.woff
│ ├── vue.svg
│ ├── waifu-tips.js
│ ├── waifu-tips.json
│ └── waifu.css
├── components
│ ├── Config.vue
│ └── Model.vue
├── hooks
│ ├── useInterval.ts
│ ├── useListenEvent.ts
│ ├── useModel.ts
│ └── useUpdate.ts
├── live2d
│ ├── App.ts
│ └── index.vue
├── main.ts
├── plugins
│ ├── autostart.ts
│ ├── checkupdate.ts
│ ├── index.ts
│ └── modelserve.ts
├── style.css
├── types
│ └── index.d.ts
├── util
│ └── index.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # 可选,将显示在 GitHub 存储库的“操作”选项卡中的工作流名称
2 | name: Release CI
3 |
4 | # 指定此工作流的触发器
5 | on:
6 | push:
7 | # 匹配特定标签 (refs/tags)
8 | tags:
9 | - "v*" # 推送事件匹配 v*, 例如 v1.0,v20.15.10 等来触发工作流
10 |
11 | # 需要运行的作业组合
12 | jobs:
13 | # 任务:创建 release 版本
14 | create-release:
15 | runs-on: ubuntu-latest
16 | outputs:
17 | RELEASE_UPLOAD_ID: ${{ steps.create_release.outputs.id }}
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | # 查询版本号(tag)
22 | - name: Query version number
23 | id: get_version
24 | shell: bash
25 | run: |
26 | echo "using version tag ${GITHUB_REF:10}"
27 | echo ::set-output name=version::"${GITHUB_REF:10}"
28 |
29 | # 根据查询到的版本号创建 release
30 | - name: Create Release
31 | id: create_release
32 | uses: actions/create-release@v1
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 | with:
36 | draft: false
37 | tag_name: "${{ steps.get_version.outputs.VERSION }}"
38 | release_name: "Live2d ${{ steps.get_version.outputs.VERSION }}"
39 | body: "See the assets to download this version and install."
40 |
41 | # 编译 Tauri
42 | build-tauri:
43 | needs: create-release
44 | strategy:
45 | fail-fast: false
46 | matrix:
47 | include:
48 | - build: linux
49 | os: ubuntu-latest
50 | arch: x86_64
51 | target: x86_64-unknown-linux-gnu
52 | - build: macos
53 | os: macos-latest
54 | arch: x86_64
55 | target: x86_64-apple-darwin
56 | - build: macos
57 | os: macos-latest
58 | arch: aarch64
59 | target: aarch64-apple-darwin
60 | - build: windows
61 | os: windows-latest
62 | arch: x86_64
63 | target: x86_64-pc-windows-msvc
64 |
65 | runs-on: ${{ matrix.os }}
66 | steps:
67 | - uses: actions/checkout@v3
68 |
69 | # 安装 Node.js
70 | - name: Setup node
71 | uses: actions/setup-node@v3
72 | with:
73 | node-version: 16
74 | - uses: pnpm/action-setup@v2
75 | name: Install pnpm
76 | id: pnpm-install
77 | with:
78 | version: 7
79 | run_install: false
80 |
81 | - name: Get pnpm store directory
82 | id: pnpm-cache
83 | shell: bash
84 | run: |
85 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
86 |
87 | - uses: actions/cache@v3
88 | name: Setup pnpm cache
89 | with:
90 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
91 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
92 | restore-keys: |
93 | ${{ runner.os }}-pnpm-store-
94 |
95 | - name: Install dependencies
96 | run: pnpm install
97 | # 安装 Rust
98 | - name: Install Rust stable
99 | uses: actions-rs/toolchain@v1
100 | with:
101 | toolchain: stable
102 |
103 | # 使用 Rust 缓存,加快安装速度 (没感觉快)
104 | - name: Rust-cache
105 | uses: Swatinem/rust-cache@v2
106 | with:
107 | prefix-key: ${{ runner.os }}-Rust
108 |
109 | # ubuntu-latest webkit2gtk-4.0相关依赖
110 | - name: Install dependencies (ubuntu only)
111 | if: matrix.platform == 'ubuntu-latest'
112 | run: |
113 | sudo apt-get update
114 | sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
115 |
116 | # 安装依赖执行构建,以及推送 github release
117 | - name: Install app dependencies and build it
118 | run: pnpm i && pnpm build
119 | - uses: tauri-apps/tauri-action@v0.3
120 | env:
121 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
122 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
123 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
124 | with:
125 | releaseId: ${{ needs.create-release.outputs.RELEASE_UPLOAD_ID }}
126 |
127 | updater:
128 | runs-on: ubuntu-latest
129 | needs: [create-release, build-tauri]
130 | steps:
131 | - uses: actions/checkout@v3
132 | - run: yarn && yarn updater
133 | env:
134 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
135 |
136 | - name: Deploy install.json
137 | uses: peaceiris/actions-gh-pages@v3
138 | with:
139 | github_token: ${{ secrets.GITHUB_TOKEN }}
140 | publish_dir: ./updater
141 | force_orphan: true
142 |
--------------------------------------------------------------------------------
/.github/workflows/test-build.yml:
--------------------------------------------------------------------------------
1 | # 可选,将显示在 GitHub 存储库的“操作”选项卡中的工作流名称
2 | name: Test Build CI
3 |
4 | # 指定此工作流的触发器
5 | on:
6 | workflow_dispatch:
7 | inputs:
8 | platform:
9 | description: "platform"
10 | required: true
11 | default: "macos-latest"
12 | type: choice
13 | options:
14 | - macos-latest
15 | - windows-latest
16 | - ubuntu-latest
17 |
18 | # 需要运行的作业组合
19 | jobs:
20 | # 编译 Tauri
21 | build-tauri:
22 | runs-on: ${{ inputs.platform}}
23 | steps:
24 | - uses: actions/checkout@v3
25 |
26 | # 安装 Node.js
27 | - name: Setup node
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: 16
31 | - uses: pnpm/action-setup@v2
32 | name: Install pnpm
33 | id: pnpm-install
34 | with:
35 | version: 7
36 | run_install: false
37 |
38 | # ubuntu-latest webkit2gtk-4.0相关依赖
39 | - name: Install dependencies (ubuntu only)
40 | if: matrix.platform == 'ubuntu-latest'
41 | run: |
42 | sudo apt-get update
43 | sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
44 |
45 | # 安装依赖执行构建,以及推送 github release
46 | - name: Install app dependencies and build it
47 | run: pnpm i && pnpm build
48 | - uses: tauri-apps/tauri-action@v0.3
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
52 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .history
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ### 为什么有这个项目 (目前退坑了)
6 |
7 | - electron 版本的 live2d,软件占用太大了(近 100M),tauri(5M),电脑内存太小放弃了 electron
8 | - tauri 没有 electron 那么完备的社区(太痛了)
9 | - tauri 有个缺点 CPU 使用率挺高的 !!!是·
10 |
11 | ### 功能演示
12 |
13 |
14 |
15 |
16 |
17 | ### TODO
18 |
19 | - 大小缩放[已实现] 问题挺多
20 | - 开机自启动[已实现]
21 | - 使用 PixiJS 加载[v2,v3 版本的]模型[已实现]
22 | - 使用本地模型、远程模型的加载[已实现]
23 |
24 | ### 已知问题
25 |
26 | - mac 下出现窗口虚线,暂不知原因,可能是透明背景下的问题
27 |
28 | ### 文件下载安装 使用 ghproxy 下载
29 |
30 | - `自动更新可能无效,请使用手动下载`
31 |
32 | - [最新版本](https://github.com/itxve/tauri-live2d/releases/latest)
33 |
34 | - 下载慢可使用[ghproxy](https://ghproxy.com/) 加速
35 |
36 | ###
37 |
38 | 代码有点乱,但是不想动了
39 |
40 | ### 鸣谢
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | [Flat-UI](https://designmodo.github.io/Flat-UI)
49 |
50 | # 推荐学习项目
51 |
52 | [ChatGPT](https://github.com/lencx/ChatGPT)
53 |
54 | # 相关项目
55 |
56 | [PPet](https://github.com/zenghongtu/PPet)
57 |
58 | [live2dviewer](https://github.com/doitian/live2dviewer)
59 |
60 | Live2DViewerEX
61 |
62 | Model 资源: [zenghongtu/live2d-model-assets](https://github.com/zenghongtu/live2d-model-assets)
63 |
64 | [模型下载](https://github.com/itxve/tauri-live2d/issues/2)
65 |
66 |
67 | # 免责声明
68 |
69 | ### 该软件仅用于个人学习使用
70 |
71 | - 禁止商用或者非法用途.
72 | - 禁止商用或者非法用途.
73 | - 禁止商用或者非法用途.
74 |
--------------------------------------------------------------------------------
/UPDATE_LOG.md:
--------------------------------------------------------------------------------
1 | # Updater Log
2 | ## v3.0.5
3 | 优化code 😆😆😆
4 |
5 | ## v3.0.4
6 | 移出未使用代码
7 |
8 | ## v3.0.3
9 |
10 | 添加自动更新
11 |
12 | ## v3.0.2
13 |
14 | 修复 bug,增加模型加载失败 tip,模型目录变更软件重启
15 |
16 | ## v3.0.1
17 |
18 | 修复 bug,增加一个默认模型
19 |
--------------------------------------------------------------------------------
/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/app-icon.png
--------------------------------------------------------------------------------
/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | export {}
3 | declare global {
4 | const EffectScope: typeof import('vue')['EffectScope']
5 | const computed: typeof import('vue')['computed']
6 | const createApp: typeof import('vue')['createApp']
7 | const customRef: typeof import('vue')['customRef']
8 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
9 | const defineComponent: typeof import('vue')['defineComponent']
10 | const effectScope: typeof import('vue')['effectScope']
11 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
12 | const getCurrentScope: typeof import('vue')['getCurrentScope']
13 | const h: typeof import('vue')['h']
14 | const inject: typeof import('vue')['inject']
15 | const isProxy: typeof import('vue')['isProxy']
16 | const isReactive: typeof import('vue')['isReactive']
17 | const isReadonly: typeof import('vue')['isReadonly']
18 | const isRef: typeof import('vue')['isRef']
19 | const markRaw: typeof import('vue')['markRaw']
20 | const nextTick: typeof import('vue')['nextTick']
21 | const onActivated: typeof import('vue')['onActivated']
22 | const onBeforeMount: typeof import('vue')['onBeforeMount']
23 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
24 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
25 | const onDeactivated: typeof import('vue')['onDeactivated']
26 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
27 | const onMounted: typeof import('vue')['onMounted']
28 | const onRenderTracked: typeof import('vue')['onRenderTracked']
29 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
30 | const onScopeDispose: typeof import('vue')['onScopeDispose']
31 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
32 | const onUnmounted: typeof import('vue')['onUnmounted']
33 | const onUpdated: typeof import('vue')['onUpdated']
34 | const provide: typeof import('vue')['provide']
35 | const reactive: typeof import('vue')['reactive']
36 | const readonly: typeof import('vue')['readonly']
37 | const ref: typeof import('vue')['ref']
38 | const resolveComponent: typeof import('vue')['resolveComponent']
39 | const resolveDirective: typeof import('vue')['resolveDirective']
40 | const shallowReactive: typeof import('vue')['shallowReactive']
41 | const shallowReadonly: typeof import('vue')['shallowReadonly']
42 | const shallowRef: typeof import('vue')['shallowRef']
43 | const toRaw: typeof import('vue')['toRaw']
44 | const toRef: typeof import('vue')['toRef']
45 | const toRefs: typeof import('vue')['toRefs']
46 | const triggerRef: typeof import('vue')['triggerRef']
47 | const unref: typeof import('vue')['unref']
48 | const useAttrs: typeof import('vue')['useAttrs']
49 | const useCssModule: typeof import('vue')['useCssModule']
50 | const useCssVars: typeof import('vue')['useCssVars']
51 | const useSlots: typeof import('vue')['useSlots']
52 | const watch: typeof import('vue')['watch']
53 | const watchEffect: typeof import('vue')['watchEffect']
54 | const watchPostEffect: typeof import('vue')['watchPostEffect']
55 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
56 | }
57 |
--------------------------------------------------------------------------------
/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/core/pull/3399
4 | import '@vue/runtime-core'
5 |
6 | export {}
7 |
8 | declare module '@vue/runtime-core' {
9 | export interface GlobalComponents {
10 | Live2dConfig: typeof import('./src/components/Live2dConfig.vue')['default']
11 | Models: typeof import('./src/components/Models.vue')['default']
12 | NButton: typeof import('naive-ui')['NButton']
13 | NCard: typeof import('naive-ui')['NCard']
14 | NFormItem: typeof import('naive-ui')['NFormItem']
15 | NSpace: typeof import('naive-ui')['NSpace']
16 | NTable: typeof import('naive-ui')['NTable']
17 | NTabPane: typeof import('naive-ui')['NTabPane']
18 | NTabs: typeof import('naive-ui')['NTabs']
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | live2d
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Tauri + Vue + TS
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/live2d.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Tauri + Vue + TS
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-tauri-app",
3 | "private": true,
4 | "version": "3.0.5",
5 | "type": "module",
6 | "license": "MIT",
7 | "keywords": [
8 | "live2d",
9 | "desktop",
10 | "pixijs",
11 | "tauri",
12 | "macos",
13 | "linux",
14 | "windows"
15 | ],
16 | "homepage": "https://github.com/itxve/tauri-live2d",
17 | "bugs": "https://github.com/itxve/tauri-live2d/issues",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/itxve/tauri-live2d"
21 | },
22 | "scripts": {
23 | "dev": "vite",
24 | "build": "vue-tsc --noEmit && vite build",
25 | "preview": "vite preview",
26 | "tauri": "tauri",
27 | "tauri:dev": "RUST_BACKTRACE=full tauri dev",
28 | "updater": "node scripts/updater.mjs",
29 | "release": "node scripts/release.mjs",
30 | "tauriversion": "node scripts/tauriversion.mjs"
31 | },
32 | "dependencies": {
33 | "@rollup/plugin-inject": "^5.0.3",
34 | "@tauri-apps/api": "^1.2.0",
35 | "pixi-live2d-display": "^0.4.0",
36 | "pixi.js": "6.5.6",
37 | "vue": "^3.2.37"
38 | },
39 | "devDependencies": {
40 | "@actions/github": "^5.1.0",
41 | "@rollup/plugin-alias": "^4.0.2",
42 | "@tauri-apps/cli": "^1.2.0",
43 | "@types/node": "^18.7.10",
44 | "@vitejs/plugin-vue": "^3.0.1",
45 | "naive-ui": "^2.34.2",
46 | "node-fetch": "^3.2.10",
47 | "typescript": "^4.6.4",
48 | "unplugin-auto-import": "^0.12.0",
49 | "unplugin-vue-components": "^0.22.11",
50 | "vite": "^3.0.2",
51 | "vue-tsc": "^1.0.0"
52 | }
53 | }
--------------------------------------------------------------------------------
/public/tauri.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/release.mjs:
--------------------------------------------------------------------------------
1 | import { createRequire } from "module";
2 | import { execSync } from "child_process";
3 | import fs from "fs";
4 |
5 | import updatelog from "./updatelog.mjs";
6 | import updateTauriVersion from "./tauriversion.mjs";
7 |
8 | const require = createRequire(import.meta.url);
9 |
10 | async function release() {
11 | const flag = process.argv[2] ?? "patch";
12 | const packageJson = require("../package.json");
13 | let [a, b, c] = packageJson.version.split(".").map(Number);
14 |
15 | if (flag === "major") {
16 | a += 1;
17 | b = 0;
18 | c = 0;
19 | } else if (flag === "minor") {
20 | b += 1;
21 | c = 0;
22 | } else if (flag === "patch") {
23 | c += 1;
24 | } else {
25 | console.log(`Invalid flag "${flag}"`);
26 | process.exit(1);
27 | }
28 |
29 | const nextVersion = `${a}.${b}.${c}`;
30 | packageJson.version = nextVersion;
31 |
32 | const nextTag = `v${nextVersion}`;
33 | await updatelog(nextTag, "release");
34 | await updateTauriVersion(nextVersion);
35 |
36 | fs.writeFileSync("./package.json", JSON.stringify(packageJson, null, 2));
37 |
38 | execSync(
39 | "git add ./package.json ./UPDATE_LOG.md ./src-tauri/tauri.conf.json"
40 | );
41 | execSync(`git commit -m "v${nextVersion}"`);
42 | execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
43 | execSync(`git push`);
44 | execSync(`git push origin v${nextVersion}`);
45 | console.log(`Publish Successfully...`);
46 | }
47 |
48 | release().catch(console.error);
49 |
--------------------------------------------------------------------------------
/scripts/tauriversion.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 |
4 | const TAURI_CONFIG = "src-tauri/tauri.conf.json";
5 |
6 | export default function updateTauriVersion(new_version) {
7 | const file = path.join(process.cwd(), TAURI_CONFIG);
8 |
9 | if (!fs.existsSync(file)) {
10 | console.log("Could not found tauri.conf.json");
11 | process.exit(1);
12 | }
13 |
14 | let content = fs.readFileSync(file, { encoding: "utf8" });
15 | content = JSON.parse(content);
16 | content.package.version = new_version;
17 | fs.writeFileSync(file, JSON.stringify(content, null, 2));
18 | }
19 |
--------------------------------------------------------------------------------
/scripts/updatelog.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 |
4 | const UPDATE_LOG = "UPDATE_LOG.md";
5 |
6 | export default function updatelog(tag, type = "updater") {
7 | const reTag = /## v[\d\.]+/;
8 |
9 | const file = path.join(process.cwd(), UPDATE_LOG);
10 |
11 | if (!fs.existsSync(file)) {
12 | console.log("Could not found UPDATE_LOG.md");
13 | process.exit(1);
14 | }
15 |
16 | let _tag;
17 | const tagMap = {};
18 | const content = fs.readFileSync(file, { encoding: "utf8" }).split("\n");
19 |
20 | content.forEach((line, index) => {
21 | if (reTag.test(line)) {
22 | _tag = line.slice(3).trim();
23 | if (!tagMap[_tag]) {
24 | tagMap[_tag] = [];
25 | return;
26 | }
27 | }
28 | if (_tag) {
29 | tagMap[_tag].push(line);
30 | }
31 | if (reTag.test(content[index + 1])) {
32 | _tag = null;
33 | }
34 | });
35 |
36 | if (!tagMap?.[tag]) {
37 | console.log(
38 | `${type === "release" ? "[UPDATE_LOG.md] " : ""}Tag ${tag} does not exist`
39 | );
40 | process.exit(1);
41 | }
42 |
43 | return tagMap[tag].join("\n").trim() || "";
44 | }
45 |
--------------------------------------------------------------------------------
/scripts/updater.mjs:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import { getOctokit, context } from "@actions/github";
3 | import fs from "fs";
4 |
5 | import updatelog from "./updatelog.mjs";
6 |
7 | const token = process.env.GITHUB_TOKEN;
8 |
9 | async function updater() {
10 | if (!token) {
11 | console.log("GITHUB_TOKEN is required");
12 | process.exit(1);
13 | }
14 |
15 | const options = { owner: context.repo.owner, repo: context.repo.repo };
16 | const github = getOctokit(token);
17 |
18 | const { data: tags } = await github.rest.repos.listTags({
19 | ...options,
20 | per_page: 10,
21 | page: 1,
22 | });
23 |
24 | const tag = tags.find((t) => t.name.startsWith("v"));
25 | // console.log(`${JSON.stringify(tag, null, 2)}`);
26 |
27 | if (!tag) return;
28 |
29 | const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
30 | ...options,
31 | tag: tag.name,
32 | });
33 |
34 | const updateData = {
35 | version: tag.name,
36 | notes: updatelog(tag.name), // use UPDATE_LOG.md
37 | pub_date: new Date().toISOString(),
38 | platforms: {
39 | win64: { signature: "", url: "" }, // compatible with older formats
40 | linux: { signature: "", url: "" }, // compatible with older formats
41 | darwin: { signature: "", url: "" }, // compatible with older formats
42 | "darwin-aarch64": { signature: "", url: "" },
43 | "darwin-x86_64": { signature: "", url: "" },
44 | "linux-x86_64": { signature: "", url: "" },
45 | "windows-x86_64": { signature: "", url: "" },
46 | // 'windows-i686': { signature: '', url: '' }, // no supported
47 | },
48 | };
49 |
50 | const setAsset = async (asset, reg, platforms) => {
51 | let sig = "";
52 | if (/.sig$/.test(asset.name)) {
53 | sig = await getSignature(asset.browser_download_url);
54 | }
55 | platforms.forEach((platform) => {
56 | if (reg.test(asset.name)) {
57 | // platform signature
58 | if (sig) {
59 | updateData.platforms[platform].signature = sig;
60 | return;
61 | }
62 | // platform url
63 | updateData.platforms[platform].url = asset.browser_download_url;
64 | }
65 | });
66 | };
67 |
68 | const promises = latestRelease.assets.map(async (asset) => {
69 | // windows
70 | await setAsset(asset, /.msi.zip/, ["win64", "windows-x86_64"]);
71 |
72 | // darwin
73 | await setAsset(asset, /.app.tar.gz/, [
74 | "darwin",
75 | "darwin-x86_64",
76 | "darwin-aarch64",
77 | ]);
78 |
79 | // linux
80 | await setAsset(asset, /.AppImage.tar.gz/, ["linux", "linux-x86_64"]);
81 | });
82 | await Promise.allSettled(promises);
83 |
84 | if (!fs.existsSync("updater")) {
85 | fs.mkdirSync("updater");
86 | }
87 | fs.writeFileSync(
88 | "./updater/install.json",
89 | JSON.stringify(updateData, null, 2)
90 | );
91 | console.log("Generate updater/install.json");
92 | }
93 |
94 | updater().catch(console.error);
95 |
96 | // get the signature file content
97 | async function getSignature(url) {
98 | try {
99 | const response = await fetch(url, {
100 | method: "GET",
101 | headers: { "Content-Type": "application/octet-stream" },
102 | });
103 | return response.text();
104 | } catch (_) {
105 | return "";
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | authors = ["you"]
3 | description = "A Tauri App For Live2d"
4 | edition = "2021"
5 | license = "MIT"
6 | name = "live2d"
7 | repository = "https://github.com/itxve/tauri-live2d"
8 | rust-version = "1.65.0"
9 | version = "1.0.0"
10 |
11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12 |
13 | [build-dependencies]
14 | tauri-build = {version = "1.2.1", features = [] }
15 |
16 | [dependencies]
17 | anyhow = "1.0.66"
18 | auto-launch = "0.4.0"
19 | glob = "0.3.0"
20 | log = "0.4.17"
21 | notify = {version = "5.0.0", features = ["serde"] }
22 | serde = {version = "1.0", features = ["derive"] }
23 | serde_json = "1.0"
24 | tauri = {version = "1.2.3", features = ["api-all", "macos-private-api", "system-tray", "updater"] }
25 | thiserror = "1.0"
26 | tokio = {version = "1.23.0", features = ["macros"] }
27 | web_server= {path = "./web_server"}
28 |
29 | [features]
30 | # by default Tauri runs in production mode
31 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
32 | default = ["custom-protocol"]
33 | # this feature is used used for production builds where `devPath` points to the filesystem
34 | # DO NOT remove this
35 | custom-protocol = ["tauri/custom-protocol"]
36 |
37 | [profile.release]
38 | lto = true
39 | opt-level = "z"
40 | strip = true
41 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/src/app/commands.rs:
--------------------------------------------------------------------------------
1 | use crate::app::config::AppConf;
2 | use crate::app::mstruct::{InitType, Rt};
3 |
4 | use crate::plugins::{Error, Result};
5 | use std::fs;
6 |
7 | #[tauri::command]
8 | pub fn read_file(file_path: std::path::PathBuf) -> Rt> {
9 | std::fs::read(file_path).map_or_else(
10 | |err| Rt {
11 | data: vec![],
12 | err: err.to_string(),
13 | },
14 | |data| Rt {
15 | data,
16 | err: String::from(""),
17 | },
18 | )
19 | }
20 |
21 | #[tauri::command]
22 | pub fn write_file(file_path: std::path::PathBuf, data: &str) -> Rt {
23 | std::fs::write(file_path, data).map_or_else(
24 | |err| Rt {
25 | data: "".to_string(),
26 | err: err.to_string(),
27 | },
28 | |_| Rt {
29 | data: "".to_string(),
30 | err: "".to_string(),
31 | },
32 | )
33 | }
34 |
35 | #[tauri::command]
36 | pub fn model_list() -> Result> {
37 | let config = AppConf::read();
38 | use glob::glob;
39 | let mut models = vec![];
40 | let api = format!("http://127.0.0.1:{}", config.port);
41 |
42 | if config.model_dir != "" {
43 | for file_name_result in glob(&format!("{}/**/*.model3.json", config.model_dir))
44 | .unwrap()
45 | .chain(glob(&format!("{}/**/index.json", config.model_dir)).unwrap())
46 | .chain(glob(&format!("{}/**/*.model.json", config.model_dir)).unwrap())
47 | {
48 | match file_name_result {
49 | Ok(file_path) => {
50 | models.push(
51 | file_path
52 | .to_str()
53 | .unwrap()
54 | .replace(config.model_dir.as_str(), api.as_str()),
55 | );
56 | }
57 | Err(e) => {
58 | eprintln!("ERROR: {}", e);
59 | }
60 | };
61 | }
62 | }
63 | Ok(models)
64 | }
65 |
66 | #[tauri::command]
67 | pub fn init_app_data_path(file_path: std::path::PathBuf) -> InitType {
68 | println!("file_path: {:?}", &file_path);
69 | if file_path.exists() {
70 | InitType::EXIST
71 | } else {
72 | fs::DirBuilder::new()
73 | .recursive(true)
74 | .create(file_path)
75 | .map_or_else(|_| InitType::CreateError, |_| InitType::SUCCESS)
76 | }
77 | }
78 |
79 | #[tauri::command]
80 | pub fn read_config() -> AppConf {
81 | AppConf::read()
82 | }
83 |
84 | #[tauri::command]
85 | pub fn write_config(value: String) -> AppConf {
86 | AppConf::read().amend_str(value).write()
87 | }
88 |
--------------------------------------------------------------------------------
/src-tauri/src/app/config.rs:
--------------------------------------------------------------------------------
1 | #![allow(unused)]
2 | use crate::utils::{app_root, create_file, exists};
3 | use log::{error, info};
4 | use serde_json::Value;
5 | use std::{collections::BTreeMap, path::PathBuf};
6 | use tauri::Manager;
7 |
8 | pub const APP_CONFIG_FILE: &str = "live2d.conf.json";
9 |
10 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
11 | pub struct AppConf {
12 | pub port: u16,
13 | pub model_dir: String,
14 | pub width: u16,
15 | pub height: u16,
16 | pub x: u16,
17 | pub y: u16,
18 | pub check_update: bool,
19 | pub remote_list: Vec,
20 | pub model_block: bool,
21 | pub auto_start: bool,
22 | }
23 |
24 | impl AppConf {
25 | pub fn new() -> Self {
26 | info!("conf_init");
27 | Self {
28 | port: 0,
29 | width: 400u16,
30 | height: 500u16,
31 | x: 100u16,
32 | y: 120u16,
33 | check_update: false,
34 | remote_list: vec![],
35 | model_block: true,
36 | model_dir: "".into(),
37 | auto_start: false,
38 | }
39 | }
40 |
41 | pub fn file_path() -> PathBuf {
42 | app_root().join(APP_CONFIG_FILE)
43 | }
44 |
45 | pub fn read() -> Self {
46 | match std::fs::read_to_string(Self::file_path()) {
47 | Ok(v) => {
48 | if let Ok(v2) = serde_json::from_str::(&v) {
49 | v2
50 | } else {
51 | error!("conf_read_parse_error");
52 | Self::default()
53 | }
54 | }
55 | Err(err) => {
56 | error!("conf_read_error: {}", err);
57 | Self::default()
58 | }
59 | }
60 | }
61 |
62 | pub fn write(self) -> Self {
63 | let path = &Self::file_path();
64 | if !exists(path) {
65 | create_file(path).unwrap();
66 | info!("conf_create");
67 | }
68 | if let Ok(v) = serde_json::to_string_pretty(&self) {
69 | std::fs::write(path, v).unwrap_or_else(|err| {
70 | error!("conf_write: {}", err);
71 | Self::default().write();
72 | });
73 | } else {
74 | error!("conf_ser");
75 | }
76 | self
77 | }
78 |
79 | pub fn amend_str(self, json: String) -> Self {
80 | let value: Value =
81 | serde_json::from_str(json.as_str()).expect("JSON was not well-formatted");
82 | self.amend(value)
83 | }
84 |
85 | pub fn amend(self, json: Value) -> Self {
86 | let val = serde_json::to_value(&self).unwrap();
87 | let mut config: BTreeMap = serde_json::from_value(val).unwrap();
88 | let new_json: BTreeMap = serde_json::from_value(json).unwrap();
89 |
90 | for (k, v) in new_json {
91 | config.insert(k, v);
92 | }
93 |
94 | match serde_json::to_string_pretty(&config) {
95 | Ok(v) => match serde_json::from_str::(&v) {
96 | Ok(v) => v,
97 | Err(err) => {
98 | error!("conf_amend_parse: {}", err);
99 | self
100 | }
101 | },
102 | Err(err) => {
103 | error!("conf_amend_str: {}", err);
104 | self
105 | }
106 | }
107 | }
108 |
109 | pub fn restart(self, app: tauri::AppHandle) {
110 | tauri::api::process::restart(&app.env());
111 | }
112 | }
113 |
114 | impl Default for AppConf {
115 | fn default() -> Self {
116 | Self::new()
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src-tauri/src/app/menu.rs:
--------------------------------------------------------------------------------
1 | use tauri::{
2 | AppHandle, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
3 | SystemTrayMenuItem, Wry,
4 | };
5 |
6 | /// system tray
7 | pub fn tray_menu() -> SystemTray {
8 | let quit = CustomMenuItem::new("quit".to_string(), "关闭软件");
9 | let show = CustomMenuItem::new("show".to_string(), "显示桌宠");
10 | let hide = CustomMenuItem::new("hide".to_string(), "隐藏桌宠");
11 | let config = CustomMenuItem::new("config".to_string(), "配置中心");
12 |
13 | let tray_menu = SystemTrayMenu::new()
14 | .add_item(show)
15 | .add_item(hide)
16 | .add_native_item(SystemTrayMenuItem::Separator)
17 | .add_item(config)
18 | .add_native_item(SystemTrayMenuItem::Separator)
19 | .add_item(quit);
20 | SystemTray::new().with_menu(tray_menu)
21 | }
22 |
23 | pub fn tray_handler(app: &AppHandle, event: SystemTrayEvent) {
24 | match event {
25 | SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
26 | "quit" => {
27 | std::process::exit(0);
28 | }
29 | "hide" => {
30 | let window = &app.get_window("main").unwrap();
31 | window.hide().unwrap();
32 | }
33 | "show" => {
34 | match app.get_window("main") {
35 | Some(w) => {
36 | w.show().unwrap();
37 | }
38 | None => {
39 | // live2d 窗口如果被关闭,重新实例化
40 | let live2d_win = tauri::WindowBuilder::new(
41 | app,
42 | "main",
43 | tauri::WindowUrl::App("live2d.html".into()),
44 | )
45 | .build()
46 | .unwrap();
47 | }
48 | };
49 | }
50 | "config" => {
51 | match app.get_window("config") {
52 | Some(w) => {
53 | w.show().unwrap();
54 | }
55 | None => {
56 | // main 窗口如果被关闭,重新实例化
57 | tauri::WindowBuilder::new(
58 | app,
59 | "config",
60 | tauri::WindowUrl::App("index.html".into()),
61 | )
62 | .title("配置")
63 | .center()
64 | .resizable(true)
65 | .always_on_top(true)
66 | .build()
67 | .unwrap();
68 | }
69 | };
70 | }
71 | _ => {}
72 | },
73 | _ => {}
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src-tauri/src/app/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod menu;
3 | pub mod mstruct;
4 | pub mod commands;
5 |
--------------------------------------------------------------------------------
/src-tauri/src/app/mstruct.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | #[derive(Serialize, Deserialize, Debug)]
4 | pub struct AppDataConfig {
5 | pub root_path: String,
6 | }
7 |
8 | #[derive(Serialize, Deserialize, Debug)]
9 | pub struct Rt {
10 | pub data: T,
11 | pub err: String,
12 | }
13 |
14 | #[derive(Serialize, Deserialize, Debug)]
15 | pub enum InitType {
16 | EXIST,
17 | CreateError,
18 | SUCCESS,
19 | }
20 |
21 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
22 | pub struct ConfigFile {
23 | pub serve_path: Option,
24 | }
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 | mod app;
6 | mod plugins;
7 | mod utils;
8 | use log::info;
9 | use web_server;
10 |
11 | use app::{config::AppConf, commands};
12 |
13 | #[tauri::command(main)]
14 | fn main() {
15 | let context = tauri::generate_context!();
16 | let app = tauri::Builder::default();
17 | let app_conf = AppConf::read().write();
18 | let port = web_server::Port(web_server::get_available_port(), app_conf.model_dir.clone());
19 | if app_conf.model_dir != "" {
20 | AppConf::read()
21 | .amend(serde_json::json!({ "port":port.0 }))
22 | .write();
23 | tauri::async_runtime::spawn(web_server::app(port.clone()));
24 | }
25 |
26 | app.manage(port.clone())
27 | .setup(|app| {
28 | info!("app running...");
29 | Ok(())
30 | })
31 | .plugin(plugins::autostart::init(
32 | plugins::autostart::MacosLauncher::LaunchAgent,
33 | None,
34 | ))
35 | .plugin(plugins::checkupdate::init())
36 | .system_tray(app::menu::tray_menu())
37 | .on_system_tray_event(app::menu::tray_handler)
38 | .invoke_handler(tauri::generate_handler![
39 | commands::read_file,
40 | commands::write_file,
41 | commands::model_list,
42 | commands::read_config,
43 | commands::write_config
44 | ])
45 | .build(context)
46 | .expect("error while running live2d application")
47 | .run(|_app_handle, event| match event {
48 | tauri::RunEvent::ExitRequested { api, .. } => {
49 | println!("last close");
50 | api.prevent_exit();
51 | }
52 | _ => {}
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/src-tauri/src/plugins/autostart.rs:
--------------------------------------------------------------------------------
1 | use auto_launch::{AutoLaunch, AutoLaunchBuilder};
2 | use tauri::{
3 | plugin::{Builder, TauriPlugin},
4 | Manager, Runtime, State,
5 | };
6 |
7 | use std::env::current_exe;
8 |
9 | #[derive(Debug, Copy, Clone)]
10 | pub enum MacosLauncher {
11 | LaunchAgent,
12 | AppleScript,
13 | }
14 |
15 | use crate::plugins::{Error, Result};
16 |
17 | pub struct AutoLaunchManager(AutoLaunch);
18 |
19 | impl AutoLaunchManager {
20 | pub fn enable(&self) -> Result<()> {
21 | self.0
22 | .enable()
23 | .map_err(|e| e.to_string())
24 | .map_err(Error::Anyhow)
25 | }
26 |
27 | pub fn disable(&self) -> Result<()> {
28 | self.0
29 | .disable()
30 | .map_err(|e| e.to_string())
31 | .map_err(Error::Anyhow)
32 | }
33 |
34 | pub fn is_enabled(&self) -> Result {
35 | self.0
36 | .is_enabled()
37 | .map_err(|e| e.to_string())
38 | .map_err(Error::Anyhow)
39 | }
40 | }
41 |
42 | pub trait ManagerExt {
43 | fn autolaunch(&self) -> State<'_, AutoLaunchManager>;
44 | }
45 |
46 | impl> ManagerExt for T {
47 | fn autolaunch(&self) -> State<'_, AutoLaunchManager> {
48 | self.state::()
49 | }
50 | }
51 |
52 | #[tauri::command]
53 | async fn enable(manager: State<'_, AutoLaunchManager>) -> Result<()> {
54 | manager.enable()
55 | }
56 |
57 | #[tauri::command]
58 | async fn disable(manager: State<'_, AutoLaunchManager>) -> Result<()> {
59 | manager.disable()
60 | }
61 |
62 | #[tauri::command]
63 | async fn is_enabled(manager: State<'_, AutoLaunchManager>) -> Result {
64 | manager.is_enabled()
65 | }
66 |
67 | /// Initializes the plugin.
68 | ///
69 | /// `args` - are passed to your app on startup.
70 | pub fn init(
71 | macos_launcher: MacosLauncher,
72 | args: Option>,
73 | ) -> TauriPlugin {
74 | Builder::new("autostart")
75 | .invoke_handler(tauri::generate_handler![enable, disable, is_enabled])
76 | .setup(move |app| {
77 | println!("TauriPlugin [autostart] ");
78 | let mut builder = AutoLaunchBuilder::new();
79 | builder.set_app_name(&app.package_info().name);
80 | if let Some(args) = args {
81 | builder.set_args(&args);
82 | }
83 | builder.set_use_launch_agent(matches!(macos_launcher, MacosLauncher::LaunchAgent));
84 |
85 | let current_exe = current_exe()?;
86 |
87 | #[cfg(windows)]
88 | builder.set_app_path(¤t_exe.display().to_string());
89 | #[cfg(target_os = "macos")]
90 | builder.set_app_path(¤t_exe.canonicalize()?.display().to_string());
91 | #[cfg(target_os = "linux")]
92 | if let Some(appimage) = app
93 | .env()
94 | .appimage
95 | .and_then(|p| p.to_str().map(|s| s.to_string()))
96 | {
97 | builder.set_app_path(&appimage);
98 | } else {
99 | builder.set_app_path(¤t_exe.display().to_string());
100 | }
101 |
102 | app.manage(AutoLaunchManager(
103 | builder.build().map_err(|e| e.to_string())?,
104 | ));
105 | Ok(())
106 | })
107 | .build()
108 | }
109 |
--------------------------------------------------------------------------------
/src-tauri/src/plugins/checkupdate.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use tauri::plugin::{Builder, TauriPlugin};
3 | use tauri::updater::UpdateResponse;
4 | use tauri::{AppHandle, Manager, Wry};
5 |
6 | #[tauri::command]
7 | pub fn run_check_update(app: AppHandle) -> () {
8 | tauri::async_runtime::spawn(async move {
9 | let result = app.updater().check().await;
10 | let update_resp = result.unwrap();
11 | if update_resp.is_update_available() {
12 | tauri::async_runtime::spawn(async move {
13 | prompt_for_install(app, update_resp).await.unwrap();
14 | });
15 | }
16 | });
17 | }
18 |
19 | // Copy private api in tauri/updater/mod.rs. TODO: refactor to public api
20 | // Prompt a dialog asking if the user want to install the new version
21 | // Maybe we should add an option to customize it in future versions.
22 | pub async fn prompt_for_install(app: AppHandle, update: UpdateResponse) -> Result<()> {
23 | let windows = app.windows();
24 | let parent_window = windows.values().next();
25 | let package_info = app.package_info().clone();
26 |
27 | let body = update.body().unwrap();
28 | // todo(lemarier): We should review this and make sure we have
29 | // something more conventional.
30 | let should_install = tauri::api::dialog::blocking::ask(
31 | parent_window,
32 | format!(r#"A new version of {} is available! "#, package_info.name),
33 | format!(
34 | r#"{} {} is now available -- you have {}.
35 |
36 | Would you like to install it now?
37 |
38 | Release Notes:
39 | {}"#,
40 | package_info.name,
41 | update.latest_version(),
42 | package_info.version,
43 | body
44 | ),
45 | );
46 |
47 | if should_install {
48 | // Launch updater download process
49 | // macOS we display the `Ready to restart dialog` asking to restart
50 | // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
51 | // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
52 | update.download_and_install().await?;
53 |
54 | // Ask user if we need to restart the application
55 | let should_exit = tauri::api::dialog::blocking::ask(
56 | parent_window,
57 | "Ready to Restart",
58 | "The installation was successful, do you want to restart the application now?",
59 | );
60 | if should_exit {
61 | app.restart();
62 | }
63 | }
64 | Ok(())
65 | }
66 |
67 | /// Initializes the plugin.
68 | pub fn init() -> TauriPlugin {
69 | Builder::new("checkupdate")
70 | .invoke_handler(tauri::generate_handler![run_check_update])
71 | .setup(move |_app| {
72 | println!("TauriPlugin [checkupdate] ");
73 | Ok(())
74 | })
75 | .build()
76 | }
77 |
--------------------------------------------------------------------------------
/src-tauri/src/plugins/mod.rs:
--------------------------------------------------------------------------------
1 | use serde::{ser::Serializer, Serialize};
2 |
3 | #[derive(Debug, thiserror::Error)]
4 | pub enum Error {
5 | #[error(transparent)]
6 | Io(#[from] std::io::Error),
7 | #[error("{0}")]
8 | Anyhow(String),
9 | }
10 |
11 | impl Serialize for Error {
12 | fn serialize(&self, serializer: S) -> std::result::Result
13 | where
14 | S: Serializer,
15 | {
16 | serializer.serialize_str(self.to_string().as_ref())
17 | }
18 | }
19 |
20 | pub type Result = std::result::Result;
21 |
22 | pub mod autostart;
23 | pub mod checkupdate;
24 |
--------------------------------------------------------------------------------
/src-tauri/src/utils.rs:
--------------------------------------------------------------------------------
1 | // https://github.com/lencx/Live2D/blob/main/src-tauri/src/utils.rs
2 | use anyhow::Result;
3 | use log::{error, info};
4 | use serde_json::Value;
5 | use std::{
6 | collections::HashMap,
7 | fs,
8 | path::{Path, PathBuf},
9 | process::Command,
10 | };
11 | use tauri::updater::UpdateResponse;
12 | use tauri::{utils::config::Config, AppHandle, Manager, Wry};
13 |
14 | pub fn app_root() -> PathBuf {
15 | tauri::api::path::home_dir().unwrap().join(".live2D")
16 | }
17 |
18 | pub fn get_tauri_conf() -> Option {
19 | let config_file = include_str!("../tauri.conf.json");
20 | let config: Config =
21 | serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
22 | Some(config)
23 | }
24 |
25 | pub fn exists(path: &Path) -> bool {
26 | Path::new(path).exists()
27 | }
28 |
29 | pub fn create_file>(filename: P) -> Result<()> {
30 | let filename = filename.as_ref();
31 | if let Some(parent) = filename.parent() {
32 | if !parent.exists() {
33 | fs::create_dir_all(parent)?;
34 | }
35 | }
36 | fs::File::create(filename)?;
37 | Ok(())
38 | }
39 |
40 | pub fn user_script() -> String {
41 | let user_script_file = app_root().join("scripts").join("main.js");
42 | let user_script_content =
43 | fs::read_to_string(user_script_file).unwrap_or_else(|_| "".to_string());
44 | format!(
45 | "window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})",
46 | user_script_content
47 | )
48 | }
49 |
50 | pub fn load_script(filename: &str) -> String {
51 | let script_file = app_root().join("scripts").join(filename);
52 | fs::read_to_string(script_file).unwrap_or_else(|_| "".to_string())
53 | }
54 |
55 | pub fn open_file(path: PathBuf) {
56 | let pathname = convert_path(path.to_str().unwrap());
57 | info!("open_file: {}", pathname);
58 | #[cfg(target_os = "macos")]
59 | Command::new("open")
60 | .arg("-R")
61 | .arg(pathname)
62 | .spawn()
63 | .unwrap();
64 |
65 | #[cfg(target_os = "windows")]
66 | Command::new("explorer.exe")
67 | .arg("/select,")
68 | .arg(pathname)
69 | .spawn()
70 | .unwrap();
71 |
72 | // https://askubuntu.com/a/31071
73 | #[cfg(target_os = "linux")]
74 | Command::new("xdg-open").arg(pathname).spawn().unwrap();
75 | }
76 |
77 | pub fn convert_path(path_str: &str) -> String {
78 | if cfg!(target_os = "windows") {
79 | path_str.replace('/', "\\")
80 | } else {
81 | String::from(path_str)
82 | }
83 | }
84 |
85 | pub fn clear_conf(app: &tauri::AppHandle) {
86 | let root = app_root();
87 | let msg = format!(
88 | "Path: {}\n
89 | Are you sure you want to clear all Live2D configurations? Performing this operation data can not be restored, please back up in advance.\n
90 | Note: The application will exit automatically after the configuration cleanup!",
91 | root.to_string_lossy()
92 | );
93 | tauri::api::dialog::ask(
94 | app.get_window("core").as_ref(),
95 | "Clear Config",
96 | msg,
97 | move |is_ok| {
98 | if is_ok {
99 | fs::remove_dir_all(root).unwrap();
100 | std::process::exit(0);
101 | }
102 | },
103 | );
104 | }
105 |
106 | pub fn merge(v: &Value, fields: &HashMap) -> Value {
107 | match v {
108 | Value::Object(m) => {
109 | let mut m = m.clone();
110 | for (k, v) in fields {
111 | m.insert(k.clone(), v.clone());
112 | }
113 | Value::Object(m)
114 | }
115 | v => v.clone(),
116 | }
117 | }
118 |
119 | pub fn run_check_update(app: AppHandle, silent: bool, has_msg: Option) {
120 | info!("run_check_update: silent={} has_msg={:?}", silent, has_msg);
121 | tauri::async_runtime::spawn(async move {
122 | if let Ok(update_resp) = app.updater().check().await {
123 | if update_resp.is_update_available() {
124 | if silent {
125 | tauri::async_runtime::spawn(async move {
126 | silent_install(app, update_resp).await.unwrap();
127 | });
128 | } else {
129 | tauri::async_runtime::spawn(async move {
130 | prompt_for_install(app, update_resp).await.unwrap();
131 | });
132 | }
133 | } else if let Some(v) = has_msg {
134 | if v {
135 | tauri::api::dialog::message(
136 | app.app_handle().get_window("core").as_ref(),
137 | "Live2D",
138 | "Your Live2D is up to date",
139 | );
140 | }
141 | }
142 | }
143 | });
144 | }
145 |
146 | // Copy private api in tauri/updater/mod.rs. TODO: refactor to public api
147 | // Prompt a dialog asking if the user want to install the new version
148 | // Maybe we should add an option to customize it in future versions.
149 | pub async fn prompt_for_install(app: AppHandle, update: UpdateResponse) -> Result<()> {
150 | info!("prompt_for_install");
151 | let windows = app.windows();
152 | let parent_window = windows.values().next();
153 | let package_info = app.package_info().clone();
154 |
155 | let body = update.body().unwrap();
156 | // todo(lemarier): We should review this and make sure we have
157 | // something more conventional.
158 | let should_install = tauri::api::dialog::blocking::ask(
159 | parent_window,
160 | format!(r#"A new version of {} is available! "#, package_info.name),
161 | format!(
162 | r#"{} {} is now available -- you have {}.
163 |
164 | Would you like to install it now?
165 |
166 | Release Notes:
167 | {}"#,
168 | package_info.name,
169 | update.latest_version(),
170 | package_info.version,
171 | body
172 | ),
173 | );
174 |
175 | if should_install {
176 | // Launch updater download process
177 | // macOS we display the `Ready to restart dialog` asking to restart
178 | // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
179 | // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
180 | update.download_and_install().await?;
181 |
182 | // Ask user if we need to restart the application
183 | let should_exit = tauri::api::dialog::blocking::ask(
184 | parent_window,
185 | "Ready to Restart",
186 | "The installation was successful, do you want to restart the application now?",
187 | );
188 | if should_exit {
189 | app.restart();
190 | }
191 | }
192 |
193 | Ok(())
194 | }
195 |
196 | pub async fn silent_install(app: AppHandle, update: UpdateResponse) -> Result<()> {
197 | info!("silent_install");
198 | let windows = app.windows();
199 | let parent_window = windows.values().next();
200 |
201 | // Launch updater download process
202 | // macOS we display the `Ready to restart dialog` asking to restart
203 | // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
204 | // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
205 | update.download_and_install().await?;
206 |
207 | // Ask user if we need to restart the application
208 | let should_exit = tauri::api::dialog::blocking::ask(
209 | parent_window,
210 | "Ready to Restart",
211 | "The silent installation was successful, do you want to restart the application now?",
212 | );
213 | if should_exit {
214 | app.restart();
215 | }
216 |
217 | Ok(())
218 | }
219 |
220 | pub fn vec_to_hashmap(
221 | vec: impl Iterator- ,
222 | key: &str,
223 | map: &mut HashMap,
224 | ) {
225 | for v in vec {
226 | if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) {
227 | map.insert(kval.to_string(), v);
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "beforeDevCommand": "pnpm dev",
4 | "beforeBuildCommand": "pnpm build",
5 | "devPath": "http://localhost:1420",
6 | "distDir": "../dist"
7 | },
8 | "package": {
9 | "productName": "live2d",
10 | "version": "3.0.5"
11 | },
12 | "tauri": {
13 | "allowlist": {
14 | "all": true,
15 | "http": {
16 | "scope": [
17 | "http://*/*",
18 | "https://*/*"
19 | ]
20 | },
21 | "fs": {
22 | "all": false,
23 | "readFile": true,
24 | "writeFile": true,
25 | "readDir": true,
26 | "createDir": true,
27 | "exists": true,
28 | "removeFile": true,
29 | "removeDir": true,
30 | "scope": [
31 | "$APPDATA/*",
32 | "$HOME/.live2d/**"
33 | ]
34 | },
35 | "dialog": {
36 | "all": true,
37 | "ask": true,
38 | "confirm": true,
39 | "message": true,
40 | "open": true,
41 | "save": true
42 | },
43 | "process": {
44 | "all": true,
45 | "exit": true,
46 | "relaunch": true,
47 | "relaunchDangerousAllowSymlinkMacos": true
48 | }
49 | },
50 | "bundle": {
51 | "active": true,
52 | "category": "DeveloperTool",
53 | "copyright": "",
54 | "deb": {
55 | "depends": []
56 | },
57 | "externalBin": [],
58 | "icon": [
59 | "icons/32x32.png",
60 | "icons/128x128.png",
61 | "icons/128x128@2x.png",
62 | "icons/icon.icns",
63 | "icons/icon.ico"
64 | ],
65 | "identifier": "com.rust.live2d",
66 | "longDescription": "Live2D Desktop Application",
67 | "macOS": {
68 | "entitlements": null,
69 | "exceptionDomain": "",
70 | "frameworks": [],
71 | "providerShortName": null,
72 | "signingIdentity": null
73 | },
74 | "resources": [],
75 | "shortDescription": "Live2D",
76 | "targets": "all",
77 | "windows": {
78 | "certificateThumbprint": null,
79 | "digestAlgorithm": "sha256",
80 | "timestampUrl": "",
81 | "webviewInstallMode": {
82 | "silent": true,
83 | "type": "embedBootstrapper"
84 | }
85 | }
86 | },
87 | "systemTray": {
88 | "iconPath": "icons/icon.png",
89 | "iconAsTemplate": true
90 | },
91 | "macOSPrivateApi": true,
92 | "security": {
93 | "csp": null
94 | },
95 | "updater": {
96 | "active": true,
97 | "dialog": false,
98 | "endpoints": [
99 | "https://cdn.jsdelivr.net/gh/itxve/tauri-live2d@gh-pages/install.json"
100 | ],
101 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QkUzMjVDRkE2RTNBQTAKUldTZ09tNzZYREsrR2RGcGV5Ykc2KzVWVWJFRTM5b2dCSXFYWlQ3TjcxZWI3aWhiK0RCTnlrN2sK",
102 | "windows": {
103 | "installMode": "quiet"
104 | }
105 | },
106 | "windows": [
107 | {
108 | "url": "live2d.html",
109 | "fullscreen": false,
110 | "transparent": true,
111 | "width": 215,
112 | "height": 200,
113 | "minWidth": 215,
114 | "minHeight": 200,
115 | "resizable": false,
116 | "decorations": false,
117 | "skipTaskbar": true,
118 | "alwaysOnTop": true
119 | }
120 | ]
121 | }
122 | }
--------------------------------------------------------------------------------
/src-tauri/web_server/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "web_server"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | rand = "0.8"
10 | axum = { version = "0.7.5", features = ["ws"] }
11 | tower = {version = "0.4", features = ["util"] }
12 | tower-http = {version = "0.5.0", features = ["fs", "trace", "cors"] }
13 | tokio = { version = "1.0", features = ["full"] }
14 | serde = "1"
--------------------------------------------------------------------------------
/src-tauri/web_server/src/lib.rs:
--------------------------------------------------------------------------------
1 | use axum::{
2 | extract::ws::{Message, WebSocket, WebSocketUpgrade},
3 | handler::HandlerWithoutStateExt,
4 | http::StatusCode,
5 | response::IntoResponse,
6 | routing::{get, get_service},
7 | Router,
8 | };
9 |
10 | use rand::Rng;
11 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener};
12 | use tower_http::cors::{Any, CorsLayer};
13 | use tower_http::{services::ServeDir, trace::TraceLayer};
14 |
15 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
16 | pub struct Port(pub u16, pub String);
17 |
18 | pub async fn app(conf: Port) {
19 | let app = Router::new()
20 | .route("/ok", get(move || async { "ok" }))
21 | .route("/ws", get(ws_handler))
22 | .nest_service(
23 | "/",
24 | get_service(ServeDir::new(conf.1).not_found_service(handle_404.into_service())),
25 | )
26 | .layer(
27 | CorsLayer::new()
28 | .allow_headers(Any)
29 | .allow_origin(Any)
30 | .allow_methods(Any),
31 | )
32 | .layer(TraceLayer::new_for_http());
33 |
34 | let addr = SocketAddr::from(([0, 0, 0, 0], conf.0));
35 | let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
36 | axum::serve(listener, app.into_make_service())
37 | .await
38 | .unwrap();
39 | }
40 | async fn handle_404() -> (StatusCode, &'static str) {
41 | (StatusCode::NOT_FOUND, "Not found")
42 | }
43 |
44 | pub fn get_available_port() -> u16 {
45 | let mut rng = rand::thread_rng();
46 | let mut port: u16;
47 |
48 | loop {
49 | port = rng.gen_range(1024..65535); // 生成一个1024到65535之间的随机端口号
50 | let addr = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port);
51 | // 尝试创建一个TCP监听器来检查端口是否可用
52 | match TcpListener::bind(addr) {
53 | Ok(_) => break, // 如果绑定成功,端口可用
54 | Err(_) => continue, // 如果绑定失败,生成新的随机端口号
55 | }
56 | }
57 |
58 | port
59 | }
60 |
61 | async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
62 | ws.on_upgrade(handle_socket)
63 | }
64 |
65 | async fn handle_socket(mut socket: WebSocket) {
66 | loop {
67 | if let Some(msg) = socket.recv().await {
68 | if let Ok(msg) = msg {
69 | match msg {
70 | Message::Text(t) => {
71 | // Echo
72 | if socket
73 | .send(Message::Text(format!("Echo from backend: {}", t)))
74 | .await
75 | .is_err()
76 | {
77 | return;
78 | }
79 | }
80 | Message::Close(_) => {
81 | return;
82 | }
83 | _ => {}
84 | }
85 | } else {
86 | return;
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
20 |
21 |
22 |
23 |
32 |
--------------------------------------------------------------------------------
/src/assets/autoload.js:
--------------------------------------------------------------------------------
1 | try {
2 | $("").attr({href: "assets/waifu.min.css?v=1.4.2", rel: "stylesheet", type: "text/css"}).appendTo('head');
3 | $('body').append('');
4 | $.ajax({url: "assets/waifu-tips.min.js?v=1.4.2", dataType:"script", cache: true, success: function() {
5 | $.ajax({url: "assets/live2d.min.js?v=1.0.5", dataType:"script", cache: true, success: function() {
6 | /* 可直接修改部分参数 */
7 | live2d_settings['hitokotoAPI'] = "hitokoto.cn"; // 一言 API
8 | live2d_settings['modelId'] = 5; // 默认模型 ID
9 | live2d_settings['modelTexturesId'] = 1; // 默认材质 ID
10 | live2d_settings['modelStorage'] = false; // 不储存模型 ID
11 | /* 在 initModel 前添加 */
12 | initModel("assets/waifu-tips.json");
13 | }});
14 | }});
15 | } catch(err) { console.log("[Error] JQuery is not defined.") }
16 |
--------------------------------------------------------------------------------
/src/assets/flat-ui-icons-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src/assets/flat-ui-icons-regular.eot
--------------------------------------------------------------------------------
/src/assets/flat-ui-icons-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/flat-ui-icons-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src/assets/flat-ui-icons-regular.ttf
--------------------------------------------------------------------------------
/src/assets/flat-ui-icons-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itxve/tauri-live2d/fa4dd2e4e1dfe55bbd09f70bb8e84686929eb415/src/assets/flat-ui-icons-regular.woff
--------------------------------------------------------------------------------
/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/waifu-tips.js:
--------------------------------------------------------------------------------
1 | window.live2d_settings = Array(); /*
2 |
3 | く__,.ヘヽ. / ,ー、 〉
4 | \ ', !-─‐-i / /´
5 | /`ー' L//`ヽ、 Live2D 看板娘 参数设置
6 | / /, /| , , ', Version 1.4.2
7 | イ / /-‐/ i L_ ハ ヽ! i Update 2018.11.12
8 | レ ヘ 7イ`ト レ'ァ-ト、!ハ| |
9 | !,/7 '0' ´0iソ| |
10 | |.从" _ ,,,, / |./ | 网页添加 Live2D 看板娘
11 | レ'| i>.、,,__ _,.イ / .i | https://www.fghrsh.net/post/123.html
12 | レ'| | / k_7_/レ'ヽ, ハ. |
13 | | |/i 〈|/ i ,.ヘ | i | Thanks
14 | .|/ / i: ヘ! \ | journey-ad / https://github.com/journey-ad/live2d_src
15 | kヽ>、ハ _,.ヘ、 /、! xiazeyu / https://github.com/xiazeyu/live2d-widget.js
16 | !'〈//`T´', \ `'7'ーr' Live2d Cubism SDK WebGL 2.1 Projrct & All model authors.
17 | レ'ヽL__|___i,___,ンレ|ノ
18 | ト-,/ |___./
19 | 'ー' !_,.:*********************************************************************************/
20 |
21 | // 后端接口
22 | live2d_settings.modelAPI = "//127.0.0.1:2333/"; // 自建 API 修改这里
23 | live2d_settings.tipsMessage = "waifu-tips.json"; // 同目录下可省略路径
24 | live2d_settings.hitokotoAPI = "jinrishici.com"; // 一言 API,可选 'lwl12.com', 'hitokoto.cn', 'jinrishici.com'(古诗词)
25 |
26 | // 默认模型
27 | live2d_settings.modelId = 1; // 默认模型 ID,可在 F12 控制台找到
28 | live2d_settings.modelTexturesId = 1; // 默认材质 ID,可在 F12 控制台找到
29 |
30 | // 工具栏设置
31 | live2d_settings.showToolMenu = true; // 显示 工具栏 ,可选 true(真), false(假)
32 | live2d_settings.canCloseLive2d = true; // 显示 关闭看板娘 按钮,可选 true(真), false(假)
33 | live2d_settings.canSwitchModel = true; // 显示 模型切换 按钮,可选 true(真), false(假)
34 | live2d_settings.canSwitchTextures = true; // 显示 材质切换 按钮,可选 true(真), false(假)
35 | live2d_settings.canSwitchHitokoto = true; // 显示 一言切换 按钮,可选 true(真), false(假)
36 | live2d_settings.canTakeScreenshot = true; // 显示 看板娘截图 按钮,可选 true(真), false(假)
37 | live2d_settings.canTurnToHomePage = true; // 显示 返回首页 按钮,可选 true(真), false(假)
38 | live2d_settings.canTurnToAboutPage = true; // 显示 跳转关于页 按钮,可选 true(真), false(假)
39 |
40 | // 模型切换模式
41 | live2d_settings.modelStorage = true; // 记录 ID (刷新后恢复),可选 true(真), false(假)
42 | live2d_settings.modelRandMode = "switch"; // 模型切换,可选 'rand'(随机), 'switch'(顺序)
43 | live2d_settings.modelTexturesRandMode = "rand"; // 材质切换,可选 'rand'(随机), 'switch'(顺序)
44 |
45 | // 提示消息选项
46 | live2d_settings.showHitokoto = true; // 显示一言
47 | live2d_settings.showF12Status = true; // 显示加载状态
48 | live2d_settings.showF12Message = true; // 显示看板娘消息
49 | live2d_settings.showF12OpenMsg = true; // 显示控制台打开提示
50 | live2d_settings.showCopyMessage = true; // 显示 复制内容 提示
51 | live2d_settings.showWelcomeMessage = true; // 显示进入面页欢迎词
52 |
53 | // 看板娘样式设置
54 | live2d_settings.waifuSize = "280x250"; // 看板娘大小,例如 '280x250', '600x535'
55 | live2d_settings.waifuTipsSize = "250x40"; // 提示框大小,例如 '250x70', '570x150'
56 | live2d_settings.waifuFontSize = "12px"; // 提示框字体,例如 '12px', '30px'
57 | live2d_settings.waifuToolFont = "14px"; // 工具栏字体,例如 '14px', '36px'
58 | live2d_settings.waifuToolLine = "20px"; // 工具栏行高,例如 '20px', '36px'
59 | live2d_settings.waifuToolTop = "0px"; // 工具栏顶部边距,例如 '0px', '-60px'
60 | live2d_settings.waifuMinWidth = "disable"; // 面页小于 指定宽度 隐藏看板娘,例如 'disable'(禁用), '768px'
61 | live2d_settings.waifuEdgeSide = "left:0"; // 看板娘贴边方向,例如 'left:0'(靠左 0px), 'right:30'(靠右 30px)
62 | live2d_settings.waifuDraggable = "disable"; // 拖拽样式,例如 'disable'(禁用), 'axis-x'(只能水平拖拽), 'unlimited'(自由拖拽)
63 | live2d_settings.waifuDraggableRevert = true; // 松开鼠标还原拖拽位置,可选 true(真), false(假)
64 |
65 | // 其他杂项设置
66 | live2d_settings.l2dVersion = "1.4.2"; // 当前版本
67 | live2d_settings.l2dVerDate = "2022.12.24"; // 版本更新日期
68 | live2d_settings.homePageUrl = "index.html"; // 主页地址,可选 'auto'(自动), '{URL 网址}'
69 | // live2d_settings.aboutPageUrl = "https://www.fghrsh.net/post/123.html"; // 关于页地址, '{URL 网址}'
70 | live2d_settings.screenshotCaptureName = "live2d.png"; // 看板娘截图文件名,例如 'live2d.png'
71 |
72 | /****************************************************************************************************/
73 |
74 | String.prototype.render = function (context) {
75 | const tokenReg = /(\\)?\{([^\{\}\\]+)(\\)?\}/g;
76 |
77 | return this.replace(tokenReg, function (word, slash1, token, slash2) {
78 | if (slash1 || slash2) {
79 | return word.replace("\\", "");
80 | }
81 |
82 | const variables = token.replace(/\s/g, "").split(".");
83 | let currentObject = context;
84 | let i, length, variable;
85 |
86 | for (i = 0, length = variables.length; i < length; ++i) {
87 | variable = variables[i];
88 | currentObject = currentObject[variable];
89 | if (currentObject === undefined || currentObject === null) return "";
90 | }
91 | return currentObject;
92 | });
93 | };
94 |
95 | const re = /x/;
96 | console.log(re);
97 |
98 | function empty(obj) {
99 | return !!(typeof obj === "undefined" || obj == null || obj == "");
100 | }
101 | function getRandText(text) {
102 | return Array.isArray(text)
103 | ? text[Math.floor(Math.random() * text.length + 1) - 1]
104 | : text;
105 | }
106 |
107 | function showMessage(text, timeout, flag) {
108 | if (
109 | flag ||
110 | sessionStorage.getItem("waifu-text") === "" ||
111 | sessionStorage.getItem("waifu-text") === null
112 | ) {
113 | if (Array.isArray(text))
114 | text = text[Math.floor(Math.random() * text.length + 1) - 1];
115 | if (live2d_settings.showF12Message)
116 | console.log("[Message]", text.replace(/<[^<>]+>/g, ""));
117 |
118 | if (flag) sessionStorage.setItem("waifu-text", text);
119 |
120 | $(".waifu-tips").stop();
121 | $(".waifu-tips").html(text).fadeTo(200, 1);
122 | if (timeout === undefined) timeout = 5000;
123 | hideMessage(timeout);
124 | }
125 | }
126 |
127 | function hideMessage(timeout) {
128 | $(".waifu-tips").stop().css("opacity", 1);
129 | if (timeout === undefined) timeout = 5000;
130 | window.setTimeout(function () {
131 | sessionStorage.removeItem("waifu-text");
132 | }, timeout);
133 | $(".waifu-tips").delay(timeout).fadeTo(200, 0);
134 | }
135 |
136 | export function initModel(waifuPath, type) {
137 | /* console welcome message */
138 | eval(
139 | (function (p, a, c, k, e, r) {
140 | e = function (c) {
141 | return (
142 | (c < a ? "" : e(parseInt(c / a))) +
143 | ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
144 | );
145 | };
146 | if (!"".replace(/^/, String)) {
147 | while (c--) r[e(c)] = k[c] || e(c);
148 | k = [
149 | function (e) {
150 | return r[e];
151 | },
152 | ];
153 | e = function () {
154 | return "\\w+";
155 | };
156 | c = 1;
157 | }
158 | while (c--)
159 | if (k[c]) p = p.replace(new RegExp("\\b" + e(c) + "\\b", "g"), k[c]);
160 | return p;
161 | })(
162 | "8.d(\" \");8.d(\"\\U,.\\y\\5.\\1\\1\\1\\1/\\1,\\u\\2 \\H\\n\\1\\1\\1\\1\\1\\b ', !-\\r\\j-i\\1/\\1/\\g\\n\\1\\1\\1 \\1 \\a\\4\\f'\\1\\1\\1 L/\\a\\4\\5\\2\\n\\1\\1 \\1 /\\1 \\a,\\1 /|\\1 ,\\1 ,\\1\\1\\1 ',\\n\\1\\1\\1\\q \\1/ /-\\j/\\1\\h\\E \\9 \\5!\\1 i\\n\\1\\1\\1 \\3 \\6 7\\q\\4\\c\\1 \\3'\\s-\\c\\2!\\t|\\1 |\\n\\1\\1\\1\\1 !,/7 '0'\\1\\1 \\X\\w| \\1 |\\1\\1\\1\\n\\1\\1\\1\\1 |.\\x\\\"\\1\\l\\1\\1 ,,,, / |./ \\1 |\\n\\1\\1\\1\\1 \\3'| i\\z.\\2,,A\\l,.\\B / \\1.i \\1|\\n\\1\\1\\1\\1\\1 \\3'| | / C\\D/\\3'\\5,\\1\\9.\\1|\\n\\1\\1\\1\\1\\1\\1 | |/i \\m|/\\1 i\\1,.\\6 |\\F\\1|\\n\\1\\1\\1\\1\\1\\1.|/ /\\1\\h\\G \\1 \\6!\\1\\1\\b\\1|\\n\\1\\1\\1 \\1 \\1 k\\5>\\2\\9 \\1 o,.\\6\\2 \\1 /\\2!\\n\\1\\1\\1\\1\\1\\1 !'\\m//\\4\\I\\g', \\b \\4'7'\\J'\\n\\1\\1\\1\\1\\1\\1 \\3'\\K|M,p,\\O\\3|\\P\\n\\1\\1\\1\\1\\1 \\1\\1\\1\\c-,/\\1|p./\\n\\1\\1\\1\\1\\1 \\1\\1\\1'\\f'\\1\\1!o,.:\\Q \\R\\S\\T v\"+e.V+\" / W \"+e.N);8.d(\" \");",
163 | 60,
164 | 60,
165 | "|u3000|uff64|uff9a|uff40|u30fd|uff8d||console|uff8a|uff0f|uff3c|uff84|log|live2d_settings|uff70|u00b4|uff49||u2010||u3000_|u3008||_|___|uff72|u2500|uff67|u30cf|u30fc||u30bd|u4ece|u30d8|uff1e|__|u30a4|k_|uff17_|u3000L_|u3000i|uff1a|u3009|uff34|uff70r|u30fdL__||___i|l2dVerDate|u30f3|u30ce|nLive2D|u770b|u677f|u5a18|u304f__|l2dVersion|FGHRSH|u00b40i".split(
166 | "|"
167 | ),
168 | 0,
169 | {}
170 | )
171 | );
172 |
173 | /* 判断 JQuery */
174 | if (typeof $.ajax !== "function")
175 | typeof jQuery.ajax === "function"
176 | ? (window.$ = jQuery)
177 | : console.log("[Error] JQuery is not defined.");
178 |
179 | /* 加载看板娘样式 */
180 | live2d_settings.waifuSize = live2d_settings.waifuSize.split("x");
181 | live2d_settings.waifuTipsSize = live2d_settings.waifuTipsSize.split("x");
182 | live2d_settings.waifuEdgeSide = live2d_settings.waifuEdgeSide.split(":");
183 |
184 | $("#live2d").attr("width", live2d_settings.waifuSize[0]);
185 | $("#live2d").attr("height", live2d_settings.waifuSize[1]);
186 | $(".waifu-tips").width(live2d_settings.waifuTipsSize[0]);
187 | $(".waifu-tips").height(live2d_settings.waifuTipsSize[1]);
188 | $(".waifu-tips").css("top", live2d_settings.waifuToolTop);
189 | $(".waifu-tips").css("font-size", live2d_settings.waifuFontSize);
190 | $(".waifu-tool").css("font-size", live2d_settings.waifuToolFont);
191 | $(".waifu-tool span").css("line-height", live2d_settings.waifuToolLine);
192 |
193 | if (live2d_settings.waifuEdgeSide[0] == "left")
194 | $(".waifu").css("left", live2d_settings.waifuEdgeSide[1] + "px");
195 | else if (live2d_settings.waifuEdgeSide[0] == "right")
196 | $(".waifu").css("right", live2d_settings.waifuEdgeSide[1] + "px");
197 |
198 | window.waifuResize = function () {
199 | $(window).width() <= Number(live2d_settings.waifuMinWidth.replace("px", ""))
200 | ? $(".waifu").hide()
201 | : $(".waifu").show();
202 | };
203 | if (live2d_settings.waifuMinWidth != "disable") {
204 | waifuResize();
205 | $(window).resize(function () {
206 | waifuResize();
207 | });
208 | }
209 |
210 | try {
211 | if (live2d_settings.waifuDraggable == "axis-x")
212 | $(".waifu").draggable({
213 | axis: "x",
214 | revert: live2d_settings.waifuDraggableRevert,
215 | });
216 | else if (live2d_settings.waifuDraggable == "unlimited")
217 | $(".waifu").draggable({ revert: live2d_settings.waifuDraggableRevert });
218 | else $(".waifu").css("transition", "all .3s ease-in-out");
219 | } catch (err) {
220 | console.log("[Error] JQuery UI is not defined.");
221 | }
222 |
223 | live2d_settings.homePageUrl =
224 | live2d_settings.homePageUrl == "auto"
225 | ? window.location.protocol + "//" + window.location.hostname + "/"
226 | : live2d_settings.homePageUrl;
227 | if (
228 | window.location.protocol == "file:" &&
229 | live2d_settings.modelAPI.substr(0, 2) == "//"
230 | )
231 | live2d_settings.modelAPI = "http:" + live2d_settings.modelAPI;
232 |
233 | $(".waifu-tool .fui-home").click(function () {
234 | // window.location = 'https://www.fghrsh.net/';
235 | window.location = live2d_settings.homePageUrl;
236 | });
237 |
238 | $(".waifu-tool .fui-info-circle").click(function () {
239 | // window.open('https://imjad.cn/archives/lab/add-dynamic-poster-girl-with-live2d-to-your-blog-02');
240 | window.open(live2d_settings.aboutPageUrl);
241 | });
242 |
243 | if (typeof waifuPath === "object") loadTipsMessage(waifuPath);
244 | else {
245 | $.ajax({
246 | cache: true,
247 | url:
248 | waifuPath == ""
249 | ? live2d_settings.tipsMessage
250 | : waifuPath.substr(waifuPath.length - 15) == "waifu-tips.json"
251 | ? waifuPath
252 | : waifuPath + "waifu-tips.json",
253 | dataType: "json",
254 | success: function (result) {
255 | loadTipsMessage(result);
256 | },
257 | });
258 | }
259 |
260 | if (!live2d_settings.showToolMenu) $(".waifu-tool").hide();
261 | if (!live2d_settings.canCloseLive2d) $(".waifu-tool .fui-cross").hide();
262 | if (!live2d_settings.canSwitchModel) $(".waifu-tool .fui-eye").hide();
263 | if (!live2d_settings.canSwitchTextures) $(".waifu-tool .fui-user").hide();
264 | if (!live2d_settings.canSwitchHitokoto) $(".waifu-tool .fui-chat").hide();
265 | if (!live2d_settings.canTakeScreenshot) $(".waifu-tool .fui-photo").hide();
266 | if (!live2d_settings.canTurnToHomePage) $(".waifu-tool .fui-home").hide();
267 | if (!live2d_settings.canTurnToAboutPage)
268 | $(".waifu-tool .fui-info-circle").hide();
269 |
270 | if (waifuPath === undefined) waifuPath = "";
271 | var modelId = localStorage.getItem("modelId");
272 | var modelTexturesId = localStorage.getItem("modelTexturesId");
273 |
274 | if (!live2d_settings.modelStorage || modelId == null) {
275 | var modelId = live2d_settings.modelId;
276 | var modelTexturesId = live2d_settings.modelTexturesId;
277 | }
278 | loadModel(modelId, modelTexturesId);
279 | }
280 |
281 | function loadModel(modelId, modelTexturesId = 0) {
282 | if (live2d_settings.modelStorage) {
283 | localStorage.setItem("modelId", modelId);
284 | localStorage.setItem("modelTexturesId", modelTexturesId);
285 | } else {
286 | sessionStorage.setItem("modelId", modelId);
287 | sessionStorage.setItem("modelTexturesId", modelTexturesId);
288 | }
289 | loadlive2d(
290 | "live2d",
291 | live2d_settings.modelAPI + "get/?id=" + modelId + "-" + modelTexturesId,
292 | live2d_settings.showF12Status
293 | ? console.log(
294 | "[Status]",
295 | "live2d",
296 | "模型",
297 | modelId + "-" + modelTexturesId,
298 | "加载完成"
299 | )
300 | : null
301 | );
302 | }
303 |
304 | function loadTipsMessage(result) {
305 | window.waifu_tips = result;
306 |
307 | $.each(result.mouseover, function (index, tips) {
308 | $(document).on("mouseover", tips.selector, function () {
309 | let text = getRandText(tips.text);
310 | text = text.render({ text: $(this).text() });
311 | showMessage(text, 3000);
312 | });
313 | });
314 | $.each(result.click, function (index, tips) {
315 | $(document).on("click", tips.selector, function () {
316 | let text = getRandText(tips.text);
317 | text = text.render({ text: $(this).text() });
318 | showMessage(text, 3000, true);
319 | });
320 | });
321 | $.each(result.seasons, function (index, tips) {
322 | const now = new Date();
323 | const after = tips.date.split("-")[0];
324 | const before = tips.date.split("-")[1] || after;
325 |
326 | if (
327 | after.split("/")[0] <= now.getMonth() + 1 &&
328 | now.getMonth() + 1 <= before.split("/")[0] &&
329 | after.split("/")[1] <= now.getDate() &&
330 | now.getDate() <= before.split("/")[1]
331 | ) {
332 | let text = getRandText(tips.text);
333 | text = text.render({ year: now.getFullYear() });
334 | showMessage(text, 6000, true);
335 | }
336 | });
337 |
338 | if (live2d_settings.showF12OpenMsg) {
339 | re.toString = function () {
340 | showMessage(getRandText(result.waifu.console_open_msg), 5000, true);
341 | return "";
342 | };
343 | }
344 |
345 | if (live2d_settings.showCopyMessage) {
346 | $(document).on("copy", function () {
347 | showMessage(getRandText(result.waifu.copy_message), 5000, true);
348 | });
349 | }
350 |
351 | $(".waifu-tool .fui-photo").click(function () {
352 | showMessage(getRandText(result.waifu.screenshot_message), 5000, true);
353 | window.Live2D.captureName = live2d_settings.screenshotCaptureName;
354 | window.Live2D.captureFrame = true;
355 | });
356 |
357 | $(".waifu-tool .fui-cross").click(function () {
358 | sessionStorage.setItem("waifu-dsiplay", "none");
359 | showMessage(getRandText(result.waifu.hidden_message), 1300, true);
360 | window.setTimeout(function () {
361 | $(".waifu").hide();
362 | }, 1300);
363 | });
364 |
365 | window.showWelcomeMessage = function (result) {
366 | let text;
367 | if (window.location.href == live2d_settings.homePageUrl) {
368 | const now = new Date().getHours();
369 | if (now > 23 || now <= 5)
370 | text = getRandText(result.waifu.hour_tips["t23-5"]);
371 | else if (now > 5 && now <= 7)
372 | text = getRandText(result.waifu.hour_tips["t5-7"]);
373 | else if (now > 7 && now <= 11)
374 | text = getRandText(result.waifu.hour_tips["t7-11"]);
375 | else if (now > 11 && now <= 14)
376 | text = getRandText(result.waifu.hour_tips["t11-14"]);
377 | else if (now > 14 && now <= 17)
378 | text = getRandText(result.waifu.hour_tips["t14-17"]);
379 | else if (now > 17 && now <= 19)
380 | text = getRandText(result.waifu.hour_tips["t17-19"]);
381 | else if (now > 19 && now <= 21)
382 | text = getRandText(result.waifu.hour_tips["t19-21"]);
383 | else if (now > 21 && now <= 23)
384 | text = getRandText(result.waifu.hour_tips["t21-23"]);
385 | else text = getRandText(result.waifu.hour_tips.default);
386 | } else {
387 | const referrer_message = result.waifu.referrer_message;
388 | if (document.referrer !== "") {
389 | const referrer = document.createElement("a");
390 | referrer.href = document.referrer;
391 | const domain = referrer.hostname.split(".")[1];
392 | if (window.location.hostname == referrer.hostname) {
393 | text =
394 | referrer_message.localhost[0] +
395 | document.title.split(referrer_message.localhost[2])[0] +
396 | referrer_message.localhost[1];
397 | } else if (domain == "baidu") {
398 | text =
399 | referrer_message.baidu[0] +
400 | referrer.search.split("&wd=")[1].split("&")[0] +
401 | referrer_message.baidu[1];
402 | } else if (domain == "so") {
403 | text =
404 | referrer_message.so[0] +
405 | referrer.search.split("&q=")[1].split("&")[0] +
406 | referrer_message.so[1];
407 | } else if (domain == "google") {
408 | text =
409 | referrer_message.google[0] +
410 | document.title.split(referrer_message.google[2])[0] +
411 | referrer_message.google[1];
412 | } else {
413 | $.each(result.waifu.referrer_hostname, function (i, val) {
414 | if (i == referrer.hostname) referrer.hostname = getRandText(val);
415 | });
416 | text =
417 | referrer_message.default[0] +
418 | referrer.hostname +
419 | referrer_message.default[1];
420 | }
421 | } else
422 | text =
423 | referrer_message.none[0] +
424 | document.title.split(referrer_message.none[2])[0] +
425 | referrer_message.none[1];
426 | }
427 | showMessage(text, 6000);
428 | };
429 | if (live2d_settings.showWelcomeMessage) showWelcomeMessage(result);
430 |
431 | const waifu_tips = result.waifu;
432 |
433 | function loadOtherModel() {
434 | const modelId = modelStorageGetItem("modelId");
435 | const modelRandMode = live2d_settings.modelRandMode;
436 |
437 | $.ajax({
438 | cache: modelRandMode == "switch",
439 | url: live2d_settings.modelAPI + modelRandMode + "/?id=" + modelId,
440 | dataType: "json",
441 | success: function (result) {
442 | loadModel(result.model.id);
443 | let message = result.model.message;
444 | $.each(waifu_tips.model_message, function (i, val) {
445 | if (i == result.model.id) message = getRandText(val);
446 | });
447 | showMessage(message, 3000, true);
448 | },
449 | });
450 | }
451 |
452 | function loadRandTextures() {
453 | const modelId = modelStorageGetItem("modelId");
454 | const modelTexturesId = modelStorageGetItem("modelTexturesId");
455 | const modelTexturesRandMode = live2d_settings.modelTexturesRandMode;
456 |
457 | $.ajax({
458 | cache: modelTexturesRandMode == "switch",
459 | url:
460 | live2d_settings.modelAPI +
461 | modelTexturesRandMode +
462 | "_textures/?id=" +
463 | modelId +
464 | "-" +
465 | modelTexturesId,
466 | dataType: "json",
467 | success: function (result) {
468 | if (
469 | result.textures.id == 1 &&
470 | (modelTexturesId == 1 || modelTexturesId == 0)
471 | ) {
472 | showMessage(waifu_tips.load_rand_textures[0], 3000, true);
473 | } else showMessage(waifu_tips.load_rand_textures[1], 3000, true);
474 | loadModel(modelId, result.textures.id);
475 | },
476 | });
477 | }
478 |
479 | function modelStorageGetItem(key) {
480 | return live2d_settings.modelStorage
481 | ? localStorage.getItem(key)
482 | : sessionStorage.getItem(key);
483 | }
484 |
485 | /* 检测用户活动状态,并在空闲时显示一言 */
486 | if (live2d_settings.showHitokoto) {
487 | window.getActed = false;
488 | window.hitokotoTimer = 0;
489 | window.hitokotoInterval = false;
490 | $(document)
491 | .mousemove(function (e) {
492 | getActed = true;
493 | })
494 | .keydown(function () {
495 | getActed = true;
496 | });
497 | setInterval(function () {
498 | if (!getActed) ifActed();
499 | else elseActed();
500 | }, 1000);
501 | }
502 |
503 | function ifActed() {
504 | if (!hitokotoInterval) {
505 | hitokotoInterval = true;
506 | hitokotoTimer = window.setInterval(showHitokotoActed, 30000);
507 | }
508 | }
509 |
510 | function elseActed() {
511 | getActed = hitokotoInterval = false;
512 | window.clearInterval(hitokotoTimer);
513 | }
514 |
515 | function showHitokotoActed() {
516 | if ($(document)[0].visibilityState == "visible") showHitokoto();
517 | }
518 |
519 | function showHitokoto() {
520 | switch (live2d_settings.hitokotoAPI) {
521 | case "lwl12.com":
522 | $.getJSON(
523 | "https://api.lwl12.com/hitokoto/v1?encode=realjson",
524 | function (result) {
525 | if (!empty(result.source)) {
526 | let text = waifu_tips.hitokoto_api_message["lwl12.com"][0];
527 | if (!empty(result.author))
528 | text += waifu_tips.hitokoto_api_message["lwl12.com"][1];
529 | text = text.render({
530 | source: result.source,
531 | creator: result.author,
532 | });
533 | window.setTimeout(function () {
534 | showMessage(
535 | text + waifu_tips.hitokoto_api_message["lwl12.com"][2],
536 | 3000,
537 | true
538 | );
539 | }, 5000);
540 | }
541 | showMessage(result.text, 5000, true);
542 | }
543 | );
544 | break;
545 | case "fghrsh.net":
546 | $.getJSON(
547 | "https://api.fghrsh.net/hitokoto/rand/?encode=jsc&uid=3335",
548 | function (result) {
549 | if (!empty(result.source)) {
550 | let text = waifu_tips.hitokoto_api_message["fghrsh.net"][0];
551 | text = text.render({ source: result.source, date: result.date });
552 | window.setTimeout(function () {
553 | showMessage(text, 3000, true);
554 | }, 5000);
555 | showMessage(result.hitokoto, 5000, true);
556 | }
557 | }
558 | );
559 | break;
560 | case "jinrishici.com":
561 | $.ajax({
562 | url: "https://v2.jinrishici.com/one.json",
563 | xhrFields: { withCredentials: true },
564 | success: function (result, status) {
565 | if (!empty(result.data.origin.title)) {
566 | let text = waifu_tips.hitokoto_api_message["jinrishici.com"][0];
567 | text = text.render({
568 | title: result.data.origin.title,
569 | dynasty: result.data.origin.dynasty,
570 | author: result.data.origin.author,
571 | });
572 | window.setTimeout(function () {
573 | showMessage(text, 3000, true);
574 | }, 5000);
575 | }
576 | showMessage(result.data.content, 5000, true);
577 | },
578 | });
579 | break;
580 | default:
581 | $.getJSON("https://v1.hitokoto.cn", function (result) {
582 | if (!empty(result.from)) {
583 | let text = waifu_tips.hitokoto_api_message["hitokoto.cn"][0];
584 | text = text.render({
585 | source: result.from,
586 | creator: result.creator,
587 | });
588 | window.setTimeout(function () {
589 | showMessage(text, 3000, true);
590 | }, 5000);
591 | }
592 | showMessage(result.hitokoto, 5000, true);
593 | });
594 | }
595 | }
596 |
597 | $(".waifu-tool .fui-eye").click(function () {
598 | loadOtherModel();
599 | });
600 | $(".waifu-tool .fui-user").click(function () {
601 | loadRandTextures();
602 | });
603 | $(".waifu-tool .fui-chat").click(function () {
604 | showHitokoto();
605 | });
606 | }
607 |
--------------------------------------------------------------------------------
/src/assets/waifu-tips.json:
--------------------------------------------------------------------------------
1 | {
2 | "waifu": {
3 | "console_open_msg": ["哈哈,你打开了控制台,是想要看看我的秘密吗?"],
4 | "copy_message": ["你都复制了些什么呀,转载要记得加上出处哦"],
5 | "screenshot_message": ["照好了嘛,是不是很可爱呢?"],
6 | "hidden_message": ["我们还能再见面的吧…"],
7 | "load_rand_textures": ["我还没有其他衣服呢", "我的新衣服好看嘛"],
8 | "hour_tips": {
9 | "t5-7": ["早上好!一日之计在于晨,美好的一天就要开始了"],
10 | "t7-11": ["上午好!工作顺利嘛,不要久坐,多起来走动走动哦!"],
11 | "t11-14": ["中午了,工作了一个上午,现在是午餐时间!"],
12 | "t14-17": ["午后很容易犯困呢,今天的运动目标完成了吗?"],
13 | "t17-19": ["傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~"],
14 | "t19-21": ["晚上好,今天过得怎么样?"],
15 | "t21-23": ["已经这么晚了呀,早点休息吧,晚安~"],
16 | "t23-5": ["你是夜猫子呀?这么晚还不睡觉,明天起的来嘛"],
17 | "default": ["嗨~ 快来逗我玩吧!"]
18 | },
19 | "referrer_message": {
20 | "localhost": ["欢迎阅读『", "』", " - "],
21 | "baidu": ["Hello! 来自 百度搜索 的朋友
你是搜索 ", " 找到的我吗?"],
22 | "so": ["Hello! 来自 360搜索 的朋友
你是搜索 ", " 找到的我吗?"],
23 | "google": ["Hello! 来自 谷歌搜索 的朋友
欢迎阅读『", "』", " - "],
24 | "default": ["Hello! 来自 ", " 的朋友"],
25 | "none": ["欢迎阅读『", "』", " - "]
26 | },
27 | "referrer_hostname": {
28 | "example.com": ["示例网站"],
29 | "www.fghrsh.net": ["FGHRSH 的博客"]
30 | },
31 | "model_message": {
32 | "1": ["来自 Potion Maker 的 Pio 酱 ~"],
33 | "2": ["来自 Potion Maker 的 Tia 酱 ~"]
34 | },
35 | "hitokoto_api_message": {
36 | "lwl12.com": ["这句一言来自 『{source}』", ",是 {creator} 投稿的", "。"],
37 | "fghrsh.net": ["这句一言出处是 『{source}』,是 FGHRSH 在 {date} 收藏的!"],
38 | "jinrishici.com": ["这句诗词出自 《{title}》,是 {dynasty}诗人 {author} 创作的!"],
39 | "hitokoto.cn": ["这句一言来自 『{source}』,是 {creator} 在 hitokoto.cn 投稿的。"]
40 | }
41 | },
42 | "mouseover": [
43 | { "selector": ".container a[href^='http']", "text": ["要看看 {text} 么?"] },
44 | { "selector": ".fui-home", "text": ["点击前往首页,想回到上一页可以使用浏览器的后退功能哦"] },
45 | { "selector": ".fui-chat", "text": ["一言一语,一颦一笑。一字一句,一颗赛艇。"] },
46 | { "selector": ".fui-eye", "text": ["嗯··· 要切换 看板娘 吗?"] },
47 | { "selector": ".fui-user", "text": ["喜欢换装 Play 吗?"] },
48 | { "selector": ".fui-photo", "text": ["要拍张纪念照片吗?"] },
49 | { "selector": ".fui-info-circle", "text": ["这里有关于我的信息呢"] },
50 | { "selector": ".fui-cross", "text": ["你不喜欢我了吗..."] },
51 | { "selector": "#tor_show", "text": ["翻页比较麻烦吗,点击可以显示这篇文章的目录呢"] },
52 | { "selector": "#comment_go", "text": ["想要去评论些什么吗?"] },
53 | { "selector": "#night_mode", "text": ["深夜时要爱护眼睛呀"] },
54 | { "selector": "#qrcode", "text": ["手机扫一下就能继续看,很方便呢"] },
55 | { "selector": ".comment_reply", "text": ["要吐槽些什么呢"] },
56 | { "selector": "#back-to-top", "text": ["回到开始的地方吧"] },
57 | { "selector": "#author", "text": ["该怎么称呼你呢"] },
58 | { "selector": "#mail", "text": ["留下你的邮箱,不然就是无头像人士了"] },
59 | { "selector": "#url", "text": ["你的家在哪里呢,好让我去参观参观"] },
60 | { "selector": "#textarea", "text": ["认真填写哦,垃圾评论是禁止事项"] },
61 | { "selector": ".OwO-logo", "text": ["要插入一个表情吗"] },
62 | { "selector": "#csubmit", "text": ["要[提交]^(Commit)了吗,首次评论需要审核,请耐心等待~"] },
63 | { "selector": ".ImageBox", "text": ["点击图片可以放大呢"] },
64 | { "selector": "input[name=s]", "text": ["找不到想看的内容?搜索看看吧"] },
65 | { "selector": ".previous", "text": ["去上一页看看吧"] },
66 | { "selector": ".next", "text": ["去下一页看看吧"] },
67 | { "selector": ".dropdown-toggle", "text": ["这里是菜单"] },
68 | { "selector": "c-player a.play-icon", "text": ["想要听点音乐吗"] },
69 | { "selector": "c-player div.time", "text": ["在这里可以调整播放进度呢"] },
70 | { "selector": "c-player div.volume", "text": ["在这里可以调整音量呢"] },
71 | { "selector": "c-player div.list-button", "text": ["播放列表里都有什么呢"] },
72 | { "selector": "c-player div.lyric-button", "text": ["有歌词的话就能跟着一起唱呢"] },
73 | { "selector": ".waifu #live2d", "text": ["干嘛呢你,快把手拿开", "鼠…鼠标放错地方了!"] }
74 | ],
75 | "click": [
76 | {
77 | "selector": ".waifu #live2d",
78 | "text": [
79 | "是…是不小心碰到了吧",
80 | "萝莉控是什么呀",
81 | "你看到我的小熊了吗",
82 | "再摸的话我可要报警了!⌇●﹏●⌇",
83 | "110吗,这里有个变态一直在摸我(ó﹏ò。)"
84 | ]
85 | }
86 | ],
87 | "seasons": [
88 | { "date": "01/01", "text": ["元旦了呢,新的一年又开始了,今年是{year}年~"] },
89 | { "date": "02/14", "text": ["又是一年情人节,{year}年找到对象了嘛~"] },
90 | { "date": "03/08", "text": ["今天是妇女节!"] },
91 | { "date": "03/12", "text": ["今天是植树节,要保护环境呀"] },
92 | { "date": "04/01", "text": ["悄悄告诉你一个秘密~今天是愚人节,不要被骗了哦~"] },
93 | { "date": "05/01", "text": ["今天是五一劳动节,计划好假期去哪里了吗~"] },
94 | { "date": "06/01", "text": ["儿童节了呢,快活的时光总是短暂,要是永远长不大该多好啊…"] },
95 | { "date": "09/03", "text": ["中国人民抗日战争胜利纪念日,铭记历史、缅怀先烈、珍爱和平、开创未来。"] },
96 | { "date": "09/10", "text": ["教师节,在学校要给老师问声好呀~"] },
97 | { "date": "10/01", "text": ["国庆节,新中国已经成立69年了呢"] },
98 | { "date": "11/05-11/12", "text": ["今年的双十一是和谁一起过的呢~"] },
99 | { "date": "12/20-12/31", "text": ["这几天是圣诞节,主人肯定又去剁手买买买了~"] }
100 | ]
101 | }
--------------------------------------------------------------------------------
/src/assets/waifu.css:
--------------------------------------------------------------------------------
1 | .waifu {
2 | position: fixed;
3 | bottom: 0;
4 | z-index: 1000;
5 | font-size: 0;
6 | /* -webkit-transform: translateY(3px); */
7 | /* transform: translateY(3px); */
8 | }
9 | .waifu:hover {
10 | -webkit-transform: translateY(0);
11 | transform: translateY(0);
12 | }
13 | .waifu-tips {
14 | opacity: 0;
15 | margin: -20px 20px;
16 | padding: 5px 10px;
17 | border: 1px solid rgba(224, 186, 140, 0.62);
18 | border-radius: 12px;
19 | background-color: rgba(236, 217, 188, 0.5);
20 | box-shadow: 0 3px 15px 2px rgba(191, 158, 118, 0.2);
21 | text-overflow: ellipsis;
22 | overflow: hidden;
23 | position: absolute;
24 | animation-delay: 5s;
25 | animation-duration: 50s;
26 | animation-iteration-count: infinite;
27 | animation-name: shake;
28 | animation-timing-function: ease-in-out;
29 | }
30 | .waifu-tool {
31 | display: none;
32 | color: #aaa;
33 | top: 50px;
34 | right: 10px;
35 | position: absolute;
36 | }
37 | .waifu:hover .waifu-tool {
38 | display: block;
39 | }
40 | .waifu-tool span {
41 | display: block;
42 | cursor: pointer;
43 | color: #5b6c7d;
44 | transition: 0.2s;
45 | }
46 | .waifu-tool span:hover {
47 | color: #34495e;
48 | }
49 | .waifu #live2d {
50 | position: relative;
51 | }
52 |
53 | @keyframes shake {
54 | 2% {
55 | transform: translate(0.5px, -1.5px) rotate(-0.5deg);
56 | }
57 |
58 | 4% {
59 | transform: translate(0.5px, 1.5px) rotate(1.5deg);
60 | }
61 |
62 | 6% {
63 | transform: translate(1.5px, 1.5px) rotate(1.5deg);
64 | }
65 |
66 | 8% {
67 | transform: translate(2.5px, 1.5px) rotate(0.5deg);
68 | }
69 |
70 | 10% {
71 | transform: translate(0.5px, 2.5px) rotate(0.5deg);
72 | }
73 |
74 | 12% {
75 | transform: translate(1.5px, 1.5px) rotate(0.5deg);
76 | }
77 |
78 | 14% {
79 | transform: translate(0.5px, 0.5px) rotate(0.5deg);
80 | }
81 |
82 | 16% {
83 | transform: translate(-1.5px, -0.5px) rotate(1.5deg);
84 | }
85 |
86 | 18% {
87 | transform: translate(0.5px, 0.5px) rotate(1.5deg);
88 | }
89 |
90 | 20% {
91 | transform: translate(2.5px, 2.5px) rotate(1.5deg);
92 | }
93 |
94 | 22% {
95 | transform: translate(0.5px, -1.5px) rotate(1.5deg);
96 | }
97 |
98 | 24% {
99 | transform: translate(-1.5px, 1.5px) rotate(-0.5deg);
100 | }
101 |
102 | 26% {
103 | transform: translate(1.5px, 0.5px) rotate(1.5deg);
104 | }
105 |
106 | 28% {
107 | transform: translate(-0.5px, -0.5px) rotate(-0.5deg);
108 | }
109 |
110 | 30% {
111 | transform: translate(1.5px, -0.5px) rotate(-0.5deg);
112 | }
113 |
114 | 32% {
115 | transform: translate(2.5px, -1.5px) rotate(1.5deg);
116 | }
117 |
118 | 34% {
119 | transform: translate(2.5px, 2.5px) rotate(-0.5deg);
120 | }
121 |
122 | 36% {
123 | transform: translate(0.5px, -1.5px) rotate(0.5deg);
124 | }
125 |
126 | 38% {
127 | transform: translate(2.5px, -0.5px) rotate(-0.5deg);
128 | }
129 |
130 | 40% {
131 | transform: translate(-0.5px, 2.5px) rotate(0.5deg);
132 | }
133 |
134 | 42% {
135 | transform: translate(-1.5px, 2.5px) rotate(0.5deg);
136 | }
137 |
138 | 44% {
139 | transform: translate(-1.5px, 1.5px) rotate(0.5deg);
140 | }
141 |
142 | 46% {
143 | transform: translate(1.5px, -0.5px) rotate(-0.5deg);
144 | }
145 |
146 | 48% {
147 | transform: translate(2.5px, -0.5px) rotate(0.5deg);
148 | }
149 |
150 | 50% {
151 | transform: translate(-1.5px, 1.5px) rotate(0.5deg);
152 | }
153 |
154 | 52% {
155 | transform: translate(-0.5px, 1.5px) rotate(0.5deg);
156 | }
157 |
158 | 54% {
159 | transform: translate(-1.5px, 1.5px) rotate(0.5deg);
160 | }
161 |
162 | 56% {
163 | transform: translate(0.5px, 2.5px) rotate(1.5deg);
164 | }
165 |
166 | 58% {
167 | transform: translate(2.5px, 2.5px) rotate(0.5deg);
168 | }
169 |
170 | 60% {
171 | transform: translate(2.5px, -1.5px) rotate(1.5deg);
172 | }
173 |
174 | 62% {
175 | transform: translate(-1.5px, 0.5px) rotate(1.5deg);
176 | }
177 |
178 | 64% {
179 | transform: translate(-1.5px, 1.5px) rotate(1.5deg);
180 | }
181 |
182 | 66% {
183 | transform: translate(0.5px, 2.5px) rotate(1.5deg);
184 | }
185 |
186 | 68% {
187 | transform: translate(2.5px, -1.5px) rotate(1.5deg);
188 | }
189 |
190 | 70% {
191 | transform: translate(2.5px, 2.5px) rotate(0.5deg);
192 | }
193 |
194 | 72% {
195 | transform: translate(-0.5px, -1.5px) rotate(1.5deg);
196 | }
197 |
198 | 74% {
199 | transform: translate(-1.5px, 2.5px) rotate(1.5deg);
200 | }
201 |
202 | 76% {
203 | transform: translate(-1.5px, 2.5px) rotate(1.5deg);
204 | }
205 |
206 | 78% {
207 | transform: translate(-1.5px, 2.5px) rotate(0.5deg);
208 | }
209 |
210 | 80% {
211 | transform: translate(-1.5px, 0.5px) rotate(-0.5deg);
212 | }
213 |
214 | 82% {
215 | transform: translate(-1.5px, 0.5px) rotate(-0.5deg);
216 | }
217 |
218 | 84% {
219 | transform: translate(-0.5px, 0.5px) rotate(1.5deg);
220 | }
221 |
222 | 86% {
223 | transform: translate(2.5px, 1.5px) rotate(0.5deg);
224 | }
225 |
226 | 88% {
227 | transform: translate(-1.5px, 0.5px) rotate(1.5deg);
228 | }
229 |
230 | 90% {
231 | transform: translate(-1.5px, -0.5px) rotate(-0.5deg);
232 | }
233 |
234 | 92% {
235 | transform: translate(-1.5px, -1.5px) rotate(1.5deg);
236 | }
237 |
238 | 94% {
239 | transform: translate(0.5px, 0.5px) rotate(-0.5deg);
240 | }
241 |
242 | 96% {
243 | transform: translate(2.5px, -0.5px) rotate(-0.5deg);
244 | }
245 |
246 | 98% {
247 | transform: translate(-1.5px, -1.5px) rotate(-0.5deg);
248 | }
249 |
250 | 0%,
251 | 100% {
252 | transform: translate(0, 0) rotate(0);
253 | }
254 | }
255 | @font-face {
256 | font-family: "Flat-UI-Icons";
257 | src: url("flat-ui-icons-regular.eot");
258 | src: url("flat-ui-icons-regular.eot?#iefix") format("embedded-opentype"),
259 | url("flat-ui-icons-regular.woff") format("woff"),
260 | url("flat-ui-icons-regular.ttf") format("truetype"),
261 | url("flat-ui-icons-regular.svg#flat-ui-icons-regular") format("svg");
262 | }
263 | [class^="fui-"],
264 | [class*="fui-"] {
265 | font-family: "Flat-UI-Icons";
266 | speak: none;
267 | font-style: normal;
268 | font-weight: normal;
269 | font-variant: normal;
270 | text-transform: none;
271 | -webkit-font-smoothing: antialiased;
272 | -moz-osx-font-smoothing: grayscale;
273 | }
274 | .fui-cross:before {
275 | content: "\e609";
276 | }
277 | .fui-info-circle:before {
278 | content: "\e60f";
279 | }
280 | .fui-photo:before {
281 | content: "\e62a";
282 | }
283 | .fui-eye:before {
284 | content: "\e62c";
285 | }
286 | .fui-chat:before {
287 | content: "\e62d";
288 | }
289 | .fui-home:before {
290 | content: "\e62e";
291 | }
292 | .fui-user:before {
293 | content: "\e631";
294 | }
295 | .fui-window:before {
296 | content: "\e62f";
297 | }
298 | .fui-lock:before {
299 | content: "\e633";
300 | }
301 | .fui-gear:before {
302 | content: "\e636";
303 | }
304 |
305 | .fui-triangle-up:before {
306 | content: "\e600";
307 | }
308 | .fui-triangle-down:before {
309 | content: "\e601";
310 | }
311 |
312 | .fui-power:before {
313 | content: "\e634";
314 | }
315 | .fui-location:before {
316 | content: "\e627";
317 | }
318 |
319 | .fui-star:before {
320 | content: "\e63e";
321 | }
322 |
323 | .fui-checkbox-unchecked:before {
324 | content: "\e60d";
325 | }
326 |
327 | .fui-alert-circle:before {
328 | content: "\e610";
329 | }
330 |
--------------------------------------------------------------------------------
/src/components/Config.vue:
--------------------------------------------------------------------------------
1 |
113 |
114 |
115 |
116 |
122 |
123 |
129 |
130 |
135 | 🌈
136 |
137 |
138 |
139 |
144 | ⬆️
145 |
146 |
147 |
148 |
149 |
150 | 打开桌宠
151 |
152 |
153 | 关闭桌宠
154 |
155 |
156 | 重置位置
157 |
158 |
159 | 开启捕获鼠标事件
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
185 |
--------------------------------------------------------------------------------
/src/components/Model.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 | 选择本地模型
13 |
14 | 刷新
15 |
16 | 添加网络模型
17 |
18 |
19 |
20 |
21 |
22 |
23 | 类型 |
24 | 模型位置 |
25 | 操作 |
26 |
27 |
28 |
29 |
30 | {{ it.type }} |
31 | {{ it.url }} |
32 |
33 |
34 | 加载
35 |
39 | 删除
40 |
41 |
42 | |
43 |
44 |
45 |
46 |
47 |
58 |
63 |
64 |
65 |
66 |
67 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/src/hooks/useInterval.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, ref, onUnmounted } from "vue";
2 | export default function (ms: number, fn: Function) {
3 | const interval = ref();
4 | onMounted(() => {
5 | interval.value = setInterval(fn, ms);
6 | });
7 | onUnmounted(() => {
8 | if (interval.value) {
9 | clearInterval(interval.value);
10 | }
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/useListenEvent.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, ref, onUnmounted } from "vue";
2 | import { listen, EventCallback, UnlistenFn } from "@tauri-apps/api/event";
3 |
4 | export default function (eventName: string, fn: EventCallback) {
5 | const listenRef = ref>();
6 | onMounted(() => {
7 | listenRef.value = listen(eventName, fn);
8 | });
9 | onUnmounted(async () => {
10 | if (listenRef.value) {
11 | const unListen = await listenRef.value;
12 | unListen();
13 | }
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/src/hooks/useModel.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue";
2 | const modelConfig = ref();
3 | /**
4 | * model
5 | */
6 |
7 | export { modelConfig };
8 |
--------------------------------------------------------------------------------
/src/hooks/useUpdate.ts:
--------------------------------------------------------------------------------
1 | import { checkupdate } from "@/plugins";
2 |
3 | export default async function () {
4 | await checkupdate.check_version_update();
5 | }
6 |
--------------------------------------------------------------------------------
/src/live2d/App.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import Live2dApp from "./index.vue";
3 |
4 | declare global {
5 | interface Window {
6 | live2d_settings: any;
7 | PIXI: any;
8 | }
9 | }
10 |
11 | createApp(Live2dApp).mount("#live2d-app");
12 |
--------------------------------------------------------------------------------
/src/live2d/index.vue:
--------------------------------------------------------------------------------
1 |
416 |
417 |
418 |
423 |
424 |
431 |
432 |
433 |
434 |
442 |
443 |
444 |
445 |
453 |
458 |
463 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
509 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import "./style.css";
3 | import App from "./App.vue";
4 |
5 | createApp(App).mount("#app");
6 |
--------------------------------------------------------------------------------
/src/plugins/autostart.ts:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/tauri";
2 |
3 | export async function isEnabled(): Promise {
4 | return await invoke("plugin:autostart|is_enabled");
5 | }
6 |
7 | export async function enable(): Promise {
8 | await invoke("plugin:autostart|enable");
9 | }
10 |
11 | export async function disable(): Promise {
12 | await invoke("plugin:autostart|disable");
13 | }
14 |
--------------------------------------------------------------------------------
/src/plugins/checkupdate.ts:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/tauri";
2 |
3 | export async function check_version_update(): Promise {
4 | return await invoke("plugin:checkupdate|run_check_update");
5 | }
6 |
--------------------------------------------------------------------------------
/src/plugins/index.ts:
--------------------------------------------------------------------------------
1 | export * as autostart from "./autostart";
2 | export * as modelserve from "./modelserve";
3 | export * as checkupdate from "./checkupdate";
4 |
--------------------------------------------------------------------------------
/src/plugins/modelserve.ts:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/tauri";
2 | import { readConfig, AppConfig } from "@/util";
3 |
4 | export interface Live2dModelItem {
5 | url: string;
6 | type: "remote" | "local";
7 | }
8 |
9 | export async function model_list(): Promise> {
10 | let config = {} as AppConfig;
11 | let list: Array = [];
12 | try {
13 | config = await readConfig();
14 | let localList: string[] = [];
15 | localList = await invoke>("model_list");
16 | list = list.concat(
17 | (list = localList.map(
18 | (it) =>
19 | ({
20 | url: it,
21 | type: "local",
22 | } as Live2dModelItem)
23 | ))
24 | );
25 | } catch (error) {
26 | list = [];
27 | }
28 |
29 | const remote_list = config.remote_list || [];
30 |
31 | list = list.concat(
32 | remote_list.map(
33 | (it) =>
34 | ({
35 | url: it,
36 | type: "remote",
37 | } as Live2dModelItem)
38 | )
39 | );
40 | return list;
41 | }
42 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3 | font-size: 16px;
4 | line-height: 24px;
5 | font-weight: 400;
6 |
7 | color: #0f0f0f;
8 | background-color: #f6f6f6;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 |
17 | .container {
18 | margin: 0;
19 | padding-top: 5vh;
20 | display: flex;
21 | flex-direction: column;
22 | justify-content: center;
23 | text-align: center;
24 | overflow: hidden;
25 | }
26 |
27 | footer {
28 | position: fixed;
29 | bottom: 0;
30 | color: green;
31 | display: flex;
32 | width: 100%;
33 | flex-direction: column;
34 | justify-content: center;
35 | text-align: center;
36 | padding-top: 20px;
37 | z-index: -10;
38 | }
39 |
40 | .logo {
41 | height: 6em;
42 | padding: 1.5em;
43 | will-change: filter;
44 | transition: 0.75s;
45 | }
46 |
47 | .logo.tauri:hover {
48 | filter: drop-shadow(0 0 2em #24c8db);
49 | }
50 |
51 | .row {
52 | display: flex;
53 | justify-content: center;
54 | }
55 |
56 | a {
57 | font-weight: 500;
58 | color: #646cff;
59 | text-decoration: inherit;
60 | }
61 |
62 | a:hover {
63 | color: #535bf2;
64 | }
65 |
66 | h1 {
67 | text-align: center;
68 | }
69 |
70 | input,
71 | button {
72 | border-radius: 8px;
73 | border: 1px solid transparent;
74 | padding: 0.6em 1.2em;
75 | font-size: 1em;
76 | font-weight: 500;
77 | font-family: inherit;
78 | color: #0f0f0f;
79 | background-color: #ffffff;
80 | transition: border-color 0.25s;
81 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
82 | }
83 |
84 | button {
85 | cursor: pointer;
86 | }
87 |
88 | button:hover {
89 | border-color: #396cd8;
90 | }
91 |
92 | input,
93 | button {
94 | outline: none;
95 | }
96 |
97 | #greet-input {
98 | margin-right: 5px;
99 | }
100 |
101 | @media (prefers-color-scheme: dark) {
102 | :root {
103 | color: #f6f6f6;
104 | background-color: #2f2f2f;
105 | }
106 |
107 | a:hover {
108 | color: #24c8db;
109 | }
110 |
111 | input,
112 | button {
113 | color: #ffffff;
114 | background-color: #0f0f0f98;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export type RustCallResult = {
2 | data: T;
3 | err: String;
4 | };
5 |
6 | export enum InitAppDataEnum {
7 | EXIST,
8 | CreateError,
9 | SUCCESS,
10 | }
11 |
--------------------------------------------------------------------------------
/src/util/index.ts:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/tauri";
2 | import { RustCallResult } from "@/types";
3 |
4 | export interface AppConfig {
5 | model_dir?: string;
6 | port: number;
7 | width?: number;
8 | height?: number;
9 | x?: number;
10 | y?: number;
11 | check_update?: boolean;
12 | remote_list?: string[];
13 | model_block?: boolean;
14 | [key: string]: any;
15 | }
16 |
17 | /**
18 | *读取配置文件
19 | * @returns
20 | */
21 | export async function readConfig(): Promise {
22 | return invoke("read_config");
23 | }
24 |
25 | /**
26 | * 写入配置文件
27 | * @param data 文件内容
28 | */
29 | export async function writeConfig(data: string) {
30 | await invoke("write_config", { value: data });
31 | }
32 |
33 | /**
34 | * 系统文件读取为数组
35 | * @param path
36 | * @returns
37 | */
38 | export async function readSysFileForArray(path: String) {
39 | const file: RustCallResult = await invoke("read_file", {
40 | filePath: path,
41 | });
42 | return file;
43 | }
44 |
45 | /**
46 | * 写入系统文件
47 | * @param filePath 系统文件目录
48 | * @param content 文件内容
49 | */
50 | export async function writeSysFileFromString(
51 | filePath: string,
52 | content: string
53 | ) {
54 | const rt = await invoke>("write_file", {
55 | filePath,
56 | data: content,
57 | });
58 | return !rt.err;
59 | }
60 |
61 | export function sleep(ms: number) {
62 | return new Promise((fn) => setTimeout(fn, ms));
63 | }
64 |
65 | // 节流时间戳版本
66 | export function throttle(func, wait) {
67 | let previous = 0;
68 | return function (this) {
69 | let now = Date.now(),
70 | context = this,
71 | args = [...arguments];
72 | if (now - previous > wait) {
73 | func.apply(context, args);
74 | previous = now; // 闭包,记录本次执行时间戳
75 | }
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module "*.vue" {
4 | import type { DefineComponent } from "vue";
5 | const component: DefineComponent<{}, {}, any>;
6 | export default component;
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "noImplicitAny": false,
10 | "sourceMap": true,
11 | "allowJs": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "esModuleInterop": true,
15 | "lib": ["ESNext", "DOM", "es2015"],
16 | "skipLibCheck": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["src/*"]
20 | }
21 | },
22 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
23 | "references": [{ "path": "./tsconfig.node.json" }]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true,
7 | "lib": ["ESNext", "DOM", "es2015"]
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import vue from "@vitejs/plugin-vue";
3 | import alias from "@rollup/plugin-alias";
4 | import { resolve } from "path";
5 |
6 | import path from "path";
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | plugins: [
11 | vue(),
12 | alias({
13 | entries: [{ find: "@", replacement: resolve("./src") }],
14 | }),
15 | ],
16 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
17 | // prevent vite from obscuring rust errors
18 | clearScreen: false,
19 | // tauri expects a fixed port, fail if that port is not available
20 | server: {
21 | port: 1420,
22 | strictPort: true,
23 | },
24 | // to make use of `TAURI_DEBUG` and other env variables
25 | // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
26 | envPrefix: ["VITE_", "TAURI_"],
27 | build: {
28 | rollupOptions: {
29 | input: {
30 | index: path.resolve(__dirname, "index.html"),
31 | live2d: path.resolve(__dirname, "live2d.html"),
32 | },
33 | },
34 | // Tauri supports es2021
35 | target: ["es2021", "chrome100", "safari13"],
36 | // don't minify for debug builds
37 | minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
38 | // produce sourcemaps for debug builds
39 | sourcemap: !!process.env.TAURI_DEBUG,
40 | },
41 | });
42 |
--------------------------------------------------------------------------------