16 Commits
Author SHA1 Message Date
SansanandClaude Opus 5.5 36329ca5cc Release 1.3.0
Build and Release / build (push) Successful in 31s
Build and Release / release (push) Successful in 3m22s
- Send the play queue ID (X-Queue-Id) with every request, and keep the
  queue across page reloads.
- Bypass browser caches for random songs.
- Start the sync time at 00:00, to match the server's minutes and seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:10:34 +02:00
SansanandClaude Opus 5.5 9c72f89703 Start the sync time at 00:00
Build and Release / build (push) Successful in 35s
Build and Release / release (push) Successful in 6s
The server now sends sync times as minutes and seconds, so the placeholder
shown before the first progress update matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:09:32 +02:00
Sansan f26438abe0 Send the play queue ID to the server
Build and Release / build (push) Successful in 33s
Build and Release / release (push) Successful in 9s
The server keeps a play queue per client, identified by the X-Queue-Id
header. Send the stored ID with every request and save the ID from every
response, so the client switches to the new queue after a reset or a sync.
The ID is kept in localStorage so a page reload keeps the same queue.

Also send Cache-Control: no-cache, so browsers that cached a random song
before the server sent no-store fetch a new one.
2026-10-07 20:09:31 +02:00
Sansan b3ffef9fd9 Use /api/v1 with token authentication
Build and Release / build (push) Successful in 50s
Build and Release / release (push) Successful in 3m18s
All server calls go through a shared axios instance (src/api.js) that gets a token
with API_PASSWORD from the runtime config, renews it before it expires and retries
once on 401. Character images are loaded with the token as blobs. Removes the
unused express and cors dependencies and bumps the version to 1.2.0.

Closes #2
2026-10-06 19:01:13 +02:00
Sansan d061e02d81 Always search with /find, which now includes fuzzy matches
Closes #3
2026-10-06 18:57:02 +02:00
Sansan 2e7be01487 Start sync with POST and show progress when a sync is running
Closes #1
2026-10-06 18:57:02 +02:00
Sansan f972b579fc Release automatically when package.json version changes
Build and Release / build (push) Successful in 23s
Build and Release / release (push) Successful in 6s
Build and release now run in one workflow on push to main: after the
build passes, if no tag exists for the package.json version, the image
is pushed and the tag and Gitea release are created. Running them in
sequence avoids the action-cache race between the separate build and
tag-triggered release workflows.
2026-10-04 21:56:15 +02:00
Sansan 71de395a20 Create Gitea release on tag and bump version to 1.1.2
Build and Test / build (push) Failing after 5s
Release Docker Image / build-and-push (push) Successful in 3m18s
2026-10-04 21:49:11 +02:00
Sansan e6d71fc77c Give modals the dark app theme and close them on outside click
Build and Test / build (push) Successful in 58s
Release Docker Image / build-and-push (push) Successful in 3m49s
- Restyle the shared modal styles to the dark app palette with orange accents
- Remove light-theme overrides in Search, Sync and Settings modals
- Add outside-click close and fade transition to the Sync modal
- Use @click.self for outside-click close in all modals
- Let tall modals scroll instead of being cut off
- Darken the mobile menu to match
2026-10-04 21:35:57 +02:00
Sansan 8b5aa3adf0 Replace game with soundtrack terminology and add fuzzy/exact search
Build and Test / build (push) Successful in 24s
Release Docker Image / build-and-push (push) Successful in 12m29s
- Replace all 'game' references with 'soundtrack' throughout frontend (112 occurrences)
- Add radio buttons for fuzzy/exact search in SearchModal
- Fuzzy search is default and uses /findfuzzy endpoint
- Exact search uses existing /find endpoint
- Fix syntax errors in SyncProgressModal from previous refactoring

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-07-03 23:06:30 +02:00
Sansan fb36038cc8 Fix release workflow tag reference
Build and Test / build (push) Successful in 27s
Release Docker Image / build-and-push (push) Successful in 12m26s
- Use gitea.ref_name directly for tag
- Hardcode username as sansan
- Remove version extraction step

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-29 19:08:27 +02:00
Sansan f7763c97ae Update Gitea workflows for Docker release
Build and Test / build (push) Successful in 1m35s
Release Docker Image / build-and-push (push) Failing after 10m3s
- Rename old release workflow to old_gitea-release.yaml
- Create new release.yaml with tag-based multi-arch Docker builds
- Update build.yaml with caching and PR triggers
- Use secrets.TOKEN for authentication

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-29 18:57:03 +02:00
Sansan 168ba205be Add search button with modal
- Create SearchModal.vue with text field and results display
- Add Search button to extraButtons.vue
- Uses /find API endpoint for server-side search
- Modal matches existing app styling

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-31 20:47:20 +02:00
Sansan 99e6419d09 Update sync to use new API with progress modal
- Replace /sync endpoint (which now just starts sync) with polling /sync/progress
- Add SyncProgressModal.vue component to show live progress and results
- Update extraButtons.vue to use new sync flow with modal

Update character modal to use API
- Fetch character list from /characters endpoint
- Load character images from /character?name=<filename> endpoint
- Add object-fit: contain to preserve aspect ratios
- Fallback to hardcoded list if API fails
- Store API URLs for profile images

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-31 19:49:22 +02:00
Sansan ef1f9386e1 Remove unnecessary files (index.js, routes.js)
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-30 22:30:51 +02:00
Sansan bc32ce5fc5 Add Docker support with runtime environment variable configuration
- Remove temporary arne.js file
- Add runtime configuration via window.__RUNTIME_CONFIG__
- Create public/config.template.js for hostname injection
- Add Dockerfile with multi-stage build (node + nginx)
- Add docker-entrypoint.sh to generate config.js at startup
- Add .dockerignore
- Update all components to use runtime config instead of arne.js
- Update index.html to load config.js before app

This allows deploying the same Docker image to different environments
by setting the API_HOSTNAME environment variable at runtime.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-30 22:25:59 +02:00
29 changed files with 1241 additions and 364 deletions

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
node_modules
.git
dist
.DS_Store
+108
View File
@@ -0,0 +1,108 @@
name: Build and Release
# Releases are driven by the version in package.json: bump it (e.g.
# `npm version patch --no-git-tag-version`) and push to main. If no tag
# v<version> exists yet, the release job pushes the Docker image and creates
# the tag and Gitea release. Build and release run in sequence in one
# workflow, so they never race for the runner's action cache.
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.sanplex.xyz/sansan"]
http = true
insecure = true
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
release:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Check version
id: version
run: |
VERSION="v$(node -p "require('./package.json').version")"
echo "tag=$VERSION" >> "$GITHUB_OUTPUT"
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
echo "$VERSION already released, nothing to do"
echo "release=false" >> "$GITHUB_OUTPUT"
else
echo "Releasing $VERSION"
echo "release=true" >> "$GITHUB_OUTPUT"
fi
- name: Set up QEMU
if: steps.version.outputs.release == 'true'
uses: https://github.com/docker/setup-qemu-action@v3
- name: Set up Docker Buildx
if: steps.version.outputs.release == 'true'
uses: https://github.com/docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.sanplex.xyz/sansan"]
http = true
insecure = true
- name: Login to Gitea Container Registry
if: steps.version.outputs.release == 'true'
uses: docker/login-action@v3
with:
registry: gitea.sanplex.xyz
username: sansan
password: ${{ secrets.TOKEN }}
- name: Build and push multi-arch image
if: steps.version.outputs.release == 'true'
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
gitea.sanplex.xyz/sansan/musicfrontend:${{ steps.version.outputs.tag }}
gitea.sanplex.xyz/sansan/musicfrontend:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Create Release
if: steps.version.outputs.release == 'true'
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
tag_name: ${{ steps.version.outputs.tag }}
target_commitish: ${{ github.sha }}
body: |
Docker image: `gitea.sanplex.xyz/sansan/musicfrontend:${{ steps.version.outputs.tag }}`
+3
View File
@@ -19,3 +19,6 @@ pnpm-debug.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Local runtime config for npm run serve
/public/config.js
+18
View File
@@ -0,0 +1,18 @@
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Runtime stage
FROM nginx:alpine
RUN apk add --no-cache gettext
COPY --from=builder /app/dist /usr/share/nginx/html
COPY public/config.template.js /usr/share/nginx/html/
COPY docker-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+15
View File
@@ -20,5 +20,20 @@ npm run build
npm run lint npm run lint
``` ```
## Configuration
The Docker image writes `config.js` from `public/config.template.js` when the container starts, using these environment variables:
| Variable | Description |
|---|---|
| `API_HOSTNAME` | URL of the MusicServer, for example `https://music.example.com`. Empty to use the same host as the frontend. On another host, add the frontend's origin to the server's `CORS_ALLOWED_ORIGINS`. |
| `API_PASSWORD` | The MusicServer's `API_PASSWORD`, used to get an API token. Empty if the server has no password. Anyone who can open the frontend can read it. |
For `npm run serve`, create `public/config.js` (ignored by git):
```js
window.__RUNTIME_CONFIG__ = { API_HOSTNAME: 'http://localhost:8080', API_PASSWORD: '' };
```
When the frontend is served by the MusicServer image, the server provides `config.js` with its own `API_PASSWORD`.
### Customize configuration ### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -e
# Generate config.js from template at runtime
envsubst < /usr/share/nginx/html/config.template.js > /usr/share/nginx/html/config.js
# Start nginx
exec "$@"
-26
View File
@@ -1,26 +0,0 @@
const express = require("express");
const path = require("path");
const routes = require("./routes.js");
const app = express();
const cors = require("cors");
/* const bodyParser = require("body-parser"); */
const PORT = process.env.PORT || 5005;
const mainUrl = "test";
console.log(`Server started on port ${PORT}`);
//app.use(bodyParser.json());
//Routes
/* app.use(express.static(path.join(__dirname, "public"))); */
/* app.use(express.static(path.join(__dirname, "assets"))); */
app.use(express.static(path.join(__dirname)));
app.use(express.static(path.join(__dirname, "dist")));
/* app.use(express.static(path.join(__dirname, "public"))); */
/* console.log(__dirname); */
app.use(cors());
app.use(routes);
app.listen(PORT, "0.0.0.0");
+62 -21
View File
@@ -1,17 +1,15 @@
{ {
"name": "music-randomizer-vue", "name": "music-randomizer-vue",
"version": "0.1.0", "version": "1.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "music-randomizer-vue", "name": "music-randomizer-vue",
"version": "0.1.0", "version": "1.3.0",
"dependencies": { "dependencies": {
"axios": "^1.7.2", "axios": "^1.7.2",
"core-js": "^3.37.1", "core-js": "^3.37.1",
"cors": "^2.8.5",
"express": "^4.19.2",
"vue": "^3.4.31", "vue": "^3.4.31",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vuex": "^4.1.0" "vuex": "^4.1.0"
@@ -3185,6 +3183,7 @@
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-types": "~2.1.34", "mime-types": "~2.1.34",
@@ -3436,6 +3435,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/array-union": { "node_modules/array-union": {
@@ -3701,6 +3701,7 @@
"version": "1.20.5", "version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "~3.1.2", "bytes": "~3.1.2",
@@ -3725,6 +3726,7 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
@@ -3734,6 +3736,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/bonjour-service": { "node_modules/bonjour-service": {
@@ -3848,6 +3851,7 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@@ -3889,6 +3893,7 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -4332,6 +4337,7 @@
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "5.2.1" "safe-buffer": "5.2.1"
@@ -4344,6 +4350,7 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -4360,6 +4367,7 @@
"version": "0.7.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -4369,6 +4377,7 @@
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/copy-webpack-plugin": { "node_modules/copy-webpack-plugin": {
@@ -4447,23 +4456,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -4998,6 +4990,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@@ -5007,6 +5000,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8", "node": ">= 0.8",
@@ -5201,6 +5195,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
@@ -5231,6 +5226,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@@ -5358,6 +5354,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
@@ -5767,6 +5764,7 @@
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -5895,6 +5893,7 @@
"version": "4.22.2", "version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
@@ -5941,6 +5940,7 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
@@ -5950,6 +5950,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
@@ -6096,6 +6097,7 @@
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
@@ -6114,6 +6116,7 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
@@ -6123,6 +6126,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/find-cache-dir": { "node_modules/find-cache-dir": {
@@ -6232,6 +6236,7 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -6255,6 +6260,7 @@
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -6784,6 +6790,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"depd": "~2.0.0", "depd": "~2.0.0",
@@ -6874,6 +6881,7 @@
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
@@ -6969,12 +6977,14 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
@@ -7781,6 +7791,7 @@
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -7803,6 +7814,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -7839,6 +7851,7 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -7862,6 +7875,7 @@
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"mime": "cli.js" "mime": "cli.js"
@@ -8107,6 +8121,7 @@
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -8264,6 +8279,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -8273,6 +8289,7 @@
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -8323,6 +8340,7 @@
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ee-first": "1.1.1" "ee-first": "1.1.1"
@@ -8591,6 +8609,7 @@
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@@ -8648,6 +8667,7 @@
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {
@@ -9515,6 +9535,7 @@
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"forwarded": "0.2.0", "forwarded": "0.2.0",
@@ -9565,6 +9586,7 @@
"version": "6.15.2", "version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"
@@ -9611,6 +9633,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -9620,6 +9643,7 @@
"version": "2.5.3", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "~3.1.2", "bytes": "~3.1.2",
@@ -9990,6 +10014,7 @@
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -10010,6 +10035,7 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sax": { "node_modules/sax": {
@@ -10076,6 +10102,7 @@
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
@@ -10100,6 +10127,7 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
@@ -10109,6 +10137,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
@@ -10202,6 +10231,7 @@
"version": "1.16.3", "version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encodeurl": "~2.0.0", "encodeurl": "~2.0.0",
@@ -10235,6 +10265,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/shallow-clone": { "node_modules/shallow-clone": {
@@ -10290,6 +10321,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -10309,6 +10341,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -10325,6 +10358,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
@@ -10343,6 +10377,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
@@ -10532,6 +10567,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@@ -10969,6 +11005,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -11028,6 +11065,7 @@
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"media-typer": "0.3.0", "media-typer": "0.3.0",
@@ -11102,6 +11140,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@@ -11166,6 +11205,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4.0" "node": ">= 0.4.0"
@@ -11197,6 +11237,7 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
+46 -46
View File
@@ -1,57 +1,57 @@
{ {
"name": "music-randomizer-vue", "name": "music-randomizer-vue",
"version": "0.1.0", "version": "1.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.2", "axios": "^1.7.2",
"core-js": "^3.37.1", "core-js": "^3.37.1",
"cors": "^2.8.5", "vue": "^3.4.31",
"express": "^4.19.2", "vue-axios": "^3.5.2",
"vue": "^3.4.31", "vuex": "^4.1.0"
"vue-axios": "^3.5.2", },
"vuex": "^4.1.0" "devDependencies": {
}, "@vue/cli-plugin-babel": "^5.0.8",
"devDependencies": { "@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-service": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8", "@vue/compiler-sfc": "^3.4.31",
"@vue/cli-service": "^5.0.8", "@babel/eslint-parser": "^7.25.1",
"@vue/compiler-sfc": "^3.4.31", "eslint": "^8.57.0",
"@babel/eslint-parser": "^7.25.1", "eslint-plugin-vue": "^9.27.0"
"eslint": "^8.57.0", },
"eslint-plugin-vue": "^9.27.0" "eslintConfig": {
}, "root": true,
"eslintConfig": { "env": {
"root": true, "node": true
"env": { },
"node": true "extends": [
}, "plugin:vue/vue3-essential",
"extends": [ "eslint:recommended"
"plugin:vue/vue3-essential", ],
"eslint:recommended" "parserOptions": {
], "parser": "@babel/eslint-parser",
"parserOptions": { "requireConfigFile": false,
"parser": "@babel/eslint-parser", "ecmaVersion": 2020,
"requireConfigFile": false, "sourceType": "module",
"ecmaVersion": 2020, "ecmaFeatures": {
"sourceType": "module", "globalReturn": false,
"ecmaFeatures": { "impliedStrict": true,
"globalReturn": false, "jsx": true
"impliedStrict": true, },
"jsx": true "babelOptions": {
}, "presets": [
"babelOptions": { "@babel/preset-env"
"presets": ["@babel/preset-env"] ]
} }
}, },
"rules": { "rules": {
"no-debugger": 1 "no-debugger": 1
} }
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
+4
View File
@@ -0,0 +1,4 @@
window.__RUNTIME_CONFIG__ = {
API_HOSTNAME: '${API_HOSTNAME}',
API_PASSWORD: '${API_PASSWORD}'
};
+1
View File
@@ -8,6 +8,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<script src="<%= BASE_URL %>config.js"></script>
</head> </head>
<body> <body>
<noscript> <noscript>
View File
Whitespace-only changes.
+21 -6
View File
@@ -87,14 +87,16 @@ body {
top: 0; top: 0;
width: 100%; /* Full width */ width: 100%; /* Full width */
height: 100%; /* Full height */ height: 100%; /* Full height */
overflow: hidden; overflow-y: auto; /* Scroll tall modals instead of cutting them off */
background-color: rgb(0, 0, 0); /* Fallback color */ background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.5); /* Black w/ opacity */ background-color: rgba(0, 0, 0, 0.6); /* Black w/ opacity */
} }
.modalContainer { .modalContainer {
background-color: #eeeeee; background-color: #333;
color: white;
margin: 10% auto; /* 15% from the top and centered */ margin: 10% auto; /* 15% from the top and centered */
border: 1px solid #888; border: 1px solid #ff9c00;
border-radius: 10px;
width: 50%; /* Could be more or less, depending on screen size */ width: 50%; /* Could be more or less, depending on screen size */
} }
.modalWrapper { .modalWrapper {
@@ -114,16 +116,29 @@ body {
top: 8px; top: 8px;
cursor: pointer; cursor: pointer;
opacity: 70%; opacity: 70%;
font-size: 2rem;
line-height: 1;
color: #ff9c00;
}
/* The close icon is a black svg, recolor it to the app's orange */
img.closeModalImg {
filter: invert(66%) sepia(96%) saturate(2783%) hue-rotate(1deg) brightness(105%) contrast(103%);
}
.closeModalImg:hover {
opacity: 100%;
} }
.modal h1 { .modal h1 {
color: black; color: white;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 10px; padding-top: 10px;
font-size: 1.9rem; font-size: 1.9rem;
} }
.modal p { .modal p {
color: black; color: white;
}
.modal a {
color: #ff9c00;
} }
@media only screen and (max-width: 1000px) { @media only screen and (max-width: 1000px) {
+107
View File
@@ -0,0 +1,107 @@
import axios from "axios";
const config = window.__RUNTIME_CONFIG__ || {};
const hostname = config.API_HOSTNAME || "";
// Request a new token this long before the current one expires
const EXPIRY_MARGIN_MS = 5 * 60 * 1000;
// Client for the MusicServer /api/v1 endpoints, with token authentication.
export const api = axios.create({ baseURL: `${hostname}/api/v1` });
// Client for the server routes outside /api/v1 (the /find search).
export const legacyApi = axios.create({ baseURL: hostname });
// The server keeps a play queue per client, identified by this header. Every music
// response returns the queue's ID (a new one after a reset or a sync), and it is sent
// back with every request. It is stored so a page reload keeps the same queue.
const QUEUE_HEADER = "X-Queue-Id";
const QUEUE_STORAGE_KEY = "queueId";
let queueId = readQueueId();
function readQueueId() {
try {
return localStorage.getItem(QUEUE_STORAGE_KEY);
} catch {
return null;
}
}
function rememberQueueId(response) {
const id = response?.headers?.[QUEUE_HEADER.toLowerCase()];
if (!id || id === queueId) {
return;
}
queueId = id;
try {
localStorage.setItem(QUEUE_STORAGE_KEY, id);
} catch {
// Without storage the queue still works until the page is reloaded.
}
}
let token = null;
let expiresAt = 0;
let pendingToken = null;
function isTokenRequest(request) {
return request.url === "/token";
}
// getToken returns a valid token, requesting a new one when needed.
// Concurrent callers share one request, since token creation is rate limited.
function getToken() {
if (token && Date.now() < expiresAt - EXPIRY_MARGIN_MS) {
return Promise.resolve(token);
}
if (!pendingToken) {
pendingToken = api
.post("/token", { password: config.API_PASSWORD || "", client_type: "web" })
.then((response) => {
token = response.data.token;
expiresAt = new Date(response.data.expires_at).getTime();
return token;
})
.finally(() => {
pendingToken = null;
});
}
return pendingToken;
}
api.interceptors.request.use(async (request) => {
if (!isTokenRequest(request)) {
request.headers.Authorization = `Bearer ${await getToken()}`;
}
if (queueId) {
request.headers[QUEUE_HEADER] = queueId;
}
// Random songs come from the same URL every time. Browsers that cached one before
// the server sent Cache-Control: no-store would otherwise replay it.
request.headers["Cache-Control"] = "no-cache";
return request;
});
api.interceptors.response.use(
(response) => {
rememberQueueId(response);
return response;
},
(error) => {
rememberQueueId(error.response);
return Promise.reject(error);
}
);
// On 401 the token is no longer valid (for example after a server restart):
// get a new one and retry the request once.
api.interceptors.response.use(undefined, (error) => {
const request = error.config;
if (error.response?.status !== 401 || !request || isTokenRequest(request) || request.retried) {
return Promise.reject(error);
}
request.retried = true;
token = null;
return api(request);
});
-3
View File
@@ -1,3 +0,0 @@
export default {
hostname: "$HOSTNAME",
}
+3 -3
View File
@@ -3,7 +3,7 @@
<div class="textWrapper"> <div class="textWrapper">
<transition-group name="displayingInfo" appear> <transition-group name="displayingInfo" appear>
<p v-if="currentTrackHidden" :key="1">????????</p> <p v-if="currentTrackHidden" :key="1">????????</p>
<p v-else :key="2">{{ currentGame }}</p> <p v-else :key="2">{{ currentSoundtrack }}</p>
<p v-if="currentTrackHidden" :key="3">??????</p> <p v-if="currentTrackHidden" :key="3">??????</p>
<p v-else :key="4">{{ currentTrack }}</p> <p v-else :key="4">{{ currentTrack }}</p>
</transition-group> </transition-group>
@@ -16,7 +16,7 @@
<p>??????</p> <p>??????</p>
</div> </div>
<div v-else class="textWrapper"> <div v-else class="textWrapper">
<p>{{ currentGame }}</p> <p>{{ currentSoundtrack }}</p>
<p>{{ currentTrack }}</p> <p>{{ currentTrack }}</p>
</div> </div>
</transition> </transition>
@@ -27,7 +27,7 @@
import { mapState } from "vuex"; import { mapState } from "vuex";
export default { export default {
computed: { computed: {
...mapState(["currentGame", "currentTrack", "currentTrackHidden"]), ...mapState(["currentSoundtrack", "currentTrack", "currentTrackHidden"]),
}, },
}; };
</script> </script>
+22 -23
View File
@@ -8,7 +8,7 @@
<input <input
v-model="searchInputText" v-model="searchInputText"
type="text" type="text"
@input="searchGame()" @input="searchSoundtrack()"
ref="inputField" ref="inputField"
/> />
</div> </div>
@@ -20,52 +20,51 @@
class="inspirationList" class="inspirationList"
ref="inspirationList" ref="inspirationList"
> >
<li v-for="game in showingGamesList" class="inspirationEntry" :key="game"> <li v-for="soundtrack in showingSoundtracksList" class="inspirationEntry" :key="soundtrack">
{{ game }} {{ soundtrack }}
</li> </li>
</transition-group> </transition-group>
</div> </div>
</template> </template>
<script> <script>
import arne from '../../arne.js'
import {mapState} from "vuex"; import {mapState} from "vuex";
export default { export default {
data() { data() {
return { return {
allGamesList: [], allSoundtracksList: [],
showingGamesList: [], showingSoundtracksList: [],
scrollDown: true, scrollDown: true,
searchInputText: "", searchInputText: "",
}; };
}, },
computed: { computed: {
...mapState(["reloadGamesList"]), ...mapState(["reloadSoundtracksList"]),
}, },
watch: { watch: {
reloadGamesList(newValue) { reloadSoundtracksList(newValue) {
if (newValue) { if (newValue) {
this.reloadGames(); this.reloadSoundtracks();
this.$store.dispatch("reloadGamesList", false); this.$store.dispatch("reloadSoundtracksList", false);
} }
} }
}, },
methods: { methods: {
clearSearch() { clearSearch() {
this.searchInputText = ""; this.searchInputText = "";
this.searchGame(); this.searchSoundtrack();
}, },
searchGame() { searchSoundtrack() {
this.showingGamesList = []; this.showingSoundtracksList = [];
for (const game of this.allGamesList) { for (const soundtrack of this.allSoundtracksList) {
if (this.searchInputText === "" || if (this.searchInputText === "" ||
game.toLowerCase().replace(/\s/g, "") soundtrack.toLowerCase().replace(/\s/g, "")
.includes(this.searchInputText.toLowerCase().replace(/\s/g, ""))) { .includes(this.searchInputText.toLowerCase().replace(/\s/g, ""))) {
this.showingGamesList.push(game); this.showingSoundtracksList.push(soundtrack);
} }
} }
if (this.searchInputText.replace(/\s/g, "") !== "") { if (this.searchInputText.replace(/\s/g, "") !== "") {
this.showingGamesList.sort((n1, n2) => { this.showingSoundtracksList.sort((n1, n2) => {
if (n1 > n2) { if (n1 > n2) {
return 1; return 1;
} }
@@ -90,16 +89,16 @@ export default {
this.scrollDown = !this.scrollDown; this.scrollDown = !this.scrollDown;
} }
}, },
reloadGames() { reloadSoundtracks() {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/all`, url: `/music/all`,
}) })
.then((response) => { .then((response) => {
this.allGamesList = response.data; this.allSoundtracksList = response.data;
this.searchInputText = ""; this.searchInputText = "";
this.searchGame(); this.searchSoundtrack();
this.$store.dispatch("updateHowManyGames", this.allGamesList.length); this.$store.dispatch("updateHowManySoundtracks", this.allSoundtracksList.length);
}) })
.catch(function(error) { .catch(function(error) {
console.log(error); console.log(error);
@@ -107,7 +106,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.reloadGames(); this.reloadSoundtracks();
window.setInterval(() => { window.setInterval(() => {
this.scrollInspiration(); this.scrollInspiration();
}, 40); }, 40);
+187
View File
@@ -0,0 +1,187 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer">
<div class="modalWrapper">
<span class="closeModalImg" @click="closeModal">&times;</span>
<h1>Search Soundtracks</h1>
<div class="searchContainer">
<input
type="text"
v-model="searchTerm"
@input="debouncedSearch"
placeholder="Search for soundtracks..."
class="searchInput"
/>
<div v-if="searchTerm.length > 0" class="searchResults">
<div v-if="loading" class="loading">Searching...</div>
<div v-else-if="results.length > 0" class="resultsList">
<div v-for="soundtrack in results" :key="soundtrack" class="resultItem">
{{ soundtrack }}
</div>
</div>
<div v-else class="noResults">No results found</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false,
searchTerm: "",
results: [],
loading: false,
searchTimeout: null,
};
},
methods: {
openModal() {
this.show = true;
this.searchTerm = "";
this.results = [];
},
closeModal() {
this.show = false;
this.searchTerm = "";
this.results = [];
this.loading = false;
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
this.searchTimeout = null;
}
},
debouncedSearch() {
// Clear existing timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// Set new timeout
this.searchTimeout = setTimeout(() => {
this.performSearch();
}, 250);
},
async performSearch() {
if (this.searchTerm.length === 0) {
this.results = [];
return;
}
this.loading = true;
try {
const response = await this.legacyAxios({
method: "post",
url: "/find",
data: new URLSearchParams({ search_term: this.searchTerm }),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
// Parse soundtracks from HTML response
this.results = this.parseSoundtracksFromResponse(response.data);
} catch (error) {
console.error("Error searching soundtracks:", error);
this.results = [];
} finally {
this.loading = false;
}
},
parseSoundtracksFromResponse(htmlData) {
// The server returns HTML from FoundSoundtracks templ component
// It generates divs containing soundtrack names in <p> tags
const soundtracks = [];
const parser = new DOMParser();
const doc = parser.parseFromString(htmlData, "text/html");
// Find all p tags inside divs (the templ generates <div><p>{soundtrack}</p></div>)
const pTags = doc.querySelectorAll("div p");
for (const p of pTags) {
const soundtrackName = p.textContent.trim();
if (soundtrackName) {
soundtracks.push(soundtrackName);
}
}
return soundtracks;
},
},
};
</script>
<style scoped>
.searchContainer {
width: 100%;
margin-top: 10px;
}
.searchInput {
width: 100%;
padding: 12px;
font-size: 1.1rem;
border: 1px solid #777;
background-color: rgb(66, 66, 66);
color: white;
border-radius: 5px;
box-sizing: border-box;
}
.searchInput:focus {
outline: none;
border-color: #ff9c00;
box-shadow: 0 0 5px rgba(255, 156, 0, 0.3);
}
.searchResults {
margin-top: 15px;
max-height: 400px;
overflow-y: auto;
}
.loading {
color: #bbb;
font-style: italic;
text-align: center;
padding: 20px;
}
.resultsList {
width: 100%;
}
.resultItem {
padding: 12px;
margin-bottom: 8px;
background-color: rgb(66, 66, 66);
border: 1px solid #555;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
.resultItem:hover {
background-color: #555;
}
.noResults {
color: #bbb;
text-align: center;
padding: 20px;
font-style: italic;
}
@media only screen and (max-width: 1000px) {
.searchInput {
font-size: 1rem;
padding: 10px;
}
.resultItem {
padding: 10px;
font-size: 0.95rem;
}
}
</style>
+337
View File
@@ -0,0 +1,337 @@
<template>
<transition name="modalAni">
<div class="modal" v-if="isOpen" @click.self="closeModal">
<div class="modalContainer syncProgressContainer">
<span class="closeModalImg" @click="closeModal">&times;</span>
<h1>Sync in Progress</h1>
<div v-if="!syncComplete" class="progressSection">
<div class="progressBarContainer">
<div class="progressBar" :style="{ width: progress + '%' }"></div>
</div>
<p class="progressText">{{ progress }}% complete</p>
<p class="timeText">Time spent: {{ timeSpent }}</p>
</div>
<div v-else class="resultsSection">
<h2>Sync Complete!</h2>
<p>Total time: {{ syncData.total_time }}</p>
<div v-if="syncData.soundtracks_added && syncData.soundtracks_added.length > 0" class="resultItem">
<h3>Soundtracks Added: {{ syncData.soundtracks_added.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_added" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.soundtracks_re_added && syncData.soundtracks_re_added.length > 0" class="resultItem">
<h3>Soundtracks Re-added: {{ syncData.soundtracks_re_added.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_re_added" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.soundtracks_removed && syncData.soundtracks_removed.length > 0" class="resultItem">
<h3>Soundtracks Removed: {{ syncData.soundtracks_removed.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_removed" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="Object.keys(syncData.soundtracks_changed_title || {}).length > 0" class="resultItem">
<h3>Soundtracks with Changed Title: {{ Object.keys(syncData.soundtracks_changed_title || {}).length }}</h3>
<ul class="soundtrackList">
<li v-for="(newTitle, oldTitle) in syncData.soundtracks_changed_title" :key="oldTitle">{{ oldTitle }} → {{ newTitle }}</li>
</ul>
</div>
<div v-if="syncData.soundtracks_changed_content && syncData.soundtracks_changed_content.length > 0" class="resultItem">
<h3>Soundtracks with Changed Content: {{ syncData.soundtracks_changed_content.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_changed_content" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.catched_errors && syncData.catched_errors.length > 0" class="resultItem errors">
<h3>Errors: {{ syncData.catched_errors.length }}</h3>
<ul class="soundtrackList">
<li v-for="error in syncData.catched_errors" :key="error">{{ error }}</li>
</ul>
</div>
<p v-if="noChanges" class="noChanges">No changes detected.</p>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
props: {
isOpen: {
type: Boolean,
required: true,
},
syncInProgress: {
type: Boolean,
default: false,
},
},
data() {
return {
progress: 0,
timeSpent: "00:00",
syncComplete: false,
syncData: {
soundtracks_added: [],
soundtracks_re_added: [],
soundtracks_changed_title: {},
soundtracks_changed_content: [],
soundtracks_removed: [],
catched_errors: [],
total_time: "",
},
pollInterval: null,
hasFetchedInitialData: false,
};
},
computed: {
noChanges() {
return (
this.syncData.soundtracks_added.length === 0 &&
this.syncData.soundtracks_re_added.length === 0 &&
Object.keys(this.syncData.soundtracks_changed_title).length === 0 &&
this.syncData.soundtracks_changed_content.length === 0 &&
this.syncData.soundtracks_removed.length === 0 &&
this.syncData.catched_errors.length === 0
);
},
},
methods: {
closeModal() {
this.$emit("close");
},
startPolling() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
this.pollInterval = setInterval(() => {
this.fetchProgress();
}, 1000);
},
stopPolling() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
},
async fetchProgress() {
try {
const response = await this.axios({
method: "get",
url: "/sync/progress",
});
const data = response.data;
// Check if we got a progress response or a result response
if (data.progress !== undefined) {
// Still syncing - update progress
this.progress = parseInt(data.progress) || 0;
this.timeSpent = data.time_spent || this.timeSpent;
this.syncComplete = false;
// If we're not actively polling but sync is in progress, start polling
if (this.syncInProgress && !this.pollInterval) {
this.startPolling();
}
} else {
// Sync complete - show results
this.syncComplete = true;
this.syncData = {
soundtracks_added: data.soundtracks_added || [],
soundtracks_re_added: data.soundtracks_re_added || [],
soundtracks_changed_title: data.soundtracks_changed_title || {},
soundtracks_changed_content: data.soundtracks_changed_content || [],
soundtracks_removed: data.soundtracks_removed || [],
catched_errors: data.catched_errors || [],
total_time: data.total_time || "",
};
this.progress = 100;
this.stopPolling();
}
this.hasFetchedInitialData = true;
} catch (error) {
console.error("Error fetching sync progress:", error);
}
},
async fetchInitialData() {
// Fetch once to check current state
await this.fetchProgress();
// If we got progress data, start polling
if (!this.syncComplete && !this.pollInterval) {
this.startPolling();
}
},
},
watch: {
isOpen(newVal) {
if (newVal) {
this.hasFetchedInitialData = false;
if (this.syncInProgress) {
// New sync in progress - start polling
this.progress = 0;
this.timeSpent = "00:00";
this.syncComplete = false;
this.syncData = {
soundtracks_added: [],
soundtracks_re_added: [],
soundtracks_changed_title: {},
soundtracks_changed_content: [],
soundtracks_removed: [],
catched_errors: [],
total_time: "",
};
// Small delay to ensure sync has started before first poll
setTimeout(() => {
this.startPolling();
}, 200);
} else {
// Check current state - might be in progress or have results
this.fetchInitialData();
}
} else {
this.stopPolling();
}
},
},
beforeUnmount() {
this.stopPolling();
},
};
</script>
<style scoped>
.syncProgressContainer {
position: relative;
width: 70%;
max-width: 800px;
padding: 20px;
box-sizing: border-box;
}
.syncProgressContainer h1 {
text-align: center;
margin-bottom: 20px;
}
.syncProgressContainer h2 {
color: white;
text-align: center;
font-size: 1.5rem;
margin-bottom: 15px;
}
.syncProgressContainer p {
text-align: center;
}
.progressSection {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.progressBarContainer {
width: 100%;
height: 30px;
background-color: rgb(66, 66, 66);
border-radius: 15px;
overflow: hidden;
margin-bottom: 10px;
}
.progressBar {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
border-radius: 15px;
transition: width 0.3s ease;
text-align: center;
line-height: 30px;
color: white;
font-weight: bold;
}
.progressText {
font-size: 1.3rem;
font-weight: bold;
}
.timeText {
font-size: 1rem;
color: #bbb;
}
.resultsSection {
max-height: 60vh;
overflow-y: auto;
}
.resultItem {
margin-bottom: 20px;
padding: 10px;
background-color: rgb(66, 66, 66);
border-radius: 8px;
border-left: 4px solid #4CAF50;
}
.resultItem.errors {
border-left-color: #f44336;
}
.resultItem h3 {
color: white;
font-size: 1.1rem;
margin-bottom: 8px;
}
.soundtrackList {
list-style-type: none;
padding: 0;
margin: 5px 0 0 0;
}
.soundtrackList li {
padding: 3px 0;
color: #ddd;
}
.noChanges {
text-align: center;
color: #bbb;
font-style: italic;
padding: 20px;
}
@media only screen and (max-width: 1000px) {
.syncProgressContainer {
width: 93%;
margin: 5% auto;
padding: 15px;
}
.syncProgressContainer h1 {
font-size: 1.6rem;
padding-top: 5px;
}
.progressBarContainer {
height: 25px;
}
.progressText {
font-size: 1.1rem;
}
}
</style>
+2 -7
View File
@@ -1,6 +1,6 @@
<template> <template>
<transition name="modalAni"> <transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal"> <div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer"> <div class="modalContainer">
<div class="modalWrapper"> <div class="modalWrapper">
<img <img
@@ -11,7 +11,7 @@
/> />
<h1>Music Player Randomizer v0.1</h1> <h1>Music Player Randomizer v0.1</h1>
<p class="descriptionText"> <p class="descriptionText">
Try your video game music knowledge with this VGM randomizer, Try your video game soundtrack knowledge with this VGM randomizer,
invite your friends and see who is the best. invite your friends and see who is the best.
</p> </p>
<p class="creditText"> <p class="creditText">
@@ -46,11 +46,6 @@ export default {
openModal() { openModal() {
this.show = true; this.show = true;
}, },
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
}, },
}; };
</script> </script>
+192 -160
View File
@@ -1,160 +1,192 @@
<template> <template>
<transition name="modalAni"> <transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal"> <div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer"> <div class="modalContainer">
<div class="modalWrapper"> <div class="modalWrapper">
<img <img
class="closeModalImg" class="closeModalImg"
src="cancel-black-36dp.svg" src="cancel-black-36dp.svg"
alt="closeModalIMG" alt="closeModalIMG"
@click="closeModal" @click="closeModal"
/> />
<h1 @click="test">{{ playerName }}, choose your fighter!</h1> <h1>{{ playerName }}, choose your fighter!</h1>
<div class="fighterDiv"> <div v-if="isLoading" class="loading">Loading characters...</div>
<div class="fighter" @click="chooseFighter('Adol')"> <div class="fighterDiv" v-else>
<img src="characters/Adol.png" alt="adol" /> <div
<p>Adol Christin</p> v-for="character in characters"
</div> :key="character"
<div class="fighter" @click="chooseFighter('Link')"> class="fighter"
<img src="characters/Link.png" alt="link" /> @click="chooseFighter(character)"
<p>Link</p> >
</div> <img
<div class="fighter" @click="chooseFighter('Barbarian')"> :src="getCharacterImageUrl(character)"
<img src="characters/Barbarian.png" alt="barbarian" /> :alt="getCharacterName(character)"
<p>Barbarian</p> />
</div> <p>{{ getCharacterName(character) }}</p>
<div class="fighter" @click="chooseFighter('Layton')"> </div>
<img src="characters/Layton.png" alt="layton" /> </div>
<p>Professor Layton</p> </div>
</div> </div>
<div class="fighter" @click="chooseFighter('Kiryu')"> </div>
<img src="characters/Kiryu.png" alt="kiryu" /> </transition>
<p>Kazuma Kiryu</p> </template>
</div>
<div class="fighter" @click="chooseFighter('Miles')"> <script>
<img src="characters/Miles.png" alt="miles" /> export default {
<p>Miles Edgeworth</p> data() {
</div> return {
<div class="fighter" @click="chooseFighter('Lemmings')"> show: false,
<img src="characters/Lemmings.png" alt="lemmings" /> playerName: "",
<p>Lemmings</p> characters: [],
</div> // Object URLs of the character images, by file name
<div class="fighter" @click="chooseFighter('Samus')"> imageUrls: {},
<img src="characters/Samus.png" alt="samus" /> isLoading: false,
<p>Samus</p> };
</div> },
<div class="fighter" @click="chooseFighter('Kratos')"> methods: {
<img src="characters/Kratos.png" alt="kratos" /> closeModal() {
<p>Kratos</p> this.show = false;
</div> },
<div class="fighter" @click="chooseFighter('Aloy')"> async openModal(playerName) {
<img src="characters/Aloy.png" alt="aloy" /> this.playerName = playerName;
<p>Aloy</p> this.show = true;
</div> await this.fetchCharacters();
<div class="fighter" @click="chooseFighter('Sora')"> },
<img src="characters/Sora.png" alt="sora" /> async fetchCharacters() {
<p>Sora</p> this.isLoading = true;
</div> try {
<div class="fighter" @click="chooseFighter('Raiden')"> const response = await this.axios({
<img src="characters/Raiden.png" alt="raiden" /> method: "get",
<p>Raiden</p> url: "/character/list",
</div> });
<div class="fighter" @click="chooseFighter('PaperMario')"> this.characters = response.data || [];
<img src="characters/PaperMario.png" alt="paper mario" /> await this.fetchImages();
<p>Paper Mario</p> } catch (error) {
</div> console.error("Failed to fetch characters:", error);
</div> // Fall back to the characters bundled with the frontend
</div> this.characters = [
</div> "Adol.png",
</div> "Link.png",
</transition> "Barbarian.png",
</template> "Layton.png",
"Kiryu.png",
<script> "Miles.png",
export default { "Lemmings.png",
data() { "Samus.png",
return { "Kratos.png",
show: false, "Aloy.png",
playerName: "", "Sora.png",
}; "Raiden.png",
}, "PaperMario.png",
methods: { ];
closeModal() { this.imageUrls = Object.fromEntries(
this.show = false; this.characters.map((character) => [character, `characters/${character}`])
}, );
openModal(playerName) { } finally {
this.playerName = playerName; this.isLoading = false;
this.show = true; }
}, },
checkIfClickShouldCloseModal(event) { // Images need the token, which <img src> can't send, so they are
if (event.target.classList[0] === "modal") { // downloaded once and shown through object URLs.
this.closeModal(); async fetchImages() {
} await Promise.all(
}, this.characters
chooseFighter(profilePicSrc) { .filter((character) => !this.imageUrls[character])
let payload = { .map(async (character) => {
playerName: this.playerName, try {
profile: "characters/" + profilePicSrc + ".png", const response = await this.axios({
}; method: "get",
this.$store.dispatch("changePlayerProfile", payload); url: "/character",
this.closeModal(); params: { name: character },
}, responseType: "blob",
}, });
}; this.imageUrls[character] = URL.createObjectURL(response.data);
</script> } catch (error) {
console.error(`Failed to fetch character ${character}:`, error);
<style scoped> }
.modalContainer { })
width: 80%; /* Could be more or less, depending on screen size */ );
} },
.fighterDiv { getCharacterImageUrl(character) {
display: flex; return this.imageUrls[character] || "characters/noCharacter.png";
width: 100%; },
justify-content: center; getCharacterName(character) {
margin-top: 30px; // Remove file extension
flex-wrap: wrap; return character.replace(/\.(png|jpg|jpeg|gif)$/i, "");
} },
chooseFighter(characterFilename) {
.fighterDiv img { let payload = {
width: 180px; playerName: this.playerName,
height: 90px; profile: this.getCharacterImageUrl(characterFilename),
} };
this.$store.dispatch("changePlayerProfile", payload);
.fighter { this.closeModal();
display: flex; },
flex-wrap: wrap; },
justify-content: center; };
text-align: center; </script>
width: 14vw;
margin-top: 30px; <style scoped>
} .modalContainer {
.fighter > * { width: 80%; /* Could be more or less, depending on screen size */
width: 100%; }
} .fighterDiv {
.fighter:hover { display: flex;
outline: 1px solid rgb(128, 83, 0); width: 100%;
outline-offset: 2px; justify-content: center;
cursor: pointer; margin-top: 30px;
} flex-wrap: wrap;
.fighter p { }
margin-top: 8px;
font-size: 1.3rem; .fighterDiv img {
} width: 180px;
height: 90px;
@media only screen and (max-width: 1000px) { object-fit: contain;
.modalContainer { }
width: 95%; /* Could be more or less, depending on screen size */
} .fighter {
.fighterDiv { display: flex;
margin-top: 30px; flex-wrap: wrap;
} justify-content: center;
.fighterDiv img { text-align: center;
width: 80px; width: 14vw;
height: 40px; margin-top: 30px;
} }
.fighter > * {
.fighter { width: 100%;
width: 25vw; }
} .fighter:hover {
} outline: 1px solid rgb(128, 83, 0);
</style> outline-offset: 2px;
cursor: pointer;
}
.fighter p {
margin-top: 8px;
font-size: 1.3rem;
}
.loading {
text-align: center;
padding: 40px;
font-size: 1.2rem;
color: #bbb;
}
@media only screen and (max-width: 1000px) {
.modalContainer {
width: 95%; /* Could be more or less, depending on screen size */
}
.fighterDiv {
margin-top: 30px;
}
.fighterDiv img {
width: 80px;
height: 40px;
object-fit: contain;
}
.fighter {
width: 25vw;
}
}
</style>
+62 -13
View File
@@ -2,17 +2,33 @@
<div class="extraButtonsDiv"> <div class="extraButtonsDiv">
<button @click="resetPlaylist">Reset playlist</button> <button @click="resetPlaylist">Reset playlist</button>
<button @click="resetPoints">Reset points</button> <button @click="resetPoints">Reset points</button>
<button @click="syncGames">Sync games</button> <button @click="handleSyncButtonClick">Sync soundtracks</button>
<button @click="startSoundTest">Sound test</button> <button @click="startSoundTest">Sound test</button>
<button @click="showSearchModal">Search</button>
<sync-progress-modal
:isOpen="showSyncModal"
:syncInProgress="syncInProgress"
@close="handleSyncComplete"
></sync-progress-modal>
<search-modal ref="searchModal"></search-modal>
</div> </div>
</template> </template>
<script> <script>
import arne from '../../arne.js' import SyncProgressModal from "./SyncProgressModal.vue";
import SearchModal from "./SearchModal.vue";
export default { export default {
components: {
SyncProgressModal,
SearchModal,
},
data() { data() {
return { return {
emptyPlaylist: [], emptyPlaylist: [],
showSyncModal: false,
syncInProgress: false,
syncCompleted: false,
}; };
}, },
methods: { methods: {
@@ -28,8 +44,39 @@ export default {
this.$store.dispatch("setCurrentlyLoadingTrack", "N/A"); this.$store.dispatch("setCurrentlyLoadingTrack", "N/A");
this.$store.dispatch("setCurrentTrackHidden", false); this.$store.dispatch("setCurrentTrackHidden", false);
}, },
async syncGames() { async startSync() {
await this.APIsyncGames(); try {
await this.APIsyncSoundtracks();
} catch (error) {
// 423 means a sync is already running: show its progress
if (!error.response || error.response.status !== 423) {
console.error("Failed to start sync:", error);
return;
}
}
// Show the modal which will poll for progress
this.syncInProgress = true;
this.showSyncModal = true;
},
handleSyncButtonClick() {
// If we have a completed sync result, just show it
// Otherwise start a new sync
if (this.syncCompleted) {
this.showSyncResults();
} else {
this.startSync();
}
},
showSyncResults() {
// Show modal with existing results (no new sync)
this.syncInProgress = false;
this.showSyncModal = true;
},
async handleSyncComplete() {
this.syncInProgress = false;
this.syncCompleted = true;
this.showSyncModal = false;
// Refresh data after sync completes
await this.APIresetPlaylist(); await this.APIresetPlaylist();
this.$store.dispatch("resetPlayerScore"); this.$store.dispatch("resetPlayerScore");
this.$store.dispatch("resetPlayerWelcomed"); this.$store.dispatch("resetPlayerWelcomed");
@@ -38,16 +85,19 @@ export default {
this.$store.dispatch("updatePlaylistHistory", this.emptyPlaylist); this.$store.dispatch("updatePlaylistHistory", this.emptyPlaylist);
this.$store.dispatch("setCurrentlyLoadingTrack", "N/A"); this.$store.dispatch("setCurrentlyLoadingTrack", "N/A");
this.$store.dispatch("setCurrentTrackHidden", false); this.$store.dispatch("setCurrentTrackHidden", false);
this.$store.dispatch("reloadGamesList", true); this.$store.dispatch("reloadSoundtracksList", true);
}, },
startSoundTest() { startSoundTest() {
this.$emit("start-sound-test"); this.$emit("start-sound-test");
}, },
showSearchModal() {
this.$refs.searchModal.openModal();
},
APIresetPlaylist() { APIresetPlaylist() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/reset`, url: `/music/reset`,
}) })
.then(() => { .then(() => {
resolve(); resolve();
@@ -58,18 +108,17 @@ export default {
}); });
}); });
}, },
APIsyncGames() { APIsyncSoundtracks() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "post",
url: `${arne.hostname}/sync`, url: "/sync",
}) })
.then(() => { .then((response) => {
resolve(); resolve(response);
}) })
.catch(function(error) { .catch(function(error) {
console.log(error); reject(error);
reject();
}); });
}); });
}, },
+2 -1
View File
@@ -109,7 +109,8 @@ export default {
} }
.mobileMenuContainer { .mobileMenuContainer {
background-color: #c4c4c4; background-color: #333;
color: white;
width: 80vw; width: 80vw;
height: 100%; height: 100%;
margin-left: 20%; margin-left: 20%;
+2 -2
View File
@@ -24,8 +24,8 @@
> >
??? - ??? ??? - ???
</p> </p>
<p v-else-if="track.Game !== ''" :class="{ activeTrack: track.CurrentlyPlaying }"> <p v-else-if="track.Soundtrack !== ''" :class="{ activeTrack: track.CurrentlyPlaying }">
{{ track.SongNo + 1 }}. {{ track.Game }} - {{ track.SongNo + 1 }}. {{ track.Soundtrack }} -
{{ displayTrack(track.Song) }} {{ displayTrack(track.Song) }}
</p> </p>
<span v-if="currentlyLoadingTrack === track.SongNo" class="loadingTrack"> <span v-if="currentlyLoadingTrack === track.SongNo" class="loadingTrack">
+2 -9
View File
@@ -1,7 +1,7 @@
<template> <template>
<!-- Round Settings, Stop after current, Don't hide title --> <!-- Round Settings, Stop after current, Don't hide title -->
<transition name="modalAni"> <transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal"> <div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer"> <div class="modalContainer">
<div class="modalWrapper"> <div class="modalWrapper">
<img <img
@@ -98,11 +98,6 @@ export default {
openModal() { openModal() {
this.show = true; this.show = true;
}, },
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
updateOptionHideTitle() { updateOptionHideTitle() {
if (this.checkboxHideTitle) { if (this.checkboxHideTitle) {
this.$store.dispatch("updateHideNextTitle", true); this.$store.dispatch("updateHideNextTitle", true);
@@ -165,6 +160,7 @@ export default {
margin-top: 20px; margin-top: 20px;
} }
.checkboxDiv input { .checkboxDiv input {
accent-color: #ff9c00;
transform: scale(1.3); transform: scale(1.3);
} }
.checkboxDiv label { .checkboxDiv label {
@@ -181,9 +177,6 @@ export default {
display: flex; display: flex;
width: 50px; width: 50px;
height: 33px; height: 33px;
background: rgb(16, 99, 194);
color: rgb(235, 235, 235);
border-color: rgb(161, 161, 161);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
line-height: 0; line-height: 0;
+3 -8
View File
@@ -1,6 +1,6 @@
<template> <template>
<transition name="modalAni"> <transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal"> <div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer"> <div class="modalContainer">
<div class="modalWrapper"> <div class="modalWrapper">
<img <img
@@ -10,7 +10,7 @@
@click="closeModal" @click="closeModal"
/> />
<h1>Statistics</h1> <h1>Statistics</h1>
<p>Total amount of games in the playlist: {{ howManyGames }}</p> <p>Total amount of soundtracks in the playlist: {{ howManySoundtracks }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -26,7 +26,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(["howManyGames"]), ...mapState(["howManySoundtracks"]),
}, },
methods: { methods: {
closeModal() { closeModal() {
@@ -35,11 +35,6 @@ export default {
openModal() { openModal() {
this.show = true; this.show = true;
}, },
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
}, },
}; };
</script> </script>
+1 -6
View File
@@ -1,6 +1,6 @@
<template> <template>
<transition name="modalAni"> <transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal"> <div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer"> <div class="modalContainer">
<div class="modalWrapper"> <div class="modalWrapper">
<img <img
@@ -38,11 +38,6 @@ export default {
this.playerName = playerName; this.playerName = playerName;
this.show = true; this.show = true;
}, },
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
}, },
}; };
</script> </script>
+11 -12
View File
@@ -28,7 +28,6 @@
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
import arne from "../../arne.js";
export default { export default {
data() { data() {
return { return {
@@ -168,7 +167,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/rand`, url: `/music/rand`,
responseType: "blob", responseType: "blob",
onDownloadProgress: (progressEvent) => { onDownloadProgress: (progressEvent) => {
let percentCompleted = Math.round( let percentCompleted = Math.round(
@@ -198,7 +197,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/rand/low`, url: `/music/rand/low`,
responseType: "blob", responseType: "blob",
onDownloadProgress: (progressEvent) => { onDownloadProgress: (progressEvent) => {
let percentCompleted = Math.round( let percentCompleted = Math.round(
@@ -228,7 +227,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/addPlayed`, url: `/music/addPlayed`,
}) })
.then(() => { .then(() => {
resolve(false); resolve(false);
@@ -243,7 +242,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/addQue`, url: `/music/addQue`,
}) })
.then(() => { .then(() => {
resolve(false); resolve(false);
@@ -258,16 +257,16 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/info`, url: `/music/info`,
}) })
.then((response) => { .then((response) => {
let gameInfoObject = { let soundtrackInfoObject = {
game: response.data.Game, soundtrack: response.data.Soundtrack,
track: response.data.Song.replace(".mp3", ""), track: response.data.Song.replace(".mp3", ""),
songNo: response.data.SongNo, songNo: response.data.SongNo,
}; };
this.$store.dispatch("setCurrentGame", gameInfoObject.game); this.$store.dispatch("setCurrentSoundtrack", soundtrackInfoObject.soundtrack);
this.$store.dispatch("setCurrentTrack", gameInfoObject.track); this.$store.dispatch("setCurrentTrack", soundtrackInfoObject.track);
resolve(false); resolve(false);
}) })
.catch(function(error) { .catch(function(error) {
@@ -280,7 +279,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music/list`, url: `/music/list`,
}) })
.then((response) => { .then((response) => {
let tracklistArray = response.data; let tracklistArray = response.data;
@@ -297,7 +296,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axios({ this.axios({
method: "get", method: "get",
url: `${arne.hostname}/music?song=${trackNumber}`, url: `/music?song=${trackNumber}`,
responseType: "blob", responseType: "blob",
}) })
.then((response) => { .then((response) => {
+18 -18
View File
@@ -1,13 +1,13 @@
import { createApp } from "vue"; import { createApp } from "vue";
import { createStore } from "vuex"; import { createStore } from "vuex";
import App from "./App.vue"; import App from "./App.vue";
import axios from "axios";
import VueAxios from "vue-axios"; import VueAxios from "vue-axios";
import { api, legacyApi } from "./api";
const store = createStore({ const store = createStore({
state() { state() {
return { return {
currentGame: "", currentSoundtrack: "",
currentTrack: ``, currentTrack: ``,
currentTrackHidden: false, currentTrackHidden: false,
currentlyLoadingTrack: "", currentlyLoadingTrack: "",
@@ -15,18 +15,18 @@ const store = createStore({
someoneHasWon: false, someoneHasWon: false,
winningScore: 20, winningScore: 20,
roundStarted: false, roundStarted: false,
reloadGamesList: false, reloadSoundtracksList: false,
listOfPlayers: [], listOfPlayers: [],
localPlaylist: [], localPlaylist: [],
playlistHistory: [ playlistHistory: [
{ {
SongNo: "", SongNo: "",
Game: "", Soundtrack: "",
Song: "", Song: "",
}, },
], ],
/* Stats */ /* Stats */
howManyGames: 0, howManySoundtracks: 0,
/* Options */ /* Options */
stopAfterCurrent: true, stopAfterCurrent: true,
hideNextTrack: true, hideNextTrack: true,
@@ -35,8 +35,8 @@ const store = createStore({
}; };
}, },
mutations: { mutations: {
setCurrentGame(state, payload) { setCurrentSoundtrack(state, payload) {
state.currentGame = payload; state.currentSoundtrack = payload;
}, },
setCurrentTrack(state, payload) { setCurrentTrack(state, payload) {
state.currentTrack = payload; state.currentTrack = payload;
@@ -89,11 +89,11 @@ const store = createStore({
updatePlaylistHistory(state, payload) { updatePlaylistHistory(state, payload) {
state.playlistHistory = payload; state.playlistHistory = payload;
}, },
updateHowManyGames(state, payload) { updateHowManySoundtracks(state, payload) {
state.howManyGames = payload; state.howManySoundtracks = payload;
}, },
reloadGamesList(state, payload) { reloadSoundtracksList(state, payload) {
state.reloadGamesList = payload; state.reloadSoundtracksList = payload;
}, },
updateStopAfterCurrent(state, payload) { updateStopAfterCurrent(state, payload) {
state.stopAfterCurrent = payload; state.stopAfterCurrent = payload;
@@ -109,8 +109,8 @@ const store = createStore({
}, },
}, },
actions: { actions: {
setCurrentGame(context, payload) { setCurrentSoundtrack(context, payload) {
context.commit("setCurrentGame", payload); context.commit("setCurrentSoundtrack", payload);
}, },
setCurrentTrack(context, payload) { setCurrentTrack(context, payload) {
context.commit("setCurrentTrack", payload); context.commit("setCurrentTrack", payload);
@@ -225,11 +225,11 @@ const store = createStore({
updatePlaylistHistory(context, payload) { updatePlaylistHistory(context, payload) {
context.commit("updatePlaylistHistory", payload); context.commit("updatePlaylistHistory", payload);
}, },
updateHowManyGames(context, payload) { updateHowManySoundtracks(context, payload) {
context.commit("updateHowManyGames", payload); context.commit("updateHowManySoundtracks", payload);
}, },
reloadGamesList(context, payload) { reloadSoundtracksList(context, payload) {
context.commit("reloadGamesList", payload); context.commit("reloadSoundtracksList", payload);
}, },
updateStopAfterCurrent(context, payload) { updateStopAfterCurrent(context, payload) {
context.commit("updateStopAfterCurrent", payload); context.commit("updateStopAfterCurrent", payload);
@@ -256,6 +256,6 @@ const store = createStore({
const app = createApp(App); const app = createApp(App);
app.use(store); app.use(store);
app.use(VueAxios, axios); app.use(VueAxios, { axios: api, legacyAxios: legacyApi });
app.mount("#app"); app.mount("#app");