feat: migrate to turbo (#22)
* feat: migrate top turbo * ci: ci test * fix: fix codeql issues * feat: ci test * chore: lint * chore: misc changes * feat: rename vite helpers * feat: use fetch to handle assets * feat: update directory * feat: fetch charword table * feat: migrate download game data and detect missing voice files * feat: symlink relative path * feat: finish wrangler upload * feat: migrate wrangler download * feat: finish * chore: auto update * ci: update ci * ci: update ci --------- Co-authored-by: Halyul <Halyul@users.noreply.github.com>
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
45
.github/workflows/cf-pages.yaml
vendored
45
.github/workflows/cf-pages.yaml
vendored
@@ -3,12 +3,12 @@ name: Build release and push to CF Pages
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_ZIP_FILENAME: cache.zip
|
DO_NOT_TRACK: 1
|
||||||
ASSETS_FOLDER: data/operator
|
|
||||||
RELEASE_FOLDER: release
|
|
||||||
CACHE_BASE_KEY: akassets
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -19,42 +19,15 @@ jobs:
|
|||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
run_install: true
|
run_install: true
|
||||||
- name: Restore cached assets
|
- name: Download Data
|
||||||
id: cache-akassets-restore
|
run: pnpm run download:data
|
||||||
uses: actions/cache@v4
|
- name: Build
|
||||||
with:
|
run: pnpm run build
|
||||||
path: |
|
|
||||||
${{ env.CACHE_ZIP_FILENAME }}
|
|
||||||
key: ${{ env.CACHE_BASE_KEY }}-${{ hashFiles('offical_update.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ env.CACHE_BASE_KEY }}
|
|
||||||
- name: Unzip assets
|
|
||||||
run: |
|
|
||||||
if test -f ${{ env.CACHE_ZIP_FILENAME }}; then
|
|
||||||
unzip -qq ${{ env.CACHE_ZIP_FILENAME }} -d .
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
- name: Download Assets
|
|
||||||
run: pnpm run cf:download
|
|
||||||
- name: Build all
|
|
||||||
run: pnpm run operator:build-all
|
|
||||||
timeout-minutes: 10
|
|
||||||
- name: Build directory
|
|
||||||
run: pnpm run directory:build
|
|
||||||
- name: Publish to Cloudflare Pages
|
- name: Publish to Cloudflare Pages
|
||||||
uses: cloudflare/pages-action@v1
|
uses: cloudflare/pages-action@v1
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
projectName: aklive2d
|
projectName: aklive2d
|
||||||
directory: ${{ env.RELEASE_FOLDER}}
|
directory: dist
|
||||||
wranglerVersion: '3'
|
wranglerVersion: '3'
|
||||||
- name: Zip assets
|
|
||||||
run: zip -qq -9 -r ${{ env.CACHE_ZIP_FILENAME }} ${{ env.ASSETS_FOLDER }}
|
|
||||||
- name: Save assets
|
|
||||||
id: cache-akassets-save
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ env.CACHE_ZIP_FILENAME }}
|
|
||||||
key: ${{ env.CACHE_BASE_KEY }}-${{ hashFiles('offical_update.json') }}
|
|
||||||
|
|||||||
32
.github/workflows/update-charwords.yaml
vendored
32
.github/workflows/update-charwords.yaml
vendored
@@ -1,32 +0,0 @@
|
|||||||
name: Update charwords
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use PNPM
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm i
|
|
||||||
- name: Update charwords
|
|
||||||
run: pnpm run charwords:update
|
|
||||||
- name: Commit changes if any
|
|
||||||
uses: stefanzweifel/git-auto-commit-action@v4
|
|
||||||
with:
|
|
||||||
commit_message: "chore(charwords): auto update"
|
|
||||||
32
.github/workflows/update-music.yaml
vendored
32
.github/workflows/update-music.yaml
vendored
@@ -1,32 +0,0 @@
|
|||||||
name: Update music
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use PNPM
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm i
|
|
||||||
- name: Update music mapping
|
|
||||||
run: pnpm run music
|
|
||||||
- name: Commit changes if any
|
|
||||||
uses: stefanzweifel/git-auto-commit-action@v4
|
|
||||||
with:
|
|
||||||
commit_message: "chore(music): auto update mapping"
|
|
||||||
32
.github/workflows/update-offical-dyn-info.yaml
vendored
32
.github/workflows/update-offical-dyn-info.yaml
vendored
@@ -1,32 +0,0 @@
|
|||||||
name: Update Offical Dyn Info
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '30 10 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use PNPM
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm i
|
|
||||||
- name: Update offical dyn info
|
|
||||||
run: pnpm run offical_update
|
|
||||||
- name: Commit changes if any
|
|
||||||
uses: stefanzweifel/git-auto-commit-action@v4
|
|
||||||
with:
|
|
||||||
commit_message: "chore(offical update): auto update"
|
|
||||||
32
.github/workflows/update.yaml
vendored
Normal file
32
.github/workflows/update.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Update
|
||||||
|
|
||||||
|
env:
|
||||||
|
DO_NOT_TRACK: 1
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 10 * * *'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use PNPM
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: true
|
||||||
|
- name: Update
|
||||||
|
run: pnpm run update
|
||||||
|
- name: Commit changes if any
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
with:
|
||||||
|
commit_message: "chore: auto update"
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -130,7 +130,6 @@ dist
|
|||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
# custom
|
# custom
|
||||||
release/*
|
|
||||||
spine-runtimes/*
|
spine-runtimes/*
|
||||||
_*.json
|
_*.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -138,5 +137,5 @@ _*.json
|
|||||||
*_v2/*
|
*_v2/*
|
||||||
assets/*
|
assets/*
|
||||||
temp/*
|
temp/*
|
||||||
operator/*
|
.turbo
|
||||||
data/operator/*
|
data/*
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"map": true,
|
|
||||||
"plugins": {
|
|
||||||
"autoprefixer": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "stylelint-config-standard-scss"
|
|
||||||
}
|
|
||||||
63
.vscode/launch.json
vendored
63
.vscode/launch.json
vendored
@@ -9,87 +9,100 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: charword",
|
"name": "Run Script: build chen",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run charwords:update",
|
"env": {
|
||||||
|
"name": "chen"
|
||||||
|
},
|
||||||
|
"command": "pnpm run build",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: build mizuki_summer_feast",
|
"name": "Run Script: build all",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run build mizuki_summer_feast",
|
"command": "pnpm run build",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: build-all",
|
"name": "Run Script: update",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run operator:build-all",
|
"command": "pnpm run update",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: test",
|
"name": "Run Script: init",
|
||||||
|
"env": {
|
||||||
|
"name": "test",
|
||||||
|
"id": "202203231"
|
||||||
|
},
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run test",
|
"command": "pnpm run init",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: dev",
|
"name": "Run Script: lint",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run dev kaltsit_remnant",
|
"command": "pnpm run lint",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: Directory dev",
|
"name": "Run Script: dev:directory",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run vite:directory:dev",
|
"command": "pnpm run dev:directory",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: charwords:update",
|
"name": "Run Script: preview:directory",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run charwords:update",
|
"command": "pnpm run preview:directory",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: charwords:build",
|
"name": "Run Script: dev:showcase",
|
||||||
|
"env": {
|
||||||
|
"name": "chen"
|
||||||
|
},
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run charwords:build",
|
"command": "pnpm run dev:showcase",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: music",
|
"name": "Run Script: preview:showcase",
|
||||||
|
"env": {
|
||||||
|
"name": "chen"
|
||||||
|
},
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run music",
|
"command": "pnpm run preview:showcase",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: offical_update",
|
"name": "Run Script: download:game",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run offical_update",
|
"command": "pnpm run download:game",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: cf:upload",
|
"name": "Run Script: upload",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run cf:upload",
|
"command": "pnpm run upload",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Run Script: cf:download",
|
"name": "Run Script: download:data",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm run cf:download",
|
"command": "pnpm run download:data",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -14,36 +14,32 @@ A list of supported operators can be found at [Directory](https://gura.ch/aklive
|
|||||||
### Command Line Tool
|
### Command Line Tool
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ npm run generate {operator_name}
|
$ pnpm run update
|
||||||
To generate operator assets for showcase page
|
Update data from official website and github repo
|
||||||
```
|
```
|
||||||
``` bash
|
``` bash
|
||||||
$ npm run dev {operator_name}
|
$ pnpm run lint
|
||||||
Live showcase page server for development
|
ESLint and StyleLint
|
||||||
```
|
```
|
||||||
``` bash
|
``` bash
|
||||||
$ npm run build {operator_name}
|
$ pnpm run build
|
||||||
|
Build showcase webpage for all operators and directory page
|
||||||
|
```
|
||||||
|
``` bash
|
||||||
|
$ name=<name> pnpm run build
|
||||||
Build showcase webpage for an operator
|
Build showcase webpage for an operator
|
||||||
```
|
```
|
||||||
``` bash
|
``` bash
|
||||||
$ npm run build-all
|
$ name=<name> id=<id> pnpm run init
|
||||||
To generate all operator assets for showcase page
|
|
||||||
```
|
|
||||||
``` bash
|
|
||||||
$ npm run init {operator_name}
|
|
||||||
To initialize folder and config file for an operator
|
To initialize folder and config file for an operator
|
||||||
```
|
```
|
||||||
``` bash
|
``` bash
|
||||||
$ npm run readme {operator_name}
|
$ name=<name> pnpm run dev:showcase
|
||||||
To add operator info to README.md
|
Run dev server for showcase webpage for an operator
|
||||||
```
|
```
|
||||||
``` bash
|
``` bash
|
||||||
$ npm run directory
|
$ name=<name> pnpm run preview:showcase
|
||||||
To generate directory.json
|
Preview built showcase webpage for an operator
|
||||||
```
|
|
||||||
``` bash
|
|
||||||
$ npm run charword
|
|
||||||
To generate the latest charword_table.json
|
|
||||||
```
|
```
|
||||||
### Webpage & JavaScript
|
### Webpage & JavaScript
|
||||||
|
|
||||||
@@ -55,6 +51,7 @@ Using JS events to change settings is recommended.
|
|||||||
|
|
||||||
## Config
|
## Config
|
||||||
### General Config
|
### General Config
|
||||||
|
in `packages/config/config.yaml`
|
||||||
``` yaml
|
``` yaml
|
||||||
folder:
|
folder:
|
||||||
operator: ./operator/ # folder for operator assets
|
operator: ./operator/ # folder for operator assets
|
||||||
@@ -83,20 +80,38 @@ operators:
|
|||||||
passager_dream_in_a_moment: !include config/passager_dream_in_a_moment.yaml
|
passager_dream_in_a_moment: !include config/passager_dream_in_a_moment.yaml
|
||||||
mizuki_summer_feast: !include config/mizuki_summer_feast.yaml
|
mizuki_summer_feast: !include config/mizuki_summer_feast.yaml
|
||||||
```
|
```
|
||||||
### Operator Config
|
### Operators Config
|
||||||
|
in `packages/operator/config.yaml`
|
||||||
|
```yaml
|
||||||
|
chen: !include config/chen.yaml
|
||||||
|
dusk: !include config/dusk.yaml
|
||||||
|
dusk_everything_is_a_miracle: !include config/dusk_everything_is_a_miracle.yaml
|
||||||
|
ling: !include config/ling.yaml
|
||||||
|
nearl: !include config/nearl.yaml
|
||||||
|
nian: !include config/nian.yaml
|
||||||
|
nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml
|
||||||
|
phatom_focus: !include config/phatom_focus.yaml
|
||||||
|
rosmontis: !include config/rosmontis.yaml
|
||||||
|
skadi: !include config/skadi.yaml
|
||||||
|
skadi_sublimation: !include config/skadi_sublimation.yaml
|
||||||
|
w: !include config/w.yaml
|
||||||
|
...
|
||||||
|
```
|
||||||
|
### Operator Config
|
||||||
|
in `packages/operator/config/<name>.yaml`
|
||||||
```yaml
|
```yaml
|
||||||
link: chen # the link to access showcase page for this operator
|
|
||||||
type: operator # operator live2d or skin live2d
|
|
||||||
date: 2021/08 # release date
|
|
||||||
title: 'Arknights: Ch''en/Chen the Holungday - 明日方舟:假日威龙陈' # page title
|
|
||||||
filename: dyn_illust_char_1013_chen2 # live2d assets name
|
filename: dyn_illust_char_1013_chen2 # live2d assets name
|
||||||
logo: logo_rhodes_override # operator logo
|
logo: logo_rhodes_override # operator logo
|
||||||
fallback_name: char_1013_chen2_2 # fallback image name
|
fallback_name: char_1013_chen2_2 # fallback image name
|
||||||
viewport_left: 0 # live2d view port settings
|
viewport_left: 0 # live2d view port settings
|
||||||
viewport_right: 0
|
viewport_right: 0
|
||||||
viewport_top: 1
|
viewport_top: 0
|
||||||
viewport_bottom: 1
|
viewport_bottom: 0
|
||||||
invert_filter: false # operator logo invert filter
|
invert_filter: false # operator logo invert filter
|
||||||
|
codename: # operator name
|
||||||
|
zh-CN: 假日威龙陈
|
||||||
|
en-US: Ch'en/Chen the Holungday
|
||||||
|
use_json: false # whether the spine skel is in json format
|
||||||
```
|
```
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
@@ -104,12 +119,14 @@ The `LICENSE` file applies to all files unless listed specifically.
|
|||||||
|
|
||||||
`LICENSE_SPINE` file applies to following files including adapted code for this repo:
|
`LICENSE_SPINE` file applies to following files including adapted code for this repo:
|
||||||
|
|
||||||
- `src/libs/spine-player.css`
|
- `apps/module/libs/spine-player.css`
|
||||||
- `src/libs/spine-player.js`
|
- `apps/module/libs/spine-player.js`
|
||||||
|
|
||||||
`Copyright © 2017 - 2023 Arknights/Hypergryph Co., Ltd` applies to following files:
|
`Copyright © 2017 - 2023 Arknights/Hypergryph Co., Ltd` applies to following files:
|
||||||
|
|
||||||
- all files under `operator` folder and its sub-folders
|
- all files under `packages/operator/data` folder and its sub-folders
|
||||||
|
- all files under `packages/music/data` folder and its sub-folders
|
||||||
|
- all files under `packages/background/data` folder and its sub-folders
|
||||||
|
|
||||||
## Instructions on Extracting In-Game Assets
|
## Instructions on Extracting In-Game Assets
|
||||||
I'm still struggling to find a command-line tool to extract in-game assets. But [AssetRipper](https://github.com/AssetRipper/AssetRipper) seems to have a command-line interface, I'm too lazy to have a deeper inverstigation.
|
I'm still struggling to find a command-line tool to extract in-game assets. But [AssetRipper](https://github.com/AssetRipper/AssetRipper) seems to have a command-line interface, I'm too lazy to have a deeper inverstigation.
|
||||||
|
|||||||
309
aklive2d.js
309
aklive2d.js
@@ -1,309 +0,0 @@
|
|||||||
/* eslint-disable no-fallthrough */
|
|
||||||
/* eslint-disable no-undef */
|
|
||||||
import assert from 'assert'
|
|
||||||
import path from 'path'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import { fork } from 'child_process';
|
|
||||||
import getConfig from './libs/config.js'
|
|
||||||
import ProjectJson from './libs/project_json.js'
|
|
||||||
import EnvGenerator from './libs/env_generator.js'
|
|
||||||
import { write, rmdir, copy, writeSync, copyDir, readdirSync, exists, mkdir } from './libs/file.js'
|
|
||||||
import AssetsProcessor from './libs/assets_processor.js'
|
|
||||||
import init from './libs/initializer.js'
|
|
||||||
import directory from './libs/directory.js'
|
|
||||||
import Background from './libs/background.js'
|
|
||||||
import CharwordTable from './libs/charword_table.js';
|
|
||||||
import Music from './libs/music.js';
|
|
||||||
import OfficalInfo from './libs/offical_info.js';
|
|
||||||
import CFPages from './libs/cf_pages.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
global.__projectRoot = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
const officalInfo = new OfficalInfo()
|
|
||||||
global.__config = getConfig(officalInfo)
|
|
||||||
global.__error = []
|
|
||||||
|
|
||||||
const OPERATOR_SOURCE_FOLDER = path.join(__projectRoot, __config.folder.operator)
|
|
||||||
const OPERATOR_SOURCE_DATA_FOLDER = path.join(__projectRoot, __config.folder.operator_data)
|
|
||||||
const OPERATOR_SHARE_FOLDER = path.join(OPERATOR_SOURCE_DATA_FOLDER, __config.folder.share)
|
|
||||||
|
|
||||||
const op = process.argv[2]
|
|
||||||
let OPERATOR_NAMES = process.argv.slice(3);
|
|
||||||
|
|
||||||
const charwordTable = new CharwordTable()
|
|
||||||
const musicTable = new Music()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip all, no need for OPERATOR_NAME
|
|
||||||
* build-all: build all assets
|
|
||||||
* directory: build directory
|
|
||||||
*/
|
|
||||||
switch (op) {
|
|
||||||
case 'operator:build-all':
|
|
||||||
for (const [key,] of Object.entries(__config.operators)) {
|
|
||||||
OPERATOR_NAMES.push(key)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'operator:preview':
|
|
||||||
assert(OPERATOR_NAMES.length !== 0, 'Please set the operator name.')
|
|
||||||
fork(path.join(__projectRoot, 'vite.config.js'), [op, OPERATOR_NAMES])
|
|
||||||
return
|
|
||||||
case 'charwords:update':
|
|
||||||
await charwordTable.process()
|
|
||||||
process.exit(0)
|
|
||||||
case 'music':
|
|
||||||
await musicTable.process()
|
|
||||||
process.exit(0)
|
|
||||||
case 'offical_update':
|
|
||||||
await officalInfo.update()
|
|
||||||
process.exit(0)
|
|
||||||
case 'cf:upload':
|
|
||||||
await (new CFPages()).upload()
|
|
||||||
process.exit(0)
|
|
||||||
case 'cf:download':
|
|
||||||
await (new CFPages()).download()
|
|
||||||
process.exit(0)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(OPERATOR_NAMES.length !== 0, 'Please set the operator name.')
|
|
||||||
|
|
||||||
const background = new Background(OPERATOR_SHARE_FOLDER, OPERATOR_SOURCE_FOLDER)
|
|
||||||
await background.process()
|
|
||||||
const backgrounds = ['operator_bg.png', ...background.files]
|
|
||||||
const { musicToCopy, musicMapping } = musicTable.copy(OPERATOR_SHARE_FOLDER)
|
|
||||||
|
|
||||||
for (const e of musicToCopy) {
|
|
||||||
const musicPath = path.join(e.source, e.filename)
|
|
||||||
if (!exists(musicPath)) {
|
|
||||||
__error.push(`Music file ${e.filename} is not found in music folder.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const e of Object.keys(musicMapping)) {
|
|
||||||
if (!backgrounds.includes(e)) {
|
|
||||||
__error.push(`Background file ${e} is not found in background folder.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const OPERATOR_NAME of OPERATOR_NAMES) {
|
|
||||||
const OPERATOR_RELEASE_FOLDER = path.join(__projectRoot, __config.folder.release, OPERATOR_NAME)
|
|
||||||
const SHOWCASE_PUBLIC_ASSSETS_FOLDER = path.join(OPERATOR_RELEASE_FOLDER, "assets")
|
|
||||||
const EXTRACTED_FOLDER = path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, 'extracted')
|
|
||||||
const VOICE_FOLDERS = __config.folder.voice.sub.map((sub) => path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, __config.folder.voice.main, sub.name))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip assets generation part
|
|
||||||
* init: init folder and config for an operator
|
|
||||||
* readme: append a new line to README.md
|
|
||||||
*/
|
|
||||||
switch (op) {
|
|
||||||
case 'init':
|
|
||||||
init(OPERATOR_NAME, [EXTRACTED_FOLDER, ...VOICE_FOLDERS], officalInfo)
|
|
||||||
process.exit(0)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
rmdir(OPERATOR_RELEASE_FOLDER)
|
|
||||||
mkdir(path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME))
|
|
||||||
|
|
||||||
const charwordTableLookup = charwordTable.lookup(OPERATOR_NAME)
|
|
||||||
const voiceJson = {}
|
|
||||||
voiceJson.config = {
|
|
||||||
default_region: charwordTableLookup.config.default_region.replace("_", "-"),
|
|
||||||
regions: charwordTableLookup.config.regions.map((item) => item.replace("_", "-")),
|
|
||||||
}
|
|
||||||
voiceJson.voiceLangs = {}
|
|
||||||
voiceJson.subtitleLangs = {}
|
|
||||||
const subtitleInfo = Object.keys(charwordTableLookup.operator.info)
|
|
||||||
subtitleInfo.forEach((item) => {
|
|
||||||
if (Object.keys(charwordTableLookup.operator.info[item]).length > 0) {
|
|
||||||
const key = item.replace("_", "-")
|
|
||||||
voiceJson.subtitleLangs[key] = {}
|
|
||||||
for (const [id, subtitles] of Object.entries(charwordTableLookup.operator.voice[item])) {
|
|
||||||
const match = id.replace(/(.+?)([A-Z]\w+)/, '$2')
|
|
||||||
if (match === id) {
|
|
||||||
voiceJson.subtitleLangs[key].default = subtitles
|
|
||||||
} else {
|
|
||||||
voiceJson.subtitleLangs[key][match] = subtitles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
voiceJson.voiceLangs[key] = {}
|
|
||||||
Object.values(charwordTableLookup.operator.info[item]).forEach((item) => {
|
|
||||||
voiceJson.voiceLangs[key] = { ...voiceJson.voiceLangs[key], ...item }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let voiceLangs = [], subtitleLangs = [];
|
|
||||||
try {
|
|
||||||
voiceLangs = Object.keys(voiceJson.voiceLangs["zh-CN"])
|
|
||||||
subtitleLangs = Object.keys(voiceJson.subtitleLangs)
|
|
||||||
|
|
||||||
writeSync(JSON.stringify(voiceJson), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, 'charword_table.json'))
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`charword_table is not available`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check whether voice files has been added
|
|
||||||
const customVoiceName = voiceLangs.filter(i => !__config.folder.voice.sub.map(e => e.lang).includes(i))[0]
|
|
||||||
const voiceLangMapping = __config.folder.voice.sub.filter(e => {
|
|
||||||
return voiceLangs.includes(e.lang) || (e.lang === "CUSTOM" && typeof customVoiceName !== 'undefined')
|
|
||||||
}).map(e => {
|
|
||||||
return {
|
|
||||||
name: e.name,
|
|
||||||
lang: e.lang === "CUSTOM" ? customVoiceName : e.lang
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (const voiceSubFolderMapping of voiceLangMapping) {
|
|
||||||
const voiceSubFolder = path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, __config.folder.voice.main, voiceSubFolderMapping.name)
|
|
||||||
if (readdirSync(voiceSubFolder).length === 0) {
|
|
||||||
__error.push(`Voice folder ${voiceSubFolderMapping.name} for ${OPERATOR_NAME} is empty.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const envPath = path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, '.env')
|
|
||||||
writeSync((new EnvGenerator()).generate([
|
|
||||||
{
|
|
||||||
key: "insight_id",
|
|
||||||
value: __config.insight_id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "link",
|
|
||||||
value: __config.operators[OPERATOR_NAME].link
|
|
||||||
}, {
|
|
||||||
key: "title",
|
|
||||||
value: __config.operators[OPERATOR_NAME].title
|
|
||||||
}, {
|
|
||||||
key: "filename",
|
|
||||||
value: __config.operators[OPERATOR_NAME].filename.replace('#', '%23')
|
|
||||||
}, {
|
|
||||||
key: "logo_filename",
|
|
||||||
value: __config.operators[OPERATOR_NAME].logo
|
|
||||||
}, {
|
|
||||||
key: "fallback_filename",
|
|
||||||
value: __config.operators[OPERATOR_NAME].fallback_name.replace('#', '%23')
|
|
||||||
}, {
|
|
||||||
key: "viewport_left",
|
|
||||||
value: __config.operators[OPERATOR_NAME].viewport_left
|
|
||||||
}, {
|
|
||||||
key: "viewport_right",
|
|
||||||
value: __config.operators[OPERATOR_NAME].viewport_right
|
|
||||||
}, {
|
|
||||||
key: "viewport_top",
|
|
||||||
value: __config.operators[OPERATOR_NAME].viewport_top
|
|
||||||
}, {
|
|
||||||
key: "viewport_bottom",
|
|
||||||
value: __config.operators[OPERATOR_NAME].viewport_bottom
|
|
||||||
}, {
|
|
||||||
key: "invert_filter",
|
|
||||||
value: __config.operators[OPERATOR_NAME].invert_filter
|
|
||||||
}, {
|
|
||||||
key: "image_width",
|
|
||||||
value: 2048
|
|
||||||
}, {
|
|
||||||
key: "image_height",
|
|
||||||
value: 2048
|
|
||||||
}, {
|
|
||||||
key: "background_files",
|
|
||||||
value: JSON.stringify(backgrounds)
|
|
||||||
}, {
|
|
||||||
key: "background_folder",
|
|
||||||
value: __config.folder.background
|
|
||||||
}, {
|
|
||||||
key: "voice_folders",
|
|
||||||
value: JSON.stringify(__config.folder.voice)
|
|
||||||
}, {
|
|
||||||
key: "music_folder",
|
|
||||||
value: __config.folder.music
|
|
||||||
}, {
|
|
||||||
key: "music_mapping",
|
|
||||||
value: JSON.stringify(musicMapping)
|
|
||||||
}, {
|
|
||||||
key: "use_json",
|
|
||||||
value: __config.operators[OPERATOR_NAME].use_json
|
|
||||||
}
|
|
||||||
]), envPath)
|
|
||||||
|
|
||||||
const projectJson = new ProjectJson(OPERATOR_NAME, OPERATOR_SHARE_FOLDER, {
|
|
||||||
backgrounds,
|
|
||||||
voiceLangs,
|
|
||||||
subtitleLangs,
|
|
||||||
music: Object.keys(musicMapping)
|
|
||||||
})
|
|
||||||
projectJson.load().then((content) => {
|
|
||||||
write(JSON.stringify(content, null, 2), path.join(OPERATOR_RELEASE_FOLDER, 'project.json'))
|
|
||||||
})
|
|
||||||
|
|
||||||
const assetsProcessor = new AssetsProcessor(OPERATOR_NAME, OPERATOR_SHARE_FOLDER)
|
|
||||||
const assetContent = await assetsProcessor.process(EXTRACTED_FOLDER)
|
|
||||||
write(JSON.stringify(assetContent.assetsJson, null), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, `assets.json`))
|
|
||||||
|
|
||||||
// copy remaining files
|
|
||||||
const filesToCopy = [
|
|
||||||
...background.getFilesToCopy(SHOWCASE_PUBLIC_ASSSETS_FOLDER),
|
|
||||||
...musicToCopy.map(entry => {
|
|
||||||
return {
|
|
||||||
...entry,
|
|
||||||
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER, __config.folder.music)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
filename: 'preview.jpg',
|
|
||||||
source: path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME),
|
|
||||||
target: path.join(OPERATOR_RELEASE_FOLDER)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'operator_bg.png',
|
|
||||||
source: path.join(OPERATOR_SHARE_FOLDER, __config.folder.background),
|
|
||||||
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER, __config.folder.background)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: `${__config.operators[OPERATOR_NAME].logo}.png`,
|
|
||||||
source: path.join(OPERATOR_SHARE_FOLDER, 'logo'),
|
|
||||||
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: `${__config.operators[OPERATOR_NAME].fallback_name}.png`,
|
|
||||||
source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME),
|
|
||||||
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: `${__config.operators[OPERATOR_NAME].fallback_name}_portrait.png`,
|
|
||||||
source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME),
|
|
||||||
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
filesToCopy.forEach((file) => {
|
|
||||||
copy(path.join(file.source, file.filename), path.join(file.target, file.filename))
|
|
||||||
})
|
|
||||||
|
|
||||||
const foldersToCopy = [
|
|
||||||
{
|
|
||||||
source: path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, __config.folder.voice.main),
|
|
||||||
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER, __config.folder.voice.main)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
foldersToCopy.forEach((folder) => {
|
|
||||||
copyDir(folder.source, folder.target)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
switch (op) {
|
|
||||||
case op.startsWith('directory'):
|
|
||||||
directory(OPERATOR_SOURCE_DATA_FOLDER, { backgrounds, musicMapping })
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (__error.length > 0) {
|
|
||||||
const str = `${__error.length} error${__error.length > 1 ? 's were' : ' was'} found:\n${__error.join('\n')}`
|
|
||||||
throw new Error(str)
|
|
||||||
} else {
|
|
||||||
for (const OPERATOR_NAME of OPERATOR_NAMES) {
|
|
||||||
fork(path.join(__projectRoot, 'vite.config.js'), [op, OPERATOR_NAME])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
data
|
||||||
3
apps/directory/.prettierignore
Normal file
3
apps/directory/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
data
|
||||||
|
auto_update
|
||||||
30
apps/directory/eslint.config.js
Normal file
30
apps/directory/eslint.config.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import baseConfig from '@aklive2d/eslint-config'
|
||||||
|
|
||||||
|
/** @type {import('eslint').Config} */
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react/jsx-no-target-blank': 'off',
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
86
apps/directory/index.html
Normal file
86
apps/directory/index.html
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>%VITE_APP_TITLE%</title>
|
||||||
|
<script
|
||||||
|
id="counterscale-script"
|
||||||
|
src="%VITE_INSIGHT_URL%"
|
||||||
|
defer
|
||||||
|
></script>
|
||||||
|
<style>
|
||||||
|
.loader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader.loaded {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader .icon,
|
||||||
|
.loader .flasher {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-left: 0.3rem solid;
|
||||||
|
border-bottom: 0.3rem solid;
|
||||||
|
border-right: 0.3rem solid;
|
||||||
|
border-top: 0.3rem solid;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 16px 2px rgb(200 14 0),
|
||||||
|
0 0 18px 5px rgb(200 14 0);
|
||||||
|
border-color: rgba(54, 0, 0, 87%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader .flasher {
|
||||||
|
position: absolute;
|
||||||
|
box-shadow: unset;
|
||||||
|
animation: icon-flash 1.2s cubic-bezier(0.2, 0.6, 0.2, 1)
|
||||||
|
infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.loader {
|
||||||
|
background-color: #131313;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.loader {
|
||||||
|
background-color: #ececec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes icon-flash {
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
border-color: rgb(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<div class="loader">
|
||||||
|
<span class="icon"></span>
|
||||||
|
<span class="flasher"></span>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/App.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
apps/directory/jsconfig.json
Normal file
9
apps/directory/jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"!/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
apps/directory/package.json
Normal file
43
apps/directory/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@aklive2d/directory",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev:directory": "vite --clearScreen false",
|
||||||
|
"build": "mode=build node runner.js",
|
||||||
|
"preview:directory": "vite preview",
|
||||||
|
"lint": "eslint \"src/**/*.js\" \"src/**/*.jsx\" && stylelint \"src/**/*.css\" \"src/**/*.scss\" && prettier --check ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"jotai": "^2.11.3",
|
||||||
|
"react-router-dom": "^7.1.5",
|
||||||
|
"react-simple-typewriter": "^5.0.1",
|
||||||
|
"reset-css": "^5.0.2",
|
||||||
|
"@aklive2d/eslint-config": "workspace:*",
|
||||||
|
"@aklive2d/stylelint-config": "workspace:*",
|
||||||
|
"@aklive2d/postcss-config": "workspace:*",
|
||||||
|
"@aklive2d/config": "workspace:*",
|
||||||
|
"@aklive2d/libs": "workspace:*",
|
||||||
|
"@aklive2d/assets": "workspace:*",
|
||||||
|
"@aklive2d/operator": "workspace:*",
|
||||||
|
"@aklive2d/vite-helpers": "workspace:*",
|
||||||
|
"@aklive2d/showcase": "workspace:*",
|
||||||
|
"@aklive2d/module": "workspace:*",
|
||||||
|
"@aklive2d/prettier-config": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.8",
|
||||||
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"vite": "^6.1.0",
|
||||||
|
"eslint-plugin-react": "^7.37.4",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.18",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"sass": "^1.84.0",
|
||||||
|
"autoprefixer": "^10.4.20"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
apps/directory/postcss.config.js
Normal file
5
apps/directory/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import baseConfig from '@aklive2d/postcss-config'
|
||||||
|
/** @type {import('postcss').Config} */
|
||||||
|
export default {
|
||||||
|
...baseConfig,
|
||||||
|
}
|
||||||
11
apps/directory/prettier.config.js
Normal file
11
apps/directory/prettier.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import baseConfig from '@aklive2d/prettier-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("prettier").Config}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
...baseConfig,
|
||||||
|
semi: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
24
apps/directory/runner.js
Normal file
24
apps/directory/runner.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { build as viteBuild } from 'vite'
|
||||||
|
import { envParser } from '@aklive2d/libs'
|
||||||
|
|
||||||
|
const build = async (namesToBuild) => {
|
||||||
|
if (!namesToBuild.length) {
|
||||||
|
// skip as directory can only build
|
||||||
|
// when all operators are built
|
||||||
|
await viteBuild()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { name } = envParser.parse({
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'n',
|
||||||
|
multiple: true,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await build(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
23
apps/directory/src/App.jsx
Normal file
23
apps/directory/src/App.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||||
|
import Root from '@/routes/Root'
|
||||||
|
import Error from '@/routes/Error'
|
||||||
|
import routes from '@/routes'
|
||||||
|
import '@/App.scss'
|
||||||
|
import 'reset-css'
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <Root />,
|
||||||
|
errorElement: <Error />,
|
||||||
|
children: routes.filter((item) => item.routeable),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
103
apps/directory/src/App.scss
Normal file
103
apps/directory/src/App.scss
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@import 'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans:wght@400;500;700&display=swap';
|
||||||
|
@import 'https://fonts.cdnfonts.com/css/bender';
|
||||||
|
@import 'https://fonts.cdnfonts.com/css/geometos';
|
||||||
|
|
||||||
|
@mixin light-theme() {
|
||||||
|
--text-color: rgb(0 0 0 / 87%);
|
||||||
|
--text-color-full: #000;
|
||||||
|
--secondary-text-color: #97958d;
|
||||||
|
--date-color: rgb(0 0 0 / 20%);
|
||||||
|
--border-color: #8f8f8f;
|
||||||
|
--link-highlight-color: #33b5e5;
|
||||||
|
--drawer-background-color: rgb(255 255 255 / 88%);
|
||||||
|
--root-background-color: #ececec;
|
||||||
|
--home-item-hover-background-color: rgb(188 188 188 / 30%);
|
||||||
|
--home-item-background-linear-gradient-color: rgb(0 0 0 / 10%);
|
||||||
|
--home-item-outline-color: rgb(41 41 41 / 30%);
|
||||||
|
--button-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark-theme() {
|
||||||
|
--text-color: rgb(255 255 255 / 87%);
|
||||||
|
--text-color-full: #fff;
|
||||||
|
--secondary-text-color: #686a72;
|
||||||
|
--date-color: rgb(255 255 255 / 20%);
|
||||||
|
--border-color: #707070;
|
||||||
|
--link-highlight-color: #33b5e5;
|
||||||
|
--drawer-background-color: rgb(0 0 0 / 88%);
|
||||||
|
--root-background-color: #131313;
|
||||||
|
--home-item-hover-background-color: rgb(67 67 67 / 30%);
|
||||||
|
--home-item-background-linear-gradient-color: rgb(255 255 255 / 10%);
|
||||||
|
--home-item-outline-color: rgb(214 214 214 / 30%);
|
||||||
|
--button-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
@include dark-theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
@include light-theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family:
|
||||||
|
Geometos, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.2em;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--root-background-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (width <= 1600px) {
|
||||||
|
& {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border: 5px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root::-webkit-scrollbar-thumb:hover {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
9
apps/directory/src/component/border.jsx
Normal file
9
apps/directory/src/component/border.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classes from './scss/border.module.scss'
|
||||||
|
|
||||||
|
export default function Border(props) {
|
||||||
|
return <section className={classes.border}>{props.children}</section>
|
||||||
|
}
|
||||||
|
Border.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
}
|
||||||
28
apps/directory/src/component/char_icon.jsx
Normal file
28
apps/directory/src/component/char_icon.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function CharIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
style={props.style}
|
||||||
|
>
|
||||||
|
{props.type === 'operator' ? (
|
||||||
|
<g>
|
||||||
|
<path d="M89 17.5 30.4 57 24.3 71.4 82.9 32.6Z"></path>
|
||||||
|
<path d="M0 17.5 58.6 57 64.7 71.4 6.1 32.7Z"> </path>
|
||||||
|
<path d="M89 0 30.4 39.5 24.3 53.9 82.9 15.1Z"> </path>
|
||||||
|
<path d="M0 0 58.6 39.5 64.7 53.9 6.1 15.2Z"> </path>
|
||||||
|
</g>
|
||||||
|
) : (
|
||||||
|
<path d="M90.4 50.6l-39.8-23.5v-4c0-4.5-5-6.5-5-6.5a5.4 5.4 0 012.2-10.1c2.7 0 5.3 1.5 5.5 4.8.4 5.3 6.4 3.9 6.4-.3a11.7 11.7 0 00-12-11c-9 0-11.6 8.8-11.6 11.6a11.5 11.5 0 001.6 6.2c2.2 3.8 6.6 4.3 6.6 6.8v2.5L4.2 50.7c-4 2.3-4.7 7.3-3.8 10.3a9.1 9.1 0 009.1 6.4h75.2c5.9 0 8.6-3.4 9.5-6.3C95 58.1 95 53.4 90.4 50.6Zm-5.6 10.3h-75.2c-2.4.1-4-3.3-1.5-4.8l39.2-22.9 39 22.8A2.7 2.7 0 0184.7 60.8Z" />
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CharIcon.propTypes = {
|
||||||
|
viewBox: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
}
|
||||||
99
apps/directory/src/component/dropdown.jsx
Normal file
99
apps/directory/src/component/dropdown.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classes from './scss/dropdown.module.scss'
|
||||||
|
|
||||||
|
export default function Dropdown(props) {
|
||||||
|
const [hidden, setHidden] = useState(true)
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
setHidden(!hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section
|
||||||
|
className={`${classes.dropdown} ${hidden ? '' : classes.active} ${props.className ? props.className : ''} ${props.left ? classes.left : ''}`}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={classes.text}
|
||||||
|
onClick={() => toggleDropdown()}
|
||||||
|
>
|
||||||
|
<span className={classes.content}>{props.text}</span>
|
||||||
|
<span
|
||||||
|
className={classes.icon}
|
||||||
|
style={props.iconStyle}
|
||||||
|
></span>
|
||||||
|
<section className={classes.popup}>
|
||||||
|
<span className={classes.text}>{props.altText}</span>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<ul className={classes.menu} style={props.activeColor}>
|
||||||
|
{props.menu.map((item) => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'date': {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
key={item.name}
|
||||||
|
className={classes.date}
|
||||||
|
>
|
||||||
|
<section className={classes.line} />
|
||||||
|
<section className={classes.text}>
|
||||||
|
{item.name}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'custom': {
|
||||||
|
return item.component
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.name}
|
||||||
|
className={`${classes.item} ${item.name === props.text || (props.activeRule && props.activeRule(item)) ? classes.active : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
props.onClick(item)
|
||||||
|
toggleDropdown()
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
item.color
|
||||||
|
? { color: item.color }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.icon ? (
|
||||||
|
<section
|
||||||
|
className={classes['item-icon']}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
<section className={classes.text}>
|
||||||
|
{item.name}
|
||||||
|
</section>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<section
|
||||||
|
className={classes.overlay}
|
||||||
|
hidden={hidden}
|
||||||
|
onClick={() => toggleDropdown()}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Dropdown.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
text: PropTypes.string,
|
||||||
|
menu: PropTypes.array,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
activeColor: PropTypes.object,
|
||||||
|
activeRule: PropTypes.func,
|
||||||
|
altText: PropTypes.string,
|
||||||
|
iconStyle: PropTypes.object,
|
||||||
|
left: PropTypes.bool,
|
||||||
|
}
|
||||||
48
apps/directory/src/component/popup.jsx
Normal file
48
apps/directory/src/component/popup.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import classes from './scss/popup.module.scss'
|
||||||
|
import ReturnButton from '@/component/return_button'
|
||||||
|
import Border from '@/component/border'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function Popup(props) {
|
||||||
|
const [hidden, setHidden] = useState(true)
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
setHidden(!hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section
|
||||||
|
className={`${classes.popup} ${hidden ? '' : classes.active}`}
|
||||||
|
>
|
||||||
|
<section className={classes.wrapper}>
|
||||||
|
<section className={classes.title}>
|
||||||
|
<section className={classes.text}>
|
||||||
|
{props.title}
|
||||||
|
</section>
|
||||||
|
<ReturnButton
|
||||||
|
onClick={toggle}
|
||||||
|
className={classes['return-button']}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<Border />
|
||||||
|
<section className={classes.content}>
|
||||||
|
{props.children}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={`${classes.overlay} ${hidden ? '' : classes.active}`}
|
||||||
|
onClick={() => toggle()}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<span className={classes['entry-text']} onClick={toggle}>
|
||||||
|
{props.title}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Popup.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
}
|
||||||
38
apps/directory/src/component/return_button.jsx
Normal file
38
apps/directory/src/component/return_button.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classes from './scss/return_button.module.scss'
|
||||||
|
|
||||||
|
export default function ReturnButton(props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section
|
||||||
|
className={`${classes['return-button']} ${props.className ? props.className : ''}`}
|
||||||
|
onClick={() => props.onClick()}
|
||||||
|
>
|
||||||
|
<section className={classes.wrapper}>
|
||||||
|
<section className={classes['arrow-left']}></section>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
<section className={classes['arrow-right']}></section>
|
||||||
|
</section>
|
||||||
|
<section className={classes.wrapper}>
|
||||||
|
<section className={classes['arrow-left']}></section>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
<section className={classes['arrow-right']}></section>
|
||||||
|
</section>
|
||||||
|
<section className={classes.wrapper}>
|
||||||
|
<section className={classes['arrow-left']}></section>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
<section className={classes['arrow-right']}></section>
|
||||||
|
</section>
|
||||||
|
<section className={classes.wrapper}>
|
||||||
|
<section className={classes['arrow-left']}></section>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
<section className={classes['arrow-right']}></section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ReturnButton.propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
|
}
|
||||||
30
apps/directory/src/component/scss/border.module.scss
Normal file
30
apps/directory/src/component/scss/border.module.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.border {
|
||||||
|
position: relative;
|
||||||
|
bottom: 1px;
|
||||||
|
border-bottom: 1px solid var(--text-color);
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
top: -2px;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
187
apps/directory/src/component/scss/dropdown.module.scss
Normal file
187
apps/directory/src/component/scss/dropdown.module.scss
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
height: 2em;
|
||||||
|
min-width: 2em;
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--root-background-color);
|
||||||
|
width: max-content;
|
||||||
|
height: max-content;
|
||||||
|
max-height: 61.8vh;
|
||||||
|
max-width: 61.8vw;
|
||||||
|
z-index: -1;
|
||||||
|
top: 2.5em;
|
||||||
|
right: 0;
|
||||||
|
cursor: auto;
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.popup {
|
||||||
|
visibility: unset;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-right: 1.2em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.5em;
|
||||||
|
right: 0.6em;
|
||||||
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-left: 0.15em solid var(--text-color);
|
||||||
|
border-bottom: 0.15em solid var(--text-color);
|
||||||
|
border-right: 0.15em solid var(--text-color);
|
||||||
|
border-top: 0.15em solid var(--text-color);
|
||||||
|
transform: translateY(-0.7em) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--root-background-color);
|
||||||
|
width: max-content;
|
||||||
|
max-height: 61.8vh;
|
||||||
|
max-width: 61.8vw;
|
||||||
|
z-index: -1;
|
||||||
|
top: 2.5em;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
visibility: hidden;
|
||||||
|
color: var(--link-highlight-color);
|
||||||
|
cursor: auto;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
left: 0;
|
||||||
|
right: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-family:
|
||||||
|
Bender, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR',
|
||||||
|
'Noto Sans', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.line {
|
||||||
|
height: 1px;
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon svg {
|
||||||
|
transition: fill cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
width: 1rem;
|
||||||
|
fill: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&.active {
|
||||||
|
.text {
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon svg {
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
/* stylelint-disable no-descending-specificity */
|
||||||
|
.popup,
|
||||||
|
.menu {
|
||||||
|
left: 0;
|
||||||
|
right: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
z-index: -1;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
.icon {
|
||||||
|
animation: icon-flash 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.menu {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes icon-flash {
|
||||||
|
50% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
apps/directory/src/component/scss/popup.module.scss
Normal file
96
apps/directory/src/component/scss/popup.module.scss
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
.entry-text {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
border: unset;
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
align-items: stretch;
|
||||||
|
max-width: 480px;
|
||||||
|
height: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: var(--root-background-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
place-content: center space-between;
|
||||||
|
align-items: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family:
|
||||||
|
Geometos, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR',
|
||||||
|
'Noto Sans', sans-serif;
|
||||||
|
|
||||||
|
.return-button {
|
||||||
|
color: var(--button-color);
|
||||||
|
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
line-height: 1.3em;
|
||||||
|
padding: 1rem 1rem 0;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
background-color: var(--root-background-color);
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 0.5;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 768px) {
|
||||||
|
.title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.return-button {
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
apps/directory/src/component/scss/return_button.module.scss
Normal file
55
apps/directory/src/component/scss/return_button.module.scss
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.return-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
width: 3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
%arrow-shared {
|
||||||
|
border-top: 0.24rem solid transparent;
|
||||||
|
border-bottom: 0.24rem solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
transform: translateY(-0.1rem) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
transform: translateY(-0.1rem) translate(90%, -100%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
transform: translateY(-0.1rem) translateY(150%) rotate(315deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
transform: translateY(-0.1rem) translate(90%, 50%) rotate(225deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
width: 1rem;
|
||||||
|
height: 0.4rem;
|
||||||
|
background-color: currentcolor;
|
||||||
|
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-left {
|
||||||
|
@extend %arrow-shared;
|
||||||
|
|
||||||
|
border-right: 0.3rem solid currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-right {
|
||||||
|
@extend %arrow-shared;
|
||||||
|
|
||||||
|
border-left: 0.3rem solid currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
apps/directory/src/component/scss/search_box.module.scss
Normal file
78
apps/directory/src/component/scss/search_box.module.scss
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 0.5rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 0.8rem;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-left: 0.15rem solid var(--text-color-full);
|
||||||
|
border-bottom: 0.15rem solid var(--text-color-full);
|
||||||
|
border-right: 0.15rem solid var(--text-color-full);
|
||||||
|
border-top: 0.15rem solid var(--text-color-full);
|
||||||
|
transform: translate(0.2rem, 0.3rem) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dot {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--text-color-full);
|
||||||
|
width: 0.15rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
transform: translate(1.2rem, 1.1rem) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
background-color: transparent;
|
||||||
|
border: unset;
|
||||||
|
border-bottom: 0.15em solid var(--home-item-outline-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus,
|
||||||
|
.input:hover {
|
||||||
|
outline: none;
|
||||||
|
border-bottom: 0.15em solid var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
position: absolute;
|
||||||
|
width: 2rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
transform: translate(0, 0.8rem) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
transform: translate(0, 0.8rem) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
apps/directory/src/component/scss/switch.module.scss
Normal file
64
apps/directory/src/component/scss/switch.module.scss
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 8px 36px 8px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.65em;
|
||||||
|
right: 18px;
|
||||||
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-left: 0.15em solid currentcolor;
|
||||||
|
border-bottom: 0.15em solid currentcolor;
|
||||||
|
border-right: 0.15em solid currentcolor;
|
||||||
|
border-top: 0.15em solid currentcolor;
|
||||||
|
transform: translate(0, -0.15em) rotate(-45deg);
|
||||||
|
transition:
|
||||||
|
right cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
|
||||||
|
background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1.1em;
|
||||||
|
right: 6px;
|
||||||
|
width: 18px;
|
||||||
|
height: 0.15em;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color: currentcolor;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
color: var(--text-color-full);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
background-color: currentcolor;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
apps/directory/src/component/scss/totop_button.module.scss
Normal file
43
apps/directory/src/component/scss/totop_button.module.scss
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.totop-button {
|
||||||
|
position: fixed;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
right: 2rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.3rem;
|
||||||
|
background-color: var(--text-color-full);
|
||||||
|
will-change: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:nth-child(1) {
|
||||||
|
transform: rotateZ(45deg) scaleX(0.5) translateX(45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:nth-child(2) {
|
||||||
|
transform: rotateZ(-45deg) scaleX(0.5) translateX(-45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:nth-child(3),
|
||||||
|
.bar:nth-child(4) {
|
||||||
|
transform: translateY(450%) rotateZ(90deg) scaleX(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
apps/directory/src/component/search_box.jsx
Normal file
50
apps/directory/src/component/search_box.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classes from './scss/search_box.module.scss'
|
||||||
|
import { useI18n } from '@/state/language'
|
||||||
|
|
||||||
|
export default function SearchBox(props) {
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const [searchField, setSearchField] = useState('')
|
||||||
|
|
||||||
|
const filterBySearch = (event) => {
|
||||||
|
const query = event.target.value
|
||||||
|
props.handleOnChange(query)
|
||||||
|
setSearchField(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section
|
||||||
|
className={`${classes['search-box']} ${props.className ? props.className : ''}`}
|
||||||
|
>
|
||||||
|
<section className={classes.icon} />
|
||||||
|
<section className={classes['icon-dot']} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={classes.input}
|
||||||
|
placeholder={i18n(props.altText)}
|
||||||
|
onChange={filterBySearch}
|
||||||
|
value={searchField}
|
||||||
|
/>
|
||||||
|
<section
|
||||||
|
className={`${classes['icon-clear']} ${searchField === '' ? '' : classes.active}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSearchField('')
|
||||||
|
props.handleOnChange('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section className={classes.line} />
|
||||||
|
<section className={classes.line} />
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SearchBox.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
text: PropTypes.string,
|
||||||
|
altText: PropTypes.string,
|
||||||
|
handleOnChange: PropTypes.func,
|
||||||
|
searchField: PropTypes.string,
|
||||||
|
}
|
||||||
35
apps/directory/src/component/switch.jsx
Normal file
35
apps/directory/src/component/switch.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classes from './scss/switch.module.scss'
|
||||||
|
import { useI18n } from '@/state/language'
|
||||||
|
|
||||||
|
export default function Switch(props) {
|
||||||
|
const [on, setOn] = useState(props.on)
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOn(props.on)
|
||||||
|
}, [props.on])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`${classes.switch} ${on ? classes.active : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (props.handleOnClick) {
|
||||||
|
props.handleOnClick(!on)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={classes.text}>{i18n(props.text)}</span>
|
||||||
|
<section className={classes.wrapper}>
|
||||||
|
<span className={classes.line}></span>
|
||||||
|
<span className={classes.icon}></span>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch.propTypes = {
|
||||||
|
on: PropTypes.bool,
|
||||||
|
text: PropTypes.string,
|
||||||
|
handleOnClick: PropTypes.func,
|
||||||
|
}
|
||||||
64
apps/directory/src/component/totop_button.jsx
Normal file
64
apps/directory/src/component/totop_button.jsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classes from './scss/totop_button.module.scss'
|
||||||
|
|
||||||
|
export default function ToTopButton(props) {
|
||||||
|
const [hidden, setHidden] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleButton = () => {
|
||||||
|
const scrollBarPos = window.scrollY || 0
|
||||||
|
setHidden(!(scrollBarPos > 100))
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleButton)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleButton)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const smoothScroll = useCallback((target) => {
|
||||||
|
const targetElement = document.querySelector(target)
|
||||||
|
const targetPosition =
|
||||||
|
targetElement.getBoundingClientRect().top + window.scrollY
|
||||||
|
const startPosition = window.scrollY
|
||||||
|
const distance = targetPosition - startPosition
|
||||||
|
const duration = 1000
|
||||||
|
let start = null
|
||||||
|
window.requestAnimationFrame(step)
|
||||||
|
function step(timestamp) {
|
||||||
|
if (!start) start = timestamp
|
||||||
|
const progress = timestamp - start
|
||||||
|
window.scrollTo(
|
||||||
|
0,
|
||||||
|
easeInOutCubic(progress, startPosition, distance, duration)
|
||||||
|
)
|
||||||
|
if (progress < duration) window.requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
function easeInOutCubic(t, b, c, d) {
|
||||||
|
t /= d / 2
|
||||||
|
if (t < 1) return (c / 2) * t * t * t + b
|
||||||
|
t -= 2
|
||||||
|
return (c / 2) * (t * t * t + 2) + b
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section
|
||||||
|
className={`${classes['totop-button']} ${hidden ? '' : classes.show} ${props.className ? props.className : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
smoothScroll('#root')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
<section className={classes.bar}></section>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ToTopButton.propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
|
}
|
||||||
46
apps/directory/src/component/voice.jsx
Normal file
46
apps/directory/src/component/voice.jsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function VoiceElement({ src, replay, handleAduioStateChange }) {
|
||||||
|
const audioRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (src) {
|
||||||
|
audioRef.current.src = src
|
||||||
|
audioRef.current.play()
|
||||||
|
} else {
|
||||||
|
audioRef.current.pause()
|
||||||
|
}
|
||||||
|
}, [src])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (replay) {
|
||||||
|
audioRef.current.currentTime = 0
|
||||||
|
audioRef.current.play()
|
||||||
|
}
|
||||||
|
}, [replay])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<audio
|
||||||
|
ref={audioRef}
|
||||||
|
preload="auto"
|
||||||
|
autoPlay
|
||||||
|
onEnded={(e) => {
|
||||||
|
if (handleAduioStateChange) handleAduioStateChange(e, 'ended')
|
||||||
|
}}
|
||||||
|
onPlay={(e) => {
|
||||||
|
if (handleAduioStateChange) handleAduioStateChange(e, 'play')
|
||||||
|
}}
|
||||||
|
onPause={(e) => {
|
||||||
|
if (handleAduioStateChange) handleAduioStateChange(e, 'pause')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<source type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VoiceElement.propTypes = {
|
||||||
|
src: PropTypes.string,
|
||||||
|
handleAduioStateChange: PropTypes.func,
|
||||||
|
replay: PropTypes.bool,
|
||||||
|
}
|
||||||
161
apps/directory/src/i18n.json
Normal file
161
apps/directory/src/i18n.json
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"available": ["zh-CN", "en-US"],
|
||||||
|
"key": {
|
||||||
|
"dynamic_compile": {
|
||||||
|
"zh-CN": "动态集录",
|
||||||
|
"en-US": "Dynamic Compile"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"zh-CN": "首页",
|
||||||
|
"en-US": "Home"
|
||||||
|
},
|
||||||
|
"official_page": {
|
||||||
|
"zh-CN": "官方页面",
|
||||||
|
"en-US": "Official Page"
|
||||||
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"zh-CN": "免责声明",
|
||||||
|
"en-US": "Disclaimer"
|
||||||
|
},
|
||||||
|
"disclaimer_content": {
|
||||||
|
"zh-CN": "本网站由 Halyul 设立并为明日方舟社区服务,Halyul 声明本网站完全独立运营,与 上海鹰角网络科技有限公司, Esoteric Software LLC 或其任何关联实体并无任何联系。",
|
||||||
|
"en-US": "This website is set up and operated by Halyul for the benefit of the Arknights Community. Halyul hereby states that this website is dedicated, but not related to Hypergryph Co., Ltd, Esoteric Software LLC or any of its affiliated entity."
|
||||||
|
},
|
||||||
|
"privacy_policy": {
|
||||||
|
"zh-CN": "隐私政策",
|
||||||
|
"en-US": "Privacy Policy"
|
||||||
|
},
|
||||||
|
"contact_us": {
|
||||||
|
"zh-CN": "联系我们",
|
||||||
|
"en-US": "Contact Us"
|
||||||
|
},
|
||||||
|
"all": {
|
||||||
|
"zh-CN": "综合",
|
||||||
|
"en-US": "All"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"zh-CN": "精英2",
|
||||||
|
"en-US": "Elite 2"
|
||||||
|
},
|
||||||
|
"skin": {
|
||||||
|
"zh-CN": "时装",
|
||||||
|
"en-US": "Skin"
|
||||||
|
},
|
||||||
|
"voice": {
|
||||||
|
"zh-CN": "语音",
|
||||||
|
"en-US": "Voice"
|
||||||
|
},
|
||||||
|
"music": {
|
||||||
|
"zh-CN": "音乐",
|
||||||
|
"en-US": "Music"
|
||||||
|
},
|
||||||
|
"showcase": {
|
||||||
|
"zh-CN": "壁纸",
|
||||||
|
"en-US": "Wallpaper"
|
||||||
|
},
|
||||||
|
"directory": {
|
||||||
|
"zh-CN": "目录页",
|
||||||
|
"en-US": "Directory Page"
|
||||||
|
},
|
||||||
|
"animation": {
|
||||||
|
"zh-CN": "动画",
|
||||||
|
"en-US": "Animation"
|
||||||
|
},
|
||||||
|
"backgrounds": {
|
||||||
|
"zh-CN": "背景",
|
||||||
|
"en-US": "Backgrounds"
|
||||||
|
},
|
||||||
|
"CN_MANDARIN": {
|
||||||
|
"zh-CN": "普通话",
|
||||||
|
"en-US": "Mandarin"
|
||||||
|
},
|
||||||
|
"JP": {
|
||||||
|
"zh-CN": "日语",
|
||||||
|
"en-US": "Japanese"
|
||||||
|
},
|
||||||
|
"KR": {
|
||||||
|
"zh-CN": "韩语",
|
||||||
|
"en-US": "Korean"
|
||||||
|
},
|
||||||
|
"EN": {
|
||||||
|
"zh-CN": "英语",
|
||||||
|
"en-US": "English"
|
||||||
|
},
|
||||||
|
"ITA": {
|
||||||
|
"zh-CN": "意大利语",
|
||||||
|
"en-US": "Italian"
|
||||||
|
},
|
||||||
|
"CN_TOPOLECT": {
|
||||||
|
"zh-CN": "中文方言",
|
||||||
|
"en-US": "Chinese Topolect"
|
||||||
|
},
|
||||||
|
"steam_workshop": {
|
||||||
|
"zh-CN": "壁纸引擎版",
|
||||||
|
"en-US": "Wallpaper Engine Version"
|
||||||
|
},
|
||||||
|
"external_links": {
|
||||||
|
"zh-CN": "外部链接",
|
||||||
|
"en-US": "External Links"
|
||||||
|
},
|
||||||
|
"web_version": {
|
||||||
|
"zh-CN": "全功能网页版",
|
||||||
|
"en-US": "Full Feature Web Version"
|
||||||
|
},
|
||||||
|
"idle": {
|
||||||
|
"zh-CN": "待机",
|
||||||
|
"en-US": "Idle"
|
||||||
|
},
|
||||||
|
"interact": {
|
||||||
|
"zh-CN": "交互",
|
||||||
|
"en-US": "Interact"
|
||||||
|
},
|
||||||
|
"special": {
|
||||||
|
"zh-CN": "特殊",
|
||||||
|
"en-US": "Special"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"zh-CN": "字幕",
|
||||||
|
"en-US": "Subtitle"
|
||||||
|
},
|
||||||
|
"zh-CN": {
|
||||||
|
"zh-CN": "简体中文",
|
||||||
|
"en-US": "Chinese (Simplified)"
|
||||||
|
},
|
||||||
|
"en-US": {
|
||||||
|
"zh-CN": "英语",
|
||||||
|
"en-US": "English"
|
||||||
|
},
|
||||||
|
"zh-TW": {
|
||||||
|
"zh-CN": "繁体中文",
|
||||||
|
"en-US": "Chinese (Traditional)"
|
||||||
|
},
|
||||||
|
"ja-JP": {
|
||||||
|
"zh-CN": "日语",
|
||||||
|
"en-US": "Japanese"
|
||||||
|
},
|
||||||
|
"ko-KR": {
|
||||||
|
"zh-CN": "韩语",
|
||||||
|
"en-US": "Korean"
|
||||||
|
},
|
||||||
|
"switch_language": {
|
||||||
|
"zh-CN": "🌐 切换语言",
|
||||||
|
"en-US": "🌐 Switch Language"
|
||||||
|
},
|
||||||
|
"fast_navigation": {
|
||||||
|
"zh-CN": "🧭 快速导航",
|
||||||
|
"en-US": "🧭 Fast Navigation"
|
||||||
|
},
|
||||||
|
"return": {
|
||||||
|
"zh-CN": "↩️ 返回",
|
||||||
|
"en-US": "↩️ Return"
|
||||||
|
},
|
||||||
|
"search_by_name": {
|
||||||
|
"zh-CN": "名字搜索",
|
||||||
|
"en-US": "Search by Name"
|
||||||
|
},
|
||||||
|
"new_op_wait_to_update": {
|
||||||
|
"zh-CN": "个新干员等待更新",
|
||||||
|
"en-US": "New Operator(s) Waiting to Update"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
197
apps/directory/src/routes/Error.jsx
Normal file
197
apps/directory/src/routes/Error.jsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
||||||
|
import { useNavigate, useRouteError } from 'react-router-dom'
|
||||||
|
import header from '@/scss/root/header.module.scss'
|
||||||
|
import classes from '@/scss/error/Error.module.scss'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
import Switch from '@/component/switch'
|
||||||
|
import ReturnButton from '@/component/return_button'
|
||||||
|
import { Typewriter } from 'react-simple-typewriter'
|
||||||
|
import { useHeader } from '@/state/header'
|
||||||
|
import VoiceElement from '@/component/voice'
|
||||||
|
import { spine } from '@aklive2d/module'
|
||||||
|
import useInsight from '@/state/insight'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
const voiceOnAtom = atomWithStorage('voiceOn', false)
|
||||||
|
const config = buildConfig.error_files
|
||||||
|
const obj = config.files[Math.floor(Math.random() * config.files.length)]
|
||||||
|
const filename = obj.key.replace(/#/g, '%23')
|
||||||
|
const padding = obj.paddings
|
||||||
|
let lastVoiceState = 'ended'
|
||||||
|
|
||||||
|
export default function Error() {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const _trackEvt = useInsight()
|
||||||
|
const error = useRouteError()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { setTitle } = useHeader()
|
||||||
|
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
|
||||||
|
const [spineDone, _setSpineDone] = useState(false)
|
||||||
|
const spineRef = useRef(null)
|
||||||
|
const spineDoneRef = useRef(spineDone)
|
||||||
|
const voiceOnRef = useRef(voiceOn)
|
||||||
|
const [voiceSrc, setVoiceSrc] = useState(null)
|
||||||
|
const [voiceReplay, setVoiceReplay] = useState(false)
|
||||||
|
const [spinePlayer, setSpinePlayer] = useState(null)
|
||||||
|
|
||||||
|
const setSpineDone = (data) => {
|
||||||
|
spineDoneRef.current = data
|
||||||
|
_setSpineDone(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = useMemo(
|
||||||
|
() => [
|
||||||
|
'エラー発生。',
|
||||||
|
'发生错误。',
|
||||||
|
'Error occured.',
|
||||||
|
'에러 발생.',
|
||||||
|
'發生錯誤。',
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(error)
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle(content[0])
|
||||||
|
}, [content, setTitle])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!voiceOn) {
|
||||||
|
setVoiceSrc(null)
|
||||||
|
} else {
|
||||||
|
setVoiceSrc(`/${buildConfig.directory_folder}/error.ogg`)
|
||||||
|
if (spinePlayer) {
|
||||||
|
spinePlayer.animationState.setAnimation(0, 'Interact', false, 0)
|
||||||
|
spinePlayer.animationState.addAnimation(0, 'Relax', true, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [spinePlayer, voiceOn])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
voiceOnRef.current = voiceOn
|
||||||
|
}, [voiceOn])
|
||||||
|
|
||||||
|
const playVoice = useCallback(() => {
|
||||||
|
if (lastVoiceState === 'ended' && voiceSrc !== null) {
|
||||||
|
setVoiceReplay(true)
|
||||||
|
}
|
||||||
|
}, [voiceSrc])
|
||||||
|
|
||||||
|
const handleAduioStateChange = useCallback((e, state) => {
|
||||||
|
lastVoiceState = state
|
||||||
|
if (state === 'ended') {
|
||||||
|
setVoiceReplay(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (spineRef.current?.children.length === 0) {
|
||||||
|
setSpinePlayer(
|
||||||
|
new spine.SpinePlayer(spineRef.current, {
|
||||||
|
skelUrl: `./_assets/${filename}.skel`,
|
||||||
|
atlasUrl: `./_assets/${filename}.atlas`,
|
||||||
|
animation: 'Relax',
|
||||||
|
premultipliedAlpha: true,
|
||||||
|
alpha: true,
|
||||||
|
backgroundColor: '#00000000',
|
||||||
|
viewport: {
|
||||||
|
debugRender: false,
|
||||||
|
padLeft: `${padding.left}%`,
|
||||||
|
padRight: `${padding.right}%`,
|
||||||
|
padTop: `${padding.top}%`,
|
||||||
|
padBottom: `${padding.bottom}%`,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
showControls: false,
|
||||||
|
touch: false,
|
||||||
|
fps: 60,
|
||||||
|
defaultMix: 0.3,
|
||||||
|
success: (player) => {
|
||||||
|
let isPlayingInteract = false
|
||||||
|
player.animationState.addListener({
|
||||||
|
end: (e) => {
|
||||||
|
if (e.animation.name == 'Interact') {
|
||||||
|
isPlayingInteract = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setSpineDone(true)
|
||||||
|
const ani = () => {
|
||||||
|
if (isPlayingInteract) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isPlayingInteract = true
|
||||||
|
player.animationState.setAnimation(
|
||||||
|
0,
|
||||||
|
'Interact',
|
||||||
|
false,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
player.animationState.addAnimation(
|
||||||
|
0,
|
||||||
|
'Relax',
|
||||||
|
true,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
if (voiceOnRef.current) playVoice()
|
||||||
|
}
|
||||||
|
ani()
|
||||||
|
player.canvas.onclick = () => {
|
||||||
|
ani()
|
||||||
|
}
|
||||||
|
player.canvas.onmouseenter = () => {
|
||||||
|
ani()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (spinePlayer) {
|
||||||
|
spinePlayer.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [playVoice, spinePlayer])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={classes.error}>
|
||||||
|
<header className={`${header.header} ${classes.header}`}>
|
||||||
|
<ReturnButton onClick={() => navigate(-1, { replace: true })} />
|
||||||
|
<Switch
|
||||||
|
key="voice"
|
||||||
|
text="voice"
|
||||||
|
on={voiceOn}
|
||||||
|
handleOnClick={() => setVoiceOn(!voiceOn)}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<main className={classes.main}>
|
||||||
|
{content.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<section key={index} className={classes.content}>
|
||||||
|
<Typewriter
|
||||||
|
words={[item]}
|
||||||
|
cursor
|
||||||
|
cursorStyle="|"
|
||||||
|
typeSpeed={100}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<section
|
||||||
|
className={`${classes.spine} ${spineDone ? classes.active : ''}`}
|
||||||
|
ref={spineRef}
|
||||||
|
/>
|
||||||
|
<VoiceElement
|
||||||
|
src={voiceSrc}
|
||||||
|
replay={voiceReplay}
|
||||||
|
handleAduioStateChange={handleAduioStateChange}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
307
apps/directory/src/routes/Root.jsx
Normal file
307
apps/directory/src/routes/Root.jsx
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import {
|
||||||
|
Outlet,
|
||||||
|
Link,
|
||||||
|
NavLink,
|
||||||
|
useNavigate,
|
||||||
|
ScrollRestoration,
|
||||||
|
} from 'react-router-dom'
|
||||||
|
import classes from '@/scss/root/Root.module.scss'
|
||||||
|
import header from '@/scss/root/header.module.scss'
|
||||||
|
import footer from '@/scss/root/footer.module.scss'
|
||||||
|
import drawer from '@/scss/root/drawer.module.scss'
|
||||||
|
import routes from '@/routes'
|
||||||
|
import { useConfig } from '@/state/config'
|
||||||
|
import { useHeader } from '@/state/header'
|
||||||
|
import { useAppbar } from '@/state/appbar'
|
||||||
|
import { useI18n, useLanguage } from '@/state/language'
|
||||||
|
import Dropdown from '@/component/dropdown'
|
||||||
|
import Popup from '@/component/popup'
|
||||||
|
import Border from '@/component/border'
|
||||||
|
import CharIcon from '@/component/char_icon'
|
||||||
|
import ToTopButton from '@/component/totop_button'
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
export default function Root() {
|
||||||
|
const [drawerHidden, setDrawerHidden] = useState(true)
|
||||||
|
const { title, tabs, setCurrentTab, headerIcon } = useHeader()
|
||||||
|
const { extraArea } = useAppbar()
|
||||||
|
const { fetchOfficialUpdate } = useConfig()
|
||||||
|
|
||||||
|
const headerTabs = useMemo(() => {
|
||||||
|
return tabs?.map((item) => {
|
||||||
|
return <HeaderTabsElement key={item.key} item={item} />
|
||||||
|
})
|
||||||
|
}, [tabs])
|
||||||
|
|
||||||
|
const toggleDrawer = useCallback(
|
||||||
|
(value) => {
|
||||||
|
setDrawerHidden(value || !drawerHidden)
|
||||||
|
},
|
||||||
|
[drawerHidden]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabs.length > 0) {
|
||||||
|
setCurrentTab(tabs[0].key)
|
||||||
|
} else {
|
||||||
|
setCurrentTab(null)
|
||||||
|
}
|
||||||
|
}, [setCurrentTab, tabs])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchOfficialUpdate()
|
||||||
|
}, [fetchOfficialUpdate])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.querySelector('.loader').classList.add('loaded')
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelector('.loader').style.display = 'none'
|
||||||
|
}, 500)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className={header.header}>
|
||||||
|
<section
|
||||||
|
className={`${header['nav-button']} ${drawerHidden ? '' : header.active}`}
|
||||||
|
onClick={() => toggleDrawer()}
|
||||||
|
>
|
||||||
|
<section className={header.bar} />
|
||||||
|
<section className={header.bar} />
|
||||||
|
<section className={header.bar} />
|
||||||
|
</section>
|
||||||
|
<HeaderButton />
|
||||||
|
<section className={header.spacer} />
|
||||||
|
<section className={header['extra-area']}>
|
||||||
|
{extraArea}
|
||||||
|
<LanguageDropdown />
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
<nav
|
||||||
|
className={`${drawer.drawer} ${drawerHidden ? '' : drawer.active}`}
|
||||||
|
>
|
||||||
|
<section className={drawer.links}>
|
||||||
|
<DrawerDestinations toggleDrawer={toggleDrawer} />
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={`${drawer.overlay} ${drawerHidden ? '' : drawer.active}`}
|
||||||
|
onClick={() => toggleDrawer()}
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
<main className={classes.main}>
|
||||||
|
<section className={classes.header}>
|
||||||
|
<section className={classes.title}>
|
||||||
|
{headerIcon && (
|
||||||
|
<section className={classes.icon}>
|
||||||
|
<CharIcon
|
||||||
|
type={headerIcon}
|
||||||
|
viewBox={
|
||||||
|
headerIcon === 'operator'
|
||||||
|
? '0 0 88.969 71.469'
|
||||||
|
: '0 0 94.563 67.437'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{title}
|
||||||
|
</section>
|
||||||
|
<section className={classes.tab}>{headerTabs}</section>
|
||||||
|
</section>
|
||||||
|
<Border />
|
||||||
|
<ToTopButton />
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
</main>
|
||||||
|
<FooterElement />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FooterElement() {
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return (
|
||||||
|
<footer className={footer.footer}>
|
||||||
|
<section className={`${footer.links} ${footer.section}`}>
|
||||||
|
<section className={footer.item}>
|
||||||
|
<Popup
|
||||||
|
className={footer.link}
|
||||||
|
title={i18n('disclaimer')}
|
||||||
|
>
|
||||||
|
{i18n('disclaimer_content')}
|
||||||
|
</Popup>
|
||||||
|
</section>
|
||||||
|
<section className={footer.item}>
|
||||||
|
<Link
|
||||||
|
reloadDocument
|
||||||
|
to="https://gura.ch/pp"
|
||||||
|
target="_blank"
|
||||||
|
className={footer.link}
|
||||||
|
>
|
||||||
|
{i18n('privacy_policy')}
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
<section className={footer.item}>
|
||||||
|
<Link
|
||||||
|
reloadDocument
|
||||||
|
to="https://gura.ch/aklive2d-gh"
|
||||||
|
target="_blank"
|
||||||
|
className={footer.link}
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
<section className={footer.item}>
|
||||||
|
<Popup
|
||||||
|
className={footer.link}
|
||||||
|
title={i18n('contact_us')}
|
||||||
|
>
|
||||||
|
ak#halyul.dev
|
||||||
|
</Popup>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={`${footer.copyright} ${footer.section}`}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
navigate('/error')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Spine Runtimes © 2013 - 2019 Esoteric Software LLC
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Assets © 2017 - {currentYear} Arknights/Hypergryph Co.,
|
||||||
|
Ltd
|
||||||
|
</span>
|
||||||
|
<span>Source Code © 2021 - {currentYear} Halyul</span>
|
||||||
|
</section>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}, [i18n, navigate])
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDestinations({ toggleDrawer }) {
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const { textDefaultLang, alternateLang } = useLanguage()
|
||||||
|
|
||||||
|
return routes
|
||||||
|
.filter((item) => item.inDrawer)
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item.element.type === 'string') {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
reloadDocument
|
||||||
|
key={item.name}
|
||||||
|
to={item.path}
|
||||||
|
target="_blank"
|
||||||
|
className={drawer.link}
|
||||||
|
onClick={() => toggleDrawer(false)}
|
||||||
|
>
|
||||||
|
<section>{i18n(item.name, textDefaultLang)}</section>
|
||||||
|
<section>{i18n(item.name, alternateLang)}</section>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={item.path}
|
||||||
|
key={item.name}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`${drawer.link} ${isActive ? drawer.active : ''}`
|
||||||
|
}
|
||||||
|
onClick={() => toggleDrawer(false)}
|
||||||
|
>
|
||||||
|
<section>{i18n(item.name, textDefaultLang)}</section>
|
||||||
|
<section>{i18n(item.name, alternateLang)}</section>
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function LanguageDropdown() {
|
||||||
|
const { language, setLanguage } = useLanguage()
|
||||||
|
const { i18n, i18nValues } = useI18n()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
text={i18n(language)}
|
||||||
|
altText={i18n('switch_language')}
|
||||||
|
menu={i18nValues.available.map((item) => {
|
||||||
|
return {
|
||||||
|
name: i18n(item),
|
||||||
|
value: item,
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
onClick={(item) => {
|
||||||
|
setLanguage(item.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}, [i18n, i18nValues.available, language, setLanguage])
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderTabsElement({ item }) {
|
||||||
|
const { currentTab, setCurrentTab } = useHeader()
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`${classes.item} ${currentTab === item.key ? classes.active : ''}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
setCurrentTab(item.key)
|
||||||
|
item.onClick && item.onClick(e, currentTab)
|
||||||
|
}}
|
||||||
|
style={item.style}
|
||||||
|
>
|
||||||
|
<section className={classes['text-wrapper']}>
|
||||||
|
<span>{i18n(item.key)}</span>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HeaderTabsElement.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderButton() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const { fastNavigation } = useHeader()
|
||||||
|
|
||||||
|
if (fastNavigation.length > 0) {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={fastNavigation}
|
||||||
|
altText={i18n('fast_navigation')}
|
||||||
|
onClick={(item) => {
|
||||||
|
navigate(item.value)
|
||||||
|
}}
|
||||||
|
className={header['fast-navigate']}
|
||||||
|
iconStyle={{
|
||||||
|
borderWidth: '0.15em',
|
||||||
|
width: '1em',
|
||||||
|
transform:
|
||||||
|
'translateY(-0.4rem) translateX(-0.7rem) rotate(-45deg)',
|
||||||
|
height: '1em',
|
||||||
|
}}
|
||||||
|
left={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<section className={header['back-arrow']}>
|
||||||
|
<Link to="/" className={header.link}>
|
||||||
|
<section className={header.arrow1} />
|
||||||
|
<section className={header.arrow2} />
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
29
apps/directory/src/routes/index.jsx
Normal file
29
apps/directory/src/routes/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Home from '@/routes/path/Home'
|
||||||
|
import Operator from '@/routes/path/Operator'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
index: true,
|
||||||
|
name: 'home',
|
||||||
|
element: <Home />,
|
||||||
|
inDrawer: true,
|
||||||
|
routeable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'https://gura.ch/dynamicCompile',
|
||||||
|
index: false,
|
||||||
|
name: 'official_page',
|
||||||
|
element: <a />,
|
||||||
|
inDrawer: true,
|
||||||
|
routeable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':key',
|
||||||
|
index: false,
|
||||||
|
name: 'operator',
|
||||||
|
element: <Operator />,
|
||||||
|
inDrawer: false,
|
||||||
|
routeable: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
451
apps/directory/src/routes/path/Home.jsx
Normal file
451
apps/directory/src/routes/path/Home.jsx
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { NavLink, Link } from 'react-router-dom'
|
||||||
|
import classes from '@/scss/home/Home.module.scss'
|
||||||
|
import { useConfig } from '@/state/config'
|
||||||
|
import { useI18n } from '@/state/language'
|
||||||
|
import { useLanguage } from '@/state/language'
|
||||||
|
import { useHeader } from '@/state/header'
|
||||||
|
import { useAppbar } from '@/state/appbar'
|
||||||
|
import VoiceElement from '@/component/voice'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
import CharIcon from '@/component/char_icon'
|
||||||
|
import Border from '@/component/border'
|
||||||
|
import useInsight from '@/state/insight'
|
||||||
|
import Switch from '@/component/switch'
|
||||||
|
import SearchBox from '@/component/search_box'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
const voiceOnAtom = atomWithStorage('voiceOn', false)
|
||||||
|
let lastVoiceState = 'ended'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const _trackEvt = useInsight()
|
||||||
|
const { setTitle, setTabs, currentTab, setHeaderIcon, setFastNavigation } =
|
||||||
|
useHeader()
|
||||||
|
const { config, operators, officialUpdate } = useConfig()
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const [content, setContent] = useState([])
|
||||||
|
const [voiceOn] = useAtom(voiceOnAtom)
|
||||||
|
const [voiceSrc, setVoiceSrc] = useState(null)
|
||||||
|
const [voiceReplay, setVoiceReplay] = useState(false)
|
||||||
|
const { language } = useLanguage()
|
||||||
|
const [navigationList, setNavigationList] = useState([])
|
||||||
|
const [searchField, setSearchField] = useState('')
|
||||||
|
const [updatedList, setUpdatedList] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle('dynamic_compile')
|
||||||
|
setTabs([
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'operator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'skin',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
setHeaderIcon(null)
|
||||||
|
}, [setHeaderIcon, setTabs, setTitle])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setContent(config?.operators || [])
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
const handleAduioStateChange = useCallback((e, state) => {
|
||||||
|
lastVoiceState = state
|
||||||
|
if (state === 'ended') {
|
||||||
|
setVoiceReplay(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isShown = useCallback(
|
||||||
|
(type) => currentTab === 'all' || currentTab === type,
|
||||||
|
[currentTab]
|
||||||
|
)
|
||||||
|
|
||||||
|
const fastNavigateDict = useMemo(() => {
|
||||||
|
const dict = {}
|
||||||
|
operators.forEach((item) => {
|
||||||
|
if (!(item.date in dict)) {
|
||||||
|
dict[item.date] = []
|
||||||
|
}
|
||||||
|
dict[item.date].push({
|
||||||
|
codename: item.codename,
|
||||||
|
link: item.link,
|
||||||
|
type: item.type,
|
||||||
|
color: item.color,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return dict
|
||||||
|
}, [operators])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const list = []
|
||||||
|
for (const [key, value] of Object.entries(fastNavigateDict)) {
|
||||||
|
const newValue = value.filter((item) => isShown(item.type))
|
||||||
|
if (newValue.length > 0) {
|
||||||
|
list.push({
|
||||||
|
name: key,
|
||||||
|
value: null,
|
||||||
|
type: 'date',
|
||||||
|
})
|
||||||
|
newValue.forEach((item) => {
|
||||||
|
list.push({
|
||||||
|
name: item.codename[language],
|
||||||
|
value: item.link,
|
||||||
|
type: 'item',
|
||||||
|
color: item.color,
|
||||||
|
icon: (
|
||||||
|
<CharIcon
|
||||||
|
type={item.type}
|
||||||
|
viewBox={
|
||||||
|
item.type === 'operator'
|
||||||
|
? '0 0 88.969 71.469'
|
||||||
|
: '0 0 94.563 67.437'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNavigationList(list)
|
||||||
|
setUpdatedList(list)
|
||||||
|
}, [fastNavigateDict, isShown, language])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const list = navigationList.filter((item) => {
|
||||||
|
return (
|
||||||
|
item.name.toLowerCase().indexOf(searchField.toLowerCase()) !==
|
||||||
|
-1 || item.type === 'date'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const newList = []
|
||||||
|
for (let i = 0; i < list.length - 1; i++) {
|
||||||
|
const firstType = list[i].type
|
||||||
|
const secondType = list[i + 1].type
|
||||||
|
if (firstType === 'date' && secondType === 'date') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newList.push(list[i])
|
||||||
|
}
|
||||||
|
if (list.length > 0 && list[list.length - 1].type !== 'date') {
|
||||||
|
newList.push(list[list.length - 1])
|
||||||
|
}
|
||||||
|
setUpdatedList(newList)
|
||||||
|
}, [navigationList, searchField])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFastNavigation([
|
||||||
|
{
|
||||||
|
type: 'custom',
|
||||||
|
component: (
|
||||||
|
<SearchBox
|
||||||
|
key="search-box"
|
||||||
|
altText={'search_by_name'}
|
||||||
|
handleOnChange={(e) => {
|
||||||
|
setSearchField(e)
|
||||||
|
}}
|
||||||
|
searchField={searchField}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...updatedList,
|
||||||
|
])
|
||||||
|
}, [searchField, setFastNavigation, updatedList])
|
||||||
|
|
||||||
|
const handleVoicePlay = useCallback(
|
||||||
|
(src) => {
|
||||||
|
if (!voiceOn) {
|
||||||
|
setVoiceSrc(null)
|
||||||
|
} else {
|
||||||
|
if (src === voiceSrc && lastVoiceState === 'ended') {
|
||||||
|
setVoiceReplay(true)
|
||||||
|
} else {
|
||||||
|
setVoiceSrc(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[voiceOn, voiceSrc]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
{officialUpdate.length > operators.length && (
|
||||||
|
<section>
|
||||||
|
<section
|
||||||
|
className={`${classes['official-update']} ${classes.group}`}
|
||||||
|
>
|
||||||
|
<section className={classes.info}>
|
||||||
|
<section className={classes.content}>
|
||||||
|
<section className={classes.text}>
|
||||||
|
{officialUpdate.length - operators.length}{' '}
|
||||||
|
{i18n('new_op_wait_to_update')}
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={`${classes['styled-selection']}`}
|
||||||
|
>
|
||||||
|
{officialUpdate.dates
|
||||||
|
.reduce((acc, cur) => {
|
||||||
|
const op = officialUpdate[cur]
|
||||||
|
return [...acc, ...op]
|
||||||
|
}, [])
|
||||||
|
.slice(
|
||||||
|
0,
|
||||||
|
officialUpdate.length -
|
||||||
|
operators.length
|
||||||
|
)
|
||||||
|
.map((entry, index) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
reloadDocument
|
||||||
|
to={entry.link}
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
color: entry.color,
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.content
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.option
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.outline
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<section
|
||||||
|
className={`${classes.text} ${classes.container}`}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.type
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CharIcon
|
||||||
|
type={
|
||||||
|
entry.type
|
||||||
|
}
|
||||||
|
viewBox={
|
||||||
|
entry.type ===
|
||||||
|
'operator'
|
||||||
|
? '0 0 88.969 71.469'
|
||||||
|
: '0 0 94.563 67.437'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.title
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
entry
|
||||||
|
.codename[
|
||||||
|
language
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes[
|
||||||
|
'arrow-icon'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.bar
|
||||||
|
}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.bar
|
||||||
|
}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.bar
|
||||||
|
}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.bar
|
||||||
|
}
|
||||||
|
></section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section className={classes.date}>
|
||||||
|
{officialUpdate.latest}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<Border />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{content.map((v) => {
|
||||||
|
const length = v.filter((v) => isShown(v.type)).length
|
||||||
|
return (
|
||||||
|
<section key={v[0].date} hidden={length === 0}>
|
||||||
|
<section className={classes.group}>
|
||||||
|
<section className={classes['operator-group']}>
|
||||||
|
{v.map((item) => {
|
||||||
|
return (
|
||||||
|
<OperatorElement
|
||||||
|
key={item.link}
|
||||||
|
item={item}
|
||||||
|
hidden={!isShown(item.type)}
|
||||||
|
handleVoicePlay={handleVoicePlay}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
<section className={classes.date}>
|
||||||
|
{v[0].date}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<Border />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<VoiceSwitchElement
|
||||||
|
src={voiceSrc}
|
||||||
|
handleAduioStateChange={handleAduioStateChange}
|
||||||
|
replay={voiceReplay}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function OperatorElement({ item, hidden, handleVoicePlay }) {
|
||||||
|
const { textDefaultLang, language, alternateLang } = useLanguage()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={`/${item.link}`}
|
||||||
|
className={classes.item}
|
||||||
|
hidden={hidden}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
onMouseEnter={() =>
|
||||||
|
handleVoicePlay(
|
||||||
|
`/${item.link}/assets/${buildConfig.voice_folders.main}/${buildConfig.app_voice_url}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section className={classes['background-filler']} />
|
||||||
|
<section className={classes.outline} />
|
||||||
|
<section className={classes.img}>
|
||||||
|
<ImageElement item={item} />
|
||||||
|
</section>
|
||||||
|
<section className={classes.info}>
|
||||||
|
<section className={classes.container}>
|
||||||
|
<section className={classes.title}>
|
||||||
|
{item.codename[language]}
|
||||||
|
</section>
|
||||||
|
<section className={classes.type}>
|
||||||
|
<CharIcon
|
||||||
|
type={item.type}
|
||||||
|
viewBox={
|
||||||
|
item.type === 'operator'
|
||||||
|
? '0 0 88.969 71.469'
|
||||||
|
: '0 0 94.563 67.437'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section className={classes.wrapper}>
|
||||||
|
<span className={classes.text}>
|
||||||
|
{
|
||||||
|
item.codename[
|
||||||
|
language.startsWith('en')
|
||||||
|
? alternateLang
|
||||||
|
: textDefaultLang
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={classes.background}
|
||||||
|
style={{
|
||||||
|
color: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
}, [
|
||||||
|
item,
|
||||||
|
hidden,
|
||||||
|
language,
|
||||||
|
alternateLang,
|
||||||
|
textDefaultLang,
|
||||||
|
handleVoicePlay,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function VoiceSwitchElement({ src, replay, handleAduioStateChange }) {
|
||||||
|
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
|
||||||
|
const { setExtraArea } = useAppbar()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExtraArea([
|
||||||
|
<Switch
|
||||||
|
key="voice"
|
||||||
|
text="voice"
|
||||||
|
on={voiceOn}
|
||||||
|
handleOnClick={() => setVoiceOn(!voiceOn)}
|
||||||
|
/>,
|
||||||
|
])
|
||||||
|
}, [voiceOn, setExtraArea, setVoiceOn])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VoiceElement
|
||||||
|
src={src}
|
||||||
|
replay={replay}
|
||||||
|
handleAduioStateChange={handleAduioStateChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
VoiceSwitchElement.propTypes = {
|
||||||
|
src: PropTypes.string,
|
||||||
|
replay: PropTypes.bool,
|
||||||
|
handleAduioStateChange: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImageElement({ item }) {
|
||||||
|
const { language } = useLanguage()
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={`/${buildConfig.directory_folder}/${item.fallback_name.replace(/#/g, '%23')}_portrait.png`}
|
||||||
|
alt={item.codename[language]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ImageElement.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
fallback_name: PropTypes.string,
|
||||||
|
codename: PropTypes.object,
|
||||||
|
}
|
||||||
694
apps/directory/src/routes/path/Operator.jsx
Normal file
694
apps/directory/src/routes/path/Operator.jsx
Normal file
@@ -0,0 +1,694 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||||
|
import classes from '@/scss/operator/Operator.module.scss'
|
||||||
|
import { useConfig } from '@/state/config'
|
||||||
|
import { useLanguage } from '@/state/language'
|
||||||
|
import { useHeader } from '@/state/header'
|
||||||
|
import { useAppbar } from '@/state/appbar'
|
||||||
|
import VoiceElement from '@/component/voice'
|
||||||
|
import useInsight from '@/state/insight'
|
||||||
|
import { spine } from '@aklive2d/module'
|
||||||
|
import Border from '@/component/border'
|
||||||
|
import { useI18n } from '@/state/language'
|
||||||
|
import Switch from '@/component/switch'
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
const musicMapping = buildConfig.music_mapping
|
||||||
|
const getVoiceFoler = (lang) => {
|
||||||
|
const folderObject = buildConfig.voice_folders
|
||||||
|
const voiceFolder =
|
||||||
|
folderObject.sub.find((e) => e.lang === lang) ||
|
||||||
|
folderObject.sub.find((e) => e.name === 'custom')
|
||||||
|
return `${folderObject.main}/${voiceFolder.name}`
|
||||||
|
}
|
||||||
|
const defaultSpineAnimationName = 'Idle'
|
||||||
|
const backgroundAtom = atom(buildConfig.default_background)
|
||||||
|
|
||||||
|
const getPartialName = (type, input) => {
|
||||||
|
let part
|
||||||
|
switch (type) {
|
||||||
|
case 'name':
|
||||||
|
part = 5
|
||||||
|
break
|
||||||
|
case 'skin':
|
||||||
|
part = 1
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return input.replace(/^(.+)( )(·|\/)( )(.+)$/, `$${part}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTabName = (item, language) => {
|
||||||
|
if (item.type === 'operator') {
|
||||||
|
return 'operator'
|
||||||
|
} else {
|
||||||
|
return getPartialName('skin', item.codename[language])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Operator() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { operators } = useConfig()
|
||||||
|
const { language } = useLanguage()
|
||||||
|
const { key } = useParams()
|
||||||
|
const { setTitle, setTabs, setHeaderIcon, setFastNavigation } = useHeader()
|
||||||
|
const { setExtraArea } = useAppbar()
|
||||||
|
const [config, setConfig] = useState(null)
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const _trackEvt = useInsight(`/${key}`)
|
||||||
|
const spineRef = useRef(null)
|
||||||
|
const [spineAnimationName, setSpineAnimationName] = useState(
|
||||||
|
defaultSpineAnimationName
|
||||||
|
)
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const [spinePlayer, setSpinePlayer] = useState(null)
|
||||||
|
const [voiceLang, _setVoiceLang] = useState(null)
|
||||||
|
const [currentBackground, setCurrentBackground] = useAtom(backgroundAtom)
|
||||||
|
const [voiceConfig, setVoiceConfig] = useState(null)
|
||||||
|
const [subtitleLang, setSubtitleLang] = useState(null)
|
||||||
|
const [hideSubtitle, setHideSubtitle] = useState(true)
|
||||||
|
const [subtitleObj, _setSubtitleObj] = useState(null)
|
||||||
|
const [currentVoiceId, setCurrentVoiceId] = useState(null)
|
||||||
|
const voiceLangRef = useRef(voiceLang)
|
||||||
|
const subtitleObjRef = useRef(subtitleObj)
|
||||||
|
const configRef = useRef(config)
|
||||||
|
const [voiceSrc, setVoiceSrc] = useState(null)
|
||||||
|
const [isVoicePlaying, _setIsVoicePlaying] = useState(false)
|
||||||
|
const isVoicePlayingRef = useRef(isVoicePlaying)
|
||||||
|
|
||||||
|
const setVoiceLang = (value) => {
|
||||||
|
voiceLangRef.current = value
|
||||||
|
_setVoiceLang(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSubtitleObj = (value) => {
|
||||||
|
subtitleObjRef.current = value
|
||||||
|
_setSubtitleObj(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIsVoicePlaying = (value) => {
|
||||||
|
isVoicePlayingRef.current = value
|
||||||
|
_setIsVoicePlaying(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExtraArea([])
|
||||||
|
setFastNavigation([])
|
||||||
|
}, [setExtraArea, setFastNavigation])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const config = operators.find((item) => item.link === key)
|
||||||
|
if (config) {
|
||||||
|
setConfig(config)
|
||||||
|
configRef.current = config
|
||||||
|
setSpineAnimationName(defaultSpineAnimationName)
|
||||||
|
setHeaderIcon(config.type)
|
||||||
|
if (spineRef.current?.children.length > 0) {
|
||||||
|
spineRef.current?.removeChild(spineRef.current?.children[0])
|
||||||
|
}
|
||||||
|
fetch(`/${key}/assets/charword_table.json`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
setVoiceConfig(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [key, operators, setHeaderIcon])
|
||||||
|
|
||||||
|
const coverToTab = useCallback(
|
||||||
|
(item, language) => {
|
||||||
|
const key = getTabName(item, language)
|
||||||
|
return {
|
||||||
|
key: key,
|
||||||
|
style: {
|
||||||
|
color: item.color,
|
||||||
|
},
|
||||||
|
onClick: (e, tab) => {
|
||||||
|
if (tab === key) return
|
||||||
|
navigate(`/${item.link}`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
)
|
||||||
|
|
||||||
|
const otherEntries = useMemo(() => {
|
||||||
|
if (!config || !language) return null
|
||||||
|
return operators
|
||||||
|
.filter(
|
||||||
|
(item) => item.id === config.id && item.link !== config.link
|
||||||
|
)
|
||||||
|
.map((item) => {
|
||||||
|
return coverToTab(item, language)
|
||||||
|
})
|
||||||
|
}, [config, language, operators, coverToTab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config) {
|
||||||
|
setTabs([coverToTab(config, language), ...otherEntries])
|
||||||
|
}
|
||||||
|
}, [config, key, coverToTab, setTabs, otherEntries, language])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config) {
|
||||||
|
setTitle(getPartialName('name', config.codename[language]))
|
||||||
|
}
|
||||||
|
}, [config, language, key, setTitle])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (spineRef.current?.children.length === 0 && configRef.current) {
|
||||||
|
const playerConfig = {
|
||||||
|
atlasUrl: `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.atlas`,
|
||||||
|
animation: spineAnimationName,
|
||||||
|
premultipliedAlpha: true,
|
||||||
|
alpha: true,
|
||||||
|
backgroundColor: '#00000000',
|
||||||
|
viewport: {
|
||||||
|
debugRender: false,
|
||||||
|
padLeft: `${configRef.current.viewport_left}%`,
|
||||||
|
padRight: `${configRef.current.viewport_right}%`,
|
||||||
|
padTop: `${configRef.current.viewport_top}%`,
|
||||||
|
padBottom: `${configRef.current.viewport_bottom}%`,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
showControls: false,
|
||||||
|
touch: false,
|
||||||
|
fps: 60,
|
||||||
|
defaultMix: 0.3,
|
||||||
|
success: (player) => {
|
||||||
|
if (
|
||||||
|
player.skeleton.data.animations
|
||||||
|
.map((e) => e.name)
|
||||||
|
.includes('Start')
|
||||||
|
) {
|
||||||
|
player.animationState.setAnimation(0, 'Start', false, 0)
|
||||||
|
player.animationState.addAnimation(0, 'Idle', true, 0)
|
||||||
|
}
|
||||||
|
let lastVoiceId = null
|
||||||
|
let currentVoiceId = null
|
||||||
|
player.canvas.onclick = () => {
|
||||||
|
if (!voiceLangRef.current) return
|
||||||
|
const voiceId = () => {
|
||||||
|
const keys = Object.keys(subtitleObjRef.current)
|
||||||
|
const id =
|
||||||
|
keys[Math.floor(Math.random() * keys.length)]
|
||||||
|
return id === lastVoiceId ? voiceId() : id
|
||||||
|
}
|
||||||
|
const id = voiceId()
|
||||||
|
currentVoiceId = id
|
||||||
|
setCurrentVoiceId(id)
|
||||||
|
setVoiceSrc(
|
||||||
|
`/${configRef.current.link}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`
|
||||||
|
)
|
||||||
|
lastVoiceId = currentVoiceId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configRef.current.use_json) {
|
||||||
|
playerConfig.jsonUrl = `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.json`
|
||||||
|
} else {
|
||||||
|
playerConfig.skelUrl = `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.skel`
|
||||||
|
}
|
||||||
|
setSpinePlayer(
|
||||||
|
new spine.SpinePlayer(spineRef.current, playerConfig)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [spineAnimationName, setSpinePlayer, spinePlayer, key])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (spinePlayer) {
|
||||||
|
spinePlayer.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [spinePlayer])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (voiceConfig && voiceLang) {
|
||||||
|
let subtitleObj = voiceConfig.subtitleLangs[subtitleLang || 'zh-CN']
|
||||||
|
let subtitleKey = 'default'
|
||||||
|
if (subtitleObj[voiceLang]) {
|
||||||
|
subtitleKey = voiceLang
|
||||||
|
}
|
||||||
|
setSubtitleObj(subtitleObj[subtitleKey])
|
||||||
|
}
|
||||||
|
}, [subtitleLang, voiceConfig, voiceLang])
|
||||||
|
|
||||||
|
const handleAduioStateChange = useCallback((e, state) => {
|
||||||
|
switch (state) {
|
||||||
|
case 'play':
|
||||||
|
setIsVoicePlaying(true)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
setIsVoicePlaying(false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (subtitleLang) {
|
||||||
|
if (isVoicePlaying) {
|
||||||
|
setHideSubtitle(false)
|
||||||
|
} else {
|
||||||
|
const autoHide = () => {
|
||||||
|
if (isVoicePlayingRef.current) return
|
||||||
|
setHideSubtitle(true)
|
||||||
|
}
|
||||||
|
setTimeout(autoHide, 5 * 1000)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(autoHide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHideSubtitle(true)
|
||||||
|
}
|
||||||
|
}, [subtitleLang, isVoicePlaying])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (voiceLang && isVoicePlaying) {
|
||||||
|
const audioUrl = `/assets/${getVoiceFoler(voiceLang)}/${currentVoiceId}.ogg`
|
||||||
|
if (
|
||||||
|
voiceSrc !==
|
||||||
|
window.location.href.replace(/\/$/g, '') + audioUrl
|
||||||
|
) {
|
||||||
|
setVoiceSrc(`/${config.link}${audioUrl}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [voiceLang, isVoicePlaying, currentVoiceId, config, voiceSrc])
|
||||||
|
|
||||||
|
const playAnimationVoice = useCallback(
|
||||||
|
(animation) => {
|
||||||
|
if (voiceLangRef.current) {
|
||||||
|
let id = null
|
||||||
|
if (animation === 'Idle') id = 'CN_011'
|
||||||
|
if (animation === 'Interact') id = 'CN_034'
|
||||||
|
if (animation === 'Special') id = 'CN_042'
|
||||||
|
if (id) {
|
||||||
|
setCurrentVoiceId(id)
|
||||||
|
setVoiceSrc(
|
||||||
|
`/${key}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[key]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!voiceLang) {
|
||||||
|
setVoiceSrc(null)
|
||||||
|
}
|
||||||
|
}, [voiceLang])
|
||||||
|
|
||||||
|
const setSpineAnimation = useCallback(
|
||||||
|
(animation) => {
|
||||||
|
playAnimationVoice(animation)
|
||||||
|
const entry = spinePlayer.animationState.setAnimation(
|
||||||
|
0,
|
||||||
|
animation,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
entry.mixDuration = 0.3
|
||||||
|
setSpineAnimationName(animation)
|
||||||
|
},
|
||||||
|
[playAnimationVoice, spinePlayer]
|
||||||
|
)
|
||||||
|
|
||||||
|
const spineSettings = [
|
||||||
|
{
|
||||||
|
name: 'animation',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'idle',
|
||||||
|
onClick: () => setSpineAnimation('Idle'),
|
||||||
|
activeRule: () => {
|
||||||
|
return spineAnimationName === 'Idle'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'interact',
|
||||||
|
onClick: () => setSpineAnimation('Interact'),
|
||||||
|
activeRule: () => {
|
||||||
|
return spineAnimationName === 'Interact'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'special',
|
||||||
|
onClick: () => setSpineAnimation('Special'),
|
||||||
|
activeRule: () => {
|
||||||
|
return spineAnimationName === 'Special'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'voice',
|
||||||
|
options:
|
||||||
|
(voiceConfig &&
|
||||||
|
Object.keys(voiceConfig?.voiceLangs['zh-CN']).map(
|
||||||
|
(item) => {
|
||||||
|
return {
|
||||||
|
name: i18n(item),
|
||||||
|
onClick: () => {
|
||||||
|
if (voiceLang !== item) {
|
||||||
|
setVoiceLang(item)
|
||||||
|
} else {
|
||||||
|
setVoiceLang(null)
|
||||||
|
}
|
||||||
|
if (!isVoicePlayingRef.current) {
|
||||||
|
playAnimationVoice(spineAnimationName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeRule: () => {
|
||||||
|
return voiceLang === item
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) ||
|
||||||
|
[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subtitle',
|
||||||
|
options:
|
||||||
|
(voiceConfig &&
|
||||||
|
Object.keys(voiceConfig?.subtitleLangs).map((item) => {
|
||||||
|
return {
|
||||||
|
name: i18n(item),
|
||||||
|
onClick: () => {
|
||||||
|
if (subtitleLang !== item) {
|
||||||
|
setSubtitleLang(item)
|
||||||
|
} else {
|
||||||
|
setSubtitleLang(null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeRule: () => {
|
||||||
|
return subtitleLang === item
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})) ||
|
||||||
|
[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'music',
|
||||||
|
el: <MusicElement />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'backgrounds',
|
||||||
|
options:
|
||||||
|
buildConfig.background_files.map((item) => {
|
||||||
|
return {
|
||||||
|
name: item,
|
||||||
|
onClick: () => {
|
||||||
|
setCurrentBackground(item)
|
||||||
|
},
|
||||||
|
activeRule: () => {
|
||||||
|
return currentBackground === item
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}) || [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!buildConfig.available_operators.includes(key)) {
|
||||||
|
throw new Error('Operator not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={classes.operator}>
|
||||||
|
<section className={classes.main}>
|
||||||
|
<section
|
||||||
|
className={classes.settings}
|
||||||
|
style={{
|
||||||
|
color: config?.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{spineSettings.map((item) => {
|
||||||
|
if (item.el) {
|
||||||
|
return <section key={item.name}>{item.el}</section>
|
||||||
|
}
|
||||||
|
if (item.options.length === 0) return null
|
||||||
|
return (
|
||||||
|
<section key={item.name}>
|
||||||
|
<section className={classes.title}>
|
||||||
|
<section className={classes.text}>
|
||||||
|
{i18n(item.name)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={classes['styled-selection']}
|
||||||
|
>
|
||||||
|
{item.options.map((option) => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`${classes.content} ${option.activeRule && option.activeRule() ? classes.active : ''}`}
|
||||||
|
onClick={(e) =>
|
||||||
|
option.onClick(e)
|
||||||
|
}
|
||||||
|
key={option.name}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={classes.option}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes.outline
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<section
|
||||||
|
className={`${classes.text} ${classes['no-overflow']}`}
|
||||||
|
>
|
||||||
|
{i18n(option.name)}
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes['tick-icon']
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<section>
|
||||||
|
<section className={classes.title}>
|
||||||
|
<section className={classes.text}>
|
||||||
|
{i18n('external_links')}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section className={classes['styled-selection']}>
|
||||||
|
<Link
|
||||||
|
reloadDocument
|
||||||
|
to={`./index.html?aklive2d`}
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
color: config?.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section className={classes.content}>
|
||||||
|
<section className={classes.option}>
|
||||||
|
<section className={classes.outline} />
|
||||||
|
<section
|
||||||
|
className={`${classes.text} ${classes['no-overflow']}`}
|
||||||
|
>
|
||||||
|
{i18n('web_version')}
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={classes['arrow-icon']}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</Link>
|
||||||
|
{config?.workshopId && (
|
||||||
|
<Link
|
||||||
|
reloadDocument
|
||||||
|
to={`https://steamcommunity.com/sharedfiles/filedetails/?id=${config.workshopId}`}
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
color: config?.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section className={classes.content}>
|
||||||
|
<section className={classes.option}>
|
||||||
|
<section
|
||||||
|
className={classes.outline}
|
||||||
|
/>
|
||||||
|
<section
|
||||||
|
className={`${classes.text} ${classes['no-overflow']}`}
|
||||||
|
>
|
||||||
|
{i18n('steam_workshop')}
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
classes['arrow-icon']
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
<section
|
||||||
|
className={classes.bar}
|
||||||
|
></section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={classes.container}
|
||||||
|
style={
|
||||||
|
currentBackground && {
|
||||||
|
backgroundImage: `url(/chen/assets/${buildConfig.background_folder}/${currentBackground})`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{config && (
|
||||||
|
<img
|
||||||
|
src={`/${config.link}/assets/${config.logo}.png`}
|
||||||
|
alt={config?.codename[language]}
|
||||||
|
className={classes.logo}
|
||||||
|
style={
|
||||||
|
config.invert_filter
|
||||||
|
? {
|
||||||
|
filter: 'invert(1)',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<section ref={spineRef} className={classes.wrapper} />
|
||||||
|
{currentVoiceId && subtitleObj && (
|
||||||
|
<section
|
||||||
|
className={`${classes.voice} ${hideSubtitle ? '' : classes.active}`}
|
||||||
|
>
|
||||||
|
<section className={classes.type}>
|
||||||
|
{subtitleObj[currentVoiceId]?.title}
|
||||||
|
</section>
|
||||||
|
<section className={classes.subtitle}>
|
||||||
|
<span>{subtitleObj[currentVoiceId]?.text}</span>
|
||||||
|
<span className={classes.triangle} />
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<Border />
|
||||||
|
<VoiceElement
|
||||||
|
src={voiceSrc}
|
||||||
|
handleAduioStateChange={handleAduioStateChange}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MusicElement() {
|
||||||
|
const [enableMusic, setEnableMusic] = useState(false)
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const musicIntroRef = useRef(null)
|
||||||
|
const musicLoopRef = useRef(null)
|
||||||
|
const [background] = useAtom(backgroundAtom)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (musicIntroRef.current && musicIntroRef.current) {
|
||||||
|
musicIntroRef.current.volume = 0.5
|
||||||
|
musicLoopRef.current.volume = 0.5
|
||||||
|
}
|
||||||
|
}, [musicIntroRef, musicLoopRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enableMusic || background) {
|
||||||
|
musicIntroRef.current.pause()
|
||||||
|
musicLoopRef.current.pause()
|
||||||
|
}
|
||||||
|
}, [enableMusic, background])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (background && enableMusic) {
|
||||||
|
const introOgg = musicMapping[background].intro
|
||||||
|
const intro = `./chen/assets/${buildConfig.music_folder}/${introOgg}`
|
||||||
|
const loop = `./chen/assets/${buildConfig.music_folder}/${musicMapping[background].loop}`
|
||||||
|
musicLoopRef.current.src = loop
|
||||||
|
if (introOgg) {
|
||||||
|
musicIntroRef.current.src = intro || loop
|
||||||
|
} else {
|
||||||
|
musicLoopRef.current.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [background, enableMusic])
|
||||||
|
|
||||||
|
const handleIntroTimeUpdate = useCallback(() => {
|
||||||
|
if (
|
||||||
|
musicIntroRef.current.currentTime >=
|
||||||
|
musicIntroRef.current.duration - 0.3
|
||||||
|
) {
|
||||||
|
musicIntroRef.current.pause()
|
||||||
|
musicLoopRef.current.play()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLoopTimeUpdate = useCallback(() => {
|
||||||
|
if (
|
||||||
|
musicLoopRef.current.currentTime >=
|
||||||
|
musicLoopRef.current.duration - 0.3
|
||||||
|
) {
|
||||||
|
musicLoopRef.current.currentTime = 0
|
||||||
|
musicLoopRef.current.play()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<section
|
||||||
|
className={classes.title}
|
||||||
|
onClick={() => setEnableMusic(!enableMusic)}
|
||||||
|
>
|
||||||
|
<section className={classes.text}>{i18n('music')}</section>
|
||||||
|
<section className={classes.switch}>
|
||||||
|
<Switch on={enableMusic} />
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<audio
|
||||||
|
ref={musicIntroRef}
|
||||||
|
preload="auto"
|
||||||
|
autoPlay
|
||||||
|
onTimeUpdate={() => handleIntroTimeUpdate()}
|
||||||
|
>
|
||||||
|
<source type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
<audio
|
||||||
|
ref={musicLoopRef}
|
||||||
|
preload="auto"
|
||||||
|
onTimeUpdate={() => handleLoopTimeUpdate()}
|
||||||
|
>
|
||||||
|
<source type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
apps/directory/src/scss/_main_share.scss
Normal file
20
apps/directory/src/scss/_main_share.scss
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.main {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 70%;
|
||||||
|
max-width: 100rem;
|
||||||
|
padding-top: 5rem;
|
||||||
|
min-height: calc(100vh - 5rem - 3rem);
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
width: 100vw;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
369
apps/directory/src/scss/_page_base.scss
Normal file
369
apps/directory/src/scss/_page_base.scss
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
.date {
|
||||||
|
margin: 1.5rem;
|
||||||
|
font-family:
|
||||||
|
Bender, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||||
|
sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--date-color);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
letter-spacing: 0.1rem;
|
||||||
|
flex: auto;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
color: var(--text-color-full);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.3em;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
fill: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.operator-group {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
overflow-y: hidden;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 12rem;
|
||||||
|
margin: 1.25rem;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--home-item-background-linear-gradient-color) 0,
|
||||||
|
var(--home-item-background-linear-gradient-color) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 5px
|
||||||
|
);
|
||||||
|
|
||||||
|
.background-filler {
|
||||||
|
border-right: 1px solid
|
||||||
|
var(--home-item-background-linear-gradient-color);
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 -1px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: -6px;
|
||||||
|
top: -6px;
|
||||||
|
border: var(--home-item-outline-color) 1px dashed;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -3px;
|
||||||
|
height: 3px;
|
||||||
|
width: 100%;
|
||||||
|
border-left: var(--text-color) solid 3px;
|
||||||
|
border-right: var(--text-color) solid 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
bottom: -3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
transition: background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
& img {
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.8rem 0.4rem;
|
||||||
|
line-height: 1.2em;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-family:
|
||||||
|
Geometos, 'Noto Sans SC', 'Noto Sans JP',
|
||||||
|
'Noto Sans KR', 'Noto Sans', sans-serif;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
70deg,
|
||||||
|
transparent 40%,
|
||||||
|
currentcolor 150%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.img {
|
||||||
|
background-color: var(--home-item-hover-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline,
|
||||||
|
.info .background {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
width: 8.08rem;
|
||||||
|
margin: 1.08rem;
|
||||||
|
|
||||||
|
.outline,
|
||||||
|
.info .background {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-selection {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
.option {
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
transform: translate3d(0, 0, 1px);
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.44rem 3.25rem 0.44rem 0.63rem;
|
||||||
|
background-color: var(--home-item-hover-background-color);
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--home-item-background-linear-gradient-color) 0 1px,
|
||||||
|
transparent 1px 4px
|
||||||
|
);
|
||||||
|
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
.outline {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: -6px;
|
||||||
|
top: -6px;
|
||||||
|
border: var(--home-item-outline-color) 1px dashed;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
border-left: var(--text-color) solid 2px;
|
||||||
|
border-right: var(--text-color) solid 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
bottom: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
.outline {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
|
||||||
|
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 60%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
currentcolor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick-icon {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
right: 0.31rem;
|
||||||
|
top: 50%;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 1rem;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
|
||||||
|
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
border-right: var(--text-color) solid 0.25rem;
|
||||||
|
border-bottom: var(--text-color) solid 0.25rem;
|
||||||
|
transform: translate(-50%, -70%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
right: -0.1em;
|
||||||
|
top: 0.55em;
|
||||||
|
width: 2em;
|
||||||
|
height: 1em;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
transition:
|
||||||
|
opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
|
||||||
|
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.25em;
|
||||||
|
background-color: var(--text-color-full);
|
||||||
|
will-change: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:nth-child(1) {
|
||||||
|
transform: rotateZ(45deg) scaleX(0.5) translateX(0.8em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:nth-child(2) {
|
||||||
|
transform: rotateZ(-45deg) scaleX(0.5) translateX(-0.8em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:nth-child(3),
|
||||||
|
.bar:nth-child(4) {
|
||||||
|
transform: translateY(1em) rotateZ(90deg) scaleX(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
transform: translate3d(6px, 0, 1px);
|
||||||
|
|
||||||
|
.option::before,
|
||||||
|
.option .outline {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.option .tick-icon {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-overflow {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
30
apps/directory/src/scss/changelogs/Changelogs.module.scss
Normal file
30
apps/directory/src/scss/changelogs/Changelogs.module.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@use '@/scss/page_base';
|
||||||
|
|
||||||
|
.group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
apps/directory/src/scss/error/Error.module.scss
Normal file
61
apps/directory/src/scss/error/Error.module.scss
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
@use '@/scss/main_share';
|
||||||
|
|
||||||
|
.error {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 16px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-top: 6rem;
|
||||||
|
font-size: 3rem;
|
||||||
|
gap: 2rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spine {
|
||||||
|
max-width: 600px;
|
||||||
|
flex: 1;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 768px) {
|
||||||
|
.main {
|
||||||
|
padding-top: 6rem;
|
||||||
|
max-height: calc(100vh - 6rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 480px) {
|
||||||
|
.main {
|
||||||
|
padding-top: 4rem;
|
||||||
|
max-height: calc(100vh - 4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
apps/directory/src/scss/home/Home.module.scss
Normal file
23
apps/directory/src/scss/home/Home.module.scss
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
@use '@/scss/page_base';
|
||||||
|
|
||||||
|
.official-update {
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-left: 1rem;
|
||||||
|
word-break: break-word;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
.title {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
apps/directory/src/scss/operator/Operator.module.scss
Normal file
142
apps/directory/src/scss/operator/Operator.module.scss
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
@use '@/scss/page_base';
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 3rem 0 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
padding-top: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1280px) {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
padding-top: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
user-select: none;
|
||||||
|
margin-left: 1rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
border-left: 3px solid currentcolor;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
bottom: -10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 30%;
|
||||||
|
height: auto;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 20%;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 85%;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 16px;
|
||||||
|
transition: all 0.5s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||||
|
visibility: hidden;
|
||||||
|
font-family:
|
||||||
|
'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
.type {
|
||||||
|
background-color: #9e9e9e;
|
||||||
|
color: #000;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: -12px;
|
||||||
|
left: -8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
max-width: 180px;
|
||||||
|
width: 65%;
|
||||||
|
box-shadow: 0 3px 6px #00000080;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
background-color: #000000a6;
|
||||||
|
color: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
box-shadow: 0 6px 12px #00000080;
|
||||||
|
position: relative;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triangle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 8px;
|
||||||
|
border-color: white transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1280px) {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1280px) {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
apps/directory/src/scss/root/Root.module.scss
Normal file
95
apps/directory/src/scss/root/Root.module.scss
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
@use '@/scss/main_share';
|
||||||
|
|
||||||
|
.main {
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.2em;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--text-color);
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 480px) {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: auto;
|
||||||
|
white-space: pre;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 3em;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 0.3rem solid transparent;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.text-wrapper {
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
color: var(--link-highlight-color);
|
||||||
|
|
||||||
|
.text-wrapper,
|
||||||
|
.text {
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-bottom-color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (width <= 430px) {
|
||||||
|
& {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
apps/directory/src/scss/root/drawer.module.scss
Normal file
55
apps/directory/src/scss/root/drawer.module.scss
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: -15rem;
|
||||||
|
width: 15rem;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: left cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.links {
|
||||||
|
padding: 8rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
background-color: var(--drawer-background-color);
|
||||||
|
height: 100%;
|
||||||
|
width: 15rem;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
color: var(--link-highlight-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
pointer-events: all;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
apps/directory/src/scss/root/footer.module.scss
Normal file
42
apps/directory/src/scss/root/footer.module.scss
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.footer {
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
font-family:
|
||||||
|
'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
flex-direction: row;
|
||||||
|
height: 2rem;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
height: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
apps/directory/src/scss/root/header.module.scss
Normal file
106
apps/directory/src/scss/root/header.module.scss
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
.header {
|
||||||
|
width: auto;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
z-index: 4;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.fast-navigate {
|
||||||
|
pointer-events: auto;
|
||||||
|
margin-left: 0.6rem;
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
width: 2rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
transform: translate(0, -200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
transform: translate(0, 200%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.bar {
|
||||||
|
&:nth-child(1) {
|
||||||
|
transform: translate(0, 100%) rotateZ(45deg) scaleX(0.5)
|
||||||
|
translate(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
transform: rotateZ(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
transform: translate(0, -100%) rotateZ(45deg) scaleX(0.5)
|
||||||
|
translate(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-arrow {
|
||||||
|
padding-left: 1rem;
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.arrow1 {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
border-left: 0.15rem solid;
|
||||||
|
border-bottom: 0.15rem solid;
|
||||||
|
transform: translateY(0.38rem) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow2 {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 0.15rem;
|
||||||
|
background-color: currentcolor;
|
||||||
|
transform: translate(0.5rem, -0.25rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/directory/src/state/appbar.js
Normal file
11
apps/directory/src/state/appbar.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
|
||||||
|
const extraAreaAtom = atom([])
|
||||||
|
|
||||||
|
export function useAppbar() {
|
||||||
|
const [extraArea, setExtraArea] = useAtom(extraAreaAtom)
|
||||||
|
return {
|
||||||
|
extraArea,
|
||||||
|
setExtraArea,
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/directory/src/state/config.js
Normal file
31
apps/directory/src/state/config.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import CONFIG from '!/config.json'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
|
||||||
|
const officialUpdateAtom = atom({})
|
||||||
|
let operators = []
|
||||||
|
CONFIG.operators.forEach((item) => {
|
||||||
|
operators = [...operators, ...item]
|
||||||
|
})
|
||||||
|
const OPERATORS = operators
|
||||||
|
|
||||||
|
export function useConfig() {
|
||||||
|
const config = CONFIG
|
||||||
|
const operators = OPERATORS
|
||||||
|
const [officialUpdate, setOfficialUpdate] = useAtom(officialUpdateAtom)
|
||||||
|
|
||||||
|
const fetchOfficialUpdate = useCallback(async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
'https://raw.githubusercontent.com/Halyul/aklive2d/main/official_update.json'
|
||||||
|
)
|
||||||
|
const data = await res.json().catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
return {
|
||||||
|
length: 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setOfficialUpdate(data)
|
||||||
|
}, [setOfficialUpdate])
|
||||||
|
|
||||||
|
return { config, operators, officialUpdate, fetchOfficialUpdate }
|
||||||
|
}
|
||||||
43
apps/directory/src/state/header.js
Normal file
43
apps/directory/src/state/header.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
import { useI18n } from '@/state/language'
|
||||||
|
|
||||||
|
const keyAtom = atom('')
|
||||||
|
const titleAtom = atom('')
|
||||||
|
const tabsAtom = atom([])
|
||||||
|
const currentTabAtom = atom(null)
|
||||||
|
const appbarExtraAreaAtom = atom([])
|
||||||
|
const headerIconAtom = atom(null)
|
||||||
|
const fastNaviationAtom = atom([])
|
||||||
|
|
||||||
|
export function useHeader() {
|
||||||
|
const [key, setTitle] = useAtom(keyAtom)
|
||||||
|
const [title, setRealTitle] = useAtom(titleAtom)
|
||||||
|
const [tabs, setTabs] = useAtom(tabsAtom)
|
||||||
|
const [currentTab, setCurrentTab] = useAtom(currentTabAtom)
|
||||||
|
const [appbarExtraArea, setAppbarExtraArea] = useAtom(appbarExtraAreaAtom)
|
||||||
|
const [headerIcon, setHeaderIcon] = useAtom(headerIconAtom)
|
||||||
|
const [fastNavigation, setFastNavigation] = useAtom(fastNaviationAtom)
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newTitle = i18n(key)
|
||||||
|
document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`
|
||||||
|
setRealTitle(newTitle)
|
||||||
|
}, [i18n, key, setRealTitle])
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
setTitle,
|
||||||
|
tabs,
|
||||||
|
setTabs,
|
||||||
|
currentTab,
|
||||||
|
setCurrentTab,
|
||||||
|
appbarExtraArea,
|
||||||
|
setAppbarExtraArea,
|
||||||
|
headerIcon,
|
||||||
|
setHeaderIcon,
|
||||||
|
fastNavigation,
|
||||||
|
setFastNavigation,
|
||||||
|
}
|
||||||
|
}
|
||||||
20
apps/directory/src/state/insight.js
Normal file
20
apps/directory/src/state/insight.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default (path = null, skipPageView = false) => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!skipPageView && import.meta.env.MODE !== 'development') {
|
||||||
|
try {
|
||||||
|
window.counterscale = {
|
||||||
|
q: [
|
||||||
|
['set', 'siteId', buildConfig.insight_id],
|
||||||
|
['trackPageview', { path }],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
window.counterscaleOnDemandTrack()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn && console.warn(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [path, skipPageView])
|
||||||
|
}
|
||||||
38
apps/directory/src/state/language.js
Normal file
38
apps/directory/src/state/language.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { atom, useAtom, useAtomValue } from 'jotai'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
import i18nObject from '@/i18n'
|
||||||
|
|
||||||
|
const language = i18nObject.available.includes(navigator.language)
|
||||||
|
? navigator.language
|
||||||
|
: 'en-US'
|
||||||
|
|
||||||
|
const textDefaultLang = 'en-US'
|
||||||
|
const languageAtom = atomWithStorage('language', language)
|
||||||
|
const alternateLangAtom = atom((get) => {
|
||||||
|
const language = get(languageAtom)
|
||||||
|
return language.startsWith('en') ? 'zh-CN' : language
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useI18n() {
|
||||||
|
const language = useAtomValue(languageAtom)
|
||||||
|
return {
|
||||||
|
i18n: (key, preferredLanguage = language) => {
|
||||||
|
if (i18nObject.key[key]) {
|
||||||
|
return i18nObject.key[key][preferredLanguage]
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
},
|
||||||
|
i18nValues: i18nObject,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLanguage() {
|
||||||
|
const [language, setLanguage] = useAtom(languageAtom)
|
||||||
|
const alternateLang = useAtomValue(alternateLangAtom)
|
||||||
|
return {
|
||||||
|
textDefaultLang,
|
||||||
|
language,
|
||||||
|
setLanguage,
|
||||||
|
alternateLang,
|
||||||
|
}
|
||||||
|
}
|
||||||
5
apps/directory/stylelint.config.js
Normal file
5
apps/directory/stylelint.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import baseConfig from '@aklive2d/stylelint-config'
|
||||||
|
/** @type {import('stylelint').Config} */
|
||||||
|
export default {
|
||||||
|
...baseConfig,
|
||||||
|
}
|
||||||
45
apps/directory/vite.config.js
Normal file
45
apps/directory/vite.config.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import config from '@aklive2d/config'
|
||||||
|
import * as showcaseDirs from '@aklive2d/showcase'
|
||||||
|
import { copyDirectoryData } from '@aklive2d/vite-helpers'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(async () => {
|
||||||
|
const dataDir = path.resolve(import.meta.dirname, config.dir_name.data)
|
||||||
|
const publicDir = path.resolve(showcaseDirs.DIST_DIR)
|
||||||
|
await copyDirectoryData({ dataDir, publicDir })
|
||||||
|
return {
|
||||||
|
envDir: dataDir,
|
||||||
|
plugins: [react()],
|
||||||
|
publicDir,
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve('./src'),
|
||||||
|
'!': dataDir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
emptyOutDir: false,
|
||||||
|
outDir: publicDir,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: `${config.directory.assets_dir}/[name]-[hash:8].js`,
|
||||||
|
chunkFileNames: `${config.directory.assets_dir}/[name]-[hash:8].js`,
|
||||||
|
assetFileNames: `${config.directory.assets_dir}/[name]-[hash:8].[ext]`,
|
||||||
|
manualChunks: (id) => {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return 'vendor' // all other package goes here
|
||||||
|
} else if (
|
||||||
|
id.includes('data') &&
|
||||||
|
id.includes('.json')
|
||||||
|
) {
|
||||||
|
return 'assets'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
1
apps/module/.eslintignore
Normal file
1
apps/module/.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
libs
|
||||||
1
apps/module/.prettierignore
Normal file
1
apps/module/.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
libs
|
||||||
4
apps/module/index.js
Normal file
4
apps/module/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import './libs/spine-player.css'
|
||||||
|
import spine from './libs/spine-player'
|
||||||
|
|
||||||
|
export { spine }
|
||||||
@@ -2188,42 +2188,64 @@ var spine;
|
|||||||
this.pathPrefix = pathPrefix;
|
this.pathPrefix = pathPrefix;
|
||||||
}
|
}
|
||||||
AssetManager.prototype.downloadText = function (url, success, error) {
|
AssetManager.prototype.downloadText = function (url, success, error) {
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
request.overrideMimeType("text/html");
|
|
||||||
if (this.rawDataUris[url])
|
if (this.rawDataUris[url])
|
||||||
url = this.rawDataUris[url];
|
url = this.rawDataUris[url];
|
||||||
request.open("GET", url, true);
|
fetch(url).then(function (response) {
|
||||||
request.onload = function () {
|
if (!response.ok) {
|
||||||
if (request.status == 200) {
|
error(response.status, response.statusText);
|
||||||
success(request.responseText);
|
|
||||||
}
|
}
|
||||||
else {
|
return response.text();
|
||||||
error(request.status, request.responseText);
|
}).then(function (text) {
|
||||||
}
|
success(text);
|
||||||
};
|
});
|
||||||
request.onerror = function () {
|
|
||||||
error(request.status, request.responseText);
|
// var request = new XMLHttpRequest();
|
||||||
};
|
// request.overrideMimeType("text/html");
|
||||||
request.send();
|
|
||||||
|
// request.open("GET", url, true);
|
||||||
|
// request.onload = function () {
|
||||||
|
// if (request.status == 200) {
|
||||||
|
// success(request.responseText);
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// error(request.status, request.responseText);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// request.onerror = function () {
|
||||||
|
// error(request.status, request.responseText);
|
||||||
|
// };
|
||||||
|
// request.send();
|
||||||
};
|
};
|
||||||
AssetManager.prototype.downloadBinary = function (url, success, error) {
|
AssetManager.prototype.downloadBinary = function (url, success, error) {
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
if (this.rawDataUris[url])
|
if (this.rawDataUris[url])
|
||||||
url = this.rawDataUris[url];
|
url = this.rawDataUris[url];
|
||||||
request.open("GET", url, true);
|
fetch(url).then(function (response) {
|
||||||
request.responseType = "arraybuffer";
|
if (!response.ok) {
|
||||||
request.onload = function () {
|
error(response.status, response.statusText);
|
||||||
if (request.status == 200) {
|
|
||||||
success(new Uint8Array(request.response));
|
|
||||||
}
|
}
|
||||||
else {
|
return response.arrayBuffer();
|
||||||
error(request.status, request.responseText);
|
}).then(function (arrayBuffer) {
|
||||||
}
|
success(new Uint8Array(arrayBuffer));
|
||||||
};
|
});
|
||||||
request.onerror = function () {
|
|
||||||
error(request.status, request.responseText);
|
|
||||||
};
|
// var request = new XMLHttpRequest();
|
||||||
request.send();
|
// if (this.rawDataUris[url])
|
||||||
|
// url = this.rawDataUris[url];
|
||||||
|
// request.open("GET", url, true);
|
||||||
|
// request.responseType = "arraybuffer";
|
||||||
|
// request.onload = function () {
|
||||||
|
// if (request.status == 200) {
|
||||||
|
// success(new Uint8Array(request.response));
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// error(request.status, request.responseText);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// request.onerror = function () {
|
||||||
|
// error(request.status, request.responseText);
|
||||||
|
// };
|
||||||
|
// request.send();
|
||||||
};
|
};
|
||||||
AssetManager.prototype.setRawDataURI = function (path, data) {
|
AssetManager.prototype.setRawDataURI = function (path, data) {
|
||||||
this.rawDataUris[this.pathPrefix + path] = data;
|
this.rawDataUris[this.pathPrefix + path] = data;
|
||||||
7
apps/module/package.json
Normal file
7
apps/module/package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "@aklive2d/module",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js"
|
||||||
|
}
|
||||||
26
apps/showcase/.gitignore
vendored
Normal file
26
apps/showcase/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
data
|
||||||
3
apps/showcase/.prettierignore
Normal file
3
apps/showcase/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
data
|
||||||
|
auto_update
|
||||||
1
apps/showcase/.stylelintignore
Normal file
1
apps/showcase/.stylelintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
spine-player.css
|
||||||
3
apps/showcase/eslint.config.js
Normal file
3
apps/showcase/eslint.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import baseConfig from '@aklive2d/eslint-config'
|
||||||
|
/** @type {import('eslint').Config} */
|
||||||
|
export default [...baseConfig, { ignores: ['src/libs/*'] }]
|
||||||
22
apps/showcase/index.html
Normal file
22
apps/showcase/index.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width,minimum-scale=1,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||||
|
/>
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<title>%VITE_APP_TITLE%</title>
|
||||||
|
<script
|
||||||
|
id="counterscale-script"
|
||||||
|
src="%VITE_INSIGHT_URL%"
|
||||||
|
defer
|
||||||
|
></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
apps/showcase/index.js
Normal file
16
apps/showcase/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import config from '@aklive2d/config'
|
||||||
|
|
||||||
|
export const DATA_DIR = path.resolve(import.meta.dirname, config.dir_name.data)
|
||||||
|
export const PUBLIC_DIR = path.resolve(DATA_DIR, config.dir_name.public)
|
||||||
|
export const PUBLIC_ASSETS_DIR = path.resolve(
|
||||||
|
PUBLIC_DIR,
|
||||||
|
config.dir_name.assets
|
||||||
|
)
|
||||||
|
export const OUT_DIR = path.resolve(import.meta.dirname, config.dir_name.dist)
|
||||||
|
export const DIST_DIR = path.resolve(
|
||||||
|
import.meta.dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
config.dir_name.dist
|
||||||
|
)
|
||||||
9
apps/showcase/jsconfig.json
Normal file
9
apps/showcase/jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"!/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
apps/showcase/package.json
Normal file
26
apps/showcase/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@aklive2d/showcase",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev:showcase": "vite --clearScreen false",
|
||||||
|
"build": "mode=build node runner.js",
|
||||||
|
"preview:showcase": "vite preview",
|
||||||
|
"lint": "eslint \"src/**/*.js\" && stylelint \"**/*.css\" && prettier --check ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.1.0",
|
||||||
|
"@aklive2d/eslint-config": "workspace:*",
|
||||||
|
"@aklive2d/postcss-config": "workspace:*",
|
||||||
|
"@aklive2d/stylelint-config": "workspace:*",
|
||||||
|
"@aklive2d/config": "workspace:*",
|
||||||
|
"@aklive2d/libs": "workspace:*",
|
||||||
|
"@aklive2d/assets": "workspace:*",
|
||||||
|
"@aklive2d/operator": "workspace:*",
|
||||||
|
"@aklive2d/vite-helpers": "workspace:*",
|
||||||
|
"@aklive2d/module": "workspace:*",
|
||||||
|
"@aklive2d/prettier-config": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/showcase/prettier.config.js
Normal file
11
apps/showcase/prettier.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import baseConfig from '@aklive2d/prettier-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("prettier").Config}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
...baseConfig,
|
||||||
|
semi: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
38
apps/showcase/runner.js
Normal file
38
apps/showcase/runner.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import { build as viteBuild } from 'vite'
|
||||||
|
import operators from '@aklive2d/operator'
|
||||||
|
import { envParser, file } from '@aklive2d/libs'
|
||||||
|
import { copyShowcaseData, copyProjectJSON } from '@aklive2d/vite-helpers'
|
||||||
|
import * as dirs from './index.js'
|
||||||
|
|
||||||
|
const build = async (namesToBuild) => {
|
||||||
|
const names = !namesToBuild.length ? Object.keys(operators) : namesToBuild
|
||||||
|
console.log('Generating assets for', names.length, 'operators')
|
||||||
|
for (const name of names) {
|
||||||
|
copyShowcaseData(name, {
|
||||||
|
dataDir: dirs.DATA_DIR,
|
||||||
|
publicAssetsDir: dirs.PUBLIC_ASSETS_DIR,
|
||||||
|
})
|
||||||
|
await viteBuild()
|
||||||
|
const releaseDir = path.join(dirs.DIST_DIR, name)
|
||||||
|
file.mv(dirs.OUT_DIR, releaseDir)
|
||||||
|
file.rm(dirs.DATA_DIR)
|
||||||
|
copyProjectJSON(name, {
|
||||||
|
releaseDir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { name } = envParser.parse({
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'n',
|
||||||
|
multiple: true,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await build(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
257
apps/showcase/src/components/aklive2d.js
Normal file
257
apps/showcase/src/components/aklive2d.js
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import Voice from '@/components/voice'
|
||||||
|
import Fallback from '@/components/fallback'
|
||||||
|
import Music from '@/components/music'
|
||||||
|
import Player from '@/components/player'
|
||||||
|
import Background from '@/components/background'
|
||||||
|
import Logo from '@/components/logo'
|
||||||
|
import Insight from '@/components/insight'
|
||||||
|
import Events from '@/components/events'
|
||||||
|
import {
|
||||||
|
isWebGLSupported,
|
||||||
|
insertHTMLChild,
|
||||||
|
addEventListeners,
|
||||||
|
updateElementPosition,
|
||||||
|
} from '@/components/helper'
|
||||||
|
import '@/components/aklive2d.css'
|
||||||
|
|
||||||
|
export default class AKLive2D {
|
||||||
|
#el = document.createElement('div')
|
||||||
|
#appEl
|
||||||
|
#queries = new URLSearchParams(window.location.search)
|
||||||
|
#voice
|
||||||
|
#music
|
||||||
|
#player
|
||||||
|
#background
|
||||||
|
#logo
|
||||||
|
#configQ = []
|
||||||
|
#isInited = false
|
||||||
|
#isSelfInited = false
|
||||||
|
#isAllInited = false
|
||||||
|
#insight = new Insight()
|
||||||
|
|
||||||
|
constructor(appEl) {
|
||||||
|
console.log(
|
||||||
|
'All resources are extracted from Arknights. Github: https://gura.ch/aklive2d-gh'
|
||||||
|
)
|
||||||
|
|
||||||
|
window.addEventListener('contextmenu', (e) => e.preventDefault())
|
||||||
|
document.addEventListener('gesturestart', (e) => e.preventDefault())
|
||||||
|
|
||||||
|
this.#appEl = appEl
|
||||||
|
this.#logo = new Logo(this.#appEl)
|
||||||
|
this.#background = new Background(this.#appEl)
|
||||||
|
this.#voice = new Voice(this.#appEl)
|
||||||
|
this.#music = new Music(this.#appEl)
|
||||||
|
if (isWebGLSupported()) {
|
||||||
|
this.#player = new Player(this.#appEl)
|
||||||
|
} else {
|
||||||
|
new Fallback(this.#appEl)
|
||||||
|
}
|
||||||
|
addEventListeners([
|
||||||
|
{
|
||||||
|
event: Events.Player.Ready.name,
|
||||||
|
handler: () => this.#selfInited(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: Events.RegisterConfig.name,
|
||||||
|
handler: (e) => this.#registerConfig(e),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
[
|
||||||
|
this.#logo,
|
||||||
|
this.#background,
|
||||||
|
this.#voice,
|
||||||
|
this.#music,
|
||||||
|
this.#player,
|
||||||
|
].map(async (e) => e && (await e.init()))
|
||||||
|
).then(() => this.#allInited())
|
||||||
|
}
|
||||||
|
|
||||||
|
#registerConfig(e) {
|
||||||
|
if (!this.#isInited) {
|
||||||
|
this.#configQ.push(e.detail)
|
||||||
|
} else {
|
||||||
|
this.#applyConfig(e.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#applyConfig(config = null) {
|
||||||
|
if (config) {
|
||||||
|
let targetObj
|
||||||
|
const target = config.target
|
||||||
|
switch (target) {
|
||||||
|
case 'player':
|
||||||
|
targetObj = this.#player
|
||||||
|
break
|
||||||
|
case 'background':
|
||||||
|
targetObj = this.#background
|
||||||
|
break
|
||||||
|
case 'logo':
|
||||||
|
targetObj = this.#logo
|
||||||
|
break
|
||||||
|
case 'music':
|
||||||
|
targetObj = this.#music
|
||||||
|
break
|
||||||
|
case 'voice':
|
||||||
|
targetObj = this.#voice
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetObj.applyConfig(config.key, config.value)
|
||||||
|
} else {
|
||||||
|
this.#configQ.map((e) => this.#applyConfig(e))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
get voice() {
|
||||||
|
return this.#voice
|
||||||
|
}
|
||||||
|
|
||||||
|
get music() {
|
||||||
|
return this.#music
|
||||||
|
}
|
||||||
|
|
||||||
|
get player() {
|
||||||
|
return this.#player
|
||||||
|
}
|
||||||
|
|
||||||
|
get background() {
|
||||||
|
return this.#background
|
||||||
|
}
|
||||||
|
|
||||||
|
get logo() {
|
||||||
|
return this.#logo
|
||||||
|
}
|
||||||
|
|
||||||
|
get events() {
|
||||||
|
return Events
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return {
|
||||||
|
player: this.#player.config,
|
||||||
|
background: this.#background.config,
|
||||||
|
logo: this.#logo.config,
|
||||||
|
music: this.#music.config,
|
||||||
|
voice: this.#voice.config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get configStr() {
|
||||||
|
return JSON.stringify(this.config, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.#el.hidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.#el.hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.#player.reset()
|
||||||
|
this.#background.reset()
|
||||||
|
this.#logo.reset()
|
||||||
|
this.#voice.reset()
|
||||||
|
this.#music.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
#allInited() {
|
||||||
|
this.#isAllInited = true
|
||||||
|
if (this.#isSelfInited) {
|
||||||
|
this.#success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#selfInited() {
|
||||||
|
this.#isSelfInited = true
|
||||||
|
if (this.#isAllInited) {
|
||||||
|
this.#success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#success() {
|
||||||
|
this.#isInited = true
|
||||||
|
this.#el.id = 'settings-box'
|
||||||
|
this.#el.hidden = true
|
||||||
|
this.#el.innerHTML = `
|
||||||
|
<div>
|
||||||
|
${this.#logo.HTML}
|
||||||
|
${this.#background.HTML}
|
||||||
|
${this.#player.HTML}
|
||||||
|
${this.#music.HTML}
|
||||||
|
${this.#voice.HTML}
|
||||||
|
<div>
|
||||||
|
<button type="button" id="settings-reset">Reset</button>
|
||||||
|
<button type="button" id="settings-close">Close</button>
|
||||||
|
<button type="button" id="settings-to-directory">Back to Directory</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
insertHTMLChild(this.#appEl, this.#el)
|
||||||
|
addEventListeners([
|
||||||
|
{
|
||||||
|
id: 'settings-reset',
|
||||||
|
event: 'click',
|
||||||
|
handler: () => this.reset(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings-close',
|
||||||
|
event: 'click',
|
||||||
|
handler: () => this.close(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings-to-directory',
|
||||||
|
event: 'click',
|
||||||
|
handler: () => {
|
||||||
|
window.location.href = '/'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...this.#logo.listeners,
|
||||||
|
...this.#background.listeners,
|
||||||
|
...this.#player.listeners,
|
||||||
|
...this.#voice.listeners,
|
||||||
|
...this.#music.listeners,
|
||||||
|
...this.#insight.listeners,
|
||||||
|
])
|
||||||
|
|
||||||
|
this.#music.link(this.#background)
|
||||||
|
this.#background.link(this.#music)
|
||||||
|
this.#voice.link(this.#player)
|
||||||
|
this.#player.success()
|
||||||
|
this.#voice.success()
|
||||||
|
this.#music.success()
|
||||||
|
this.#insight.success()
|
||||||
|
|
||||||
|
this.#applyConfig()
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.#queries.has('aklive2d') ||
|
||||||
|
import.meta.env.MODE === 'development'
|
||||||
|
) {
|
||||||
|
this.open()
|
||||||
|
}
|
||||||
|
this.#registerBackCompatibilityFns()
|
||||||
|
}
|
||||||
|
|
||||||
|
#registerBackCompatibilityFns() {
|
||||||
|
const _this = this
|
||||||
|
window.voice = _this.#voice
|
||||||
|
window.music = _this.#music
|
||||||
|
window.settings = {
|
||||||
|
elementPosition: updateElementPosition,
|
||||||
|
open: _this.open,
|
||||||
|
close: _this.close,
|
||||||
|
reset: _this.reset,
|
||||||
|
..._this.#player.backCompatibilityFns,
|
||||||
|
..._this.#logo.backCompatibilityFns,
|
||||||
|
..._this.#music.backCompatibilityFns,
|
||||||
|
..._this.#background.backCompatibilityFns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
apps/showcase/src/components/background.css
Normal file
23
apps/showcase/src/components/background.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#background-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
z-index: -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-box #video-src {
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
298
apps/showcase/src/components/background.js
Normal file
298
apps/showcase/src/components/background.js
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
import {
|
||||||
|
readFile,
|
||||||
|
updateHTMLOptions,
|
||||||
|
showRelatedHTML,
|
||||||
|
syncHTMLValue,
|
||||||
|
insertHTMLChild,
|
||||||
|
} from '@/components/helper'
|
||||||
|
import '@/components/background.css'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default class Background {
|
||||||
|
#el = document.createElement('div')
|
||||||
|
#parentEl
|
||||||
|
#videoEl
|
||||||
|
#default = {
|
||||||
|
location: `${import.meta.env.BASE_URL}assets/${buildConfig.background_folder}/`,
|
||||||
|
image: buildConfig.default_background,
|
||||||
|
}
|
||||||
|
#config = {
|
||||||
|
video: {
|
||||||
|
name: null,
|
||||||
|
volume: 100,
|
||||||
|
},
|
||||||
|
useVideo: false,
|
||||||
|
name: null,
|
||||||
|
}
|
||||||
|
#musicObj
|
||||||
|
|
||||||
|
constructor(el) {
|
||||||
|
this.#parentEl = el
|
||||||
|
this.#el.id = 'background-box'
|
||||||
|
this.image = this.#default.location + this.#default.image
|
||||||
|
this.#el.innerHTML = `
|
||||||
|
<video autoplay loop disablepictureinpicture id="video-src" />
|
||||||
|
`
|
||||||
|
insertHTMLChild(this.#parentEl, this.#el)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.#videoEl = document.getElementById('video-src')
|
||||||
|
}
|
||||||
|
|
||||||
|
resetImage() {
|
||||||
|
document.getElementById('custom-background').value = ''
|
||||||
|
document.getElementById('custom-background-clear').disabled = true
|
||||||
|
this.#config.name = null
|
||||||
|
this.image = this.#default.location + this.#default.image
|
||||||
|
}
|
||||||
|
|
||||||
|
resetVideo() {
|
||||||
|
this.#config.video.name = null
|
||||||
|
this.#videoEl.src = ''
|
||||||
|
document.getElementById('custom-video-background').value = ''
|
||||||
|
document.getElementById('custom-video-background-clear').disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.resetImage()
|
||||||
|
this.resetVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
link(musicObj) {
|
||||||
|
this.#musicObj = musicObj
|
||||||
|
}
|
||||||
|
|
||||||
|
get useVideo() {
|
||||||
|
return this.#config.useVideo
|
||||||
|
}
|
||||||
|
|
||||||
|
set useVideo(v) {
|
||||||
|
this.#config.useVideo = v
|
||||||
|
}
|
||||||
|
|
||||||
|
set image(v) {
|
||||||
|
this.#el.style.backgroundImage = `url("${v}")`
|
||||||
|
}
|
||||||
|
|
||||||
|
set video(v) {
|
||||||
|
if (!v) {
|
||||||
|
this.resetVideo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const update = (url, v = null) => {
|
||||||
|
this.#config.video.name = {
|
||||||
|
isLocalFile: v !== null,
|
||||||
|
value: v ? v.name : url,
|
||||||
|
}
|
||||||
|
this.#videoEl.src = url
|
||||||
|
this.#videoEl.load()
|
||||||
|
document.getElementById('custom-video-background-clear').disabled =
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
readFile(v, (blobURL) => update(blobURL, v))
|
||||||
|
} else {
|
||||||
|
update(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get volume() {
|
||||||
|
return this.#config.video.volume
|
||||||
|
}
|
||||||
|
|
||||||
|
set volume(v) {
|
||||||
|
v = parseInt(v)
|
||||||
|
this.#config.video.volume = v
|
||||||
|
this.#videoEl.volume = v / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
return this.#config.name || this.#default.image
|
||||||
|
}
|
||||||
|
|
||||||
|
set default(v) {
|
||||||
|
this.#default.image = v
|
||||||
|
this.#musicObj.music = v
|
||||||
|
this.image = this.#default.location + this.#default.image
|
||||||
|
}
|
||||||
|
|
||||||
|
set custom(v) {
|
||||||
|
if (!v) {
|
||||||
|
this.resetImage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const update = (url, v = null) => {
|
||||||
|
this.#config.name = {
|
||||||
|
isLocalFile: v !== null,
|
||||||
|
value: v ? v.name : url,
|
||||||
|
}
|
||||||
|
this.image = url
|
||||||
|
document.getElementById('custom-background-clear').disabled = false
|
||||||
|
}
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
readFile(v, (blobURL) => update(blobURL, v))
|
||||||
|
} else {
|
||||||
|
update(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return {
|
||||||
|
default: this.#default.image,
|
||||||
|
...this.#config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get backCompatibilityFns() {
|
||||||
|
const _this = this
|
||||||
|
return {
|
||||||
|
currentBackground: _this.current,
|
||||||
|
setBackgoundImage: (v) => (_this.image = v),
|
||||||
|
setDefaultBackground: (v) => (_this.default = v),
|
||||||
|
setBackground: (v) => (_this.custom = v),
|
||||||
|
resetBackground: _this.resetImage,
|
||||||
|
setVideo: (e) => (_this.video = e.target.files[0]),
|
||||||
|
getVideoVolume: () => _this.volume,
|
||||||
|
setVideoVolume: (v) => (_this.volume = v),
|
||||||
|
setVideoFromWE: (url) => (_this.video = url),
|
||||||
|
resetVideo: _this.resetVideo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get HTML() {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label for="default-background-select">Choose a default background:</label>
|
||||||
|
<select name="default-backgrounds" id="default-background-select">
|
||||||
|
${updateHTMLOptions(buildConfig.background_files, null, this.#default.image)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="custom-background">Custom Background (Store Locally)</label>
|
||||||
|
<input type="file" id="custom-background" accept="image/*"/>
|
||||||
|
<button type="button" id="custom-background-clear" ${this.#config.name ? (this.#config.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="custom-background-url">Custom Background URL:</label>
|
||||||
|
<input type="text" id="custom-background-url" name="custom-background-url" value="${this.#config.name ? this.#config.name.value : ''}">
|
||||||
|
<button type="button" id="custom-background-url-apply">Apply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="video">Video</label>
|
||||||
|
<input type="checkbox" id="video" name="video" ${this.useVideo ? 'checked' : ''}/>
|
||||||
|
<div id="video-realted" ${this.useVideo ? '' : 'hidden'}>
|
||||||
|
<div>
|
||||||
|
<label for="custom-video-background">Custom Video Background (Store Locally)</label>
|
||||||
|
<input type="file" id="custom-video-background" accept="video/*"/>
|
||||||
|
<button type="button" id="custom-video-background-clear" ${this.#config.video.name ? (this.#config.video.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="custom-video-background-url">Custom Video Background URL:</label>
|
||||||
|
<input type="text" id="custom-video-background-url" name="custom-video-background-url" value="${this.#config.video.name ? this.#config.video.name.value : ''}">
|
||||||
|
<button type="button" id="custom-video-background-url-apply">Apply</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="video-volume">Video Volume</label>
|
||||||
|
<input type="range" min="0" max="100" step="1" id="video-volume-slider" value="${this.volume}" />
|
||||||
|
<input type="number" id="video-volume-input" min="0" max="100" step="1" name="video-volume" value="${this.volume}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get listeners() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'default-background-select',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
this.default = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-background',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => (this.custom = e.target.files[0]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-background-clear',
|
||||||
|
event: 'click',
|
||||||
|
handler: () => this.resetImage(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-background-url-apply',
|
||||||
|
event: 'click',
|
||||||
|
handler: () =>
|
||||||
|
(this.custom = document.getElementById(
|
||||||
|
'custom-background-url'
|
||||||
|
).value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'video',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
showRelatedHTML(e.currentTarget, 'video-realted')
|
||||||
|
this.useVideo = e.currentTarget.checked
|
||||||
|
if (!e.currentTarget.checked) this.resetVideo()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-video-background',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => (this.video = e.target.files[0]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-video-background-clear',
|
||||||
|
event: 'click',
|
||||||
|
handler: () => this.resetVideo(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-video-background-url-apply',
|
||||||
|
event: 'click',
|
||||||
|
handler: () =>
|
||||||
|
(this.video = document.getElementById(
|
||||||
|
'custom-video-background-url'
|
||||||
|
).value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'video-volume-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'video-volume-input')
|
||||||
|
this.volume = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'video-volume-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'video-volume-slider')
|
||||||
|
this.volume = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case 'default':
|
||||||
|
this.default = value
|
||||||
|
break
|
||||||
|
case 'custom':
|
||||||
|
this.custom = value
|
||||||
|
break
|
||||||
|
case 'video':
|
||||||
|
this.video = value
|
||||||
|
break
|
||||||
|
case 'volume':
|
||||||
|
this.volume = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/showcase/src/components/events.js
Normal file
11
apps/showcase/src/components/events.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createCustomEvent } from '@/components/helper'
|
||||||
|
import { Events as Insight } from '@/components/insight'
|
||||||
|
import { Events as Player } from '@/components/player'
|
||||||
|
|
||||||
|
const RegisterConfig = createCustomEvent('register-config', true)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Insight,
|
||||||
|
Player,
|
||||||
|
RegisterConfig,
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fallback {
|
#fallback {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
40
apps/showcase/src/components/fallback.js
Normal file
40
apps/showcase/src/components/fallback.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { insertHTMLChild } from '@/components/helper'
|
||||||
|
import '@/components/fallback.css'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default class Fallback {
|
||||||
|
#el = document.createElement('div')
|
||||||
|
|
||||||
|
constructor(parentEl) {
|
||||||
|
alert('WebGL is unavailable. Fallback image will be used.')
|
||||||
|
const calculateScale = (width, height) => {
|
||||||
|
return {
|
||||||
|
x: window.innerWidth / width,
|
||||||
|
y: window.innerHeight / height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fallback = () => {
|
||||||
|
const el = document.getElementById('fallback-container')
|
||||||
|
const scale = calculateScale(
|
||||||
|
buildConfig.image_width,
|
||||||
|
buildConfig.image_height
|
||||||
|
)
|
||||||
|
el.style.width = '100%'
|
||||||
|
el.style.height =
|
||||||
|
buildConfig.image_height *
|
||||||
|
(scale.x > scale.y ? scale.y : scale.x) +
|
||||||
|
'px'
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', fallback, true)
|
||||||
|
this.#el.id = 'fallback-box'
|
||||||
|
this.#el.innerHTML = `
|
||||||
|
<div id="fallback-container">
|
||||||
|
<div id="fallback"
|
||||||
|
style="background-image: url(./assets/${buildConfig.fallback_name}.png)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
insertHTMLChild(parentEl, this.#el)
|
||||||
|
fallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
101
apps/showcase/src/components/helper.js
Normal file
101
apps/showcase/src/components/helper.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
export const isWebGLSupported = () => {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx =
|
||||||
|
canvas.getContext('webgl') ||
|
||||||
|
canvas.getContext('experimental-webgl')
|
||||||
|
return ctx != null
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertHTMLChild = (parent, child) => {
|
||||||
|
parent.appendChild(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertHTMLNodeBefore = (parent, sibling, child) => {
|
||||||
|
parent.insertBefore(child, sibling)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIntPx = (value) => parseInt(value.replace('px', ''))
|
||||||
|
export const updateElementPosition = (el, position) => {
|
||||||
|
const computedStyle = getComputedStyle(el)
|
||||||
|
const elWidth = getIntPx(computedStyle.width)
|
||||||
|
const elHeight = getIntPx(computedStyle.height)
|
||||||
|
const elMarginLeft = getIntPx(computedStyle.marginLeft)
|
||||||
|
const elMarginRight = getIntPx(computedStyle.marginRight)
|
||||||
|
const windowWidth = window.innerWidth
|
||||||
|
const windowHeight = window.innerHeight
|
||||||
|
const xRange = windowWidth - (elWidth + elMarginLeft + elMarginRight)
|
||||||
|
const yRange = windowHeight - elHeight
|
||||||
|
const xpx = (position.x * xRange) / 100
|
||||||
|
const ypx = (position.y * yRange) / 100
|
||||||
|
el.style.transform = `translate(${xpx}px, ${ypx}px)`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateHTMLOptions = (array, id = null, selected = null) => {
|
||||||
|
const value = array.map(
|
||||||
|
(item) =>
|
||||||
|
`<option value="${item}" ${item === selected ? 'selected' : ''}>${item}</option>`
|
||||||
|
)
|
||||||
|
if (id) {
|
||||||
|
document.getElementById(id).innerHTML = value.join('')
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addEventListeners = (listeners) => {
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
if (listener.id) {
|
||||||
|
document
|
||||||
|
.getElementById(listener.id)
|
||||||
|
.addEventListener(listener.event, (e) => listener.handler(e))
|
||||||
|
} else {
|
||||||
|
document.addEventListener(listener.event, (e) =>
|
||||||
|
listener.handler(e)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showRelatedHTML = (e, relatedSettingsID, revert = false) => {
|
||||||
|
const eRelatedSettings = document.getElementById(relatedSettingsID)
|
||||||
|
const checked = revert ? !e.checked : e.checked
|
||||||
|
if (checked) {
|
||||||
|
eRelatedSettings.hidden = false
|
||||||
|
} else {
|
||||||
|
eRelatedSettings.hidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCurrentHTMLOptions = (id, value) => {
|
||||||
|
const e = document.getElementById(id)
|
||||||
|
const options = [...e]
|
||||||
|
const toSelecteIndex = options.findIndex(
|
||||||
|
(i) => options.find((o) => o.value === value) === i
|
||||||
|
)
|
||||||
|
e.selectedIndex = toSelecteIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncHTMLValue = (source, targetID) => {
|
||||||
|
if (typeof source === 'string') source = document.getElementById(source)
|
||||||
|
document.getElementById(targetID).value = source.value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readFile = (file, callback = () => {}) => {
|
||||||
|
if (!file) return
|
||||||
|
callback(URL.createObjectURL(file.slice()), file.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCustomEvent = (name, withArg = false) => {
|
||||||
|
const ret = {
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
if (withArg) {
|
||||||
|
ret.handler = (detail) => new CustomEvent(name, { detail })
|
||||||
|
} else {
|
||||||
|
ret.handler = () => new Event(name)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
46
apps/showcase/src/components/insight.js
Normal file
46
apps/showcase/src/components/insight.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { createCustomEvent } from '@/components/helper'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default class Insight {
|
||||||
|
#isInsightInited = false
|
||||||
|
|
||||||
|
success() {
|
||||||
|
this.insight(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
insight(doNotTrack, isFromWallpaperEngine = false) {
|
||||||
|
if (this.#isInsightInited || import.meta.env.MODE === 'development')
|
||||||
|
return
|
||||||
|
this.#isInsightInited = true
|
||||||
|
if (doNotTrack) return
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
path: `/${buildConfig.link}`,
|
||||||
|
}
|
||||||
|
if (isFromWallpaperEngine)
|
||||||
|
config.hostname = 'file://wallpaperengine.local'
|
||||||
|
window.counterscale = {
|
||||||
|
q: [
|
||||||
|
['set', 'siteId', buildConfig.insight_id],
|
||||||
|
['trackPageview', config],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
window.counterscaleOnDemandTrack()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn && console.warn(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get listeners() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
event: Events.Register.name,
|
||||||
|
handler: (e) => this.insight(e.detail, true),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Events = {
|
||||||
|
Register: createCustomEvent('insight-register', true),
|
||||||
|
}
|
||||||
6
apps/showcase/src/components/logo.css
Normal file
6
apps/showcase/src/components/logo.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#logo-box {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
411
apps/showcase/src/components/logo.js
Normal file
411
apps/showcase/src/components/logo.js
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import {
|
||||||
|
insertHTMLChild,
|
||||||
|
updateElementPosition,
|
||||||
|
readFile,
|
||||||
|
showRelatedHTML,
|
||||||
|
syncHTMLValue,
|
||||||
|
} from '@/components/helper'
|
||||||
|
import '@/components/logo.css'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default class Logo {
|
||||||
|
#el = document.createElement('div')
|
||||||
|
#imageEl
|
||||||
|
#parentEl
|
||||||
|
#default = {
|
||||||
|
location: `${import.meta.env.BASE_URL}assets/`,
|
||||||
|
image: `${buildConfig.logo_filename}.png`,
|
||||||
|
useInvertFilter: buildConfig.invert_filter === 'true',
|
||||||
|
ratio: 61.8,
|
||||||
|
opacity: 30,
|
||||||
|
hidden: false,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
#config = {
|
||||||
|
hidden: this.#default.hidden,
|
||||||
|
ratio: this.#default.ratio,
|
||||||
|
opacity: this.#default.opacity,
|
||||||
|
position: { ...this.#default.position },
|
||||||
|
name: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(el) {
|
||||||
|
this.#parentEl = el
|
||||||
|
this.#el.id = 'logo-box'
|
||||||
|
this.#el.innerHTML = `
|
||||||
|
<img src="${this.#default.location + this.#default.image}" id="logo" alt="operator logo" />
|
||||||
|
`
|
||||||
|
insertHTMLChild(this.#parentEl, this.#el)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.#imageEl = document.getElementById('logo')
|
||||||
|
this.#setInvertFilter(this.#default.useInvertFilter)
|
||||||
|
this.opacity = this.#default.opacity
|
||||||
|
this.#updateSizeOnWindowResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(src, invertFilter = false) {
|
||||||
|
this.#imageEl.src = src
|
||||||
|
this.#resize()
|
||||||
|
this.#setInvertFilter(invertFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPosition() {
|
||||||
|
this.position = { ...this.#default.position }
|
||||||
|
document.getElementById('logo-position-x-slider').value =
|
||||||
|
this.#default.position.x
|
||||||
|
document.getElementById('logo-position-x-input').value =
|
||||||
|
this.#default.position.x
|
||||||
|
document.getElementById('logo-position-y-slider').value =
|
||||||
|
this.#default.position.y
|
||||||
|
document.getElementById('logo-position-y-input').value =
|
||||||
|
this.#default.position.y
|
||||||
|
}
|
||||||
|
|
||||||
|
resetImage() {
|
||||||
|
this.#config.name = null
|
||||||
|
this.setImage(
|
||||||
|
this.#default.location + this.#default.image,
|
||||||
|
this.#default.useInvertFilter
|
||||||
|
)
|
||||||
|
document.getElementById('logo-image-clear').disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resetOpacity() {
|
||||||
|
this.opacity = this.#default.opacity
|
||||||
|
document.getElementById('logo-opacity-slider').value =
|
||||||
|
this.#default.opacity
|
||||||
|
document.getElementById('logo-opacity-input').value =
|
||||||
|
this.#default.opacity
|
||||||
|
}
|
||||||
|
|
||||||
|
resetHidden() {
|
||||||
|
this.hidden = this.#default.hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRatio() {
|
||||||
|
this.ratio = this.#default.ratio
|
||||||
|
document.getElementById('logo-ratio-slider').value = this.#default.ratio
|
||||||
|
document.getElementById('logo-ratio-input').value = this.#default.ratio
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.resetPosition()
|
||||||
|
this.resetImage()
|
||||||
|
this.resetRatio()
|
||||||
|
this.resetOpacity()
|
||||||
|
this.resetHidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
#resize(_this, value) {
|
||||||
|
_this = _this || this
|
||||||
|
_this.#imageEl.width =
|
||||||
|
((window.innerWidth / 2) * (value || _this.ratio)) / 100
|
||||||
|
updateElementPosition(_this.#imageEl, _this.#config.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
#setInvertFilter(v) {
|
||||||
|
if (!v) {
|
||||||
|
this.#imageEl.style.filter = 'invert(0)'
|
||||||
|
} else {
|
||||||
|
this.#imageEl.style.filter = 'invert(1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateLogoPosition() {
|
||||||
|
updateElementPosition(this.#imageEl, this.#config.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateSizeOnWindowResize() {
|
||||||
|
const _this = this
|
||||||
|
const resize = () => {
|
||||||
|
_this.#resize(_this)
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', resize, true)
|
||||||
|
resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
set image(v) {
|
||||||
|
if (!v) {
|
||||||
|
this.resetImage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const update = (url, v = null) => {
|
||||||
|
this.#config.name = {
|
||||||
|
isLocalFile: v !== null,
|
||||||
|
value: v ? v.name : url,
|
||||||
|
}
|
||||||
|
this.setImage(url, false)
|
||||||
|
document.getElementById('logo-image-clear').disabled = false
|
||||||
|
}
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
readFile(v, (blobURL) => update(blobURL, v))
|
||||||
|
} else {
|
||||||
|
update(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hidden() {
|
||||||
|
return this.#config.hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
set hidden(v) {
|
||||||
|
this.#config.hidden = v
|
||||||
|
this.#imageEl.hidden = v
|
||||||
|
}
|
||||||
|
|
||||||
|
get ratio() {
|
||||||
|
return this.#config.ratio
|
||||||
|
}
|
||||||
|
|
||||||
|
set ratio(v) {
|
||||||
|
v = parseInt(v)
|
||||||
|
this.#config.ratio = v
|
||||||
|
this.#resize(this, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
get opacity() {
|
||||||
|
return this.#config.opacity
|
||||||
|
}
|
||||||
|
|
||||||
|
set opacity(v) {
|
||||||
|
v = parseInt(v)
|
||||||
|
this.#imageEl.style.opacity = v / 100
|
||||||
|
this.#config.opacity = v
|
||||||
|
}
|
||||||
|
|
||||||
|
get x() {
|
||||||
|
return this.position.x
|
||||||
|
}
|
||||||
|
|
||||||
|
set x(v) {
|
||||||
|
this.position = {
|
||||||
|
x: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get y() {
|
||||||
|
return this.position.y
|
||||||
|
}
|
||||||
|
|
||||||
|
set y(v) {
|
||||||
|
this.position = {
|
||||||
|
y: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get position() {
|
||||||
|
return this.#config.position
|
||||||
|
}
|
||||||
|
|
||||||
|
set position(v) {
|
||||||
|
if (typeof v !== 'object') return
|
||||||
|
if (v.x) v.x = parseInt(v.x)
|
||||||
|
if (v.y) v.y = parseInt(v.y)
|
||||||
|
this.#config.position = { ...this.#config.position, ...v }
|
||||||
|
this.#updateLogoPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
get backCompatibilityFns() {
|
||||||
|
const _this = this
|
||||||
|
return {
|
||||||
|
setLogoDisplay: (v) => (_this.hidden = v),
|
||||||
|
setLogo: _this.setImage,
|
||||||
|
setLogoImage: (e) => (_this.image = e.target.files[0]),
|
||||||
|
resetLogoImage: _this.resetImage,
|
||||||
|
setLogoRatio: (v) => (_this.ratio = v),
|
||||||
|
setLogoOpacity: (v) => (_this.opacity = v),
|
||||||
|
logoPadding: (key, value) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'x':
|
||||||
|
this.position = {
|
||||||
|
x: value,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'y':
|
||||||
|
this.position = {
|
||||||
|
y: value,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.position = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logoReset: _this.resetPosition,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return {
|
||||||
|
...this.#config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get HTML() {
|
||||||
|
return `
|
||||||
|
<label for="operator-logo">Operator Logo</label>
|
||||||
|
<input type="checkbox" id="operator-logo" name="operator-logo" ${this.hidden ? '' : 'checked'}/>
|
||||||
|
<div id="operator-logo-realted" ${this.hidden ? 'hidden' : ''}>
|
||||||
|
<div>
|
||||||
|
<label for="logo-image">Logo Image (Store Locally)</label>
|
||||||
|
<input type="file" id="logo-image" accept="image/*"/>
|
||||||
|
<button type="button" id="logo-image-clear" ${this.#config.name ? (this.#config.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="logo-image-url">Logo Image URL:</label>
|
||||||
|
<input type="text" id="logo-image-url" name="logo-image-url" value="${this.#config.name ? this.#config.name.value : ''}">
|
||||||
|
<button type="button" id="logo-image-url-apply">Apply</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="logo-ratio">Logo Ratio</label>
|
||||||
|
<input type="range" min="0" max="100" step="0.1" id="logo-ratio-slider" value="${this.ratio}" />
|
||||||
|
<input type="number" id="logo-ratio-input" name="logo-ratio" value="${this.ratio}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="logo-opacity">Logo Opacity</label>
|
||||||
|
<input type="range" min="0" max="100" data-css-class="logo" step="1" id="logo-opacity-slider" value="${this.opacity}" />
|
||||||
|
<input type="number" id="logo-opacity-input" name="logo-opacity" value="${this.opacity}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="logo-position-x">Logo X Position</label>
|
||||||
|
<input type="range" min="0" max="100" id="logo-position-x-slider" value="${this.position.x}" />
|
||||||
|
<input type="number" id="logo-position-x-input" name="logo-position-x" value="${this.position.x}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="logo-position-y">Logo Y Position</label>
|
||||||
|
<input type="range" min="0" max="100" id="logo-position-y-slider" value="${this.position.y}" />
|
||||||
|
<input type="number" id="logo-position-y-input" name="logo-position-y" value="${this.position.y}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get listeners() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'operator-logo',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
showRelatedHTML(e.currentTarget, 'operator-logo-realted')
|
||||||
|
this.hidden = !e.currentTarget.checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-image',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => (this.image = e.target.files[0]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-image-clear',
|
||||||
|
event: 'click',
|
||||||
|
handler: () => this.resetImage(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-image-url-apply',
|
||||||
|
event: 'click',
|
||||||
|
handler: () =>
|
||||||
|
(this.image =
|
||||||
|
document.getElementById('logo-image-url').value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-ratio-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-ratio-input')
|
||||||
|
this.ratio = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-ratio-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-ratio-slider')
|
||||||
|
this.ratio = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-opacity-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-opacity-input')
|
||||||
|
this.opacity = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-opacity-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-opacity-slider')
|
||||||
|
this.opacity = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-position-x-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-position-x-input')
|
||||||
|
this.position = {
|
||||||
|
x: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-position-x-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-position-x-slider')
|
||||||
|
this.position = {
|
||||||
|
x: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-position-y-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-position-y-input')
|
||||||
|
this.position = {
|
||||||
|
y: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logo-position-y-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'logo-position-y-slider')
|
||||||
|
this.position = {
|
||||||
|
y: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case 'hidden':
|
||||||
|
this.hidden = value
|
||||||
|
break
|
||||||
|
case 'ratio':
|
||||||
|
this.ratio = value
|
||||||
|
break
|
||||||
|
case 'opacity':
|
||||||
|
this.opacity = value
|
||||||
|
break
|
||||||
|
case 'image':
|
||||||
|
this.image = value
|
||||||
|
break
|
||||||
|
case 'position':
|
||||||
|
this.position = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
365
apps/showcase/src/components/music.js
Normal file
365
apps/showcase/src/components/music.js
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import {
|
||||||
|
insertHTMLChild,
|
||||||
|
updateHTMLOptions,
|
||||||
|
showRelatedHTML,
|
||||||
|
syncHTMLValue,
|
||||||
|
readFile,
|
||||||
|
getCurrentHTMLOptions,
|
||||||
|
} from '@/components/helper'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default class Music {
|
||||||
|
#el = document.createElement('div')
|
||||||
|
#parentEl
|
||||||
|
#audio = {
|
||||||
|
intro: {
|
||||||
|
id: 'music-intro',
|
||||||
|
el: null,
|
||||||
|
},
|
||||||
|
loop: {
|
||||||
|
id: 'music-loop',
|
||||||
|
el: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
#music = {
|
||||||
|
mapping: buildConfig.music_mapping,
|
||||||
|
location: buildConfig.music_folder,
|
||||||
|
current: null,
|
||||||
|
isUsingCustom: false,
|
||||||
|
list: [],
|
||||||
|
}
|
||||||
|
#config = {
|
||||||
|
useMusic: false,
|
||||||
|
timeOffset: 0.3,
|
||||||
|
volume: 50,
|
||||||
|
name: null,
|
||||||
|
}
|
||||||
|
#backgroundObj
|
||||||
|
|
||||||
|
constructor(el) {
|
||||||
|
this.#parentEl = el
|
||||||
|
this.#el.id = 'music-box'
|
||||||
|
this.#el.innerHTML = `
|
||||||
|
<audio id="${this.#audio.intro.id}" preload="auto">
|
||||||
|
<source type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
<audio id="${this.#audio.loop.id}" preload="auto">
|
||||||
|
<source type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
`
|
||||||
|
insertHTMLChild(this.#parentEl, this.#el)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.#music.list = Object.keys(this.#music.mapping)
|
||||||
|
this.#audio.intro.el = document.getElementById(this.#audio.intro.id)
|
||||||
|
this.#audio.loop.el = document.getElementById(this.#audio.loop.id)
|
||||||
|
this.#audio.intro.el.volume = this.#volume
|
||||||
|
this.#audio.loop.el.volume = this.#volume
|
||||||
|
this.#audio.intro.el.ontimeupdate = () => {
|
||||||
|
if (
|
||||||
|
this.#audio.intro.el.currentTime >=
|
||||||
|
this.#audio.intro.el.duration - this.#config.timeOffset
|
||||||
|
) {
|
||||||
|
this.#audio.intro.el.pause()
|
||||||
|
this.#audio.loop.el.currentTime = 0
|
||||||
|
this.#audio.loop.el.volume = this.#volume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#audio.loop.el.ontimeupdate = () => {
|
||||||
|
if (
|
||||||
|
this.#audio.loop.el.currentTime >=
|
||||||
|
this.#audio.loop.el.duration - this.#config.timeOffset
|
||||||
|
) {
|
||||||
|
this.#audio.loop.el.currentTime = 0
|
||||||
|
this.#audio.loop.el.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
if (this.#music.current === null)
|
||||||
|
this.music = this.#backgroundObj.current
|
||||||
|
}
|
||||||
|
|
||||||
|
link(backgroundObj) {
|
||||||
|
this.#backgroundObj = backgroundObj
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
document.getElementById('custom-music').value = ''
|
||||||
|
document.getElementById('custom-music-clear').disabled = true
|
||||||
|
this.#music.isUsingCustom = false
|
||||||
|
this.#config.name = null
|
||||||
|
if (this.#config.useMusic) {
|
||||||
|
this.#playMusic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setMusic(data, type) {
|
||||||
|
this.#audio.loop.el.src = data
|
||||||
|
this.#audio.loop.el.querySelector('source').type = type
|
||||||
|
this.#music.isUsingCustom = true
|
||||||
|
this.#playMusic()
|
||||||
|
}
|
||||||
|
|
||||||
|
#playMusic() {
|
||||||
|
if (!this.#music.isUsingCustom) {
|
||||||
|
const introOgg = this.#music.mapping[this.#music.current].intro
|
||||||
|
const intro = `./assets/${this.#music.location}/${introOgg}`
|
||||||
|
const loop = `./assets/${this.#music.location}/${this.#music.mapping[this.#music.current].loop}`
|
||||||
|
this.#audio.loop.el.src = loop
|
||||||
|
this.#audio.loop.el.querySelector('source').type = 'audio/ogg'
|
||||||
|
if (introOgg) {
|
||||||
|
this.#audio.intro.el.src = intro || loop
|
||||||
|
this.#audio.intro.el.querySelector('source').type = 'audio/ogg'
|
||||||
|
this.#audio.intro.el.play()
|
||||||
|
this.#audio.loop.el.volume = 0
|
||||||
|
this.#audio.loop.el.play()
|
||||||
|
} else {
|
||||||
|
this.#audio.loop.el.volume = this.#volume
|
||||||
|
this.#audio.loop.el.play()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#audio.intro.el.pause()
|
||||||
|
this.#audio.loop.el.volume = this.#volume
|
||||||
|
this.#audio.loop.el.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#stopMusic() {
|
||||||
|
this.#audio.intro.el.pause()
|
||||||
|
this.#audio.loop.el.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
get timeOffset() {
|
||||||
|
return this.#config.timeOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
set timeOffset(value) {
|
||||||
|
value = value < 0 ? 0 : parseFloat(value)
|
||||||
|
this.#config.timeOffset = value
|
||||||
|
}
|
||||||
|
|
||||||
|
get volume() {
|
||||||
|
return this.#config.volume
|
||||||
|
}
|
||||||
|
|
||||||
|
get #volume() {
|
||||||
|
return this.#config.volume / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
set volume(value) {
|
||||||
|
value = value < 0 ? 0 : value > 100 ? 100 : parseInt(value)
|
||||||
|
this.#config.volume = value
|
||||||
|
this.#audio.intro.el.volume = this.#volume
|
||||||
|
if (this.#audio.intro.el.paused)
|
||||||
|
this.#audio.loop.el.volume = this.#volume
|
||||||
|
}
|
||||||
|
|
||||||
|
get musics() {
|
||||||
|
return this.#music.list
|
||||||
|
}
|
||||||
|
|
||||||
|
get useMusic() {
|
||||||
|
return this.#config.useMusic
|
||||||
|
}
|
||||||
|
|
||||||
|
set useMusic(value) {
|
||||||
|
this.#config.useMusic = value
|
||||||
|
if (value) {
|
||||||
|
this.#playMusic()
|
||||||
|
} else {
|
||||||
|
this.#stopMusic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get music() {
|
||||||
|
return this.#music.current
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUsingCustom() {
|
||||||
|
return this.#music.isUsingCustom
|
||||||
|
}
|
||||||
|
|
||||||
|
set music(name) {
|
||||||
|
if (name !== null && name !== this.#music.current) {
|
||||||
|
this.#music.current = name
|
||||||
|
if (this.#config.useMusic && !this.#music.isUsingCustom) {
|
||||||
|
this.#audio.loop.el.pause()
|
||||||
|
this.#audio.intro.el.pause()
|
||||||
|
this.#playMusic()
|
||||||
|
}
|
||||||
|
getCurrentHTMLOptions('music-select', name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set custom(url) {
|
||||||
|
if (!url) {
|
||||||
|
this.reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const update = (url, type, v = null) => {
|
||||||
|
this.#config.name = {
|
||||||
|
isLocalFile: v !== null,
|
||||||
|
value: v ? v.name : url,
|
||||||
|
}
|
||||||
|
this.#setMusic(url, type)
|
||||||
|
document.getElementById('custom-music-clear').disabled = false
|
||||||
|
}
|
||||||
|
if (typeof url === 'object') {
|
||||||
|
readFile(url, (blobURL, type) => update(blobURL, type, url))
|
||||||
|
} else {
|
||||||
|
update(url, url.split('.').pop())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentMusic() {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
return this.music
|
||||||
|
}
|
||||||
|
|
||||||
|
changeMusic(name) {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
this.music = name
|
||||||
|
}
|
||||||
|
|
||||||
|
get backCompatibilityFns() {
|
||||||
|
const _this = this
|
||||||
|
return {
|
||||||
|
setMusicFromWE: (url) => (_this.custom = url),
|
||||||
|
setMusic: (e) => (_this.custom = e.target.files[0]),
|
||||||
|
resetMusic: _this.reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return {
|
||||||
|
default: this.#music.current,
|
||||||
|
...this.#config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get HTML() {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<label for="music">Music</label>
|
||||||
|
<input type="checkbox" id="music" name="music" ${this.useMusic ? 'checked' : ''}/>
|
||||||
|
<div id="music-realted" ${this.useMusic ? '' : 'hidden'}>
|
||||||
|
<div>
|
||||||
|
<label for="music-select">Choose theme music:</label>
|
||||||
|
<select name="music-select" id="music-select">
|
||||||
|
${updateHTMLOptions(this.musics)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="custom-music">Custom Music (Store Locally)</label>
|
||||||
|
<input type="file" id="custom-music" accept="audio/*"/>
|
||||||
|
<button type="button" id="custom-music-clear" ${this.#config.name ? (this.#config.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="custom-music-url">Custom Music URL:</label>
|
||||||
|
<input type="text" id="custom-music-url" name="custom-music-url" value="${this.#config.name ? this.#config.name.value : ''}">
|
||||||
|
<button type="button" id="custom-music-url-apply">Apply</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="music-volume">Music Volume</label>
|
||||||
|
<input type="range" min="0" max="100" step="1" id="music-volume-slider" value="${this.volume}" />
|
||||||
|
<input type="number" id="music-volume-input" min="0" max="100" step="1" name="music-volume" value="${this.volume}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="music-switch-offset">Music Swtich Offset</label>
|
||||||
|
<input type="range" min="0" max="1" step="0.01" id="music-switch-offset-slider" value="${this.timeOffset}" />
|
||||||
|
<input type="number" id="music-switch-offset-input" min="0" max="1" step="0.01" name="music-switch-offset" value="${this.timeOffset}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get listeners() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'music',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
showRelatedHTML(e.currentTarget, 'music-realted')
|
||||||
|
this.useMusic = e.currentTarget.checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'music-select',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => (this.music = e.currentTarget.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-music',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => (this.custom = e.target.files[0]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-music-clear',
|
||||||
|
event: 'click',
|
||||||
|
handler: () => this.reset(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom-music-url-apply',
|
||||||
|
event: 'click',
|
||||||
|
handler: () =>
|
||||||
|
(this.custom =
|
||||||
|
document.getElementById('custom-music-url').value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'music-volume-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'music-volume-input')
|
||||||
|
this.volume = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'music-volume-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'music-volume-slider')
|
||||||
|
this.volume = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'music-switch-offset-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'music-switch-offset-input')
|
||||||
|
this.timeOffset = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'music-switch-offset-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'music-switch-offset-slider')
|
||||||
|
this.timeOffset = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case 'music':
|
||||||
|
this.music = value
|
||||||
|
break
|
||||||
|
case 'use-music':
|
||||||
|
this.useMusic = value
|
||||||
|
break
|
||||||
|
case 'volume':
|
||||||
|
this.volume = value
|
||||||
|
break
|
||||||
|
case 'custom':
|
||||||
|
this.custom = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
588
apps/showcase/src/components/player.js
Normal file
588
apps/showcase/src/components/player.js
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
import {
|
||||||
|
insertHTMLChild,
|
||||||
|
updateHTMLOptions,
|
||||||
|
showRelatedHTML,
|
||||||
|
syncHTMLValue,
|
||||||
|
createCustomEvent,
|
||||||
|
} from '@/components/helper'
|
||||||
|
import { spine } from '@aklive2d/module'
|
||||||
|
import '@/components/player.css'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default class Player {
|
||||||
|
#el = document.createElement('div')
|
||||||
|
#parentEl
|
||||||
|
#showControls = new URLSearchParams(window.location.search).has('controls')
|
||||||
|
#resetTime = window.performance.now()
|
||||||
|
#isPlayingInteract = false
|
||||||
|
#spine
|
||||||
|
#default = {
|
||||||
|
fps: 60,
|
||||||
|
padding: {
|
||||||
|
left: parseInt(buildConfig.viewport_left),
|
||||||
|
right: parseInt(buildConfig.viewport_right),
|
||||||
|
top: parseInt(buildConfig.viewport_top),
|
||||||
|
bottom: parseInt(buildConfig.viewport_bottom),
|
||||||
|
},
|
||||||
|
scale: 1,
|
||||||
|
}
|
||||||
|
#config = {
|
||||||
|
fps: this.#default.fps,
|
||||||
|
useStartAnimation: true,
|
||||||
|
usePadding: false,
|
||||||
|
padding: {
|
||||||
|
...this.#default.padding,
|
||||||
|
},
|
||||||
|
scale: this.#default.scale,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(el) {
|
||||||
|
this.#parentEl = el
|
||||||
|
this.#el.id = 'player-box'
|
||||||
|
insertHTMLChild(this.#parentEl, this.#el)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const _this = this
|
||||||
|
const playerConfig = {
|
||||||
|
atlasUrl: `./assets/${buildConfig.filename}.atlas`,
|
||||||
|
premultipliedAlpha: true,
|
||||||
|
alpha: true,
|
||||||
|
backgroundColor: '#00000000',
|
||||||
|
viewport: {
|
||||||
|
debugRender: false,
|
||||||
|
padLeft: `${buildConfig.viewport_left}%`,
|
||||||
|
padRight: `${buildConfig.viewport_right}%`,
|
||||||
|
padTop: `${buildConfig.viewport_top}%`,
|
||||||
|
padBottom: `${buildConfig.viewport_bottom}%`,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
showControls: _this.#showControls,
|
||||||
|
touch: _this.#showControls,
|
||||||
|
fps: 60,
|
||||||
|
defaultMix: 0,
|
||||||
|
success: function (widget) {
|
||||||
|
if (
|
||||||
|
widget.skeleton.data.animations
|
||||||
|
.map((e) => e.name)
|
||||||
|
.includes('Start') &&
|
||||||
|
_this.useStartAnimation
|
||||||
|
) {
|
||||||
|
widget.animationState.setAnimation(0, 'Start', false)
|
||||||
|
}
|
||||||
|
widget.animationState.addAnimation(0, 'Idle', true, 0)
|
||||||
|
widget.animationState.addListener({
|
||||||
|
end: (e) => {
|
||||||
|
if (e.animation.name == 'Interact') {
|
||||||
|
_this.#isPlayingInteract = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
if (
|
||||||
|
window.performance.now() - _this.#resetTime >=
|
||||||
|
8 * 1000 &&
|
||||||
|
Math.random() < 0.3
|
||||||
|
) {
|
||||||
|
_this.#resetTime = window.performance.now()
|
||||||
|
let entry = widget.animationState.setAnimation(
|
||||||
|
0,
|
||||||
|
'Special',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
entry.mixDuration = 0.3
|
||||||
|
widget.animationState.addAnimation(
|
||||||
|
0,
|
||||||
|
'Idle',
|
||||||
|
true,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
widget.canvas.onclick = function () {
|
||||||
|
if (_this.#isPlayingInteract) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_this.#isPlayingInteract = true
|
||||||
|
let entry = widget.animationState.setAnimation(
|
||||||
|
0,
|
||||||
|
'Interact',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
entry.mixDuration = 0.3
|
||||||
|
widget.animationState.addAnimation(0, 'Idle', true, 0)
|
||||||
|
}
|
||||||
|
document.dispatchEvent(Events.Ready.handler())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (buildConfig.use_json === 'true') {
|
||||||
|
playerConfig.jsonUrl = `./assets/${buildConfig.filename}.json`
|
||||||
|
} else {
|
||||||
|
playerConfig.skelUrl = `./assets/${buildConfig.filename}.skel`
|
||||||
|
}
|
||||||
|
this.#spine = new spine.SpinePlayer(this.#el, playerConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
this.#loadViewport()
|
||||||
|
updateHTMLOptions(
|
||||||
|
this.#spine.skeleton.data.animations.map((e) => e.name),
|
||||||
|
'animation-selection'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPadding() {
|
||||||
|
this.padding = { ...this.#default.padding }
|
||||||
|
document.getElementById('position-padding-left-slider').value =
|
||||||
|
this.#default.padding.left
|
||||||
|
document.getElementById('position-padding-left-input').value =
|
||||||
|
this.#default.padding.left
|
||||||
|
document.getElementById('position-padding-right-slider').value =
|
||||||
|
this.#default.padding.right
|
||||||
|
document.getElementById('position-padding-right-input').value =
|
||||||
|
this.#default.padding.right
|
||||||
|
document.getElementById('position-padding-top-slider').value =
|
||||||
|
this.#default.padding.top
|
||||||
|
document.getElementById('position-padding-top-input').value =
|
||||||
|
this.#default.padding.top
|
||||||
|
document.getElementById('position-padding-bottom-slider').value =
|
||||||
|
this.#default.padding.bottom
|
||||||
|
document.getElementById('position-padding-bottom-input').value =
|
||||||
|
this.#default.padding.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
resetScale() {
|
||||||
|
this.scale = this.#default.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFPS() {
|
||||||
|
this.fps = this.#default.fps
|
||||||
|
document.getElementById('fps-slider').value = this.#default.fps
|
||||||
|
document.getElementById('fps-input').value = this.#default.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.resetFPS()
|
||||||
|
this.resetPadding()
|
||||||
|
this.resetScale()
|
||||||
|
this.#spine.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
#loadViewport() {
|
||||||
|
this.#spine.updateViewport({
|
||||||
|
padLeft: `${this.#config.padding.left}%`,
|
||||||
|
padRight: `${this.#config.padding.right}%`,
|
||||||
|
padTop: `${this.#config.padding.top}%`,
|
||||||
|
padBottom: `${this.#config.padding.bottom}%`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get usePadding() {
|
||||||
|
return this.#config.usePadding
|
||||||
|
}
|
||||||
|
|
||||||
|
set usePadding(v) {
|
||||||
|
this.#config.usePadding = v
|
||||||
|
}
|
||||||
|
|
||||||
|
set useStartAnimation(v) {
|
||||||
|
this.#config.useStartAnimation = v
|
||||||
|
}
|
||||||
|
|
||||||
|
get useStartAnimation() {
|
||||||
|
return this.#config.useStartAnimation
|
||||||
|
}
|
||||||
|
|
||||||
|
get spine() {
|
||||||
|
return this.#spine
|
||||||
|
}
|
||||||
|
|
||||||
|
set fps(v) {
|
||||||
|
v = parseInt(v)
|
||||||
|
this.#config.fps = v
|
||||||
|
this.#spine.setFps(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
get fps() {
|
||||||
|
return this.#config.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
set scale(v) {
|
||||||
|
v = parseInt(v)
|
||||||
|
this.#config.scale = 1 / v
|
||||||
|
this.#spine.setOperatorScale(1 / v)
|
||||||
|
}
|
||||||
|
|
||||||
|
get scale() {
|
||||||
|
return this.#config.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
get node() {
|
||||||
|
return this.#el
|
||||||
|
}
|
||||||
|
|
||||||
|
get padLeft() {
|
||||||
|
return this.padding.left
|
||||||
|
}
|
||||||
|
|
||||||
|
set padLeft(v) {
|
||||||
|
this.padding = {
|
||||||
|
left: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get padRight() {
|
||||||
|
return this.padding.right
|
||||||
|
}
|
||||||
|
|
||||||
|
set padRight(v) {
|
||||||
|
this.padding = {
|
||||||
|
right: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get padTop() {
|
||||||
|
return this.padding.top
|
||||||
|
}
|
||||||
|
|
||||||
|
set padTop(v) {
|
||||||
|
this.padding = {
|
||||||
|
top: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get padBottom() {
|
||||||
|
return this.padding.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
set padBottom(v) {
|
||||||
|
this.padding = {
|
||||||
|
bottom: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get padding() {
|
||||||
|
return this.#config.padding
|
||||||
|
}
|
||||||
|
|
||||||
|
set padding(v) {
|
||||||
|
if (!v) {
|
||||||
|
this.resetPadding()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof v !== 'object') return
|
||||||
|
if (v.left) v.left = parseInt(v.left)
|
||||||
|
if (v.right) v.right = parseInt(v.right)
|
||||||
|
if (v.top) v.top = parseInt(v.top)
|
||||||
|
if (v.bottom) v.bottom = parseInt(v.bottom)
|
||||||
|
this.#config.padding = { ...this.#config.padding, ...v }
|
||||||
|
this.#loadViewport()
|
||||||
|
}
|
||||||
|
|
||||||
|
get backCompatibilityFns() {
|
||||||
|
const _this = this
|
||||||
|
return {
|
||||||
|
spinePlayer: _this.#spine,
|
||||||
|
setFPS: (fps) => (_this.fps = fps),
|
||||||
|
loadViewport: _this.#loadViewport,
|
||||||
|
setScale: (v) => (this.scale = v),
|
||||||
|
scale: _this.scale,
|
||||||
|
positionPadding: (key, value) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'left':
|
||||||
|
this.padding = {
|
||||||
|
left: value,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'right':
|
||||||
|
this.padding = {
|
||||||
|
right: value,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'top':
|
||||||
|
this.padding = {
|
||||||
|
top: value,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'bottom':
|
||||||
|
this.padding = {
|
||||||
|
bottom: value,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.#config.padding = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
positionReset: _this.resetPadding,
|
||||||
|
scaleReset: _this.resetScale,
|
||||||
|
useStartAnimation: _this.useStartAnimation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return { ...this.#config }
|
||||||
|
}
|
||||||
|
|
||||||
|
get HTML() {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label for="fps">FPS</label>
|
||||||
|
<input type="range" min="1" max="60" value="${this.fps}" step="1" id="fps-slider"/>
|
||||||
|
<input type="number" id="fps-input" min="1" max="60" name="fps" value="${this.fps}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="animation-select">Animation:</label>
|
||||||
|
<select name="animation-select" id="animation-selection"></select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="use-start-animation">Use Start Animation</label>
|
||||||
|
<input type="checkbox" id="use-start-animation" name="use-start-animation" checked/>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="player-play" disabled>Play</button>
|
||||||
|
<button type="button" id="player-pause">Pause</button>
|
||||||
|
<div>
|
||||||
|
<label for="scale">Scale</label>
|
||||||
|
<input type="range" min="0.1" max="10" step="0.1" id="scale-slider" value="${this.scale}" />
|
||||||
|
<input type="number" id="scale-input" name="scale" value="${this.scale}" step="0.1"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="position">Position</label>
|
||||||
|
<input type="checkbox" id="position" name="position" ${this.usePadding ? 'checked' : ''}/>
|
||||||
|
<div id="position-realted" ${this.usePadding ? '' : 'hidden'}>
|
||||||
|
<div>
|
||||||
|
<label for="position-padding-left">Padding Left</label>
|
||||||
|
<input type="range" min="-100" max="100" id="position-padding-left-slider" value="${this.padding.left}" />
|
||||||
|
<input type="number" id="position-padding-left-input" name="position-padding-left" value="${this.padding.left}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="position-padding-right">Padding Right</label>
|
||||||
|
<input type="range" min="-100" max="100" id="position-padding-right-slider" value="${this.padding.right}" />
|
||||||
|
<input type="number" id="position-padding-right-input" name="position-padding-right" value="${this.padding.right}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="position-padding-top">Padding Top</label>
|
||||||
|
<input type="range" min="-100" max="100" id="position-padding-top-slider" value="${this.padding.top}" />
|
||||||
|
<input type="number" id="position-padding-top-input" name="position-padding-top" value="${this.padding.top}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="position-padding-bottom">Padding Bottom</label>
|
||||||
|
<input type="range" min="-100" max="100" id="position-padding-bottom-slider" value="${this.padding.bottom}" />
|
||||||
|
<input type="number" id="position-padding-bottom-input" name="position-padding-bottom" value="${this.padding.bottom}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get listeners() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'fps-slider',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'fps-input')
|
||||||
|
this.fps = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fps-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'fps-slider')
|
||||||
|
this.fps = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'animation-selection',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
this.spine.animationState.setAnimation(
|
||||||
|
0,
|
||||||
|
e.currentTarget.value,
|
||||||
|
false,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
this.spine.animationState.addAnimation(0, 'Idle', true, 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'use-start-animation',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
this.useStartAnimation = e.currentTarget.checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'player-play',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
this.spine.play()
|
||||||
|
e.currentTarget.disabled = true
|
||||||
|
document.getElementById('player-pause').disabled = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'player-pause',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
this.spine.pause()
|
||||||
|
e.currentTarget.disabled = true
|
||||||
|
document.getElementById('player-play').disabled = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scale-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'scale-input')
|
||||||
|
this.scale = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scale-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'scale-slider')
|
||||||
|
this.scale = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
showRelatedHTML(e.currentTarget, 'position-realted')
|
||||||
|
this.usePadding = e.currentTarget.checked
|
||||||
|
if (!e.currentTarget.checked) this.resetPadding()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-left-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(
|
||||||
|
e.currentTarget,
|
||||||
|
'position-padding-left-input'
|
||||||
|
)
|
||||||
|
this.padding = {
|
||||||
|
left: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-left-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(
|
||||||
|
e.currentTarget,
|
||||||
|
'position-padding-left-slider'
|
||||||
|
)
|
||||||
|
this.padding = {
|
||||||
|
left: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-right-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(
|
||||||
|
e.currentTarget,
|
||||||
|
'position-padding-right-input'
|
||||||
|
)
|
||||||
|
this.padding = {
|
||||||
|
right: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-right-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(
|
||||||
|
e.currentTarget,
|
||||||
|
'position-padding-right-slider'
|
||||||
|
)
|
||||||
|
this.padding = {
|
||||||
|
right: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-top-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'position-padding-top-input')
|
||||||
|
this.padding = {
|
||||||
|
top: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-top-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(
|
||||||
|
e.currentTarget,
|
||||||
|
'position-padding-top-slider'
|
||||||
|
)
|
||||||
|
this.padding = {
|
||||||
|
top: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-bottom-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(
|
||||||
|
e.currentTarget,
|
||||||
|
'position-padding-bottom-input'
|
||||||
|
)
|
||||||
|
this.padding = {
|
||||||
|
bottom: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-padding-bottom-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(
|
||||||
|
e.currentTarget,
|
||||||
|
'position-padding-bottom-slider'
|
||||||
|
)
|
||||||
|
this.padding = {
|
||||||
|
bottom: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case 'fps':
|
||||||
|
this.fps = value
|
||||||
|
break
|
||||||
|
case 'scale':
|
||||||
|
this.scale = value
|
||||||
|
break
|
||||||
|
case 'position':
|
||||||
|
this.padding = value
|
||||||
|
break
|
||||||
|
case 'use-start-animation':
|
||||||
|
this.useStartAnimation = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Events = {
|
||||||
|
Ready: createCustomEvent('player-ready'),
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans:wght@400;500;700&display=swap";
|
@import 'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans:wght@400;500;700&display=swap';
|
||||||
|
|
||||||
#voice-box {
|
#voice-box {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
width: 480px;
|
width: 480px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
font-family: 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans', sans-serif;
|
font-family:
|
||||||
|
'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans', sans-serif;
|
||||||
transition: opacity 0.5s cubic-bezier(0.65, 0.05, 0.36, 1);
|
transition: opacity 0.5s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
641
apps/showcase/src/components/voice.js
Normal file
641
apps/showcase/src/components/voice.js
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
import {
|
||||||
|
insertHTMLChild,
|
||||||
|
updateElementPosition,
|
||||||
|
updateHTMLOptions,
|
||||||
|
showRelatedHTML,
|
||||||
|
syncHTMLValue,
|
||||||
|
} from '@/components/helper'
|
||||||
|
import '@/components/voice.css'
|
||||||
|
import buildConfig from '!/config.json'
|
||||||
|
|
||||||
|
export default class Voice {
|
||||||
|
#el = document.createElement('div')
|
||||||
|
#parentEl
|
||||||
|
#charwordTable
|
||||||
|
#default = {
|
||||||
|
region: buildConfig.voice_default_region,
|
||||||
|
duration: {
|
||||||
|
idle: 10 * 60 * 1000,
|
||||||
|
next: 3 * 60 * 1000,
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
voice: null,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
x: 0,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
#voice = {
|
||||||
|
id: {
|
||||||
|
current: null,
|
||||||
|
last: null,
|
||||||
|
},
|
||||||
|
listener: {
|
||||||
|
idle: -1,
|
||||||
|
next: -1,
|
||||||
|
},
|
||||||
|
lastClickToNext: false,
|
||||||
|
locations: null,
|
||||||
|
list: null,
|
||||||
|
}
|
||||||
|
#audio = {
|
||||||
|
id: 'voice-audio',
|
||||||
|
el: new Audio(),
|
||||||
|
isPlaying: false,
|
||||||
|
}
|
||||||
|
#config = {
|
||||||
|
useSubtitle: false,
|
||||||
|
useVoice: false,
|
||||||
|
useVoiceActor: false,
|
||||||
|
language: null,
|
||||||
|
subtitle: {
|
||||||
|
language: this.#default.region,
|
||||||
|
...this.#default.subtitle,
|
||||||
|
},
|
||||||
|
duration: { ...this.#default.duration },
|
||||||
|
}
|
||||||
|
#playerObj
|
||||||
|
|
||||||
|
constructor(el) {
|
||||||
|
this.#parentEl = el
|
||||||
|
this.#el.id = 'voice-box'
|
||||||
|
this.#el.hidden = true
|
||||||
|
this.#el.innerHTML = `
|
||||||
|
<audio id="${this.#audio.id}" autoplay>
|
||||||
|
<source type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
<div class="voice-wrapper" id="voice-wrapper">
|
||||||
|
<div class="voice-title" id="voice-title"></div>
|
||||||
|
<div class="voice-subtitle">
|
||||||
|
<div id="voice-subtitle"></div>
|
||||||
|
<div class="voice-triangle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="voice-actor-box" hidden>
|
||||||
|
<div class="voice-actor">
|
||||||
|
<span class="voice-actor-icon"></span>
|
||||||
|
<span id="voice-actor-name" class="voice-actor-name"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
insertHTMLChild(this.#parentEl, this.#el)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const res = await fetch('./assets/charword_table.json')
|
||||||
|
this.#charwordTable = await res.json()
|
||||||
|
this.#voice.languages = Object.keys(
|
||||||
|
this.#charwordTable.voiceLangs[this.#default.region]
|
||||||
|
)
|
||||||
|
this.#default.language.voice = this.#voice.languages[0]
|
||||||
|
this.#config.language = this.#default.language.voice
|
||||||
|
this.#voice.locations = this.#getVoiceLocations()
|
||||||
|
this.#voice.list = Object.keys(this.#getVoices())
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
const audioEndedFunc = () => {
|
||||||
|
this.#audio.isPlaying = false
|
||||||
|
this.#setCurrentSubtitle(null)
|
||||||
|
this.#audio.lastClickToNext = false
|
||||||
|
}
|
||||||
|
this.#audio.el.addEventListener('ended', audioEndedFunc)
|
||||||
|
this.#playEntryVoice()
|
||||||
|
this.#initNextVoiceTimer()
|
||||||
|
this.#playerObj.node.addEventListener('click', () => {
|
||||||
|
this.#audio.lastClickToNext = true
|
||||||
|
this.#nextVoice()
|
||||||
|
})
|
||||||
|
document.addEventListener('mousemove', () => {
|
||||||
|
if (this.#voice.listener.idle === -1) {
|
||||||
|
this.#initIdleVoiceTimer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
link(playerObj) {
|
||||||
|
this.#playerObj = playerObj
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPosition() {
|
||||||
|
this.position = { ...this.#default.subtitle }
|
||||||
|
document.getElementById('subtitle-padding-x-slider').value =
|
||||||
|
this.#default.subtitle.x
|
||||||
|
document.getElementById('subtitle-padding-x-input').value =
|
||||||
|
this.#default.subtitle.x
|
||||||
|
document.getElementById('subtitle-padding-y-slider').value =
|
||||||
|
this.#default.subtitle.y
|
||||||
|
document.getElementById('subtitle-padding-y-input').value =
|
||||||
|
this.#default.subtitle.y
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.resetPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
#getVoiceLocations() {
|
||||||
|
const folders = buildConfig.voice_folders
|
||||||
|
const customVoiceName = this.#voice.languages.filter(
|
||||||
|
(i) => !folders.sub.map((e) => e.lang).includes(i)
|
||||||
|
)[0]
|
||||||
|
folders.sub = folders.sub.map((e) => {
|
||||||
|
return {
|
||||||
|
name: e.name,
|
||||||
|
lang: e.lang === 'CUSTOM' ? customVoiceName : e.lang,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return folders
|
||||||
|
}
|
||||||
|
|
||||||
|
#getVoices() {
|
||||||
|
return this.#charwordTable.subtitleLangs[this.#config.subtitle.language]
|
||||||
|
.default
|
||||||
|
}
|
||||||
|
|
||||||
|
#playEntryVoice() {
|
||||||
|
this.#playSpecialVoice('问候')
|
||||||
|
}
|
||||||
|
|
||||||
|
#playSpecialVoice(matcher) {
|
||||||
|
const voices = this.#getVoices()
|
||||||
|
const voiceId = Object.keys(voices).find(
|
||||||
|
(e) => voices[e].title === matcher
|
||||||
|
)
|
||||||
|
this.#playVoice(voiceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
#playVoice(id) {
|
||||||
|
if (!this.useVoice) return
|
||||||
|
this.#voice.id.last = this.#voice.id.current
|
||||||
|
this.#voice.id.current = id
|
||||||
|
this.#audio.el.src = `./assets/${this.#getVoiceLocation()}/${id}.ogg`
|
||||||
|
let startPlayPromise = this.#audio.el.play()
|
||||||
|
if (startPlayPromise !== undefined) {
|
||||||
|
startPlayPromise
|
||||||
|
.then(() => {
|
||||||
|
this.#audio.isPlaying = true
|
||||||
|
this.#setCurrentSubtitle(id)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#getVoiceLocation() {
|
||||||
|
const locations = this.#voice.locations
|
||||||
|
return `${locations.main}/${locations.sub.find((e) => e.lang === this.#config.language).name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
#setCurrentSubtitle(id) {
|
||||||
|
if (id === null) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.#audio.isPlaying) return
|
||||||
|
this.#toggleSubtitle(0)
|
||||||
|
}, 5 * 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const subtitle = this.#getSubtitleById(id)
|
||||||
|
const title = subtitle.title
|
||||||
|
const content = subtitle.text
|
||||||
|
const cvInfo =
|
||||||
|
this.#charwordTable.voiceLangs[this.subtitleLanguage][
|
||||||
|
this.#config.language
|
||||||
|
]
|
||||||
|
document.getElementById('voice-title').innerText = title
|
||||||
|
document.getElementById('voice-subtitle').innerText = content
|
||||||
|
document.getElementById('voice-actor-name').innerText = cvInfo.join('')
|
||||||
|
if (this.#audio.isPlaying) {
|
||||||
|
this.#toggleSubtitle(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggleSubtitle(v) {
|
||||||
|
this.#el.style.opacity = v ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#getSubtitleById(id) {
|
||||||
|
const obj =
|
||||||
|
this.#charwordTable.subtitleLangs[this.#config.subtitle.language]
|
||||||
|
let key = 'default'
|
||||||
|
if (obj[this.#config.language]) {
|
||||||
|
key = this.#config.language
|
||||||
|
}
|
||||||
|
return obj[key][id]
|
||||||
|
}
|
||||||
|
|
||||||
|
#getSubtitleLanguages() {
|
||||||
|
return Object.keys(this.#charwordTable.subtitleLangs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateSubtitlePosition() {
|
||||||
|
updateElementPosition(this.#el, {
|
||||||
|
x: this.position.x,
|
||||||
|
y: this.position.y - 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#initNextVoiceTimer() {
|
||||||
|
this.#voice.listener.next = setInterval(() => {
|
||||||
|
if (!this.#voice.lastClickToNext) {
|
||||||
|
this.#nextVoice()
|
||||||
|
}
|
||||||
|
}, this.#config.duration.next)
|
||||||
|
}
|
||||||
|
|
||||||
|
#nextVoice() {
|
||||||
|
const getVoiceId = () => {
|
||||||
|
const id =
|
||||||
|
this.#voice.list[
|
||||||
|
Math.floor(Math.random() * this.#voice.list.length)
|
||||||
|
]
|
||||||
|
return id === this.#voice.id.last ? getVoiceId() : id
|
||||||
|
}
|
||||||
|
this.#playVoice(getVoiceId())
|
||||||
|
}
|
||||||
|
|
||||||
|
#initIdleVoiceTimer() {
|
||||||
|
this.#voice.listener.idle = setInterval(() => {
|
||||||
|
this.#playSpecialVoice('闲置')
|
||||||
|
clearInterval(this.#voice.listener.idle)
|
||||||
|
this.#voice.listener.idle = -1
|
||||||
|
}, this.#config.duration.idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
set useSubtitle(show) {
|
||||||
|
this.#config.useSubtitle = show
|
||||||
|
this.#el.hidden = !show
|
||||||
|
}
|
||||||
|
|
||||||
|
get useSubtitle() {
|
||||||
|
return this.#config.useSubtitle
|
||||||
|
}
|
||||||
|
|
||||||
|
set useVoice(show) {
|
||||||
|
this.#config.useVoice = show
|
||||||
|
this.#playEntryVoice()
|
||||||
|
if (!show && this.#audio.isPlaying) {
|
||||||
|
this.#audio.el.pause()
|
||||||
|
}
|
||||||
|
this.#toggleSubtitle(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
get useVoice() {
|
||||||
|
return this.#config.useVoice
|
||||||
|
}
|
||||||
|
|
||||||
|
set useVoiceActor(show) {
|
||||||
|
this.#config.useVoiceActor = show
|
||||||
|
document.getElementById('voice-actor-box').hidden = !show
|
||||||
|
}
|
||||||
|
|
||||||
|
get useVoiceActor() {
|
||||||
|
return this.#config.useVoiceActor
|
||||||
|
}
|
||||||
|
|
||||||
|
set subtitleLanguage(lang) {
|
||||||
|
if (this.#getSubtitleLanguages().includes(lang)) {
|
||||||
|
this.#config.subtitle.language = lang
|
||||||
|
} else {
|
||||||
|
this.#config.subtitle.language = this.#default.region
|
||||||
|
}
|
||||||
|
this.#setCurrentSubtitle(this.#voice.id.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
get subtitleLanguage() {
|
||||||
|
return this.#config.subtitle.language
|
||||||
|
}
|
||||||
|
|
||||||
|
get subtitleLanguages() {
|
||||||
|
return this.#getSubtitleLanguages()
|
||||||
|
}
|
||||||
|
|
||||||
|
get x() {
|
||||||
|
return this.position.x
|
||||||
|
}
|
||||||
|
|
||||||
|
set x(v) {
|
||||||
|
this.position = {
|
||||||
|
x: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get y() {
|
||||||
|
return this.position.y
|
||||||
|
}
|
||||||
|
|
||||||
|
set y(v) {
|
||||||
|
this.position = {
|
||||||
|
y: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get position() {
|
||||||
|
return {
|
||||||
|
x: this.#config.subtitle.x,
|
||||||
|
y: this.#config.subtitle.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set position(v) {
|
||||||
|
if (typeof v !== 'object') return
|
||||||
|
if (v.x) v.x = parseInt(v.x)
|
||||||
|
if (v.y) v.y = parseInt(v.y)
|
||||||
|
this.#config.subtitle = { ...this.#config.subtitle, ...v }
|
||||||
|
console.log(v)
|
||||||
|
this.#updateSubtitlePosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
set language(lang) {
|
||||||
|
if (this.#voice.languages.includes(lang)) {
|
||||||
|
this.#config.language = lang
|
||||||
|
} else {
|
||||||
|
this.#config.language = this.#default.language.voice
|
||||||
|
}
|
||||||
|
const availableSubtitleLang = this.#getSubtitleLanguages()
|
||||||
|
if (!availableSubtitleLang.includes(this.#config.subtitle.language)) {
|
||||||
|
this.#config.subtitle.language = availableSubtitleLang[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get language() {
|
||||||
|
return this.#config.language
|
||||||
|
}
|
||||||
|
|
||||||
|
get languages() {
|
||||||
|
return this.#voice.languages
|
||||||
|
}
|
||||||
|
|
||||||
|
get duration() {
|
||||||
|
return {
|
||||||
|
idle: this.#config.duration.idle / 60 / 1000,
|
||||||
|
next: this.#config.duration.next / 60 / 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set duration(v) {
|
||||||
|
if (typeof v !== 'object') return
|
||||||
|
if (v.idle) {
|
||||||
|
clearInterval(this.#voice.listener.idle)
|
||||||
|
if (v.idle !== 0) {
|
||||||
|
this.#config.duration.idle = parseInt(v.idle) * 60 * 1000
|
||||||
|
this.#initIdleVoiceTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v.next) {
|
||||||
|
clearInterval(this.#voice.listener.next)
|
||||||
|
if (v.next !== 0) {
|
||||||
|
this.#config.duration.next = parseInt(v.next) * 60 * 1000
|
||||||
|
this.#initNextVoiceTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get durationIdle() {
|
||||||
|
return this.duration.idle
|
||||||
|
}
|
||||||
|
|
||||||
|
set durationIdle(duration) {
|
||||||
|
this.duration = {
|
||||||
|
idle: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set durationNext(duration) {
|
||||||
|
this.duration = {
|
||||||
|
next: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get durationNext() {
|
||||||
|
return this.duration.next
|
||||||
|
}
|
||||||
|
|
||||||
|
set subtitleX(x) {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
this.position = {
|
||||||
|
x,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get subtitleX() {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
return this.position.x
|
||||||
|
}
|
||||||
|
|
||||||
|
set subtitleY(y) {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
this.position = {
|
||||||
|
y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get subtitleY() {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
return this.position.y
|
||||||
|
}
|
||||||
|
|
||||||
|
set idleDuration(duration) {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
this.duration = {
|
||||||
|
idle: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get idleDuration() {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
return this.duration.idle
|
||||||
|
}
|
||||||
|
|
||||||
|
set nextDuration(duration) {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
this.duration.next = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
get nextDuration() {
|
||||||
|
// Note: Back Compatibility
|
||||||
|
return this.duration.next
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return { ...this.#config }
|
||||||
|
}
|
||||||
|
|
||||||
|
get HTML() {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<label for="voice">Voice</label>
|
||||||
|
<input type="checkbox" id="voice" name="voice" ${this.useVoice ? 'checked' : ''}/>
|
||||||
|
<div id="voice-realted" ${this.useVoice ? '' : 'hidden'}>
|
||||||
|
<div>
|
||||||
|
<label for="voice-lang-select">Choose the language of voice:</label>
|
||||||
|
<select name="voice-lang" id="voice-lang-select">
|
||||||
|
${updateHTMLOptions(this.languages)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="voice-idle-duration">Idle Duration (min)</label>
|
||||||
|
<input type="number" id="voice-idle-duration-input" min="0" name="voice-idle-duration" value="${this.duration.idle}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="voice-next-duration">Next Duration (min)</label>
|
||||||
|
<input type="number" id="voice-next-duration-input" name="voice-next-duration" value="${this.duration.next}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="subtitle">Subtitle</label>
|
||||||
|
<input type="checkbox" id="subtitle" name="subtitle" ${this.useSubtitle ? 'checked' : ''}/>
|
||||||
|
<div id="subtitle-realted" ${this.useSubtitle ? '' : 'hidden'}>
|
||||||
|
<div>
|
||||||
|
<label for="subtitle-lang-select">Choose the language of subtitle:</label>
|
||||||
|
<select name="subtitle-lang" id="subtitle-lang-select">
|
||||||
|
${updateHTMLOptions(this.subtitleLanguages)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="subtitle-padding-x">Subtitle X Position</label>
|
||||||
|
<input type="range" min="0" max="100" id="subtitle-padding-x-slider" value="${this.position.x}" />
|
||||||
|
<input type="number" id="subtitle-padding-x-input" name="subtitle-padding-x" value="${this.position.x}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="subtitle-padding-y">Subtitle Y Position</label>
|
||||||
|
<input type="range" min="0" max="100" id="subtitle-padding-y-slider" value="${this.position.y}" />
|
||||||
|
<input type="number" id="subtitle-padding-y-input" name="subtitle-padding-y" value="${this.position.y}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="voice-actor">Voice Actor</label>
|
||||||
|
<input type="checkbox" id="voice-actor" name="voice-actor" ${this.useVoiceActor ? 'checked' : ''}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get listeners() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'voice',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
showRelatedHTML(e.currentTarget, 'voice-realted')
|
||||||
|
this.useVoice = e.currentTarget.checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'voice-lang-select',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
this.language = e.currentTarget.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'voice-idle-duration-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
this.duration = {
|
||||||
|
idle: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'voice-next-duration-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
this.duration = {
|
||||||
|
next: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
showRelatedHTML(e.currentTarget, 'subtitle-realted')
|
||||||
|
this.useSubtitle = e.currentTarget.checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle-lang-select',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => (this.subtitleLanguage = e.currentTarget.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle-padding-x-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'subtitle-padding-x-input')
|
||||||
|
this.position = {
|
||||||
|
x: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle-padding-x-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'subtitle-padding-x-slider')
|
||||||
|
this.position = {
|
||||||
|
x: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle-padding-y-slider',
|
||||||
|
event: 'input',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'subtitle-padding-y-input')
|
||||||
|
this.position = {
|
||||||
|
y: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle-padding-y-input',
|
||||||
|
event: 'change',
|
||||||
|
handler: (e) => {
|
||||||
|
syncHTMLValue(e.currentTarget, 'subtitle-padding-y-slider')
|
||||||
|
this.position = {
|
||||||
|
y: e.currentTarget.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'voice-actor',
|
||||||
|
event: 'click',
|
||||||
|
handler: (e) => {
|
||||||
|
this.useVoiceActor = e.currentTarget.checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case 'use-voice':
|
||||||
|
this.useVoice = value
|
||||||
|
break
|
||||||
|
case 'language':
|
||||||
|
this.language = value
|
||||||
|
break
|
||||||
|
case 'duration':
|
||||||
|
this.duration = value
|
||||||
|
break
|
||||||
|
case 'use-subtitle':
|
||||||
|
this.useSubtitle = value
|
||||||
|
break
|
||||||
|
case 'subtitle-language':
|
||||||
|
this.subtitleLanguage = value
|
||||||
|
break
|
||||||
|
case 'subtitle-position':
|
||||||
|
this.position = value
|
||||||
|
break
|
||||||
|
case 'use-voice-actor':
|
||||||
|
this.useVoiceActor = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
376
apps/showcase/src/components/wallpaper_engine.js
Normal file
376
apps/showcase/src/components/wallpaper_engine.js
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import Events from '@/components/events'
|
||||||
|
|
||||||
|
window.wallpaperPropertyListener = {
|
||||||
|
applyGeneralProperties: function (properties) {
|
||||||
|
if (properties.fps) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'fps',
|
||||||
|
value: properties.fps,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyUserProperties: function (properties) {
|
||||||
|
console.log(properties)
|
||||||
|
if (properties.privacydonottrack) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.Insight.Register.handler(
|
||||||
|
!properties.privacydonottrack.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.logo) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'logo',
|
||||||
|
key: 'hidden',
|
||||||
|
value: !properties.logo.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.logoratio) {
|
||||||
|
if (properties.logoratio.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'logo',
|
||||||
|
key: 'ratio',
|
||||||
|
value: properties.logoratio.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.logoopacity) {
|
||||||
|
if (properties.logoopacity.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'logo',
|
||||||
|
key: 'opacity',
|
||||||
|
value: properties.logoopacity.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.logoimage) {
|
||||||
|
if (properties.logoimage.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'logo',
|
||||||
|
key: 'image',
|
||||||
|
value: 'file:///' + properties.logoimage.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'logo',
|
||||||
|
key: 'image',
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.logox) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'logo',
|
||||||
|
key: 'position',
|
||||||
|
value: {
|
||||||
|
x: properties.logox.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.logoy) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'logo',
|
||||||
|
key: 'position',
|
||||||
|
value: {
|
||||||
|
y: properties.logoy.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.defaultbackground) {
|
||||||
|
if (properties.defaultbackground.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'background',
|
||||||
|
key: 'default',
|
||||||
|
value: properties.defaultbackground.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.background) {
|
||||||
|
if (properties.background.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'background',
|
||||||
|
key: 'custom',
|
||||||
|
value: `file:///${properties.background.value}`,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'background',
|
||||||
|
key: 'custom',
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.voicetitle) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'use-voice',
|
||||||
|
value: properties.voicetitle.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voicelanguage) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'language',
|
||||||
|
value: properties.voicelanguage.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voiceidle) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'duration',
|
||||||
|
value: {
|
||||||
|
idle: properties.voiceidle.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voicenext) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'duration',
|
||||||
|
value: {
|
||||||
|
next: properties.voicenext.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voicesubtitle) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'use-subtitle',
|
||||||
|
value: properties.voicesubtitle.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voicesubtitlelanguage) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'subtitle-language',
|
||||||
|
value: properties.voicesubtitlelanguage.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voicesubtitlex) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'subtitle-position',
|
||||||
|
value: {
|
||||||
|
x: properties.voicesubtitlex.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voicesubtitley) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'subtitle-position',
|
||||||
|
value: {
|
||||||
|
y: properties.voicesubtitley.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.voiceactor) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'voice',
|
||||||
|
key: 'use-voice-actor',
|
||||||
|
value: properties.voiceactor.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.music_selection) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'music',
|
||||||
|
key: 'music',
|
||||||
|
value: properties.music_selection.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.music_title) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'music',
|
||||||
|
key: 'use-music',
|
||||||
|
value: properties.music_title.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.music_volume) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'music',
|
||||||
|
key: 'volume',
|
||||||
|
value: properties.music_volume.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.custom_music) {
|
||||||
|
if (properties.custom_music.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'music',
|
||||||
|
key: 'custom',
|
||||||
|
value: `file:///${properties.custom_music.value}`,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'music',
|
||||||
|
key: 'custom',
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.custom_video) {
|
||||||
|
if (properties.custom_video.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'background',
|
||||||
|
key: 'video',
|
||||||
|
value: `file:///${properties.custom_video.value}`,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'background',
|
||||||
|
key: 'video',
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.video_volume) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'background',
|
||||||
|
key: 'volume',
|
||||||
|
value: properties.video_volume.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.scale) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'scale',
|
||||||
|
value: properties.scale.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.position) {
|
||||||
|
if (!properties.position.value) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'position',
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'position',
|
||||||
|
value: {
|
||||||
|
left: properties.paddingleft.value,
|
||||||
|
right: properties.paddingright.value,
|
||||||
|
top: properties.paddingtop.value,
|
||||||
|
bottom: properties.paddingbottom.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.paddingleft) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'position',
|
||||||
|
value: {
|
||||||
|
left: properties.paddingleft.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.paddingright) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'position',
|
||||||
|
value: {
|
||||||
|
right: properties.paddingright.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.paddingtop) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'position',
|
||||||
|
value: {
|
||||||
|
top: properties.paddingtop.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.paddingbottom) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'position',
|
||||||
|
value: {
|
||||||
|
bottom: properties.paddingbottom.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (properties.useStartAnimation) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
Events.RegisterConfig.handler({
|
||||||
|
target: 'player',
|
||||||
|
key: 'use-start-animation',
|
||||||
|
value: properties.useStartAnimation.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
22
apps/showcase/src/index.css
Normal file
22
apps/showcase/src/index.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
html {
|
||||||
|
user-select: none;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#widget-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
6
apps/showcase/src/index.js
Normal file
6
apps/showcase/src/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import '@/index.css'
|
||||||
|
import '@/components/wallpaper_engine'
|
||||||
|
import AKLive2D from '@/components/aklive2d'
|
||||||
|
;(() => {
|
||||||
|
window.aklive2d = new AKLive2D(document.getElementById('app'))
|
||||||
|
})()
|
||||||
5
apps/showcase/stylelint.config.js
Normal file
5
apps/showcase/stylelint.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import baseConfig from '@aklive2d/stylelint-config'
|
||||||
|
/** @type {import('stylelint').Config} */
|
||||||
|
export default {
|
||||||
|
...baseConfig,
|
||||||
|
}
|
||||||
57
apps/showcase/vite.config.js
Normal file
57
apps/showcase/vite.config.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { has } from '@aklive2d/operator'
|
||||||
|
import { envParser, file } from '@aklive2d/libs'
|
||||||
|
import { copyShowcaseData } from '@aklive2d/vite-helpers'
|
||||||
|
import * as dirs from './index.js'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(({ command, isPreview }) => {
|
||||||
|
let newOutDir = dirs.OUT_DIR
|
||||||
|
if (command === 'serve') {
|
||||||
|
const { name } = envParser.parse({
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'n',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('Please set the operator name.')
|
||||||
|
}
|
||||||
|
if (!has(name)) {
|
||||||
|
throw new Error(`Invalid operator name: ${name}`)
|
||||||
|
}
|
||||||
|
if (isPreview) {
|
||||||
|
newOutDir = path.join(dirs.DIST_DIR, name)
|
||||||
|
} else {
|
||||||
|
file.rm(dirs.DATA_DIR)
|
||||||
|
copyShowcaseData(name, {
|
||||||
|
dataDir: dirs.DATA_DIR,
|
||||||
|
publicAssetsDir: dirs.PUBLIC_ASSETS_DIR,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: '',
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve('./src'),
|
||||||
|
'!': dirs.DATA_DIR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
envDir: dirs.DATA_DIR,
|
||||||
|
publicDir: dirs.PUBLIC_DIR,
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 10000,
|
||||||
|
outDir: newOutDir,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: `assets/[name].js`,
|
||||||
|
chunkFileNames: `assets/[name].js`,
|
||||||
|
assetFileNames: `assets/[name].[ext]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
99
config.yaml
99
config.yaml
@@ -1,99 +0,0 @@
|
|||||||
akassets:
|
|
||||||
project_name: akassets
|
|
||||||
url: https://akassets.halyul.dev
|
|
||||||
insight_id: aklive2d
|
|
||||||
folder:
|
|
||||||
auto_update_data: ./data/auto_update
|
|
||||||
operator_data: ./data/operator/
|
|
||||||
directory_src: directory
|
|
||||||
showcase_src: showcase
|
|
||||||
operator: ./operator/
|
|
||||||
release: ./release/
|
|
||||||
background: background
|
|
||||||
music: music
|
|
||||||
directory: _assets
|
|
||||||
share: _share
|
|
||||||
voice:
|
|
||||||
main: voice
|
|
||||||
sub:
|
|
||||||
- name: jp
|
|
||||||
lang: JP
|
|
||||||
- name: cn
|
|
||||||
lang: CN_MANDARIN
|
|
||||||
- name: en
|
|
||||||
lang: EN
|
|
||||||
- name: kr
|
|
||||||
lang: KR
|
|
||||||
- name: custom
|
|
||||||
lang: CUSTOM
|
|
||||||
share:
|
|
||||||
title:
|
|
||||||
zh-CN: "明日方舟:"
|
|
||||||
en-US: "Arknights: "
|
|
||||||
directory:
|
|
||||||
title: AKLive2D
|
|
||||||
voice: jp/CN_037.ogg
|
|
||||||
error:
|
|
||||||
files: !include config/_directory.yaml
|
|
||||||
voice: CN_034.ogg
|
|
||||||
operators:
|
|
||||||
chen: !include config/chen.yaml
|
|
||||||
dusk: !include config/dusk.yaml
|
|
||||||
dusk_everything_is_a_miracle: !include config/dusk_everything_is_a_miracle.yaml
|
|
||||||
ling: !include config/ling.yaml
|
|
||||||
nearl: !include config/nearl.yaml
|
|
||||||
nian: !include config/nian.yaml
|
|
||||||
nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml
|
|
||||||
phatom_focus: !include config/phatom_focus.yaml
|
|
||||||
rosmontis: !include config/rosmontis.yaml
|
|
||||||
skadi: !include config/skadi.yaml
|
|
||||||
skadi_sublimation: !include config/skadi_sublimation.yaml
|
|
||||||
w: !include config/w.yaml
|
|
||||||
w_wonder: !include config/w_wonder.yaml
|
|
||||||
specter: !include config/specter.yaml
|
|
||||||
gavial: !include config/gavial.yaml
|
|
||||||
surtr_colorful_wonderland: !include config/surtr_colorful_wonderland.yaml
|
|
||||||
lee_trust_your_eyes: !include config/lee_trust_your_eyes.yaml
|
|
||||||
texas_the_omertosa: !include config/texas_the_omertosa.yaml
|
|
||||||
nearl_relight: !include config/nearl_relight.yaml
|
|
||||||
rosmontis_become_anew: !include config/rosmontis_become_anew.yaml
|
|
||||||
passager_dream_in_a_moment: !include config/passager_dream_in_a_moment.yaml
|
|
||||||
mizuki_summer_feast: !include config/mizuki_summer_feast.yaml
|
|
||||||
chongyue: !include config/chongyue.yaml
|
|
||||||
ling_it_does_wash_the_strings: !include config/ling_it_does_wash_the_strings.yaml
|
|
||||||
pozemka_snowy_plains_in_words: !include config/pozemka_snowy_plains_in_words.yaml
|
|
||||||
chen_ten_thousand_mountains: !include config/chen_ten_thousand_mountains.yaml
|
|
||||||
specter_born_as_one: !include config/specter_born_as_one.yaml
|
|
||||||
muelsyse: !include config/muelsyse.yaml
|
|
||||||
kaltsit_remnant: !include config/kaltsit_remnant.yaml
|
|
||||||
eyjafjalla_the_hvit_aska: !include config/eyjafjalla_the_hvit_aska.yaml
|
|
||||||
goldenglow_summer_flowers_fa394: !include config/goldenglow_summer_flowers_fa394.yaml
|
|
||||||
gavial_the_invincible_holiday_hd26: !include config/gavial_the_invincible_holiday_hd26.yaml
|
|
||||||
ling_towering_is_cliff_of_nostalgia: !include config/ling_towering_is_cliff_of_nostalgia.yaml
|
|
||||||
virtuosa: !include config/virtuosa.yaml
|
|
||||||
texas_the_omertosa_wingbreaker: !include config/texas_the_omertosa_wingbreaker.yaml
|
|
||||||
mwynar_w_dali: !include config/mwynar_w_dali.yaml
|
|
||||||
reed_the_frame_shadow_curator: !include config/reed_the_frame_shadow_curator.yaml
|
|
||||||
shu: !include config/shu.yaml
|
|
||||||
lin_heavenly_mirage: !include config/lin_heavenly_mirage.yaml
|
|
||||||
chongyue_alighting: !include config/chongyue_alighting.yaml
|
|
||||||
wisadel: !include config/wisadel.yaml
|
|
||||||
muelsyse_young_branch: !include config/muelsyse_young_branch.yaml
|
|
||||||
skadi_the_corrupting_heart_red_countess: !include config/skadi_the_corrupting_heart_red_countess.yaml
|
|
||||||
ines_under_the_flaming_dome: !include config/ines_under_the_flaming_dome.yaml
|
|
||||||
silverash_never_melting_ice: !include config/silverash_never_melting_ice.yaml
|
|
||||||
reed_the_frame_shadow_summer_flower: !include config/reed_the_frame_shadow_summer_flower.yaml
|
|
||||||
eyjafjalla_the_hvit_aska_a_picnic_before_a_long_trip: !include config/eyjafjalla_the_hvit_aska_a_picnic_before_a_long_trip.yaml
|
|
||||||
pepe: !include config/pepe.yaml
|
|
||||||
nightingale_iakhu_of_flows: !include config/nightingale_iakhu_of_flows.yaml
|
|
||||||
degenbrecher_the_shadow_of_dark_moon: !include config/degenbrecher_the_shadow_of_dark_moon.yaml
|
|
||||||
lappland_the_decadenza: !include config/lappland_the_decadenza.yaml
|
|
||||||
texas_the_omertosa_il_se_de_no: !include config/texas_the_omertosa_il_se_de_no.yaml
|
|
||||||
executor_the_ex_foedere_allmind_as_one: !include config/executor_the_ex_foedere_allmind_as_one.yaml
|
|
||||||
virtuosa_diversity_oneness: !include config/virtuosa_diversity_oneness.yaml
|
|
||||||
chongyue_allround_actor: !include config/chongyue_allround_actor.yaml
|
|
||||||
nian_thunderbolt_director: !include config/nian_thunderbolt_director.yaml
|
|
||||||
amiya_solo_around_the_world: !include config/amiya_solo_around_the_world.yaml
|
|
||||||
zuole_youthful_journey: !include config/zuole_youthful_journey.yaml
|
|
||||||
shu_spring_feast: !include config/shu_spring_feast.yaml
|
|
||||||
yu: !include config/yu.yaml
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user