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()
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
user-select: auto;
|
user-select: auto;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -13,4 +13,4 @@
|
|||||||
background-size: cover;
|
background-size: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#player-box {
|
#player-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +72,4 @@
|
|||||||
color: white;
|
color: white;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
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