├── .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 | 2 | 3 | 4 | 5 | 6 | 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 | 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 | 4 | 5 | 6 | { 7 | "fontFamily": "flat-ui-icons", 8 | "majorVersion": 1, 9 | "minorVersion": 1, 10 | "fontURL": "http://designmodo.com/flat", 11 | "designer": "Sergey Shmidt", 12 | "designerURL": "http://designmodo.com", 13 | "license": "Attribution-NonCommercial-NoDerivs 3.0 Unported", 14 | "licenseURL": "http://creativecommons.org/licenses/by-nc-nd/3.0/", 15 | "version": "Version 1.1", 16 | "fontId": "flat-ui-icons", 17 | "psName": "flat-ui-icons", 18 | "subFamily": "Regular", 19 | "fullName": "flat-ui-icons", 20 | "description": "Generated by IcoMoon" 21 | } 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /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 | 169 | 170 | 185 | -------------------------------------------------------------------------------- /src/components/Model.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | --------------------------------------------------------------------------------