Author SHA1 Message Date
SansanandClaude Opus 5.5 a7e2280969 Upload new soundtracks through the API
CI / integration (pull_request) Successful in 21s
CI / test (pull_request) Successful in 26s
CI / publish (pull_request) Skipped
CI / docker (pull_request) Successful in 43s
A client starts an upload with the soundtrack's name, sends one MP3 per
request, and finishes it. The server then moves the songs into a new folder
in the music folder and syncs only that folder, so the soundtrack is in the
library, lists and search right away.

- POST /api/v1/soundtracks/uploads, GET/DELETE .../uploads/{id},
  PUT .../uploads/{id}/files/{file} and POST .../uploads/{id}/finish.
- Names must work as file names on every system and be free, ignoring case.
  File names are plain: clients flatten disc folders.
- Only MP3 files. Size limits from UPLOAD_MAX_FILE_MB (50) and
  UPLOAD_MAX_TOTAL_MB (2048).
- Refused while a sync runs, and a sync cannot start while an upload is
  finished. Songs equal to an existing soundtrack's are refused, as the sync
  would take them for that soundtrack renamed.
- Unfinished uploads live in MUSIC_PATH/.uploads and are removed after a day.
  The sync now skips every hidden folder.

Closes #33

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:38:26 +02:00
SansanandClaude Opus 5.5 439ffede18 Build the image with Go 1.26
CI / integration (push) Successful in 18s
CI / test (push) Successful in 21s
CI / publish (push) Skipped
CI / docker (push) Successful in 50s
The Prometheus client and the golang.org/x updates raised go.mod to Go 1.26,
so the Docker build with golang:1.25-alpine failed in go mod download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:59:05 +02:00
SansanandClaude Opus 5.5 c70eb4d13c Add GET /api/v1/download/linux-package
CI / docker (push) Failing after 20s
CI / test (push) Successful in 23s
CI / integration (push) Successful in 25s
CI / publish (push) Skipped
Redirects to the newest MusicPlayer release's _linux.tar.gz, which has the
icon, a menu entry and install.sh. /download/linux keeps serving the bare
.x86_64, which updating in place needs. Releases without the package get 404.

Closes #34

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:40:22 +02:00
SansanandClaude Opus 5.5 bb8de5548d Add alternative names and cover art for soundtracks
CI / docker (push) Failing after 22s
CI / integration (push) Successful in 26s
CI / test (push) Successful in 26s
CI / publish (push) Skipped
Alternative names (#32):
- PUT /api/v1/soundtracks/{uuid}/alternative-names replaces a soundtrack's
  alternative names (trimmed, deduplicated, without the soundtrack's own name).
- alternative_names is in the soundtrack and statistics responses.
- /find matches them too and shows which name matched.

Covers (#12), stored in COVERS_PATH and off when it is not set:
- GET, PUT (upload) and DELETE /api/v1/soundtracks/{uuid}/cover, and
  PUT .../cover/source to pick a SteamGridDB game or AniList anime by ID.
  Uploaded and picked covers are never replaced automatically.
- With STEAMGRIDDB_API_KEY, a background job searches SteamGridDB, then
  AniList, by name, alternative names and the name without bracketed parts,
  at startup, after a sync and after names change. Search results are ranked
  by matching words, so "Civilization IV" gets "Sid Meier's Civilization IV".
- cover_modified (Unix seconds) is in the soundtrack and statistics responses,
  so clients can cache covers.

The new columns are added to migration 000006, which is not released yet.

Closes #12, closes #32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:57:18 +02:00
SansanandClaude Opus 5.5 f79f51ea4c Expose Prometheus metrics on a separate port
CI / integration (push) Failing after 14s
CI / docker (push) Failing after 20s
CI / test (push) Successful in 1m10s
CI / publish (push) Skipped
Metrics are served on METRICS_PORT (default 2112, "off" to disable), never on
the public port. Besides the Go runtime and process metrics this adds:

- http_requests_total and http_request_duration_seconds per route pattern,
  from an Echo middleware.
- musicserver_songs_requested_total and musicserver_songs_played_total by
  pick mode (random, low, classic). The mode is now stored with each queued
  song (migration 000006, not released yet).
- Library size, sync duration, last successful sync, broken songs from the
  last full sync, and the database connection pool.

Refs #35, #36, #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:57:58 +02:00
Sansan c62b65d7cd Add GET /api/v1/character/details with each image's change time
CI / integration (push) Successful in 21s
CI / test (push) Successful in 23s
CI / publish (push) Skipped
CI / docker (push) Successful in 48s
Returns every character image with when it was last changed, in Unix seconds,
so clients can cache the images and download only the ones that changed.
2026-10-07 21:57:26 +02:00
Sansan 76a1b946eb Stop browsers from caching songs
CI / docker (push) Successful in 1m6s
CI / test (push) Successful in 22s
CI / integration (push) Successful in 23s
CI / publish (push) Skipped
/music/rand and the other song endpoints return a different song from the
same URL, but the responses had a Last-Modified header and no Cache-Control,
so browsers cached them. The same song played again and the server never
picked a new one, so /music/addQue added nothing. Send Cache-Control:
no-store again, as the server did before the rewrite.

Also allow the Cache-Control request header in CORS, which the frontend
sends to bypass responses that browsers have already cached.
2026-10-07 20:09:30 +02:00
Sansan d31bf3551c Allow CORS origins from CORS_ALLOWED_ORIGINS
CI / test (push) Successful in 23s
CI / publish (push) Skipped
CI / docker (push) Successful in 46s
CI / integration (push) Successful in 19s
Echo compares allowed origins literally, so "https://*" and "http://*" never
matched a real origin and no CORS headers were sent. The allowed origins now
come from CORS_ALLOWED_ORIGINS (comma-separated, checked at startup). When it
is empty, only same-origin requests work, which is enough for the bundled
frontend.

Credentials are no longer allowed since the API uses Bearer tokens. Browsers
may now send Range and X-Queue-Id and read X-Queue-Id and Content-Range.
2026-10-07 20:03:13 +02:00
Sansan a705d7b476 Store each client's play queue in the database
The queue lived in memory and was shared by all clients, so a restart or a
finished sync cleared it. Every client now has its own queue in the new
queue and queue_song tables (added to the undeployed migration 000006).

The music endpoints return the queue ID in the X-Queue-Id header and expect
it back. A request without an ID, or with an ended one, starts a new queue.
/music/reset ends the queue and returns a new ID, and a sync ends all
running queues. Ended queues are kept as history.

The deprecated legacy routes without the /api/v1 prefix are removed.
2026-10-07 20:03:03 +02:00
Sansan b5dc42b654 Load every soundtrack when the search page opens
CI / integration (push) Successful in 18s
CI / test (push) Successful in 21s
CI / publish (push) Skipped
CI / docker (push) Successful in 44s
2026-10-07 16:46:31 +02:00
Sansan 080bacae80 Add GET /api/v1/statistics/soundtracks with every soundtrack and its songs
CI / integration (push) Successful in 18s
CI / test (push) Successful in 22s
CI / publish (push) Skipped
CI / docker (push) Successful in 41s
The other soundtrack statistics are capped at 100, so a client that shows every soundtrack (the MusicPlayer statistics tab) had no way to get them all. This one has no limit and is sorted by name.
2026-10-06 23:14:49 +02:00
Sansan fc7d787014 Serve the frontend's config.js with the API password
CI / docker (push) Successful in 47s
CI / integration (push) Successful in 17s
CI / test (push) Successful in 21s
CI / publish (push) Skipped
The bundled frontend gets a token with API_PASSWORD from config.js. The server now
generates config.js from its own config, so setting API_PASSWORD on the server
container is enough.
2026-10-06 19:01:17 +02:00
Sansan 6da96f79e7 Add a command to write the OpenAPI spec without a server
CI / test (push) Successful in 28s
CI / integration (push) Successful in 39s
CI / publish (push) Skipped
CI / docker (push) Successful in 51s
`just openapi [dir]` (go run ./cmd/openapi) writes openapi.json (3.1) and openapi-3.0.json without a database or configuration. A test checks it matches the spec the server serves.
2026-10-06 17:01:12 +02:00
Sansan 973ebf224b Generate the OpenAPI spec with Huma instead of swag
CI / integration (push) Successful in 49s
CI / docker (push) Successful in 1m0s
CI / test (push) Successful in 1m6s
CI / publish (push) Skipped
The spec is now built from the same calls that register the routes, so it
can't drift from the router like the swag comments did (all /api/v1
routes were missing).

- /api/v1 endpoints are typed Huma operations; the spec is OpenAPI 3.1 at
  /openapi.json (3.0 at /openapi-3.0.json, YAML too), docs at /docs.
  /openapi and /swagger/* redirect there
- Bearer token security is declared per operation; token creation and
  the new GET /api/v1/health are public
- Legacy routes use the same handlers, are hidden from the spec and keep
  the Deprecation header
- /sync/reset is hidden (debug only); the /sync/new, /sync/quick and
  /sync/new/full aliases are removed (use /sync and /sync/full)
- Invalid parameters return 422 (validation); every error is
  {"error": "..."}; empty lists are [] instead of null
- /sync/progress is documented as progress or result (oneOf)
- Tests validate the spec and check real responses of every documented
  operation against it (kin-openapi)
- Removed swag annotations, cmd/docs, echo-swagger and the swag tool
2026-10-06 16:31:06 +02:00
Sansan 0ec2425f78 Return every error as {"error": "..."}
Echo errors, middleware and handlers used three different formats (message object, error object, bare string). One error handler now writes all of them the same way; status codes are unchanged.
2026-10-06 16:21:14 +02:00
Sansan bc7d406e41 Remove Godot OpenAPI generator script
It produced code that doesn't parse in Godot 4. Typed response classes will be generated in MusicPlayer instead (Sansan/MusicPlayer#40).
2026-10-06 16:12:05 +02:00
Sansan 2c3e97190d Prefer UUIDs in the API (Phase 3 of the UUID migration)
CI / docker (push) Successful in 46s
CI / integration (push) Successful in 23s
CI / test (push) Successful in 30s
CI / publish (push) Skipped
- New /api/v1 endpoints: GET /soundtracks, /soundtracks/{uuid},
  /songs/{uuid} and /songs/{uuid}/file (streams the song, Range support,
  locked during sync)
- UUIDs added to every response with soundtracks or songs: queue info and
  list (SoundtrackUuid, SongUuid) and statistics (soundtrack_uuid,
  song_uuid). Existing fields stay for backward compatibility
- Play counts and broken-song removal use song UUIDs instead of
  soundtrack ID + song name, so songs with the same name are counted
  separately
- Statistics return an empty song list for soundtracks without songs
  instead of a single empty entry
- The legacy .id file cleanup stays until Phase 4 (#31)

Closes #29
2026-10-06 15:54:41 +02:00
Sansan 8c974a49cc Merge workflows into one CI pipeline
CI / test (push) Successful in 31s
CI / integration (push) Successful in 39s
CI / publish (push) Skipped
CI / docker (push) Successful in 54s
- Unit tests, integration tests and the Docker build check run in
  parallel on branches and pull requests
- Integration tests run on every push instead of only manually
- Branch builds only build amd64 and check the arm64 cross-compile in the
  test job; both platforms are built and pushed only for version tags,
  after all tests pass
- Docker layers cached through the runner cache, Go module and build
  caches mounted in the Dockerfile
- A newer push to the same branch cancels the running pipeline
2026-10-06 15:35:32 +02:00
Sansan 9db61956d3 Publish Docker image when a version tag is pushed
Build / test (push) Successful in 8s
Build / docker (push) Successful in 49s
Releases are triggered by tags like v5.0.0 instead of version branches.
2026-10-06 15:24:06 +02:00
Sansan bd250ecaee Run tests in CI and build multi-arch Docker images
Build / test (push) Successful in 38s
Build / docker (push) Successful in 2m2s
- Build (main, develop): vet and unit tests, then a linux/amd64 and
  linux/arm64 image build without pushing
- Publish (version branches): same tests, then build and push both
  platforms
- Integration tests (manual): run the testcontainers suite
2026-10-06 15:22:13 +02:00
Sansan 59dfff38bc Clean up build tooling and Dockerfile
- Dockerfile cross-compiles Go for the target platform (amd64/arm64
  without emulation), uses templ pinned in go.mod, runs as uid 1000 on
  alpine and lets FRONTEND_IMAGE pin the frontend version
- justfile: tools via go tool, `test` (unit) and `test-integration`
  (uses the active docker context), no hardcoded paths, no tailwind or
  frontend recipes
- Removed app.yaml, init.sh, tailwind.config.js and the tracked .idea
  folder
2026-10-06 15:21:46 +02:00
Sansan 7b144ae419 Rewrite tests to run without a database
- Unit tests use an in-memory fake database (internal/testutil/fakedb),
  temp folders and httptest; `go test ./...` needs no Docker or env vars
- Integration tests (build tag "integration") start PostgreSQL with
  testcontainers: migrations step by step, SQL dumps (skipped if
  missing) and a full sync against a real database
- Removed tests that only checked copies of the logic or had empty
  bodies, the broken root integration_test.go, compose.test.yaml and
  .env.test with hardcoded paths
- testMusic MP3s replaced with tiny placeholders (sync never decodes
  audio), 194 MB -> 168 KB
2026-10-06 15:20:31 +02:00
Sansan 090e794460 Restructure backend into services and fix security, crash and sync issues
Security
- Serve character images through os.Root so names like ../ can't escape
  the characters folder
- Sync start and /sync/reset are POST only (legacy and /api/v1)
- /api/v1/token/cleanup requires a token, password compared in constant
  time, tokens no longer logged
- Echo's rate limiter replaces the custom one; TRUSTED_PROXIES enables
  X-Forwarded-For for reverse proxies

Crashes and races
- Sync state lives in SyncService with atomics and a mutex instead of
  unsynchronised globals (fixes concurrent map writes during full sync)
- Only one sync can run; /api/v1 sync and music routes are locked during
  sync like the legacy ones
- No log.Fatal outside startup: errors become 4xx/5xx responses
- Search uses an in-order character match instead of unescaped regexes
- Empty queue / no songs return 404 instead of panicking or looping

Behaviour
- Sync result is computed once and stays the same between polls
- Swagger and /openapi serve the embedded spec
- Shutdown stops HTTP before closing the database
- Expired tokens are deleted with a context that outlives the request
- Files are served with Range support and correct content types; audio
  has no write deadline

Performance
- Migration 000006 adds soundtrack.fingerprint (names, sizes, mtimes) so
  unchanged folders are skipped without reading any files
- Songs are reconciled per soundtrack with one query plus needed writes;
  renames keep play counts
- Moved MUSIC_PATH updates soundtrack and song paths
- .id cleanup only runs for changed soundtracks or a full sync
- Classic random uses one SQL query; Gitea release lookups are cached
  with a timeout

Structure
- internal/config reads all environment variables once
- sqlc emits a Querier interface; backend globals become services
  injected into handlers
- One route registration shared by legacy and /api/v1
- Removed dead code (dbHelper.go, unused middleware, duplicate Gitea
  functions, embedded Swagger UI copy) and lib/pq
- templ and swag pinned as go tools
2026-10-06 15:20:14 +02:00
Sansan 75b6f0c1cf Remove frontend source from cmd
The frontend is built separately and copied into the Docker image from
the musicfrontend image.
2026-10-06 15:19:49 +02:00
234 changed files with 11645 additions and 26044 deletions

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ tmp_dir = "tmp"
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "frontend", "swagger", "cmd/frontend", "cmd/swagger"]
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
+3 -9
View File
@@ -14,18 +14,12 @@ compose.yaml
*.pprof
main
conf.yaml
output.css
tailwindcss
.env
# Node.js (frontend build artifacts)
node_modules
package.json
package-lock.json
# Test files
# Test files and fixtures
*_test.go
integration_test.go
internal/testutil
*_dump.sql
# OS
.DS_Store
-19
View File
@@ -1,19 +0,0 @@
# Test Database Configuration
DB_HOST=localhost
DB_PORT=5433
DB_USERNAME=testuser
DB_PASSWORD=testpass
DB_NAME=music_server_test
# Test Paths
MUSIC_PATH=/Users/sebastian/projects/MusicServer/testMusic
CHARACTERS_PATH=/Users/sebastian/projects/MusicServer/testCharacters
# Server Configuration
PORT=8081
LOG_LEVEL=debug
LOG_JSON=false
# Session Authentication (for testing)
# Set to disable session auth during development
DISABLE_SESSION_AUTH=true
+107
View File
@@ -0,0 +1,107 @@
name: CI
# Branches and pull requests: unit tests, integration tests and a check that the image builds (amd64 only).
# Version tags (v1.2.3): the same tests, then build amd64 + arm64 and push the image.
on:
push:
branches:
- main
- develop
tags:
- "v*.*.*"
pull_request:
workflow_dispatch:
# A newer push to the same branch cancels the run that is still going.
concurrency:
group: ci-${{ gitea.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Generate templ components
run: go tool templ generate
- name: Vet
run: go vet ./... && go vet -tags integration ./...
- name: Unit tests
run: go test -race ./...
- name: Cross-compile for arm64
run: GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o /dev/null ./cmd
integration:
runs-on: ubuntu-latest
env:
# The test helper stops its own container, so the reaper container is not needed.
TESTCONTAINERS_RYUK_DISABLED: "true"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Generate templ components
run: go tool templ generate
- name: Integration tests (PostgreSQL via testcontainers)
run: go test -tags integration -race -timeout 15m ./internal/db/... ./internal/backend/...
# Checks that the image builds. Runs in parallel with the tests; nothing is pushed.
docker:
if: ${{ !startsWith(gitea.ref, 'refs/tags/') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.sanplex.xyz/sansan"]
http = true
insecure = true
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: gitea.sanplex.xyz
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }}
- name: Build (amd64)
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: false
cache-from: type=gha
cache-to: type=gha,mode=max,ignore-error=true
# Only for version tags, and only when all tests have passed.
publish:
if: ${{ startsWith(gitea.ref, 'refs/tags/v') }}
needs: [test, integration]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.sanplex.xyz/sansan"]
http = true
insecure = true
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: gitea.sanplex.xyz
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }}
- name: Build and push (amd64 + arm64)
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=gha
tags: |
gitea.sanplex.xyz/sansan/musicserver:${{ gitea.ref_name }}
gitea.sanplex.xyz/sansan/musicserver:latest
-42
View File
@@ -1,42 +0,0 @@
name: Build
#on:
# release:
# types: [published]
on:
push:
branches:
- main
- develop
jobs:
# test:
# runs-on: ubuntu-latest
# steps:
# - run: echo "The release ${{ gitea.ref }} ${{ gitea.ref_name }} was published"
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: Login to Gitea
uses: docker/login-action@v2
with:
registry: gitea.sanplex.xyz
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }}
- name: Build
uses: https://github.com/docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: false
#tags: "gitea.sanplex.xyz/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.xyz/sansan/musicserver:latest"
-41
View File
@@ -1,41 +0,0 @@
name: Publish
#on:
# release:
# types: [published]
on:
push:
branches:
- '*.*'
jobs:
# test:
# runs-on: ubuntu-latest
# steps:
# - run: echo "The release ${{ gitea.ref }} ${{ gitea.ref_name }} was published"
publish:
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: Login to Gitea
uses: docker/login-action@v2
with:
registry: gitea.sanplex.xyz
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }}
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: "gitea.sanplex.xyz/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.xyz/sansan/musicserver:latest"
-47
View File
@@ -1,47 +0,0 @@
name: Integration Tests
on:
workflow_dispatch: # Manual trigger only
jobs:
integration-test:
runs-on: ubuntu-latest
container:
image: golang:1.25
options: --privileged
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: music_server_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: go mod download
- name: Install testcontainers
run: go install github.com/testcontainers/testcontainers-go@latest
- name: Run integration tests
env:
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: testuser
DB_PASSWORD: testpass
DB_NAME: music_server_test
MUSIC_PATH: ./testMusic
CHARACTERS_PATH: ./testCharacters
run: go test -v -timeout 30m ./...
+7 -5
View File
@@ -3,15 +3,17 @@ tmp
.idea
*templ.go
conf.yaml
output.css
compose.yaml
tailwindcss
.env
node_modules
package.json
package-lock.json
cpu.pprof
main
# Database dumps
*_dump.sql
# Generated with `just openapi`
/openapi.json
/openapi-3.0.json
# Local cover images (COVERS_PATH)
/covers/
-8
View File
@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="Go" enabled="true">
<buildTags>
<option name="customFlags">
<array>
<option value="prod" />
</array>
</option>
</buildTags>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
-5
View File
@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
-36
View File
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="music_prod@192.168.86.100" uuid="e63caf71-2b1a-400d-9cc4-4b87b1f9b807">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://192.168.86.100:5432/music_prod</jdbc-url>
</data-source>
<data-source source="LOCAL" name="music_test@192.168.86.100" uuid="d063e147-d7f0-48db-9f04-57a108261ef1">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://192.168.86.100:5432/music_test</jdbc-url>
</data-source>
<data-source source="LOCAL" name="music_test@ssh.sanplex.xyz" uuid="a9cf3eb8-abc2-479b-9f27-12b4cbbfc321">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://ssh.sanplex.xyz:9432/music_test</jdbc-url>
</data-source>
<data-source source="LOCAL" name="music_prod@ssh.sanplex.xyz" uuid="20917ace-4e9a-4273-bdb7-13ebc774cbba">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://ssh.sanplex.xyz:9432/music_prod</jdbc-url>
</data-source>
<data-source source="LOCAL" name="music_test2@localhost" uuid="a423ab0a-55b0-42e1-8070-25d8ef34bfac">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/music_test2</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/MusicServer.iml" filepath="$PROJECT_DIR$/.idea/MusicServer.iml" />
</modules>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+35 -26
View File
@@ -1,36 +1,45 @@
# Stage 1: Build backend
FROM golang:1.25-alpine as build_go
RUN apk add --no-cache curl
# The frontend is built and published separately; override to pin a version.
ARG FRONTEND_IMAGE=gitea.sanplex.xyz/sansan/musicfrontend:latest
# Stage 1: Build the backend on the build machine and cross-compile for the target platform,
# so multi-platform builds need no emulation.
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
RUN find . -name "*_test.go" -delete && \
find . -name "integration_test.go" -delete
RUN go install github.com/a-h/templ/cmd/templ@latest
RUN templ generate
RUN go build -o main cmd/main.go
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
go tool templ generate
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags="-s -w" -tags timetzdata -o main ./cmd
# Stage 2: Final image
FROM golang:1.25-alpine
# Stage 2: Frontend static files (the same for every platform)
FROM --platform=$BUILDPLATFORM ${FRONTEND_IMAGE} AS frontend
# Stage 3: Final image. No RUN steps here, so no emulation is needed for other architectures.
FROM alpine:3.22
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
WORKDIR /app
COPY --from=build /app/main .
COPY --from=frontend /usr/share/nginx/html /frontend
COPY ./songs/ ./songs/
# The music volume must be readable by uid 1000, and writable for the .id file cleanup during sync
# and for soundtrack uploads (kept in .uploads until finished).
USER 1000:1000
EXPOSE 8080
# Prometheus metrics (METRICS_PORT). Keep it off the public proxy.
EXPOSE 2112
VOLUME /sorted
VOLUME /characters
COPY --from=build_go /app/main .
COPY --from=gitea.sanplex.xyz/sansan/musicfrontend:latest /usr/share/nginx/html /frontend
# Generate config.js with empty API_HOSTNAME (relative paths)
RUN echo "window.__RUNTIME_CONFIG__ = { API_HOSTNAME: '' };" > /frontend/config.js
COPY ./songs/ ./songs/
# Required: DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_NAME, MUSIC_PATH, CHARACTERS_PATH
# Covers (optional): COVERS_PATH, a writable folder such as a /covers volume, turns covers on.
# STEAMGRIDDB_API_KEY also turns on the automatic search (SteamGridDB, then AniList).
# Uploads (optional): UPLOAD_MAX_FILE_MB (default 50) and UPLOAD_MAX_TOTAL_MB (default 2048).
ENV PORT=8080
ENV PORT 8080
ENV DB_HOST ""
ENV DB_PORT ""
ENV DB_USERNAME ""
ENV DB_PASSWORD ""
ENV DB_NAME ""
ENV MUSIC_PATH ""
ENV CHARACTERS_PATH ""
CMD ./main
CMD ["./main"]
-8
View File
@@ -1,8 +0,0 @@
application: musicserver
version: 2.2.0
runtime: go115
api_version: go1
handlers:
- url: /.*
script: _go_app
-1554
View File
File diff suppressed because it is too large. Load diff
-224
View File
@@ -1,224 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import re
from pathlib import Path
from collections import defaultdict
def load_openapi_spec(path: str) -> dict:
"""Load OpenAPI spec from YAML or JSON file."""
try:
import yaml
with open(path, "r") as f:
return yaml.safe_load(f)
except ImportError:
# Fallback to JSON if PyYAML is not installed
with open(path, "r") as f:
return json.load(f)
def map_type(openapi_type: str) -> str:
"""Map OpenAPI types to GDScript types."""
type_mapping = {
"integer": "int",
"number": "float",
"boolean": "bool",
"array": "Array",
"object": "Dictionary",
}
return type_mapping.get(openapi_type, "String")
def default_value(openapi_type: str):
"""Return default values for GDScript types."""
default_mapping = {
"integer": "0",
"number": "0.0",
"boolean": "false",
"array": "[]",
"object": "{}",
}
return default_mapping.get(openapi_type, '""')
def sanitize_class_name(name: str) -> str:
"""Convert a name to a valid GDScript class name."""
# Replace invalid characters with underscores
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
# Capitalize first letter
return name[0].upper() + name[1:] if name else "Model"
def generate_model_class(class_name: str, schema: dict) -> str:
"""Generate a GDScript class for a model."""
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
]
# Add properties
properties = schema.get("properties", {})
for prop_name, prop_schema in properties.items():
prop_type = map_type(prop_schema.get("type", "string"))
lines.append(f"var {prop_name}: {prop_type}")
# Add _init method
lines.extend([
"",
"func _init(data: Dictionary):",
])
for prop_name in properties:
prop_type = map_type(properties[prop_name].get("type", "string"))
default = default_value(properties[prop_name].get("type", "string"))
lines.append(f' {prop_name} = data.get("{prop_name}", {default})')
return "\n".join(lines)
def generate_api_client(path: str, method: str, endpoint: dict) -> str:
"""Generate a GDScript API client for an endpoint."""
# Sanitize path for class name
class_name = sanitize_class_name(path.replace("/", "_").replace("{", "").replace("}", "")) + method.capitalize()
# Format URL (replace {param} with %s for Godot's string formatting)
url = path.replace("{", "%").replace("}", "s")
full_url = f'"{BASE_URL}{url}"'
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
"var http_request: HTTPRequest",
"",
"func _init(node: Node):",
" http_request = HTTPRequest.new()",
" node.add_child(http_request)",
' http_request.connect("request_completed", self, "_on_request_completed")',
"",
f"func call(params: Dictionary, callback: Callable):",
f" var url := {full_url}",
' var headers = ["User-Agent: MyGodotApp"]',
" var error := http_request.request(url, headers)",
" if error != OK:",
' push_error("HTTP request failed.")',
" return",
" http_request.set_meta(\"callback\", callback)",
"",
"func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray):",
" var callback := http_request.get_meta(\"callback\")",
" if callback:",
" var response_body = body.get_string_from_utf8()",
" var json = JSON.new()",
" if json.parse(response_body) == OK:",
" callback.call(json.get_data())",
" else:",
" callback.call(null)",
]
return "\n".join(lines)
def generate_tag_client(tag: str, endpoints: list) -> str:
"""Generate a GDScript API client for all endpoints with a given tag."""
class_name = sanitize_class_name(tag) + "API"
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
"var http_request: HTTPRequest",
"var base_url: String",
"",
"func _init(node: Node, base_url_param: String):",
" http_request = HTTPRequest.new()",
" node.add_child(http_request)",
' http_request.connect("request_completed", self, "_on_request_completed")',
" base_url = base_url_param",
"",
]
# Generate a method for each endpoint
for path, method, endpoint in endpoints:
# Sanitize method name for GDScript
method_name = method.lower()
# Create a valid function name from the path
func_name = "call_" + path.replace("/", "_").replace("{", "").replace("}", "").replace("-", "_")
# Format URL (replace {param} with %s for Godot's string formatting)
url = path.replace("{", "%").replace("}", "s")
lines.extend([
f"func {func_name}(params: Dictionary = {{}}, callback: Callable):",
f' var url := base_url + "{url}"',
' var headers = ["User-Agent: MyGodotApp"]',
f" var error := http_request.request(url, headers, false, HTTPClient.METHOD_{method.upper()})",
" if error != OK:",
' push_error("HTTP request failed.")',
" return",
" http_request.set_meta(\"callback\", callback)",
"",
])
# Add the completion handler
lines.extend([
"func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray):",
" var callback := http_request.get_meta(\"callback\")",
" if callback:",
" var response_body = body.get_string_from_utf8()",
" var json = JSON.new()",
" if json.parse(response_body) == OK:",
" callback.call(json.get_data())",
" else:",
" callback.call(null)",
])
return "\n".join(lines)
def generate_code(spec: dict, output_dir: str):
"""Generate all GDScript files from the OpenAPI spec."""
# Create output directory
Path(output_dir).mkdir(parents=True, exist_ok=True)
# Generate models
schemas = spec.get("definitions", {}) # Swagger 2.0 uses "definitions"
if not schemas:
schemas = spec.get("components", {}).get("schemas", {})
for schema_name, schema in schemas.items():
class_name = sanitize_class_name(schema_name)
code = generate_model_class(class_name, schema)
output_path = Path(output_dir) / f"{class_name}.gd"
with open(output_path, "w") as f:
f.write(code)
print(f"Generated model: {output_path}")
# Group endpoints by tag
paths = spec.get("paths", {})
tag_endpoints = defaultdict(list)
for path, methods in paths.items():
for method, endpoint in methods.items():
tags = endpoint.get("tags", ["default"])
for tag in tags:
tag_endpoints[tag].append((path, method, endpoint))
# Generate one file per tag
for tag, endpoints in tag_endpoints.items():
code = generate_tag_client(tag, endpoints)
class_name = sanitize_class_name(tag) + "API"
output_path = Path(output_dir) / f"{class_name}.gd"
with open(output_path, "w") as f:
f.write(code)
print(f"Generated API client for tag '{tag}': {output_path}")
def main():
parser = argparse.ArgumentParser(description="Generate Godot API clients from OpenAPI spec")
parser.add_argument("input", help="Path to the OpenAPI JSON/YAML file")
parser.add_argument("-o", "--output", default="godot_generated", help="Output directory for GDScript files")
args = parser.parse_args()
spec = load_openapi_spec(args.input)
generate_code(spec, args.output)
print("Done!")
print("Note: When initializing the API classes, pass the base URL as a parameter:")
print(" var music_api = MusicAPI.new()")
print(" music_api._init(get_node('/root'), 'http://localhost:8080')")
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff
-976
View File
@@ -1,976 +0,0 @@
definitions:
backend.SongInfoForStats:
properties:
file_name:
type: string
path:
type: string
song_name:
type: string
soundtrack_id:
type: integer
soundtrack_name:
type: string
times_played:
type: integer
type: object
backend.SoundtrackWithSongs:
properties:
songs:
items:
$ref: '#/definitions/backend.SongInfoForStats'
type: array
soundtrack_id:
type: integer
soundtrack_last_played:
type: string
soundtrack_name:
type: string
soundtrack_played:
type: integer
type: object
backend.StatisticsSummary:
properties:
avg_soundtrack_plays:
type: number
max_soundtrack_plays:
type: integer
min_soundtrack_plays:
type: integer
never_played_soundtracks:
type: integer
played_soundtracks:
type: integer
total_soundtrack_plays:
type: integer
total_soundtracks:
type: integer
type: object
backend.VersionData:
properties:
changelog:
example:
- '["Initial release"'
- '"Bug fixes"]'
items:
type: string
type: array
version:
example: 1.0.0
type: string
type: object
server.TokenRequest:
properties:
client_type:
description: 'Optional: "web", "mobile", "api"'
type: string
password:
description: 'Required: API password'
type: string
type: object
server.TokenResponse:
properties:
client_type:
type: string
expires_at:
type: string
token:
type: string
type: object
info:
contact: {}
paths:
/api/v1/statistics/songs/least-played:
get:
consumes:
- application/json
description: Returns the top N least played songs with their soundtrack info
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SongInfoForStats'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get least played songs
tags:
- statistics
/api/v1/statistics/songs/most-played:
get:
consumes:
- application/json
description: Returns the top N most played songs with their soundtrack info
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SongInfoForStats'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get most played songs
tags:
- statistics
/api/v1/statistics/soundtracks/last-played:
get:
consumes:
- application/json
description: Returns the most recently played soundtracks
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SoundtrackWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get last played soundtracks
tags:
- statistics
/api/v1/statistics/soundtracks/least-played:
get:
consumes:
- application/json
description: Returns the top N least played soundtracks with their songs
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SoundtrackWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get least played soundtracks
tags:
- statistics
/api/v1/statistics/soundtracks/most-played:
get:
consumes:
- application/json
description: Returns the top N most played soundtracks with their songs
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SoundtrackWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get most played soundtracks
tags:
- statistics
/api/v1/statistics/soundtracks/never-played:
get:
consumes:
- application/json
description: Returns all soundtracks that have never been played (times_played
= 0)
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SoundtrackWithSongs'
type: array
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get never played soundtracks
tags:
- statistics
/api/v1/statistics/soundtracks/oldest-played:
get:
consumes:
- application/json
description: Returns the least recently played soundtracks (that have been played
at least once)
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SoundtrackWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get oldest played soundtracks
tags:
- statistics
/api/v1/statistics/summary:
get:
consumes:
- application/json
description: Returns overall statistics about the music library
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/backend.StatisticsSummary'
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get statistics summary
tags:
- statistics
/api/v1/token:
delete:
consumes:
- application/json
description: Deletes the current session token
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Invalidate session token
tags:
- auth
post:
consumes:
- application/json
description: Returns a new session token for API access. Requires valid API
password.
parameters:
- description: Password and client type
in: body
name: request
required: true
schema:
$ref: '#/definitions/server.TokenRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/server.TokenResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Create session token
tags:
- auth
/api/v1/token/cleanup:
post:
consumes:
- application/json
description: Removes all expired session tokens from the database
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Cleanup expired sessions
tags:
- auth
/character:
get:
consumes:
- application/json
description: Returns the image for a specific character
parameters:
- description: Character name
in: query
name: name
required: true
type: string
produces:
- image/png
responses:
"200":
description: OK
schema:
type: file
summary: Get character image
tags:
- characters
/characters:
get:
consumes:
- application/json
description: Returns a list of all available characters
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
type: string
type: array
summary: Get list of characters
tags:
- characters
/download:
get:
consumes:
- application/json
description: Checks for the latest version of the application
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Check for latest version
tags:
- download
/download/linux:
get:
description: Redirects to download the latest Linux version
produces:
- application/octet-stream
responses:
"302":
description: Found
schema:
type: string
summary: Download latest Linux version
tags:
- download
/download/list:
get:
consumes:
- application/json
description: Lists all assets available for the latest version
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
type: string
type: array
summary: List assets of latest version
tags:
- download
/download/windows:
get:
description: Redirects to download the latest Windows version
produces:
- application/octet-stream
responses:
"302":
description: Found
schema:
type: string
summary: Download latest Windows version
tags:
- download
/health:
get:
consumes:
- application/json
description: Returns the health status of the server
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Check server health
tags:
- health
/music:
get:
consumes:
- application/json
description: Returns a specific song by name
parameters:
- description: Song name
in: query
name: song
required: true
type: string
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"400":
description: song can't be empty
schema:
type: string
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get a specific song
tags:
- music
/music/addPlayed:
get:
consumes:
- application/json
description: Adds the latest song to the played list
responses:
"204":
description: ""
"423":
description: Syncing is in progress
schema:
type: string
summary: Add latest to played
tags:
- music
/music/addQue:
get:
consumes:
- application/json
description: Adds the latest song to the queue
responses:
"204":
description: ""
"423":
description: Syncing is in progress
schema:
type: string
summary: Add latest to queue
tags:
- music
/music/all/order:
get:
consumes:
- application/json
description: Returns a list of all soundtracks in order
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
"423":
description: Syncing is in progress
schema:
type: string
summary: Get all soundtracks
tags:
- music
/music/all/random:
get:
consumes:
- application/json
description: Returns a list of all soundtracks in random order
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
"423":
description: Syncing is in progress
schema:
type: string
summary: Get all soundtracks random
tags:
- music
/music/info:
get:
consumes:
- application/json
description: Returns information about the current song
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
summary: Get current song info
tags:
- music
/music/list:
get:
consumes:
- application/json
description: Returns a list of played songs
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
summary: Get played songs list
tags:
- music
/music/next:
get:
description: Returns the next song in the queue
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get next song
tags:
- music
/music/played:
put:
consumes:
- application/json
description: Marks a song as played by its ID
parameters:
- description: Song ID
in: query
name: song
required: true
type: integer
produces:
- application/json
responses:
"204":
description: ""
"400":
description: Bad Request
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Mark song as played
tags:
- music
/music/previous:
get:
description: Returns the previous song in the queue
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get previous song
tags:
- music
/music/rand:
get:
description: Returns a random song
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get random song
tags:
- music
/music/rand/classic:
get:
description: Returns a random song from the classic selection
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get random classic song
tags:
- music
/music/rand/low:
get:
description: Returns a random song with low chance selection
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get random song with low chance
tags:
- music
/music/reset:
get:
consumes:
- application/json
description: Resets the music state
responses:
"204":
description: ""
"423":
description: Syncing is in progress
schema:
type: string
summary: Reset music state
tags:
- music
/music/soundTest:
get:
description: Returns the sound check song
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get sound check song
tags:
- music
/sync:
get:
consumes:
- application/json
description: Starts syncing soundtracks with only new changes
produces:
- application/json
responses:
"200":
description: Start syncing soundtracks
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Sync soundtracks with only changes
tags:
- sync
/sync/full:
get:
consumes:
- application/json
description: Starts a full sync of all soundtracks
produces:
- application/json
responses:
"200":
description: Start syncing soundtracks full
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Sync all soundtracks fully
tags:
- sync
/sync/progress:
get:
consumes:
- application/json
description: Returns the current sync progress or result
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
summary: Get sync progress
tags:
- sync
/sync/reset:
get:
consumes:
- application/json
description: Resets the soundtracks database by deleting all soundtracks and
songs
produces:
- application/json
responses:
"200":
description: Soundtracks and songs are deleted from the database
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Reset soundtracks database
tags:
- sync
/version:
get:
consumes:
- application/json
description: get latest version info
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/backend.VersionData'
"404":
description: Not Found
schema:
type: string
summary: Getting the latest version of the backend
tags:
- version
/version/history:
get:
consumes:
- application/json
description: get version history
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.VersionData'
type: array
"404":
description: Not Found
schema:
type: string
summary: Getting the version history of the backend
tags:
- version
swagger: "2.0"
-21
View File
@@ -1,21 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-24
View File
@@ -1,24 +0,0 @@
# music-randomizer-vue
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
-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");
-12192
View File
File diff suppressed because it is too large. Load diff
-60
View File
@@ -1,60 +0,0 @@
{
"name": "music-randomizer-vue",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.7.2",
"core-js": "^3.37.1",
"cors": "^2.8.5",
"express": "^4.19.2",
"vue": "^3.4.31",
"vue-axios": "^3.5.2",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/compiler-sfc": "^3.4.31",
"@babel/eslint-parser": "^7.25.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser",
"requireConfigFile": false,
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"globalReturn": false,
"impliedStrict": true,
"jsx": true
},
"babelOptions": {
"presets": ["@babel/preset-env"]
}
},
"rules": {
"no-debugger": 1
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" width="36px" height="36px"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>

Before

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 788 B

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" width="36px" height="36px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>

Before

Width:  |  Height:  |  Size: 212 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" width="36px" height="36px"><path d="M0 0h24v24H0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>

Before

Width:  |  Height:  |  Size: 201 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="black" width="36px" height="36px"><g><rect fill="none" height="24" width="24"/></g><g><path d="M13,8c0-2.21-1.79-4-4-4S5,5.79,5,8s1.79,4,4,4S13,10.21,13,8z M15,10v2h3v3h2v-3h3v-2h-3V7h-2v3H15z M1,18v2h16v-2 c0-2.66-5.33-4-8-4S1,15.34,1,18z"/></g></svg>

Before

Width:  |  Height:  |  Size: 352 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="black" width="36px" height="36px"><g><rect fill="none" height="24" width="24"/></g><g><path d="M14,8c0-2.21-1.79-4-4-4S6,5.79,6,8s1.79,4,4,4S14,10.21,14,8z M17,10v2h6v-2H17z M2,18v2h16v-2c0-2.66-5.33-4-8-4 S2,15.34,2,18z"/></g></svg>

Before

Width:  |  Height:  |  Size: 333 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Whitespace-only changes.
-147
View File
@@ -1,147 +0,0 @@
<template>
<the-header @start-sound-test="startSoundTest"></the-header>
<the-main ref="theMain"></the-main>
</template>
<script>
import TheHeader from "./components/layout/TheHeader.vue";
import TheMain from "./components/layout/TheMain.vue";
export default {
components: {
TheHeader,
TheMain,
},
data() {
return {
screenWidth: 1000,
};
},
computed: {
displayWhenDesktop() {
if (this.screenWidth > 600) {
return true;
} else {
return false;
}
},
},
methods: {
startSoundTest() {
this.$refs.theMain.startSoundTest();
},
},
mounted() {
this.screenWidth = window.innerWidth;
window.addEventListener("resize", () => (this.screenWidth = window.innerWidth));
},
};
</script>
<style>
* {
margin: 0;
padding: 0;
font-family: Helvetica;
}
button {
background: rgb(54, 54, 54);
border-radius: 10px;
border-color: #ff9c00;
color: #ff9c00;
font-size: 1em;
padding: 10px;
outline: none;
}
.app {
display: flex;
height: 100vh;
}
body {
background-color: #555;
}
/* Global modal stuff */
.modalAni-enter-from,
.modalAni-leave-to {
opacity: 0;
transform: translateY(-20px);
}
.modalAni-enter-to,
.modalAni-leave-from {
opacity: 1;
}
.modalAni-enter-active,
.modalAni-leave-active {
transition: all 0.25s ease-out;
}
.modal {
position: fixed; /* Stay in place */
z-index: 10; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: hidden;
background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.5); /* Black w/ opacity */
}
.modalContainer {
background-color: #eeeeee;
margin: 10% auto; /* 15% from the top and centered */
border: 1px solid #888;
width: 50%; /* Could be more or less, depending on screen size */
}
.modalWrapper {
display: flex;
position: relative;
flex-wrap: wrap;
padding: 10px 40px;
}
.modalWrapper:last-child {
margin-bottom: 50px;
}
.closeModalImg {
position: absolute;
right: 8px;
top: 8px;
cursor: pointer;
opacity: 70%;
}
.modal h1 {
color: black;
margin-left: auto;
margin-right: auto;
padding-top: 10px;
font-size: 1.9rem;
}
.modal p {
color: black;
}
@media only screen and (max-width: 1000px) {
.modalContainer {
margin: 10% auto; /* 15% from the top and centered */
width: 93%; /* Could be more or less, depending on screen size */
}
.modalWrapper {
padding: 10px 10px;
}
.modalWrapper:last-child {
margin-bottom: 10px;
}
.modal h1 {
padding-top: 10px;
font-size: 1.6rem;
}
}
</style>
-3
View File
@@ -1,3 +0,0 @@
export default {
hostname: "$HOSTNAME",
}
@@ -1,95 +0,0 @@
<template>
<!-- <div class="currentlyPlayingDiv">
<div class="textWrapper">
<transition-group name="displayingInfo" appear>
<p v-if="currentTrackHidden" :key="1">????????</p>
<p v-else :key="2">{{ currentGame }}</p>
<p v-if="currentTrackHidden" :key="3">??????</p>
<p v-else :key="4">{{ currentTrack }}</p>
</transition-group>
</div>
</div> -->
<div class="currentlyPlayingDiv">
<transition name="displayingInfo">
<div v-if="currentTrackHidden" class="textWrapper">
<p>????????</p>
<p>??????</p>
</div>
<div v-else class="textWrapper">
<p>{{ currentGame }}</p>
<p>{{ currentTrack }}</p>
</div>
</transition>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: {
...mapState(["currentGame", "currentTrack", "currentTrackHidden"]),
},
};
</script>
<style scoped>
.displayingInfo-enter-from,
.displayingInfo-leave-to {
opacity: 0;
}
.displayingInfo-enter-to,
.displayingInfo-leave-from {
opacity: 1;
}
.displayingInfo-enter-active {
transition: opacity 0.7s ease-out;
}
.displayingInfo-leave-active {
display: none;
}
.currentlyPlayingDiv {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 60vw;
height: 27vh;
margin-right: 0.5vw;
color: #ff8800;
background-color: #333;
border-radius: 10px;
text-align: center;
overflow: hidden;
}
.currentlyPlayingDiv .textWrapper {
padding: 0.75vw;
}
.currentlyPlayingDiv .textWrapper p {
width: 100%;
font-size: 2.3em;
}
.currentlyPlayingDiv p:first-child {
margin-top: 1.5vw;
margin-bottom: 1.5vw;
font-size: 2.8em;
}
@media only screen and (max-width: 1000px) {
.currentlyPlayingDiv {
width: 100vw;
height: 35vw;
margin-right: 0;
border-radius: 0px;
align-content: center;
}
.currentlyPlayingDiv .textWrapper p {
font-size: 1.1em;
}
.currentlyPlayingDiv p:first-child {
margin-bottom: 2vw;
font-size: 1.4em;
}
}
</style>
@@ -1,170 +0,0 @@
<template>
<div class="inspirationDiv">
<div class="inspirationHeader">
<div class="inspirationTitle">
<h1>Inspiration</h1>
</div>
<div class="searchInput">
<input
v-model="searchInputText"
type="text"
@input="searchGame()"
ref="inputField"
/>
</div>
<button @click="clearSearch()">Clear</button>
</div>
<transition-group
tag="ul"
name="inspirationList"
class="inspirationList"
ref="inspirationList"
>
<li v-for="game in showingGamesList" class="inspirationEntry" :key="game">
{{ game }}
</li>
</transition-group>
</div>
</template>
<script>
import arne from '../../arne.js'
import {mapState} from "vuex";
export default {
data() {
return {
allGamesList: [],
showingGamesList: [],
scrollDown: true,
searchInputText: "",
};
},
computed: {
...mapState(["reloadGamesList"]),
},
watch: {
reloadGamesList(newValue) {
if (newValue) {
this.reloadGames();
this.$store.dispatch("reloadGamesList", false);
}
}
},
methods: {
clearSearch() {
this.searchInputText = "";
this.searchGame();
},
searchGame() {
this.showingGamesList = [];
for (const game of this.allGamesList) {
if (this.searchInputText === "" ||
game.toLowerCase().replace(/\s/g, "")
.includes(this.searchInputText.toLowerCase().replace(/\s/g, ""))) {
this.showingGamesList.push(game);
}
}
if (this.searchInputText.replace(/\s/g, "") !== "") {
this.showingGamesList.sort((n1, n2) => {
if (n1 > n2) {
return 1;
}
if (n1 < n2) {
return -1;
}
return 0;
});
}
},
scrollInspiration() {
let inspirationListDOM = document.querySelector(".inspirationList");
let scrollSpeed = 1;
let currentScrollLocation = 0;
currentScrollLocation = inspirationListDOM.scrollTop;
this.scrollDown
? inspirationListDOM.scrollBy(0, scrollSpeed)
: inspirationListDOM.scrollBy(0, -scrollSpeed);
if (currentScrollLocation === inspirationListDOM.scrollTop) {
this.scrollDown = !this.scrollDown;
}
},
reloadGames() {
this.axios({
method: "get",
url: `${arne.hostname}/music/all`,
})
.then((response) => {
this.allGamesList = response.data;
this.searchInputText = "";
this.searchGame();
this.$store.dispatch("updateHowManyGames", this.allGamesList.length);
})
.catch(function(error) {
console.log(error);
});
}
},
mounted() {
this.reloadGames();
window.setInterval(() => {
this.scrollInspiration();
}, 40);
},
};
</script>
<style scoped>
.inspirationDiv {
width: 38vw;
height: 20vh;
color: white;
}
.inspirationTitle {
display: flex;
flex: auto;
align-items: flex-start;
justify-content: flex-start;
padding: 5px 10px;
}
.searchInput {
display: flex;
flex: auto;
align-items: flex-end;
justify-content: flex-end;
padding: 0 10px;
}
.inspirationHeader {
display: flex;
background-color: #333;
height: 5vh;
justify-content: center;
align-items: center;
}
.inspirationList {
display: inline-block;
background-color: rgb(66, 66, 66);
width: 38vw;
height: 22vh;
list-style: none;
overflow: hidden;
}
.inspirationEntry {
width: 100%;
max-width: 38vw;
padding: 0.3vh 0.8vh;
font-size: 1.2rem;
}
@media only screen and (max-width: 1000px) {
.inspirationDiv {
display: none;
}
}
</style>
@@ -1,78 +0,0 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
class="closeModalImg"
src="cancel-black-36dp.svg"
alt="closeModalIMG"
@click="closeModal"
/>
<h1>Music Player Randomizer v0.1</h1>
<p class="descriptionText">
Try your video game music knowledge with this VGM randomizer,
invite your friends and see who is the best.
</p>
<p class="creditText">
Back-end done by Sebastian Olsson,
<a href="https://twitter.com/Raida91">Twitter</a>
</p>
<p class="creditText">
Front-end done by Peter Johansson,
<a href="https://twitter.com/WebDevPJ">Twitter</a>
</p>
<!-- <p class="patchNotesText">
Full patch notes can be found
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">here</a>
</p> -->
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false,
};
},
methods: {
closeModal() {
this.show = false;
},
openModal() {
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
},
};
</script>
<style scoped>
p {
font-size: 1.2rem;
width: 100%;
}
.descriptionText {
margin-top: 35px;
}
.creditText {
margin-top: 30px;
}
.creditText:nth-child(5) {
margin-top: 0px;
}
.patchNotesText {
margin-top: 20px;
}
</style>
@@ -1,160 +0,0 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
class="closeModalImg"
src="cancel-black-36dp.svg"
alt="closeModalIMG"
@click="closeModal"
/>
<h1 @click="test">{{ playerName }}, choose your fighter!</h1>
<div class="fighterDiv">
<div class="fighter" @click="chooseFighter('Adol')">
<img src="characters/Adol.png" alt="adol" />
<p>Adol Christin</p>
</div>
<div class="fighter" @click="chooseFighter('Link')">
<img src="characters/Link.png" alt="link" />
<p>Link</p>
</div>
<div class="fighter" @click="chooseFighter('Barbarian')">
<img src="characters/Barbarian.png" alt="barbarian" />
<p>Barbarian</p>
</div>
<div class="fighter" @click="chooseFighter('Layton')">
<img src="characters/Layton.png" alt="layton" />
<p>Professor Layton</p>
</div>
<div class="fighter" @click="chooseFighter('Kiryu')">
<img src="characters/Kiryu.png" alt="kiryu" />
<p>Kazuma Kiryu</p>
</div>
<div class="fighter" @click="chooseFighter('Miles')">
<img src="characters/Miles.png" alt="miles" />
<p>Miles Edgeworth</p>
</div>
<div class="fighter" @click="chooseFighter('Lemmings')">
<img src="characters/Lemmings.png" alt="lemmings" />
<p>Lemmings</p>
</div>
<div class="fighter" @click="chooseFighter('Samus')">
<img src="characters/Samus.png" alt="samus" />
<p>Samus</p>
</div>
<div class="fighter" @click="chooseFighter('Kratos')">
<img src="characters/Kratos.png" alt="kratos" />
<p>Kratos</p>
</div>
<div class="fighter" @click="chooseFighter('Aloy')">
<img src="characters/Aloy.png" alt="aloy" />
<p>Aloy</p>
</div>
<div class="fighter" @click="chooseFighter('Sora')">
<img src="characters/Sora.png" alt="sora" />
<p>Sora</p>
</div>
<div class="fighter" @click="chooseFighter('Raiden')">
<img src="characters/Raiden.png" alt="raiden" />
<p>Raiden</p>
</div>
<div class="fighter" @click="chooseFighter('PaperMario')">
<img src="characters/PaperMario.png" alt="paper mario" />
<p>Paper Mario</p>
</div>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false,
playerName: "",
};
},
methods: {
closeModal() {
this.show = false;
},
openModal(playerName) {
this.playerName = playerName;
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
chooseFighter(profilePicSrc) {
let payload = {
playerName: this.playerName,
profile: "characters/" + profilePicSrc + ".png",
};
this.$store.dispatch("changePlayerProfile", payload);
this.closeModal();
},
},
};
</script>
<style scoped>
.modalContainer {
width: 80%; /* Could be more or less, depending on screen size */
}
.fighterDiv {
display: flex;
width: 100%;
justify-content: center;
margin-top: 30px;
flex-wrap: wrap;
}
.fighterDiv img {
width: 180px;
height: 90px;
}
.fighter {
display: flex;
flex-wrap: wrap;
justify-content: center;
text-align: center;
width: 14vw;
margin-top: 30px;
}
.fighter > * {
width: 100%;
}
.fighter:hover {
outline: 1px solid rgb(128, 83, 0);
outline-offset: 2px;
cursor: pointer;
}
.fighter p {
margin-top: 8px;
font-size: 1.3rem;
}
@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;
}
.fighter {
width: 25vw;
}
}
</style>
@@ -1,111 +0,0 @@
<template>
<div class="extraButtonsDiv">
<button @click="resetPlaylist">Reset playlist</button>
<button @click="resetPoints">Reset points</button>
<button @click="syncGames">Sync games</button>
<button @click="startSoundTest">Sound test</button>
</div>
</template>
<script>
import arne from '../../arne.js'
export default {
data() {
return {
emptyPlaylist: [],
};
},
methods: {
async resetPoints() {
this.$store.dispatch("resetPlayerScore");
this.$store.dispatch("resetPlayerWelcomed");
this.$store.dispatch("resetPlayerMatchPoint");
this.$store.dispatch("setRoundStarted", false);
},
async resetPlaylist() {
await this.APIresetPlaylist();
this.$store.dispatch("updatePlaylistHistory", this.emptyPlaylist);
this.$store.dispatch("setCurrentlyLoadingTrack", "N/A");
this.$store.dispatch("setCurrentTrackHidden", false);
},
async syncGames() {
await this.APIsyncGames();
await this.APIresetPlaylist();
this.$store.dispatch("resetPlayerScore");
this.$store.dispatch("resetPlayerWelcomed");
this.$store.dispatch("resetPlayerMatchPoint");
this.$store.dispatch("setRoundStarted", false);
this.$store.dispatch("updatePlaylistHistory", this.emptyPlaylist);
this.$store.dispatch("setCurrentlyLoadingTrack", "N/A");
this.$store.dispatch("setCurrentTrackHidden", false);
this.$store.dispatch("reloadGamesList", true);
},
startSoundTest() {
this.$emit("start-sound-test");
},
APIresetPlaylist() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music/reset`,
})
.then(() => {
resolve();
})
.catch(function(error) {
console.log(error);
reject();
});
});
},
APIsyncGames() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/sync`,
})
.then(() => {
resolve();
})
.catch(function(error) {
console.log(error);
reject();
});
});
},
},
};
</script>
<style scoped>
.extraButtonsDiv {
display: flex;
margin-top: 2px;
margin-left: 10px;
}
button {
display: flex;
align-items: center;
height: 65%;
margin-right: 0.4vw;
border-radius: 10px;
border-width: 1px;
}
@media only screen and (max-width: 1000px) {
.extraButtonsDiv {
display: flex;
position: relative;
flex-wrap: wrap;
justify-content: center;
}
button {
width: 51vw;
height: 40px;
margin-bottom: 10px;
justify-content: center;
}
}
</style>
@@ -1,136 +0,0 @@
<template>
<div
v-if="show"
class="mobileMenu"
@click="checkIfClickShouldCloseMobileMenu"
>
<transition name="mobileMenuAni">
<div v-if="showContent" class="mobileMenuContainer">
<div class="mobileMenuWrapper">
<h1>Menu</h1>
<button class="settingsButton" @click="showSettingsModal">
Settings
</button>
<button class="statisticsButton" @click="showStatisticsModal">
Statistics
</button>
<button class="aboutButton" @click="showAboutModal">
About
</button>
<h1>Control</h1>
<extra-buttons
@start-sound-test="startSoundTest"
></extra-buttons>
</div>
</div>
</transition>
</div>
</template>
<script>
import extraButtons from "./extraButtons.vue";
export default {
components: {
extraButtons,
},
data() {
return {
show: false,
showContent: false,
};
},
methods: {
closeMobileMenu() {
this.showContent = false;
setTimeout(() => {
this.show = false;
}, 250);
},
openMobileMenu() {
this.show = true;
},
openMobileMenuContent() {
this.showContent = true;
},
checkIfClickShouldCloseMobileMenu(event) {
if (event.target.classList[0] === "mobileMenu") {
this.closeMobileMenu();
}
},
showSettingsModal() {
this.show = false;
this.showContent = false;
this.$emit("show-settings-modal");
},
showStatisticsModal() {
this.show = false;
this.showContent = false;
this.$emit("show-statistics-modal");
},
showAboutModal() {
this.show = false;
this.showContent = false;
this.$emit("show-about-modal");
},
startSoundTest() {
this.$emit("start-sound-test");
},
},
};
</script>
<style scoped>
.mobileMenuAni-enter-from,
.mobileMenuAni-leave-to {
opacity: 0.6;
transform: translateX(80vw);
}
.mobileMenuAni-enter-to,
.mobileMenuAni-leave-from {
opacity: 1;
transform: translateX(0vw);
}
.mobileMenuAni-enter-active,
.mobileMenuAni-leave-active {
transition: all 0.3s ease-out;
}
.mobileMenu {
position: fixed;
z-index: 8;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.5);
}
.mobileMenuContainer {
background-color: #c4c4c4;
width: 80vw;
height: 100%;
margin-left: 20%;
}
.mobileMenuWrapper {
display: flex;
position: relative;
flex-wrap: wrap;
justify-content: center;
}
h1 {
margin: 10px 0px;
width: 100%;
text-align: center;
}
button {
width: 51vw;
height: 40px;
margin-bottom: 10px;
}
</style>
@@ -1,354 +0,0 @@
<template>
<div class="playersWindowDiv">
<div @click="toggleDisplay" class="playerListTitle">
<img v-if="display" class="arrow arrowToggle" src="keyboard_arrow_up-black-36dp.svg" />
<img v-else class="arrow arrowToggle" src="keyboard_arrow_down-black-36dp.svg" />
<h1>Players</h1>
<img
src="person_add_alt_1-black-36dp.svg"
alt="addPlayer"
class="addPlayerIMG"
@click="toggleNewPlayerInput()"
/>
</div>
<transition-group tag="div" v-if="display" name="playerList" class="playerList">
<div class="player" v-for="player in listOfPlayers" :key="player.playerName">
<p>{{ player.playerName }}: {{ player.score }}</p>
<img
class="profilePicture"
:src="player.profile"
@click="openCharacterModal(player.playerName)"
/>
<button @click="changeScore(player.playerName, 1)">+1</button>
<button @click="changeScore(player.playerName, -1)">-1</button>
<img
src="person_remove-black-36dp.svg"
@click="removePlayer(player.playerName)"
alt=""
/>
</div>
<div class="newPlayerInput" v-if="showNewPlayerInput === true">
<input
v-model="newPlayerInputText"
type="text"
@keypress.enter="addNewPlayer()"
ref="inputField"
placeholder="Player name"
/>
<button @click="addNewPlayer()">Add</button>
</div>
</transition-group>
<winning-modal ref="winningModal"></winning-modal>
<character-modal ref="characterModal"></character-modal>
</div>
</template>
<script>
import { mapState } from "vuex";
import winningModal from "../items/winningModal.vue";
import characterModal from "../items/characterModal.vue";
export default {
components: {
winningModal,
characterModal,
},
data() {
return {
showNewPlayerInput: false,
newPlayerInputText: "",
display: true,
};
},
computed: {
...mapState(["listOfPlayers", "winningScore"]),
},
methods: {
removePlayer(playerName) {
this.$store.dispatch("removePlayer", playerName);
},
newPlayerValidation() {
/* Validation variables */
let errors = [];
let newName = this.newPlayerInputText;
/* Check for empty value */
if (newName.trim() === "") {
errors.push("No player name was entered");
}
/* Prepare data of players and check for already entered ones */
let copyOfPlayerList = this.listOfPlayers;
let arrayOfPlayers = copyOfPlayerList.map((player) => player.playerName.toLowerCase());
if (arrayOfPlayers.includes(newName.toLowerCase())) {
errors.push("This player already exists");
}
/* Check amount of players */
if (arrayOfPlayers.length > 6) {
errors.push("Too many players already exists");
}
/* If everything went fine, return an empty */
return errors;
},
addNewPlayer() {
let errors = this.newPlayerValidation();
if (errors.length === 0) {
this.$store.dispatch("addNewPlayer", this.newPlayerInputText);
this.newPlayerInputText = "";
} else {
console.log(errors);
}
},
toggleNewPlayerInput() {
this.showNewPlayerInput = !this.showNewPlayerInput;
if (this.showNewPlayerInput === true) {
this.$nextTick(() => this.$refs.inputField.focus());
}
},
changeScore(playerName, score) {
let payload = {
playerName: playerName,
score: score,
};
this.$store.dispatch("changePlayerScore", payload);
for (let i = 0; i < this.listOfPlayers.length; i++) {
/* If a player received their first point */
if (this.listOfPlayers[i].score == 1 && this.listOfPlayers[i].welcomed === false) {
let payload = {
playerName: playerName,
welcomeSet: true,
};
this.$store.dispatch("changePlayerWelcomed", payload);
this.$emit("play-welcome-sound");
}
/* If a player reach match point */
if (this.listOfPlayers[i].score === this.winningScore-1) {
let payload = {
playerName: playerName,
matchPointSet: true,
};
this.$store.dispatch("changePlayerMatchPoint", payload);
this.$emit("play-match-point-sound");
}
/* If a player won */
if (this.listOfPlayers[i].score === this.winningScore) {
this.$refs.winningModal.openModal(this.listOfPlayers[i].playerName);
this.$emit("play-winning-sound");
}
}
},
openCharacterModal(playerName) {
this.$refs.characterModal.openModal(playerName);
},
getProfileSrc(profileToGet) {
console.log(this.listOfPlayers[0]);
let src = `characters/${profileToGet}`;
console.log(src);
return src;
},
toggleDisplay(event) {
if (event.target.classList[0] != "addPlayerIMG") {
if (window.innerWidth < 600) {
this.display = !this.display;
}
}
},
},
};
</script>
<style scoped>
.playerList-enter-from,
.playerList-leave-to {
opacity: 0;
}
.playerList-enter-to,
.playerList-leave-from {
opacity: 1;
}
.playerList-enter-active {
transition: all 0.5s ease-out;
}
.playerList-leave-active {
position: absolute;
transition: all 0.2s ease-in;
}
.playerList-move {
transition: 0.25s;
transition-property: transform, opacity;
}
.playerList-move.newPlayerInput {
transition: 0.2s;
transition-property: transform;
}
/* End of Animation block */
.playersWindowDiv {
display: inline-block;
width: 38vw;
color: white;
}
.playerListTitle {
display: flex;
width: 100%;
background-color: #333;
height: 5vh;
align-content: center;
align-items: center;
text-align: center;
}
.playerListTitle h1 {
text-align: center;
position: relative;
margin-left: auto;
margin-right: auto;
flex-grow: 100;
}
.playerListTitle .addPlayerIMG {
justify-self: flex-end;
margin-left: auto;
margin-right: 5px;
flex-grow: 1;
filter: invert(66%) sepia(96%) saturate(2783%) hue-rotate(1deg) brightness(105%) contrast(103%);
}
.playerListTitle .arrow {
flex-grow: 1;
}
.playerListTitle .arrowToggle {
display: none;
}
.playerList {
display: inline-block;
background-color: rgb(66, 66, 66);
min-height: 37vh;
}
.playerList .player {
display: flex;
flex-wrap: nowrap;
justify-content: flex-end;
width: 38vw;
height: 6vh;
font-size: 1.8rem;
align-content: center;
align-items: center;
}
.playerList .player p {
margin-left: 5px;
margin-right: auto;
}
.playerList .player .profilePicture {
width: 100px;
height: 50px;
background-color: red;
margin-right: 2vw;
cursor: pointer;
}
.playerList .player button {
align-content: center;
padding: 3px;
width: 50px;
height: 4vh;
margin-right: 0.6vw;
line-height: 0;
}
.playerList .player button:last-of-type {
margin-right: 3vw;
}
.playerList .player img {
margin-right: 5px;
}
.newPlayerInput {
display: flex;
width: 38vw;
height: 50px;
align-items: center;
justify-content: center;
padding: 5px 0;
background-color: rgb(107, 106, 106);
}
.newPlayerInput p {
font-size: 1.3rem;
flex-grow: 1;
text-align: center;
}
.newPlayerInput input {
height: 70%;
width: 250px;
font-size: 1.5rem;
margin: 0 5px;
text-align: center;
}
.newPlayerInput button {
margin: 0 10px;
height: 80%;
align-self: center;
}
@media only screen and (max-width: 1000px) {
.playersWindowDiv {
width: 100%;
}
.playerListTitle {
height: 10vw;
font-size: 0.8rem;
}
.playerListTitle .addPlayerIMG {
height: 9vw;
}
.playerListTitle .arrowToggle {
display: block;
max-height: 100%;
left: 0;
filter: invert(77%) sepia(8%) saturate(6%) hue-rotate(317deg) brightness(95%) contrast(87%);
}
.playerList {
width: 100%;
height: 60vw;
min-height: 190px;
overflow: scroll;
}
.playerList .player {
width: 100vw;
height: 12vw;
font-size: 1rem;
}
.playerList .player .profilePicture {
width: 60px;
height: 30px;
margin-right: 1.5vw;
}
.playerList .player button {
align-content: center;
padding: 3px;
width: 45px;
height: 10vw;
margin-right: 1vw;
line-height: 0;
}
.newPlayerInput {
width: 100%;
height: 30px;
}
.newPlayerInput input {
height: 90%;
width: 200px;
font-size: 1.2rem;
}
.newPlayerInput button {
margin: 0 10px;
height: 40px;
border-radius: 2px;
}
}
</style>
@@ -1,191 +0,0 @@
<template>
<div class="playlistHistoryDiv">
<div @click="toggleDisplay" class="playlistHistoryTitle">
<h1>Playlist history</h1>
<img v-if="display" src="keyboard_arrow_up-black-36dp.svg" alt="" />
<img v-else src="keyboard_arrow_down-black-36dp.svg" alt="" />
</div>
<transition-group
tag="div"
v-if="display"
name="playlistHistory"
class="playlistHistory"
ref="playlistHistory"
>
<div
class="track"
v-for="track in playlistHistory"
:key="track.SongNo"
@click="playSelectedTrack(track, $event)"
>
<p
v-if="checkIfHidden(track, playlistHistory)"
:class="{ activeTrack: track.CurrentlyPlaying }"
>
??? - ???
</p>
<p v-else-if="track.Game !== ''" :class="{ activeTrack: track.CurrentlyPlaying }">
{{ track.SongNo + 1 }}. {{ track.Game }} -
{{ displayTrack(track.Song) }}
</p>
<span v-if="currentlyLoadingTrack === track.SongNo" class="loadingTrack">
-- Loading --
</span>
</div>
</transition-group>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
display: true,
};
},
computed: {
...mapState(["playlistHistory", "currentTrackHidden", "currentlyLoadingTrack"]),
},
methods: {
checkIfHidden(track, playlistHistory) {
return track.SongNo === playlistHistory.length - 1 && this.currentTrackHidden;
},
displayTrack(trackName) {
/* Todo: Is this if-check necessary? */
if (trackName === undefined) {
return "";
} else {
return trackName.replace(".mp3", "");
}
},
playSelectedTrack(track, event) {
console.log(event.path);
this.$store.dispatch("setCurrentlyLoadingTrack", track.SongNo);
this.$emit("play-selected-track", track);
},
scrollToBottom() {
if (this.display) {
this.$nextTick(() => {
this.$refs.playlistHistory.$el.scrollTop = this.$refs.playlistHistory.$el.scrollHeight;
});
}
},
toggleDisplay() {
if (window.innerWidth < 600) {
this.display = !this.display;
}
},
},
created() {
if (window.innerWidth < 600) {
this.display = false;
}
},
};
</script>
<style scoped>
/* Scroll */
.playlistHistory::-webkit-scrollbar {
width: 17px;
}
.playlistHistory {
scrollbar-color: #4f5253c2;
}
.playlistHistory::-webkit-scrollbar-track {
background: #393c4179;
border-radius: 5px;
}
.playlistHistory::-webkit-scrollbar-thumb {
background-color: #6d7679c2;
border-radius: 5px;
border: 2px solid #3c434e79;
}
/* End of Scroll */
.playlistHistoryDiv {
display: inline-block;
width: 60vw;
color: white;
margin-right: 0.5vw;
}
.playlistHistoryTitle {
display: flex;
background-color: #333;
height: 5vh;
justify-content: center;
align-items: center;
}
.playlistHistoryTitle img {
display: none;
}
.playlistHistory {
background-color: rgb(66, 66, 66);
width: 60vw;
height: 37vh;
overflow-x: hidden;
overflow-y: scroll;
}
.playlistHistory .track {
display: flex;
flex-wrap: nowrap;
justify-content: flex-start;
width: 57vw;
height: auto;
min-height: 30px;
line-height: 1.5;
font-size: 1.1rem;
align-items: center;
padding: 0.3vh 0.6vh;
cursor: pointer;
}
.playlistHistory .track:nth-child(even) {
background-color: #4e4e4e;
}
.playlistHistory .activeTrack {
color: yellow;
}
.loadingTrack {
color: #ff9c00;
margin-left: 20px;
}
@media only screen and (max-width: 1000px) {
.playlistHistory::-webkit-scrollbar {
width: 5px;
}
.playlistHistoryDiv {
width: 100%;
margin: 0 0 0.3vw;
}
.playlistHistoryTitle {
height: 10vw;
font-size: 0.8rem;
}
.playlistHistoryTitle img {
display: block;
position: absolute;
max-height: 100%;
left: 0;
filter: invert(77%) sepia(8%) saturate(6%) hue-rotate(317deg) brightness(95%) contrast(87%);
}
.playlistHistory {
width: 100vw;
height: 56vw;
}
.playlistHistory .track {
width: 98vw;
font-size: 0.8rem;
padding: 1.5vw 1vw;
}
.loadingTrack {
display: none;
}
}
</style>
@@ -1,209 +0,0 @@
<template>
<!-- Round Settings, Stop after current, Don't hide title -->
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
class="closeModalImg"
src="cancel-black-36dp.svg"
alt="closeModalIMG"
@click="closeModal"
/>
<h1>Settings</h1>
<div class="checkboxDiv">
<input
type="checkbox"
name="stopAfterCurrent"
id="stopAfterCurrent"
v-model="checkboxStopAfterCurrent"
@change="updateOptionStopAfterCurrent"
/>
<label for="stopAfterCurrent">Stop after current</label>
</div>
<div class="checkboxDiv">
<input
type="checkbox"
name="hideTitle"
id="hideTitle"
v-model="checkboxHideTitle"
@change="updateOptionHideTitle"
/>
<label for="hideTitle">Hide next track</label>
</div>
<div class="checkboxDiv">
<input
type="checkbox"
name="addPlayed"
id="addPlayed"
v-model="checkboxAddPlayed"
@change="updateOptionAddPlayed"
/>
<label for="addPlayed">Turn on played to database</label>
</div>
<div class="checkboxDiv">
<input
type="checkbox"
name="lowPlayed"
id="lowPlayed"
v-model="checkboxLowPlayed"
@change="updateOptionLowPlayed"
/>
<label for="lowPlayed">Use low played mode</label>
</div>
<div class="winningScoreDiv">
<span>Winning Score: {{ winningScore }}</span>
<button
:class="{ disabled: roundStarted }"
:disabled="roundStarted"
@click="changeScore(-1)"
>
-1
</button>
<button
:class="{ disabled: roundStarted }"
:disabled="roundStarted"
@click="changeScore(1)"
>
+1
</button>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
show: false,
checkboxStopAfterCurrent: false,
checkboxHideTitle: false,
checkboxAddPlayed: false,
checkboxLowPlayed: false,
};
},
computed: {
...mapState(["winningScore", "roundStarted", "stopAfterCurrent", "hideNextTrack", "addPlayed", "lowPlayed"]),
},
methods: {
closeModal() {
this.show = false;
},
openModal() {
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
updateOptionHideTitle() {
if (this.checkboxHideTitle) {
this.$store.dispatch("updateHideNextTitle", true);
} else {
this.$store.dispatch("updateHideNextTitle", false);
}
},
updateOptionAddPlayed() {
if (this.checkboxAddPlayed) {
this.$store.dispatch("updateAddPlayed", true);
} else {
this.$store.dispatch("updateAddPlayed", false);
}
},
updateOptionLowPlayed() {
if (this.checkboxLowPlayed) {
this.$store.dispatch("updateLowPlayed", true);
} else {
this.$store.dispatch("updateLowPlayed", false);
}
},
updateOptionStopAfterCurrent() {
if (this.checkboxStopAfterCurrent) {
this.$store.dispatch("updateStopAfterCurrent", true);
} else {
this.$store.dispatch("updateStopAfterCurrent", false);
}
},
changeScore(score) {
if (score < 0) {
this.$store.dispatch("setWinningScore", -1);
} else {
this.$store.dispatch("setWinningScore", 1);
}
},
},
mounted() {
this.checkboxStopAfterCurrent = this.stopAfterCurrent;
this.checkboxHideTitle = this.hideNextTrack;
this.checkboxAddPlayed = this.addPlayed;
this.checkboxLowPlayed = this.lowPlayed;
},
};
</script>
<style scoped>
.winningScoreDiv {
width: 100%;
display: inline-flex;
align-items: center;
flex-wrap: nowrap;
margin-top: 30px;
}
.checkboxDiv {
margin: 5px;
width: 100%;
}
.checkboxDiv:first-of-type {
margin-top: 20px;
}
.checkboxDiv input {
transform: scale(1.3);
}
.checkboxDiv label {
margin-left: 10px;
font-size: 1.5rem;
}
.winningScoreDiv span {
font-size: 1.7rem;
margin-right: 10px;
width: 245px;
}
.winningScoreDiv button {
display: flex;
width: 50px;
height: 33px;
background: rgb(16, 99, 194);
color: rgb(235, 235, 235);
border-color: rgb(161, 161, 161);
justify-content: center;
align-items: center;
line-height: 0;
font-size: 1.5rem;
margin: 3px;
}
.disabled {
opacity: 50%;
}
@media only screen and (max-width: 1000px) {
.checkboxDiv label {
font-size: 1.3rem;
}
.winningScoreDiv span {
font-size: 1.3rem;
margin-right: 10px;
width: 245px;
}
}
</style>
@@ -1,68 +0,0 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
class="closeModalImg"
src="cancel-black-36dp.svg"
alt="closeModalIMG"
@click="closeModal"
/>
<h1>Statistics</h1>
<p>Total amount of games in the playlist: {{ howManyGames }}</p>
</div>
</div>
</div>
</transition>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
show: false,
};
},
computed: {
...mapState(["howManyGames"]),
},
methods: {
closeModal() {
this.show = false;
},
openModal() {
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
},
};
</script>
<style scoped>
p {
font-size: 1.2rem;
width: 100%;
margin-top: 30px;
}
.descriptionText {
margin-top: 35px;
}
.creditText {
margin-top: 30px;
}
.creditText:nth-child(5) {
margin-top: 0px;
}
.patchNotesText {
margin-top: 20px;
}
</style>
@@ -1,50 +0,0 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
class="closeModalImg"
src="cancel-black-36dp.svg"
alt="closeModalIMG"
@click="closeModal"
/>
<h1>{{ playerName }} won!!</h1>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false,
playerName: "",
};
},
methods: {
closeModal() {
this.show = false;
},
showPeter() {
let randomNumber = Math.floor(Math.random() * 10);
console.log(randomNumber);
console.log("Test");
return randomNumber < 2;
},
openModal(playerName) {
this.playerName = playerName;
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
},
};
</script>
<style scoped></style>
@@ -1,441 +0,0 @@
<template>
<footer>
<div class="audioAndButtonsDiv">
<div class="audioButtonsDiv">
<button
class="bigButton"
:class="{ disabled: disableButtons }"
:disabled="disableButtons"
@click="randomizeTrack()"
>
<p v-if="!disableButtons"><u>R</u>andomize new track</p>
<p v-else>Loading...</p>
</button>
<button class="bigButton" @click="showAnswer()"><u>S</u>how answer</button>
</div>
<div class="audioDiv">
<audio
controls
class="audioPlayer"
:src="currentTrackSrcFile"
ref="audioPlayer"
:onended="trackHasEnded"
></audio>
</div>
</div>
</footer>
</template>
<script>
import { mapState } from "vuex";
import arne from "../../arne.js";
export default {
data() {
return {
currentTrackSrcFile: "",
disableButtons: false,
loadingBar: "",
};
},
computed: {
...mapState([
"stopAfterCurrent",
"localPlaylist",
"hideNextTrack",
"roundStarted",
"specialTrackIsPlaying",
"addPlayed",
"lowPlayed",
]),
},
methods: {
async randomizeTrack() {
console.log("Randomizing track");
/* Prevents anyone from changing the winning score after the round has started */
if (this.roundStarted === false) {
this.$store.dispatch("setRoundStarted", true);
}
let trackAddedToQue = false;
this.disableButtons = true;
let copyOfPlaylist = this.localPlaylist;
setTimeout(() => this.isHidden = false, 2000);
//If the local playlist of MP3s is empty, wait for a new track to load
if (copyOfPlaylist.length === 0) {
console.log("No track preloaded, loading a new one");
if (this.lowPlayed) {
await this.APIgetRandomizedLowTrack();
} else {
await this.APIgetRandomizedTrack();
}
if (this.addPlayed) {
await this.APIaddPlayed();
}
await this.APIaddToQue();
trackAddedToQue = true;
}
//Update the source file for the media player (binded property to Audio tag in the template)
this.currentTrackSrcFile = window.URL.createObjectURL(copyOfPlaylist[0]);
if (!trackAddedToQue) {
if (this.addPlayed) {
await this.APIaddPlayed();
}
await this.APIaddToQue();
}
await this.APIgetPlaylistHistory();
this.$refs.audioPlayer.play();
this.$emit("scroll-to-bottom-playlist-history");
copyOfPlaylist.shift();
if (this.hideNextTrack) {
this.$store.dispatch("setCurrentTrackHidden", true);
} else {
this.$store.dispatch("setCurrentTrackHidden", false);
}
await this.APIgetInfoAboutCurrentTrack();
await this.randomizeTrackInAdvance();
this.disableButtons = false;
},
async randomizeTrackInAdvance() {
console.log("Randomizing track in advance");
try {
if (this.lowPlayed) {
await this.APIgetRandomizedLowTrack();
} else {
await this.APIgetRandomizedTrack();
}
} catch (error) {
console.log(error);
console.log("Error: A track couldn't be loaded in advance");
}
return new Promise((resolve) => {
resolve();
});
},
async playSpecificTrack(track) {
await this.APIplaySpecificTrack(track.SongNo);
this.$store.dispatch("setCurrentlyLoadingTrack", "");
await this.APIgetPlaylistHistory();
this.$store.dispatch("setCurrentTrackHidden", false);
await this.APIgetInfoAboutCurrentTrack();
},
showAnswer() {
this.$store.dispatch("setCurrentTrackHidden", false);
},
startSoundTest() {
this.$refs.audioPlayer.pause();
this.currentTrackSrcFile = "sounds/check.mp3";
this.$store.dispatch("setSpecialTrackIsPlaying", true);
this.$nextTick(() => {
this.$refs.audioPlayer.currentTime = 0;
this.$refs.audioPlayer.play();
});
},
playWelcomeSound() {
let randomNumber = Math.floor(Math.random() * 10);
this.$refs.audioPlayer.pause();
if (randomNumber === 0) {
this.currentTrackSrcFile = "sounds/sound1.mp3";
} else if (randomNumber < 5) {
this.currentTrackSrcFile = "sounds/intro_short.mp3";
} else {
this.currentTrackSrcFile = "sounds/intro_long.mp3";
}
this.$store.dispatch("setSpecialTrackIsPlaying", true);
this.$nextTick(() => {
this.$refs.audioPlayer.currentTime = 0;
this.$refs.audioPlayer.play();
});
},
playMatchSound() {
this.$refs.audioPlayer.pause();
this.currentTrackSrcFile = "sounds/sound0.mp3";
this.$store.dispatch("setSpecialTrackIsPlaying", true);
this.$nextTick(() => {
this.$refs.audioPlayer.currentTime = 0;
this.$refs.audioPlayer.play();
});
},
playWinningSound() {
this.$refs.audioPlayer.pause();
this.currentTrackSrcFile = "sounds/winning.mp3";
this.$store.dispatch("setSpecialTrackIsPlaying", true);
this.$nextTick(() => {
this.$refs.audioPlayer.currentTime = 0;
this.$refs.audioPlayer.play();
});
},
APIgetRandomizedTrack() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music/rand`,
responseType: "blob",
onDownloadProgress: (progressEvent) => {
let percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
if (percentCompleted % 10 === 0) {
//Debug for slow downloading
//console.log(percentCompleted);
}
},
})
.then((response) => {
let mp3 = new Blob([response.data], { type: "audio/mp3" });
this.$store.dispatch("updateLocalPlaylist", mp3);
this.$nextTick(() => {
resolve();
});
})
.catch(function(error) {
console.log("An error happened when fetching the track");
console.log(error);
reject();
});
});
},
APIgetRandomizedLowTrack() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music/rand/low`,
responseType: "blob",
onDownloadProgress: (progressEvent) => {
let percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
if (percentCompleted % 10 === 0) {
//Debug for slow downloading
//console.log(percentCompleted);
}
},
})
.then((response) => {
let mp3 = new Blob([response.data], { type: "audio/mp3" });
this.$store.dispatch("updateLocalPlaylist", mp3);
this.$nextTick(() => {
resolve();
});
})
.catch(function(error) {
console.log("An error happened when fetching the track");
console.log(error);
reject();
});
});
},
APIaddPlayed() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music/addPlayed`,
})
.then(() => {
resolve(false);
})
.catch(function(error) {
console.log(error);
reject();
});
});
},
APIaddToQue() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music/addQue`,
})
.then(() => {
resolve(false);
})
.catch(function(error) {
console.log(error);
reject();
});
});
},
APIgetInfoAboutCurrentTrack() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music/info`,
})
.then((response) => {
let gameInfoObject = {
game: response.data.Game,
track: response.data.Song.replace(".mp3", ""),
songNo: response.data.SongNo,
};
this.$store.dispatch("setCurrentGame", gameInfoObject.game);
this.$store.dispatch("setCurrentTrack", gameInfoObject.track);
resolve(false);
})
.catch(function(error) {
console.log(error);
reject();
});
});
},
APIgetPlaylistHistory() {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music/list`,
})
.then((response) => {
let tracklistArray = response.data;
this.$store.dispatch("updatePlaylistHistory", tracklistArray);
resolve();
})
.catch(function(error) {
console.log(error);
reject();
});
});
},
APIplaySpecificTrack(trackNumber) {
return new Promise((resolve, reject) => {
this.axios({
method: "get",
url: `${arne.hostname}/music?song=${trackNumber}`,
responseType: "blob",
})
.then((response) => {
let mp3 = new Blob([response.data], { type: "audio/mp3" });
this.currentTrackSrcFile = window.URL.createObjectURL(mp3);
this.$nextTick(() => {
this.$refs.audioPlayer.play();
resolve();
});
})
.catch(function(error) {
console.log(error);
reject();
});
});
},
trackHasEnded() {
if (this.specialTrackIsPlaying) {
this.$store.dispatch("setSpecialTrackIsPlaying", false);
return 0;
}
if (!this.stopAfterCurrent) {
this.randomizeTrack();
}
},
},
async mounted() {
await this.APIgetPlaylistHistory();
},
};
</script>
<style scoped>
footer {
display: flex;
position: absolute;
bottom: 0;
justify-content: flex-end;
width: 100%;
}
.audioAndButtonsDiv {
display: flex;
flex-wrap: wrap;
width: 80vw;
margin-right: 1vh;
}
.audioButtonsDiv {
display: flex;
flex-direction: row-reverse;
width: 100%;
margin-bottom: 2vh;
align-items: center;
}
.audioButtonsDiv button:nth-child(1) {
margin-right: 3vw;
width: 200px;
}
.audioButtonsDiv button:nth-child(2) {
margin-right: 25vw;
width: 220px;
}
.audioButtonsDiv .bigButton {
height: 7vh;
}
.disabled {
opacity: 0.5;
}
.audioDiv {
width: 100%;
}
.audioDiv audio {
width: 100%;
font-size: 2rem;
}
audio::-webkit-media-controls-panel {
background-color: rgb(185, 185, 185);
}
audio::-webkit-media-controls-current-time-display {
color: rgb(0, 0, 0);
font-size: 4em;
}
audio::-webkit-media-controls-time-remaining-display {
color: rgb(0, 0, 0);
font-size: 4em;
}
@media only screen and (max-width: 1000px) {
footer {
position: static;
justify-content: center;
}
.audioAndButtonsDiv {
width: 100vw;
margin: 0;
}
.audioButtonsDiv {
margin-bottom: 2vw;
font-size: 1rem;
}
.audioButtonsDiv button:nth-child(1) {
margin: 0;
width: 50vw;
border-radius: 2px;
}
.audioButtonsDiv button:nth-child(2) {
margin: 0;
width: 50vw;
border-radius: 2px;
}
.audioButtonsDiv .bigButton {
height: 22vw;
}
.disabled {
opacity: 0.5;
}
.audioDiv {
width: 100%;
}
.audioDiv audio {
font-size: 1rem;
}
audio::-webkit-media-controls-panel {
background-color: rgb(185, 185, 185);
border-radius: 2px;
}
audio::-webkit-media-controls-current-time-display {
color: rgb(0, 0, 0);
font-size: 2em;
}
audio::-webkit-media-controls-time-remaining-display {
color: rgb(0, 0, 0);
font-size: 2em;
}
}
</style>
@@ -1,139 +0,0 @@
<template>
<nav>
<p class="title">Music Player Randomizer</p>
<div class="navMenuWrapper">
<button class="settingsButton" @click="showSettingsModal">
Settings
</button>
<button class="statisticsButton" @click="showStatisticsModal">
Statistics
</button>
<button class="aboutButton" @click="showAboutModal">About</button>
</div>
<p v-if="!$parent.displayWhenDesktop" class="mobileMenuButton" @click="showMobileMenu">
Menu
</p>
<settingsModal ref="settingsModal"></settingsModal>
<statisticsModal ref="statisticsModal"></statisticsModal>
<aboutModal ref="aboutModal"></aboutModal>
<mobileMenu
ref="mobileMenu"
@start-sound-test="startSoundTest"
@show-settings-modal="showSettingsModal"
@show-statistics-modal="showStatisticsModal"
@show-about-modal="showAboutModal"
></mobileMenu>
</nav>
</template>
<script>
import aboutModal from "../items/aboutModal.vue";
import statisticsModal from "../items/statisticsModal.vue";
import settingsModal from "../items/settingsModal.vue";
import mobileMenu from "../items/mobileMenu.vue";
export default {
components: {
aboutModal,
settingsModal,
statisticsModal,
mobileMenu,
},
methods: {
playSelectedTrack(track) {
this.$refs.theFooter.playSpecificTrack(track);
},
scrollToBottomPlaylistHistory() {
this.$refs.playlistHistory.scrollToBottom();
},
showAboutModal() {
this.$refs.aboutModal.openModal();
},
showSettingsModal() {
this.$refs.settingsModal.openModal();
},
showStatisticsModal() {
this.$refs.statisticsModal.openModal();
},
showMobileMenu() {
this.$refs.mobileMenu.openMobileMenu();
this.showMobileMenuContent();
},
showMobileMenuContent() {
setTimeout(() => {
this.$refs.mobileMenu.openMobileMenuContent();
}, 20);
},
startSoundTest() {
this.$emit("start-sound-test");
},
},
};
</script>
<style scoped>
nav {
background: #333;
display: flex;
align-content: center;
align-items: center;
width: 100%;
height: 5vh;
}
.title {
color: #fff;
font-size: 2rem;
margin-left: 2vw;
font-style: normal;
}
.navMenuWrapper {
margin-left: auto;
height: 5vh;
}
.navMenuWrapper button {
background: #333;
color: #ff9c00;
font-size: 1.3rem;
outline: none;
cursor: pointer;
border: none;
border-radius: 3px;
height: 100%;
padding: 0 20px;
}
.navMenuWrapper button:hover {
background-color: #555;
}
/* Mobile */
@media only screen and (max-width: 1000px) {
nav {
justify-content: center;
height: 10vw;
}
.title {
font-size: 1.2rem;
margin: 0;
padding: 0;
width: 100%;
margin-left: 5px;
/* text-align: ; */
}
.navMenuWrapper {
display: none;
}
.mobileMenuButton {
color: white;
background: grey;
padding: 6px;
border-top-left-radius: 7px;
border-bottom-left-radius: 7px;
}
}
</style>
@@ -1,103 +0,0 @@
<template>
<div class="theMainDiv">
<div class="mainFirstRow">
<currently-playing></currently-playing>
<inspiration-window></inspiration-window>
</div>
<div class="mainSecondRow">
<playlist-history
ref="playlistHistory"
@play-selected-track="playSelectedTrack"
></playlist-history>
<players-window
@play-welcome-sound="playWelcomeSound"
@play-match-point-sound="playMatchSound"
@play-winning-sound="playWinningSound"
></players-window>
</div>
<extra-buttons
v-if="$parent.displayWhenDesktop"
@start-sound-test="startSoundTest"
></extra-buttons>
<the-footer
ref="theFooter"
@scroll-to-bottom-playlist-history="scrollToBottomPlaylistHistory"
></the-footer>
<!-- <the-footer ref="theFooter"></the-footer> -->
</div>
</template>
<script>
import currentlyPlaying from "../items/CurrentlyPlaying.vue";
import inspirationWindow from "../items/InspirationWindow.vue";
import playersWindow from "../items/playersWindow.vue";
import playlistHistory from "../items/playlistHistory.vue";
import extraButtons from "../items/extraButtons.vue";
import theFooter from "./TheFooter.vue";
export default {
components: {
currentlyPlaying,
playersWindow,
playlistHistory,
inspirationWindow,
extraButtons,
theFooter,
},
methods: {
playSelectedTrack(track) {
this.$refs.theFooter.playSpecificTrack(track);
},
scrollToBottomPlaylistHistory() {
this.$refs.playlistHistory.scrollToBottom();
},
startSoundTest() {
this.$refs.theFooter.startSoundTest();
},
playWelcomeSound() {
this.$refs.theFooter.playWelcomeSound();
},
playMatchSound() {
this.$refs.theFooter.playMatchSound();
},
playWinningSound() {
this.$refs.theFooter.playWinningSound();
},
},
};
</script>
<style scoped>
.theMainDiv {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.mainFirstRow {
display: flex;
flex-wrap: nowrap;
width: 100%;
margin: 0.5vw;
}
.mainSecondRow {
display: flex;
flex-wrap: nowrap;
width: 100%;
margin: 0 0.5vw;
}
@media only screen and (max-width: 1000px) {
.mainFirstRow {
margin: 0;
margin-top: 0.5vw;
}
.mainSecondRow {
display: flex;
flex-wrap: wrap;
width: 100%;
margin: 0;
margin-top: 0.5vw;
}
}
</style>
-261
View File
@@ -1,261 +0,0 @@
import { createApp } from "vue";
import { createStore } from "vuex";
import App from "./App.vue";
import axios from "axios";
import VueAxios from "vue-axios";
const store = createStore({
state() {
return {
currentGame: "",
currentTrack: ``,
currentTrackHidden: false,
currentlyLoadingTrack: "",
specialTrackIsPlaying: false,
someoneHasWon: false,
winningScore: 20,
roundStarted: false,
reloadGamesList: false,
listOfPlayers: [],
localPlaylist: [],
playlistHistory: [
{
SongNo: "",
Game: "",
Song: "",
},
],
/* Stats */
howManyGames: 0,
/* Options */
stopAfterCurrent: true,
hideNextTrack: true,
addPlayed: true,
lowPlayed: false,
};
},
mutations: {
setCurrentGame(state, payload) {
state.currentGame = payload;
},
setCurrentTrack(state, payload) {
state.currentTrack = payload;
},
setCurrentTrackHidden(state, payload) {
state.currentTrackHidden = payload;
},
setCurrentlyLoadingTrack(state, payload) {
state.currentlyLoadingTrack = payload;
},
setSpecialTrackIsPlaying(state, payload) {
state.specialTrackIsPlaying = payload;
},
setSomeoneHasWon(state, payload) {
state.someoneHasWon = payload;
},
setWinningScore(state, payload) {
state.winningScore += payload;
},
setRoundStarted(state, payload) {
state.roundStarted = payload;
},
updateListOfPlayers(state, payload) {
state.listOfPlayers = payload;
},
changePlayerScore(state, payload) {
state.listOfPlayers[payload.indexOfPlayer].score += payload.scoreToChange;
},
changePlayerWelcomed(state, payload) {
state.listOfPlayers[payload.indexOfPlayer].welcomed = payload.playerWelcomeTrueOrFalse;
},
changePlayerMatchPoint(state, payload) {
state.listOfPlayers[payload.indexOfPlayer].matchPoint = payload.playerMatchPointTrueOrFalse;
},
changePlayerProfile(state, payload) {
state.listOfPlayers[payload.indexOfPlayer].profile = payload.profileSrc;
},
resetPlayerScore(state, payload) {
state.listOfPlayers = payload;
},
resetPlayerWelcomed(state, payload) {
state.listOfPlayers = payload;
},
resetPlayerMatchPoint(state, payload) {
state.listOfPlayers = payload;
},
updateLocalPlaylist(state, payload) {
state.localPlaylist.push(payload);
},
updatePlaylistHistory(state, payload) {
state.playlistHistory = payload;
},
updateHowManyGames(state, payload) {
state.howManyGames = payload;
},
reloadGamesList(state, payload) {
state.reloadGamesList = payload;
},
updateStopAfterCurrent(state, payload) {
state.stopAfterCurrent = payload;
},
updateHideNextTitle(state, payload) {
state.hideNextTrack = payload;
},
updateAddPlayed(state, payload) {
state.addPlayed = payload;
},
updateLowPlayed(state, payload) {
state.lowPlayed = payload;
},
},
actions: {
setCurrentGame(context, payload) {
context.commit("setCurrentGame", payload);
},
setCurrentTrack(context, payload) {
context.commit("setCurrentTrack", payload);
},
setCurrentTrackHidden(context, payload) {
context.commit("setCurrentTrackHidden", payload);
},
setCurrentlyLoadingTrack(context, payload) {
context.commit("setCurrentlyLoadingTrack", payload);
},
setSpecialTrackIsPlaying(context, payload) {
context.commit("setSpecialTrackIsPlaying", payload);
},
setSomeoneHasWon(context, payload) {
context.commit("someoneHasWon", payload);
},
setWinningScore(context, payload) {
context.commit("setWinningScore", payload);
},
setRoundStarted(context, payload) {
context.commit("setRoundStarted", payload);
},
removePlayer(context, payload) {
let newPlayerList = this.state.listOfPlayers.filter(
(player) => player.playerName != payload
);
context.commit("updateListOfPlayers", newPlayerList);
},
addNewPlayer(context, payload) {
let newPlayerList = this.state.listOfPlayers;
let newPlayer = {
playerName: payload,
score: 0,
welcomed: false,
matchPoint: false,
profile: "characters/noCharacter.png",
};
newPlayerList.push(newPlayer);
context.commit("updateListOfPlayers", newPlayerList);
},
changePlayerScore(context, payload) {
let copyOfPlayerList = this.state.listOfPlayers;
let indexOfPlayer = copyOfPlayerList.findIndex(
(player) => player.playerName === payload.playerName
);
let scoreToChange = payload.score;
context.commit("changePlayerScore", {
indexOfPlayer,
scoreToChange,
});
},
changePlayerWelcomed(context, payload) {
let copyOfPlayerList = this.state.listOfPlayers;
let indexOfPlayer = copyOfPlayerList.findIndex(
(player) => player.playerName === payload.playerName
);
let playerWelcomeTrueOrFalse = payload.welcomeSet;
context.commit("changePlayerWelcomed", {
indexOfPlayer,
playerWelcomeTrueOrFalse,
});
},
changePlayerMatchPoint(context, payload) {
let copyOfPlayerList = this.state.listOfPlayers;
let indexOfPlayer = copyOfPlayerList.findIndex(
(player) => player.playerName === payload.playerName
);
let playerMatchPointTrueOrFalse = payload.matchPointSet;
context.commit("changePlayerMatchPoint", {
indexOfPlayer,
playerMatchPointTrueOrFalse,
});
},
changePlayerProfile(context, payload) {
let copyOfPlayerList = this.state.listOfPlayers;
let indexOfPlayer = copyOfPlayerList.findIndex(
(player) => player.playerName === payload.playerName
);
let profileSrc = payload.profile;
context.commit("changePlayerProfile", {
indexOfPlayer,
profileSrc,
});
},
resetPlayerScore(context) {
/* The JSON.parse and JSON.stringify parts are used to make a copy that doesn't affect the original */
let copyOfPlayerList = JSON.parse(JSON.stringify(this.state.listOfPlayers));
for (let i = 0; i < copyOfPlayerList.length; i++) {
copyOfPlayerList[i].score = 0;
}
context.commit("resetPlayerScore", copyOfPlayerList);
},
resetPlayerWelcomed(context) {
/* The JSON.parse and JSON.stringify parts are used to make a copy that doesn't affect the original */
let copyOfPlayerList = JSON.parse(JSON.stringify(this.state.listOfPlayers));
for (let i = 0; i < copyOfPlayerList.length; i++) {
copyOfPlayerList[i].welcomed = false;
}
context.commit("resetPlayerWelcomed", copyOfPlayerList);
},
resetPlayerMatchPoint(context) {
/* The JSON.parse and JSON.stringify parts are used to make a copy that doesn't affect the original */
let copyOfPlayerList = JSON.parse(JSON.stringify(this.state.listOfPlayers));
for (let i = 0; i < copyOfPlayerList.length; i++) {
copyOfPlayerList[i].matchPoint = false;
}
context.commit("resetPlayerMatchPoint", copyOfPlayerList);
},
updateLocalPlaylist(context, payload) {
context.commit("updateLocalPlaylist", payload);
},
updatePlaylistHistory(context, payload) {
context.commit("updatePlaylistHistory", payload);
},
updateHowManyGames(context, payload) {
context.commit("updateHowManyGames", payload);
},
reloadGamesList(context, payload) {
context.commit("reloadGamesList", payload);
},
updateStopAfterCurrent(context, payload) {
context.commit("updateStopAfterCurrent", payload);
},
updateHideNextTitle(context, payload) {
context.commit("updateHideNextTitle", payload);
},
updateAddPlayed(context, payload) {
context.commit("updateAddPlayed", payload);
},
updateLowPlayed(context, payload) {
context.commit("updateLowPlayed", payload);
},
},
getters: {
/* Empty ATM */
/* getListOfPlayers: (state) => {
return state.listOfPlayers;
}, */
},
});
const app = createApp(App);
app.use(store);
app.use(VueAxios, axios);
app.mount("#app");
+19 -62
View File
@@ -2,84 +2,41 @@ package main
import (
"context"
"music-server/internal/logging"
"music-server/internal/server"
"net/http"
"os/signal"
"syscall"
"time"
"music-server/internal/logging"
"music-server/internal/server"
"go.uber.org/zap"
)
//
// @Title Swagger Example API
// @version 0.5
// @description This is a sample server Petstore server.
// @termsOfService http://swagger.io/terms/
const shutdownTimeout = 5 * time.Second
//
// @contact.name Sebastian Olsson
// @contact.email zarnor91@gmail.com
//
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
func main() {
/*f, perr := os.Create("cpu.pprof")
if perr != nil {
log.Fatal(perr)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()*/
appServer := server.NewServerInstance()
httpServer := appServer.HTTPServer()
// Create a done channel to signal when the shutdown is complete
done := make(chan bool, 1)
// Run graceful shutdown in a separate goroutine
go gracefulShutdown(appServer, httpServer, done)
logging.GetLogger().Info("Server starting", zap.String("address", httpServer.Addr))
err := httpServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
logging.GetLogger().Fatal("HTTP server error", zap.String("error", err.Error()))
}
// Wait for the graceful shutdown to complete
<-done
logging.GetLogger().Info("Graceful shutdown complete")
}
func gracefulShutdown(appServer *server.Server, httpServer *http.Server, done chan bool) {
// Create context that listens for the interrupt signal from the OS.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Listen for the interrupt signal.
<-ctx.Done()
s := server.NewServerInstance()
serveErr := make(chan error, 1)
go func() { serveErr <- s.ListenAndServe() }()
select {
case err := <-serveErr:
if err != nil {
logging.GetLogger().Fatal("HTTP server error", zap.Error(err))
}
case <-ctx.Done():
stop()
logging.GetLogger().Info("Shutting down gracefully, press Ctrl+C again to force")
// Close database connection
if appServer != nil && appServer.DB() != nil {
appServer.DB().Close()
}
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := httpServer.Shutdown(ctx); err != nil {
logging.GetLogger().Error("Server forced to shutdown with error", zap.String("error", err.Error()))
if err := s.Shutdown(shutdownCtx); err != nil {
logging.GetLogger().Error("Server forced to shutdown with error", zap.Error(err))
}
logging.GetLogger().Info("Server exiting")
// Notify the main goroutine that the shutdown is complete
done <- true
logging.GetLogger().Info("Graceful shutdown complete")
}
+51
View File
@@ -0,0 +1,51 @@
// Command openapi writes the MusicServer API description without starting the server.
//
// go run ./cmd/openapi -out .
//
// It writes openapi.json (OpenAPI 3.1) and openapi-3.0.json (OpenAPI 3.0, for tools
// that don't support 3.1 yet). A running server serves the same files at /openapi.json
// and /openapi-3.0.json.
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"music-server/internal/server"
)
func main() {
out := flag.String("out", ".", "directory to write openapi.json and openapi-3.0.json to")
flag.Parse()
if err := run(*out); err != nil {
fmt.Fprintln(os.Stderr, "openapi:", err)
os.Exit(1)
}
}
func run(out string) error {
spec := server.OpenAPISpec()
v31, err := spec.MarshalJSON()
if err != nil {
return err
}
v30, err := spec.Downgrade()
if err != nil {
return err
}
if err := os.MkdirAll(out, 0o755); err != nil {
return err
}
for name, data := range map[string][]byte{"openapi.json": v31, "openapi-3.0.json": v30} {
path := filepath.Join(out, name)
if err := os.WriteFile(path, data, 0o644); err != nil {
return err
}
fmt.Println("Wrote", path)
}
return nil
}
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunWritesBothVersions(t *testing.T) {
dir := t.TempDir()
require.NoError(t, run(dir))
for name, version := range map[string]string{"openapi.json": "3.1.0", "openapi-3.0.json": "3.0.3"} {
data, err := os.ReadFile(filepath.Join(dir, name))
require.NoError(t, err)
var spec struct {
OpenAPI string `json:"openapi"`
Paths map[string]any `json:"paths"`
}
require.NoError(t, json.Unmarshal(data, &spec), name)
assert.Equal(t, version, spec.OpenAPI, name)
assert.Contains(t, spec.Paths, "/api/v1/music/info", name)
}
}
+6
View File
@@ -135,6 +135,12 @@ main {
margin-top: 1.5rem;
}
.matched-name {
margin: 0.25rem 0 0;
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Responsive adjustments */
@media (max-width: 768px) {
#search_term {
-3
View File
@@ -4,6 +4,3 @@ import "embed"
//go:embed "assets"
var Assets embed.FS
//go:embed "swagger"
var Swagger embed.FS
+49 -51
View File
@@ -1,77 +1,75 @@
package web
import (
"log"
"music-server/internal/backend"
"net/http"
"strings"
"music-server/internal/backend"
"music-server/internal/logging"
"go.uber.org/zap"
)
var soundtracks_added []string
func FindSoundtrackWebHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
// NewSearchHandler returns a handler that renders the soundtracks matching the posted search term.
func NewSearchHandler(cache *backend.SearchCache) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
search_term := r.FormValue("search_term")
search(search_term)
results := soundtracks_added
component := FoundSoundtracks(results)
err = component.Render(r.Context(), w)
soundtracks, err := cache.Get(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Fatalf("Error rendering in FindSoundtrackWebHandler: %e", err)
}
logging.GetLogger().Error("Failed to load search data", zap.Error(err))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
func search(searchText string) {
soundtracks_added = nil
soundtrackData := backend.GetSearchCache()
results := searchWithData(searchText, soundtrackData)
soundtracks_added = results
results := search(r.FormValue("search_term"), soundtracks)
if err := FoundSoundtracks(results).Render(r.Context(), w); err != nil {
logging.GetLogger().Error("Failed to render search results", zap.Error(err))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
})
}
// searchWithData is a testable version of search that takes soundtrack data as a parameter
func searchWithData(searchText string, soundtrackData []backend.SoundtrackData) []string {
var results []string
// SearchResult is a soundtrack found by search. MatchedName is the alternative name that
// matched, or empty when the soundtrack's own name matched.
type SearchResult struct {
Name string
MatchedName string
}
// search returns the soundtracks matching searchText: exact matches first, then matches
// ignoring spaces and special characters, then matches with the letters in order. Each
// step tries the soundtrack's own name before its alternative names.
func search(searchText string, soundtrackData []backend.SoundtrackData) []SearchResult {
var results []SearchResult
seen := make(map[string]bool)
// Precompute cleaned and lowercase search terms once
cleanedSearchTerm := strings.ToLower(searchText)
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, " ", "")
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "é", "e")
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "+", "plus")
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "&", "and")
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "'n", "and")
lowercaseSearchText := strings.ToLower(searchText)
// Pass 1: Exact match (case-insensitive contains)
add := func(match func(backend.SearchName) bool) {
for _, data := range soundtrackData {
if !seen[data.Original] && (searchText == "" || strings.Contains(data.Lowercase, lowercaseSearchText)) {
results = append(results, data.Original)
if seen[data.Original] {
continue
}
matched, ok := "", match(data.SearchName)
for _, alternative := range data.Alternatives {
if ok {
break
}
matched, ok = alternative.Original, match(alternative)
}
if ok {
results = append(results, SearchResult{Name: data.Original, MatchedName: matched})
seen[data.Original] = true
}
}
// Pass 2: Contains match with cleaned terms
for _, data := range soundtrackData {
if !seen[data.Original] && (cleanedSearchTerm == "" || strings.Contains(data.Cleaned, cleanedSearchTerm)) {
results = append(results, data.Original)
seen[data.Original] = true
}
}
// Pass 3: Regex match with cleaned terms
for _, data := range soundtrackData {
if !seen[data.Original] && backend.MatchRegexCached(cleanedSearchTerm, data.Cleaned) {
results = append(results, data.Original)
seen[data.Original] = true
}
}
lowercase := strings.ToLower(searchText)
cleaned := backend.CleanSearchTerm(searchText)
add(func(n backend.SearchName) bool { return strings.Contains(n.Lowercase, lowercase) })
add(func(n backend.SearchName) bool { return strings.Contains(n.Cleaned, cleaned) })
add(func(n backend.SearchName) bool { return backend.MatchesSubsequence(cleaned, n.Cleaned) })
return results
}
+7 -6
View File
@@ -3,14 +3,12 @@ package web
templ SearchForm() {
@Base() {
<button id="dark-mode-toggle">🌙</button>
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#soundtracks-container"/>
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="load, keyup changed delay:0.25s" hx-target="#soundtracks-container"/>
<button type="button" id="clear" name="clear">Clear</button>
<div id="soundtracks-container"></div>
<script>
document.addEventListener('readystatechange', () => {
if (document.readyState == 'complete') {
// Initialize search
htmlx.ajax('POST', '/find', '#soundtracks-container');
document.getElementById("search_term").focus();
// Initialize theme from localStorage (default to dark)
@@ -35,17 +33,20 @@ templ SearchForm() {
document.getElementById("clear").addEventListener("click", function (event) {
document.getElementById("search_term").value = "";
htmlx.ajax('POST', '/find', '#soundtracks-container');
htmx.ajax('POST', '/find', '#soundtracks-container');
document.getElementById("search_term").focus();
});
</script>
}
}
templ FoundSoundtracks(soundtracks []string) {
templ FoundSoundtracks(soundtracks []SearchResult) {
for _, soundtrack := range soundtracks {
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
<p class="soundtrack-text">{ soundtrack }</p>
<p class="soundtrack-text">{ soundtrack.Name }</p>
if soundtrack.MatchedName != "" {
<p class="matched-name">Also known as { soundtrack.MatchedName }</p>
}
</div>
}
}
+110 -160
View File
@@ -1,193 +1,143 @@
package web
import (
"music-server/internal/backend"
"context"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"music-server/internal/backend"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestCleanTerm tests that search terms are cleaned correctly
func TestCleanTerm(t *testing.T) {
tests := []struct {
input string
expected string
}{
// CleanSearchTermForMatch removes spaces, replaces specific chars, and lowercases
{"MGS4", "mgs4"},
{"RE2", "re2"},
{"FFVII", "ffvii"},
{"Ys", "ys"},
{" test ", "test"},
{"Test 123", "test123"},
{"A+B", "aplusb"},
{"é", "e"},
// Note: CleanSearchTermForMatch does NOT remove apostrophes, it only replaces "'n" with "and"
{"It's", "it's"},
// But it does replace "'n" with "and"
{"I'n", "iand"},
{"", ""},
var soundtracks = []string{
"Call of Duty: Modern Warfare",
"Final Fantasy VII",
"Joe & Mac",
"Kingdom Hearts",
"Lemmings (SNES + Mega Drive)",
"MG",
"Metal Gear Solid 4 - Guns of the Patriots",
"Pokémon HeartGold & SoulSilver",
"Resident Evil 2 (Remake)",
"Ys III - Wanderers from Ys (+The Oath in Felghana)",
}
for _, tt := range tests {
result := backend.CleanSearchTermForMatch(tt.input)
if result != tt.expected {
t.Errorf("CleanSearchTermForMatch(%q) = %q, want %q", tt.input, result, tt.expected)
func newCache(names []string, alternatives ...map[string][]string) *backend.SearchCache {
soundtracks := make([]backend.SearchSoundtrack, len(names))
for i, name := range names {
soundtracks[i] = backend.SearchSoundtrack{Name: name}
for _, a := range alternatives {
soundtracks[i].AlternativeNames = a[name]
}
}
return backend.NewSearchCache(func(context.Context) ([]backend.SearchSoundtrack, error) { return soundtracks, nil })
}
// searchWithDataHelper is a helper to create backend.SoundtrackData from original names
func searchWithDataHelper(originals []string) []backend.SoundtrackData {
data := make([]backend.SoundtrackData, len(originals))
for i, orig := range originals {
data[i] = backend.SoundtrackData{
Original: orig,
Lowercase: strings.ToLower(orig),
Cleaned: backend.CleanSearchTermForMatch(orig),
// names returns the soundtrack names of results.
func names(results []SearchResult) []string {
var n []string
for _, r := range results {
n = append(n, r.Name)
}
return n
}
func searchData(t *testing.T) []backend.SoundtrackData {
data, err := newCache(soundtracks).Get(context.Background())
require.NoError(t, err)
return data
}
// TestNormalSearch tests the normal search function
func TestNormalSearch(t *testing.T) {
// Set up comprehensive test data with all user-provided soundtracks
testData := searchWithDataHelper([]string{
"A Bird Story",
"Actraiser",
"Actraiser 2",
"Bangai-O Spirits",
"Ciconia When they Cry",
"Civilization IV",
"Civilization VI",
"Clair Obscur - Expedition 33",
"Club House Games (42 All-Time Classics)",
"Final Fantasy Dissidia 012 Duodecim (Sista Fantasin Meningsskiljaktigheter 012 Tolv)",
"Final Fantasy VII Crisis Core",
"Final Fantasy VII Rebirth",
"Final Fantasy VII Remake",
"Final Fantasy VII",
"Joe & Mac",
"Judgment",
"Kingdom Hearts",
"Kingdom Hearts Birth By Sleep",
"Kingdom Hearts II",
"Kingdom Hearts III",
"Lemmings (SNES + Mega Drive)",
"Lemmings 3D",
"Mega Man X4",
"Metal Gear Rising - Revengeance",
"Metal Gear Solid (+Twin Snakes)",
"Metal Gear Solid 2 - Sons of Liberty",
"Metal Gear Solid 3 - Snake Eater",
"Metal Gear Solid 4 - Guns of the Patriots",
"Metroid Prime",
"Octopath Traveler",
"Octopath Traveler 2",
"Ori and the Blind Forest",
"Ori and the Will of the Wisps",
"OutRun 2006 - Coast 2 Coast",
"Paper Mario - The Thousand Year Door",
"Pokémon HeartGold & SoulSilver",
"Resident Evil 2 (Remake)",
"Shenmue",
"Super Mario Galaxy",
"Trails in the Sky FC (Sora no Kiseki FC)",
"Trails in the Sky SC (Sora no Kiseki SC)",
"Turrican II - The Final Fight",
"Umineko no Naku Koro ni",
"Umineko no Naku Koro ni Chiru",
"VVVVVV",
"YS VIII - Lacrimosa of DANA",
"Yakuza 0",
"Ys (Chronicles)",
"Ys - Memories of Celceta",
"Ys - The Ark of Napishtim",
"Ys II (Chronicles)",
"Ys III - Wanderers from Ys (+The Oath in Felghana)",
"Ys IV - The Dawn of Ys",
"Ys Origin",
"Ys Seven",
"Ys X Nordics",
"Call of Duty: Modern Warfare",
"Half-Life 3",
})
// Test cases covering all match types
func TestSearch(t *testing.T) {
data := searchData(t)
tests := []struct {
query string
wantFound string
want []string
}{
// Exact substring matches
{"Metal", "Metal Gear Solid 4 - Guns of the Patriots"},
{"Resident", "Resident Evil 2 (Remake)"},
{"Final", "Final Fantasy VII"},
{"Ys", "Ys III - Wanderers from Ys (+The Oath in Felghana)"},
// Contains matches (on cleaned terms)
{"gotp", "Metal Gear Solid 4 - Guns of the Patriots"},
{"mgs", "Metal Gear Solid 4 - Guns of the Patriots"},
{"kh", "Kingdom Hearts"},
{"mg", "Metal Gear Solid 4 - Guns of the Patriots"},
// Regex matches
{"mgs4go", "Metal Gear Solid 4 - Guns of the Patriots"},
{"re2r", "Resident Evil 2 (Remake)"},
{"ffv", "Final Fantasy VII"},
{"yiwfyt", "Ys III - Wanderers from Ys (+The Oath in Felghana)"},
{"codmw", "Call of Duty: Modern Warfare"},
{"mgs4gop", "Metal Gear Solid 4 - Guns of the Patriots"},
// Exact (case-insensitive) substring
{"resident", []string{"Resident Evil 2 (Remake)"}},
// Ignoring spaces and special characters
{"joeandmac", []string{"Joe & Mac"}},
{"snes+mega", []string{"Lemmings (SNES + Mega Drive)"}},
{"pokemon", []string{"Pokémon HeartGold & SoulSilver"}},
// Letters in order
{"codmw", []string{"Call of Duty: Modern Warfare"}},
{"mgs4gotp", []string{"Metal Gear Solid 4 - Guns of the Patriots"}},
{"re2r", []string{"Resident Evil 2 (Remake)"}},
// No match
{"zzz", nil},
// Regex special characters are plain text
{"(remake)", []string{"Resident Evil 2 (Remake)"}},
{"[", nil},
}
for _, tt := range tests {
t.Run(tt.query, func(t *testing.T) {
results := searchWithData(tt.query, testData)
// Check that the expected item is in the results
found := false
for _, result := range results {
if result == tt.wantFound {
found = true
break
}
}
if !found {
t.Errorf("searchWithData(%q, ...) did not find expected item %q, got %v", tt.query, tt.wantFound, results)
}
assert.Equal(t, tt.want, names(search(tt.query, data)))
})
}
}
// TestSearchOrdering tests that Pass 1 matches come before Pass 2 matches
func TestSearchEmptyReturnsEverything(t *testing.T) {
assert.Equal(t, soundtracks, names(search("", searchData(t))))
}
func TestSearchOrdering(t *testing.T) {
// "MG" matches Pass 1 (exact substring on original)
// "Metal Gear" matches Pass 2 (contains on cleaned) but NOT Pass 1 (no "mg" substring in "metal gear")
testData := searchWithDataHelper([]string{
"Metal Gear",
"MG",
})
results := searchWithData("mg", testData)
// Find indices
indexPass1 := -1
indexPass2 := -1
for i, result := range results {
if result == "MG" {
indexPass1 = i
}
if result == "Metal Gear" {
indexPass2 = i
}
// "MG" is an exact match, "Metal Gear Solid..." only matches by letters in order.
results := names(search("mg", searchData(t)))
require.GreaterOrEqual(t, len(results), 2)
assert.Equal(t, "MG", results[0])
assert.Contains(t, results, "Metal Gear Solid 4 - Guns of the Patriots")
}
// Pass 1 match (MG) should come before Pass 2 match (Metal Gear)
if indexPass1 >= 0 && indexPass2 >= 0 && indexPass1 >= indexPass2 {
t.Errorf("Expected Pass 1 match 'MG' to come before Pass 2 match 'Metal Gear', but got order: %v", results)
}
func TestSearchAlternativeNames(t *testing.T) {
data, err := newCache(soundtracks, map[string][]string{
"Final Fantasy VII": {"FF7", "ファイナルファンタジーVII"},
"Kingdom Hearts": {"KH"},
}).Get(context.Background())
require.NoError(t, err)
assert.Equal(t, []SearchResult{{Name: "Final Fantasy VII", MatchedName: "FF7"}}, search("ff7", data))
assert.Equal(t, []SearchResult{{Name: "Final Fantasy VII", MatchedName: "ファイナルファンタジーVII"}}, search("ファイナル", data))
assert.Equal(t, []SearchResult{{Name: "Final Fantasy VII"}}, search("fantasy", data),
"no matched name when the soundtrack's own name matches")
// An exact match on an alternative name comes before a letters-in-order match on a name.
results := search("kh", data)
require.NotEmpty(t, results)
assert.Equal(t, SearchResult{Name: "Kingdom Hearts", MatchedName: "KH"}, results[0])
}
// Note: The tests above verify:
// 1. clean_term - the term cleaning logic
// 2. search - the complete normal search with exact, contains, and regex matching
// 3. ordering - exact matches come before contains matches
func TestFoundSoundtracksShowsMatchedName(t *testing.T) {
var out strings.Builder
require.NoError(t, FoundSoundtracks([]SearchResult{
{Name: "Final Fantasy VII", MatchedName: "FF7"}, {Name: "Kingdom Hearts"},
}).Render(context.Background(), &out))
assert.Contains(t, out.String(), "Final Fantasy VII")
assert.Contains(t, out.String(), "Also known as FF7")
assert.Equal(t, 1, strings.Count(out.String(), "Also known as"))
}
func TestSearchHandler(t *testing.T) {
handler := NewSearchHandler(newCache(soundtracks))
req := httptest.NewRequest(http.MethodPost, "/find", strings.NewReader(url.Values{"search_term": {"kingdom"}}.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Kingdom Hearts")
assert.NotContains(t, rec.Body.String(), "Final Fantasy")
}
func TestSearchHandlerLoadError(t *testing.T) {
cache := backend.NewSearchCache(func(context.Context) ([]backend.SearchSoundtrack, error) { return nil, errors.New("db down") })
rec := httptest.NewRecorder()
NewSearchHandler(cache).ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/find", nil))
assert.Equal(t, http.StatusInternalServerError, rec.Code)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

-60
View File
@@ -1,60 +0,0 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "./swagger.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>
-75
View File
@@ -1,75 +0,0 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
window.addEventListener('DOMContentLoaded', function () {
run();
});
</script>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIBundle=t():e.SwaggerUIBundle=t()}(this,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=479)}([function(e,t,n){"use strict";e.exports=n(555)},function(e,t,n){e.exports=function(){"use strict";var e=Array.prototype.slice;function t(e,t){t&&(e.prototype=Object.create(t.prototype)),e.prototype.constructor=e}function n(e){return i(e)?e:J(e)}function r(e){return u(e)?e:K(e)}function o(e){return s(e)?e:Y(e)}function a(e){return i(e)&&!c(e)?e:G(e)}function i(e){return!(!e||!e[f])}function u(e){return!(!e||!e[p])}function s(e){return!(!e||!e[h])}function c(e){return u(e)||s(e)}function l(e){return!(!e||!e[d])}t(r,n),t(o,n),t(a,n),n.isIterable=i,n.isKeyed=u,n.isIndexed=s,n.isAssociative=c,n.isOrdered=l,n.Keyed=r,n.Indexed=o,n.Set=a;var f="@@__IMMUTABLE_ITERABLE__@@",p="@@__IMMUTABLE_KEYED__@@",h="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",m="delete",v=5,g=1<<v,y=g-1,b={},w={value:!1},x={value:!1};function E(e){return e.value=!1,e}function _(e){e&&(e.value=!0)}function S(){}function k(e,t){t=t||0;for(var n=Math.max(0,e.length-t),r=new Array(n),o=0;o<n;o++)r[o]=e[o+t];return r}function A(e){return void 0===e.size&&(e.size=e.__iterate(C)),e.size}function O(e,t){if("number"!=typeof t){var n=t>>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?A(e)+t:t}function C(){return!0}function j(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function T(e,t){return P(e,t,0)}function I(e,t){return P(e,t,t)}function P(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var N=0,M=1,R=2,D="function"==typeof Symbol&&Symbol.iterator,L="@@iterator",B=D||L;function F(e){this.next=e}function z(e,t,n,r){var o=0===e?t:1===e?n:[t,n];return r?r.value=o:r={value:o,done:!1},r}function U(){return{value:void 0,done:!0}}function q(e){return!!H(e)}function V(e){return e&&"function"==typeof e.next}function W(e){var t=H(e);return t&&t.call(e)}function H(e){var t=e&&(D&&e[D]||e[L]);if("function"==typeof t)return t}function $(e){return e&&"number"==typeof e.length}function J(e){return null==e?ie():i(e)?e.toSeq():ce(e)}function K(e){return null==e?ie().toKeyedSeq():i(e)?u(e)?e.toSeq():e.fromEntrySeq():ue(e)}function Y(e){return null==e?ie():i(e)?u(e)?e.entrySeq():e.toIndexedSeq():se(e)}function G(e){return(null==e?ie():i(e)?u(e)?e.entrySeq():e:se(e)).toSetSeq()}F.prototype.toString=function(){return"[Iterator]"},F.KEYS=N,F.VALUES=M,F.ENTRIES=R,F.prototype.inspect=F.prototype.toSource=function(){return this.toString()},F.prototype[B]=function(){return this},t(J,n),J.of=function(){return J(arguments)},J.prototype.toSeq=function(){return this},J.prototype.toString=function(){return this.__toString("Seq {","}")},J.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},J.prototype.__iterate=function(e,t){return fe(this,e,t,!0)},J.prototype.__iterator=function(e,t){return pe(this,e,t,!0)},t(K,J),K.prototype.toKeyedSeq=function(){return this},t(Y,J),Y.of=function(){return Y(arguments)},Y.prototype.toIndexedSeq=function(){return this},Y.prototype.toString=function(){return this.__toString("Seq [","]")},Y.prototype.__iterate=function(e,t){return fe(this,e,t,!1)},Y.prototype.__iterator=function(e,t){return pe(this,e,t,!1)},t(G,J),G.of=function(){return G(arguments)},G.prototype.toSetSeq=function(){return this},J.isSeq=ae,J.Keyed=K,J.Set=G,J.Indexed=Y;var Q,Z,X,ee="@@__IMMUTABLE_SEQ__@@";function te(e){this._array=e,this.size=e.length}function ne(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function re(e){this._iterable=e,this.size=e.length||e.size}function oe(e){this._iterator=e,this._iteratorCache=[]}function ae(e){return!(!e||!e[ee])}function ie(){return Q||(Q=new te([]))}function ue(e){var t=Array.isArray(e)?new te(e).fromEntrySeq():V(e)?new oe(e).fromEntrySeq():q(e)?new re(e).fromEntrySeq():"object"==typeof e?new ne(e):void 0;if(Line truncated
//# sourceMappingURL=swagger-ui-bundle.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["webpack://SwaggerUIBundle/webpack/universalModuleDefinition","webpack://SwaggerUIBundle/webpack/bootstrap","webpack://SwaggerUIBundle/./node_modules/react/index.js","webpack://SwaggerUIBundle/./node_modules/immutable/dist/immutable.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/concat.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/defineProperty.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/classCallCheck.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createClass.js","webpack://SwaggerUIBundle/./src/core/utils.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/inherits.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createSuper.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/assertThisInitialized.js","webpack://SwaggerUIBundle/./node_modules/prop-types/index.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/filter.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/slicedToArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/toConsumableArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/object/keys.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/export.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/global.js","webpack://SwaggerUIBundle/./node_modules/reselect/es/defaultMemoize.js","webpack://SwaggerUIBundle/./node_modules/reselect/es/index.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/typeof.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/index-of.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/slice.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/array/is-array.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/for-each.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/object/assign.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/objectSpread2.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/extends.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-uncurry-this.js","webpack://SwaggerUIBundle/./src/core/window.js","webpack://SwaggerUIBundle/./node_modules/react-immutable-proptypes/dist/ImmutablePropTypes.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/json/stringify.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reduce.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/an-object.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/fails.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/path.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/object-is-prototype-of.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-call.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-pure.js","webpack://SwaggerUIBundle/./src/core/plugins/oas3/helpers.jsx","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/well-known-symbol.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/entry-virtual.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-callable.js","webpack://SwaggerUIBundle/./node_modules/lodash/get.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/regenerator/index.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-object.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/a-callable.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/has-own-property.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/define-well-known-symbol.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/iterate.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/bind.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/descriptors.js","webpack://SwaggerUIBundle/./src/core/plugins/spec/actions.js","webpack://SwaggerUIBundle/./node_modules/classnames/index.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-bind-context.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/find.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/hLine truncated
@@ -1,3 +0,0 @@
/*! For license information please see swagger-ui-es-bundle-core.js.LICENSE.txt */
module.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var a=t[r]={i:r,l:!1,exports:{}};return e[r].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(r,a,function(t){return e[t]}.bind(null,a));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=452)}([function(e,t){e.exports=require("react")},function(e,t){e.exports=require("immutable")},function(e,t,n){e.exports=n(492)},function(e,t,n){var r=n(178);e.exports=function(e,t,n){return t in e?r(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){e.exports=n(522)},function(e,t){e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){var r=n(178);function a(e,t){for(var n=0;n<t.length;n++){var a=t[n];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),r(e,a.key,a)}}e.exports=function(e,t,n){return t&&a(e.prototype,t),n&&a(e,n),r(e,"prototype",{writable:!1}),e},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){"use strict";(function(e){n.d(t,"z",(function(){return be})),n.d(t,"i",(function(){return Ee})),n.d(t,"v",(function(){return xe})),n.d(t,"r",(function(){return Se})),n.d(t,"t",(function(){return we})),n.d(t,"s",(function(){return je})),n.d(t,"p",(function(){return Oe})),n.d(t,"u",(function(){return Ce})),n.d(t,"x",(function(){return _e})),n.d(t,"y",(function(){return Ae})),n.d(t,"J",(function(){return ke})),n.d(t,"f",(function(){return Ie})),n.d(t,"n",(function(){return Pe})),n.d(t,"h",(function(){return Te})),n.d(t,"D",(function(){return Re})),n.d(t,"K",(function(){return Me})),n.d(t,"o",(function(){return ze})),n.d(t,"C",(function(){return Ve})),n.d(t,"a",(function(){return Fe})),n.d(t,"H",(function(){return Je})),n.d(t,"b",(function(){return We})),n.d(t,"G",(function(){return He})),n.d(t,"F",(function(){return $e})),n.d(t,"E",(function(){return Ke})),n.d(t,"k",(function(){return Ye})),n.d(t,"d",(function(){return Ge})),n.d(t,"g",(function(){return Ze})),n.d(t,"m",(function(){return Xe})),n.d(t,"l",(function(){return Qe})),n.d(t,"e",(function(){return et})),n.d(t,"I",(function(){return tt})),n.d(t,"w",(function(){return nt})),n.d(t,"A",(function(){return rt})),n.d(t,"B",(function(){return at})),n.d(t,"j",(function(){return ot})),n.d(t,"c",(function(){return it})),n.d(t,"q",(function(){return ct}));var r=n(14),a=n.n(r),o=(n(13),n(19)),i=n.n(o),s=n(56),c=n.n(s),u=n(22),l=n.n(u),p=n(4),f=n.n(p),d=n(83),h=n.n(d),m=n(2),v=n.n(m),g=n(23),y=n.n(g),b=n(12),E=n.n(b),x=n(15),S=n.n(x),w=(n(50),n(31)),j=n.n(w),O=n(24),C=n.n(O),_=n(194),A=n.n(_),k=n(21),I=n.n(k),P=n(74),T=n.n(P),R=(n(32),n(30)),N=n.n(R),M=n(20),q=n.n(M),D=n(55),B=n.n(D),U=n(106),L=n.n(U),z=n(104),V=n.n(z),F=n(1),J=n.n(F),W=n(418),H=n(419),$=n.n(H),K=n(244),Y=n.n(K),G=n(107),Z=n.n(G),X=n(420),Q=n.n(X),ee=n(306),te=n.n(ee),ne=n(102),re=n.n(ne),ae=n(103),oe=n.n(ae),ie=n(133),se=n(28),ce=n(424),ue=n.n(ce),le=n(138),pe=n(117),fe=n.n(pe),de=n(425),he=n.n(de),me=n(73),ve=n.n(me),ge="default",ye=function(e){return J.a.Iterable.isIterable(e)};function be(e){return we(e)?ye(e)?e.toJS():e:{}}function Ee(e){var t,n;if(ye(e))return e;if(e instanceof se.a.File)return e;if(!we(e))return e;if(l()(e))return f()(n=J.a.Seq(e)).call(n,Ee).toList();if(oe()(h()(e))){var r,a=function(e){if(!oe()(h()(e)))return e;var t,n={},r="_**[]",a={},o=c()(h()(e).call(e));try{for(o.s();!(t=o.n()).done;){var i=t.value;if(n[i[0]]||a[i[0]]&&a[i[0]].containsMultiple){var s,u,l,p;if(!a[i[0]])a[i[0]]={containsMultiple:!0,length:1},n[v()(l=v()(p="".concat(i[0])).call(p,r)).call(l,a[i[0]].length)]=n[i[0]],delete n[i[0]];a[i[0]].length+=1,n[v()(s=v()(u="".concat(i[0])).call(u,r)).call(s,a[i[0]].length)]=i[1]}else n[i[0]]=i[1]}}catch(e){o.e(e)}finally{o.f()}return n}(e);return f()(r=J.a.OrderedMap(a)).call(r,Ee)}return f()(t=J.a.OrderedMap(e)).call(t,Ee)}function xe(e){return l()(e)?e:[e]}function Se(e){return"function"==typeof e}function we(e){return!!e&&"object"===i()(e)}function je(e){return"function"==typeof e}function Oe(e){return l()(e)}var Ce=Z.a;function _e(e,t){var n;return j()(n=S()(e)).call(n,(function(n,r){return n[r]=t(e[r],r),n}),{})}function Ae(e,t){var n;return j()(n=S()(e)).call(n,(function(n,r){var a=t(e[r],r);retuLine truncated
//# sourceMappingURL=swagger-ui-es-bundle-core.js.map
@@ -1 +0,0 @@
{"version":3,"sources":["webpack://SwaggerUIBundle/webpack/bootstrap","webpack://SwaggerUIBundle/external \"react\"","webpack://SwaggerUIBundle/external \"immutable\"","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/concat.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/defineProperty.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/classCallCheck.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createClass.js","webpack://SwaggerUIBundle/./src/core/utils.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/inherits.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createSuper.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/assertThisInitialized.js","webpack://SwaggerUIBundle/external \"prop-types\"","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/filter.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/slicedToArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/toConsumableArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/object/keys.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/export.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/global.js","webpack://SwaggerUIBundle/external \"reselect\"","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/typeof.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/index-of.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/slice.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/array/is-array.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/for-each.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/object/assign.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/objectSpread2.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/extends.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-uncurry-this.js","webpack://SwaggerUIBundle/./src/core/window.js","webpack://SwaggerUIBundle/external \"react-immutable-proptypes\"","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/json/stringify.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reduce.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/an-object.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/fails.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/path.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/object-is-prototype-of.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-call.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-pure.js","webpack://SwaggerUIBundle/./src/core/plugins/oas3/helpers.jsx","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/well-known-symbol.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/entry-virtual.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-callable.js","webpack://SwaggerUIBundle/./node_modules/lodash/get.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/regenerator/index.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-object.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/a-callable.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/has-own-property.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/define-well-known-symbol.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/iterate.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/bind.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/descriptors.js","webpack://SwaggerUIBundle/./src/core/plugins/spec/actions.js","webpack://SwaggerUIBundle/external \"classnames\"","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-bind-context.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/find.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createForOfIteratorHelper.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/get-built-in.js","webpack://SwaggerUIBundle/./node_modules/lodash/isArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/ruLine truncated
-3
View File
@@ -1,3 +0,0 @@
/*! For license information please see swagger-ui-es-bundle.js.LICENSE.txt */
module.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=479)}([function(e,t,n){"use strict";e.exports=n(555)},function(e,t,n){e.exports=function(){"use strict";var e=Array.prototype.slice;function t(e,t){t&&(e.prototype=Object.create(t.prototype)),e.prototype.constructor=e}function n(e){return i(e)?e:J(e)}function r(e){return u(e)?e:K(e)}function o(e){return s(e)?e:Y(e)}function a(e){return i(e)&&!c(e)?e:G(e)}function i(e){return!(!e||!e[f])}function u(e){return!(!e||!e[p])}function s(e){return!(!e||!e[h])}function c(e){return u(e)||s(e)}function l(e){return!(!e||!e[d])}t(r,n),t(o,n),t(a,n),n.isIterable=i,n.isKeyed=u,n.isIndexed=s,n.isAssociative=c,n.isOrdered=l,n.Keyed=r,n.Indexed=o,n.Set=a;var f="@@__IMMUTABLE_ITERABLE__@@",p="@@__IMMUTABLE_KEYED__@@",h="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",m="delete",v=5,g=1<<v,y=g-1,b={},w={value:!1},x={value:!1};function E(e){return e.value=!1,e}function _(e){e&&(e.value=!0)}function S(){}function k(e,t){t=t||0;for(var n=Math.max(0,e.length-t),r=new Array(n),o=0;o<n;o++)r[o]=e[o+t];return r}function A(e){return void 0===e.size&&(e.size=e.__iterate(C)),e.size}function O(e,t){if("number"!=typeof t){var n=t>>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?A(e)+t:t}function C(){return!0}function j(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function T(e,t){return P(e,t,0)}function I(e,t){return P(e,t,t)}function P(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var N=0,M=1,R=2,D="function"==typeof Symbol&&Symbol.iterator,L="@@iterator",B=D||L;function F(e){this.next=e}function z(e,t,n,r){var o=0===e?t:1===e?n:[t,n];return r?r.value=o:r={value:o,done:!1},r}function U(){return{value:void 0,done:!0}}function q(e){return!!H(e)}function V(e){return e&&"function"==typeof e.next}function W(e){var t=H(e);return t&&t.call(e)}function H(e){var t=e&&(D&&e[D]||e[L]);if("function"==typeof t)return t}function $(e){return e&&"number"==typeof e.length}function J(e){return null==e?ie():i(e)?e.toSeq():ce(e)}function K(e){return null==e?ie().toKeyedSeq():i(e)?u(e)?e.toSeq():e.fromEntrySeq():ue(e)}function Y(e){return null==e?ie():i(e)?u(e)?e.entrySeq():e.toIndexedSeq():se(e)}function G(e){return(null==e?ie():i(e)?u(e)?e.entrySeq():e:se(e)).toSetSeq()}F.prototype.toString=function(){return"[Iterator]"},F.KEYS=N,F.VALUES=M,F.ENTRIES=R,F.prototype.inspect=F.prototype.toSource=function(){return this.toString()},F.prototype[B]=function(){return this},t(J,n),J.of=function(){return J(arguments)},J.prototype.toSeq=function(){return this},J.prototype.toString=function(){return this.__toString("Seq {","}")},J.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},J.prototype.__iterate=function(e,t){return fe(this,e,t,!0)},J.prototype.__iterator=function(e,t){return pe(this,e,t,!0)},t(K,J),K.prototype.toKeyedSeq=function(){return this},t(Y,J),Y.of=function(){return Y(arguments)},Y.prototype.toIndexedSeq=function(){return this},Y.prototype.toString=function(){return this.__toString("Seq [","]")},Y.prototype.__iterate=function(e,t){return fe(this,e,t,!1)},Y.prototype.__iterator=function(e,t){return pe(this,e,t,!1)},t(G,J),G.of=function(){return G(arguments)},G.prototype.toSetSeq=function(){return this},J.isSeq=ae,J.Keyed=K,J.Set=G,J.Indexed=Y;var Q,Z,X,ee="@@__IMMUTABLE_SEQ__@@";function te(e){this._array=e,this.size=e.length}function ne(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function re(e){this._iterable=e,this.size=e.length||e.size}function oe(e){this._iterator=e,this._iteratorCache=[]}function ae(e){return!(!e||!e[ee])}function ie(){return Q||(Q=new te([]))}function ue(e){var t=Array.isArray(e)?new te(e).fromEntrySeq():V(e)?new oe(e).fromEntrySeq():q(e)?new re(e).fromEntrySeq():"object"==typeof e?new ne(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function se(e){var t=le(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return tLine truncated
//# sourceMappingURL=swagger-ui-es-bundle.js.map
@@ -1 +0,0 @@
{"version":3,"sources":["webpack://SwaggerUIBundle/webpack/bootstrap","webpack://SwaggerUIBundle/./node_modules/react/index.js","webpack://SwaggerUIBundle/./node_modules/immutable/dist/immutable.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/concat.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/defineProperty.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/classCallCheck.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createClass.js","webpack://SwaggerUIBundle/./src/core/utils.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/inherits.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createSuper.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/assertThisInitialized.js","webpack://SwaggerUIBundle/./node_modules/prop-types/index.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/filter.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/slicedToArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/toConsumableArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/object/keys.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/export.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/global.js","webpack://SwaggerUIBundle/./node_modules/reselect/es/defaultMemoize.js","webpack://SwaggerUIBundle/./node_modules/reselect/es/index.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/typeof.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/index-of.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/slice.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/array/is-array.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/for-each.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/object/assign.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/objectSpread2.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/extends.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-uncurry-this.js","webpack://SwaggerUIBundle/./src/core/window.js","webpack://SwaggerUIBundle/./node_modules/react-immutable-proptypes/dist/ImmutablePropTypes.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/json/stringify.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reduce.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/an-object.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/fails.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/path.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/object-is-prototype-of.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-call.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-pure.js","webpack://SwaggerUIBundle/./src/core/plugins/oas3/helpers.jsx","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/well-known-symbol.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/entry-virtual.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-callable.js","webpack://SwaggerUIBundle/./node_modules/lodash/get.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/regenerator/index.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/is-object.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/a-callable.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/has-own-property.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/define-well-known-symbol.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/iterate.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/bind.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/descriptors.js","webpack://SwaggerUIBundle/./src/core/plugins/spec/actions.js","webpack://SwaggerUIBundle/./node_modules/classnames/index.js","webpack://SwaggerUIBundle/./node_modules/core-js-pure/internals/function-bind-context.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/find.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs3/helpers/createForOfIteratorHelper.js","webpack://SwaggerUIBundlLine truncated
@@ -1,3 +0,0 @@
/*! For license information please see swagger-ui-standalone-preset.js.LICENSE.txt */
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SwaggerUIStandalonePreset=e():t.SwaggerUIStandalonePreset=e()}(this,(function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(r,i,function(e){return t[e]}.bind(null,i));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="/dist",n(n.s=247)}([function(t,e,n){"use strict";var r=n(1),i=n(72),o=n(6),u=n(15),s=n(74).f,a=n(256),c=n(16),f=n(25),l=n(51),p=n(20),h=function(t){var e=function(n,r,o){if(this instanceof e){switch(arguments.length){case 0:return new t;case 1:return new t(n);case 2:return new t(n,r)}return new t(n,r,o)}return i(t,this,arguments)};return e.prototype=t.prototype,e};t.exports=function(t,e){var n,i,d,y,v,g,w,M,_=t.target,L=t.global,x=t.stat,m=t.proto,j=L?r:x?r[_]:(r[_]||{}).prototype,b=L?c:c[_]||l(c,_,{})[_],N=b.prototype;for(d in e)n=!a(L?d:_+(x?".":"#")+d,t.forced)&&j&&p(j,d),v=b[d],n&&(g=t.noTargetGet?(M=s(j,d))&&M.value:j[d]),y=n&&g?g:e[d],n&&typeof v==typeof y||(w=t.bind&&n?f(y,r):t.wrap&&n?h(y):m&&u(y)?o(y):y,(t.sham||y&&y.sham||v&&v.sham)&&l(w,"sham",!0),l(b,d,w),m&&(p(c,i=_+"Prototype")||l(c,i,{}),l(c[i],d,y),t.real&&N&&!N[d]&&l(N,d,y)))}},function(t,e,n){(function(e){var n=function(t){return t&&t.Math==Math&&t};t.exports=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof e&&e)||function(){return this}()||Function("return this")()}).call(this,n(33))},function(t,e,n){"use strict";t.exports=n(320)},function(t,e,n){t.exports=function(){"use strict";var t=Array.prototype.slice;function e(t,e){e&&(t.prototype=Object.create(e.prototype)),t.prototype.constructor=t}function n(t){return u(t)?t:J(t)}function r(t){return s(t)?t:Z(t)}function i(t){return a(t)?t:X(t)}function o(t){return u(t)&&!c(t)?t:V(t)}function u(t){return!(!t||!t[l])}function s(t){return!(!t||!t[p])}function a(t){return!(!t||!t[h])}function c(t){return s(t)||a(t)}function f(t){return!(!t||!t[d])}e(r,n),e(i,n),e(o,n),n.isIterable=u,n.isKeyed=s,n.isIndexed=a,n.isAssociative=c,n.isOrdered=f,n.Keyed=r,n.Indexed=i,n.Set=o;var l="@@__IMMUTABLE_ITERABLE__@@",p="@@__IMMUTABLE_KEYED__@@",h="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",y="delete",v=5,g=1<<v,w=g-1,M={},_={value:!1},L={value:!1};function x(t){return t.value=!1,t}function m(t){t&&(t.value=!0)}function j(){}function b(t,e){e=e||0;for(var n=Math.max(0,t.length-e),r=new Array(n),i=0;i<n;i++)r[i]=t[i+e];return r}function N(t){return void 0===t.size&&(t.size=t.__iterate(D)),t.size}function S(t,e){if("number"!=typeof e){var n=e>>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?N(t)+e:e}function D(){return!0}function I(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function A(t,e){return C(t,e,0)}function T(t,e){return C(t,e,e)}function C(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}var E=0,O=1,z=2,k="function"==typeof Symbol&&Symbol.iterator,U="@@iterator",Y=k||U;function R(t){this.next=t}function P(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function Q(){return{value:void 0,done:!0}}function B(t){return!!W(t)}function F(t){return t&&"function"==typeof t.next}function G(t){var e=W(t);return e&&e.call(t)}function W(t){var e=t&&(k&&t[k]||t[U]);if("function"==typeof e)return e}function q(t){return t&&"number"==typeof t.length}function J(t){return null==t?ut():u(t)?t.toSeq():ct(t)}function Z(t){return null==t?ut().toKeyedSeq():u(t)?s(t)?t.toSeq():t.fromEntrySeq():st(t)}function X(t){return null==t?ut():u(t)?s(t)?t.entrySeq():t.toIndexedSeq():at(t)}function V(t){return(null==t?ut():u(t)?s(t)?t.entrySeq():t:at(t)).toSetSeq()}R.prototype.toString=function(){return"[Iterator]"},R.KEYS=E,R.VALUES=O,R.ENTRIES=z,R.prototype.inspect=R.prototype.toSource=function(){return this.toString()},R.prototype[Y]=function(){return this},e(J,n),J.of=function(){return J(arguments)},J.prototype.toSeq=function(){return this},J.prototype.toString=function(){return this.__toString("Seq {","}")},J.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().Line truncated
//# sourceMappingURL=swagger-ui-standalone-preset.js.map
@@ -1 +0,0 @@
{"version":3,"sources":["webpack://SwaggerUIStandalonePreset/webpack/universalModuleDefinition","webpack://SwaggerUIStandalonePreset/webpack/bootstrap","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/export.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/global.js","webpack://SwaggerUIStandalonePreset/./node_modules/react/index.js","webpack://SwaggerUIStandalonePreset/./node_modules/immutable/dist/immutable.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/an-object.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/fails.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/function-uncurry-this.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/concat.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/core-js-stable/array/is-array.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/object-is-prototype-of.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/is-pure.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/function-call.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/define-well-known-symbol.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/well-known-symbol.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/entry-virtual.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/is-callable.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/path.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/a-callable.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/descriptors.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/is-object.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/has-own-property.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/iterate.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/for-each.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js","webpack://SwaggerUIStandalonePreset/./src/core/utils.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/function-bind-context.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/defineProperty.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/to-object.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/object-define-property.js","webpack://SwaggerUIStandalonePreset/./node_modules/inherits/inherits_browser.js","webpack://SwaggerUIStandalonePreset/./node_modules/safe-buffer/index.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/typeof.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/get-built-in.js","webpack://SwaggerUIStandalonePreset/(webpack)/buildin/global.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/length-of-array-like.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/isArray.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/get-map-iterator.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/classCallCheck.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/createClass.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/inherits.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/createSuper.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/to-indexed-object.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/array-iteration.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/_root.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/assertThisInitialized.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/core-js-stable/instance/filter.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/core-js-stable/object/keys.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/classof.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/memoize.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs3/helpers/toConsumableArray.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/classof-raw.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js-pure/internals/create-non-enumerable-property.js","webpack://SwaggerUIStandalonePresetLine truncated
-4
View File
@@ -1,4 +0,0 @@
.swagger-ui{color:#3b4151;
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */font-family:sans-serif}.swagger-ui html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.15}.swagger-ui body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui footer,.swagger-ui header,.swagger-ui nav,.swagger-ui section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;overflow:visible}.swagger-ui pre{font-family:monospace,monospace;font-size:1em}.swagger-ui a{-webkit-text-decoration-skip:objects;background-color:transparent}.swagger-ui abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.swagger-ui b,.swagger-ui strong{font-weight:inherit;font-weight:bolder}.swagger-ui code,.swagger-ui kbd,.swagger-ui samp{font-family:monospace,monospace;font-size:1em}.swagger-ui dfn{font-style:italic}.swagger-ui mark{background-color:#ff0;color:#000}.swagger-ui small{font-size:80%}.swagger-ui sub,.swagger-ui sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.swagger-ui sub{bottom:-.25em}.swagger-ui sup{top:-.5em}.swagger-ui audio,.swagger-ui video{display:inline-block}.swagger-ui audio:not([controls]){display:none;height:0}.swagger-ui img{border-style:none}.swagger-ui svg:not(:root){overflow:hidden}.swagger-ui button,.swagger-ui input,.swagger-ui optgroup,.swagger-ui select,.swagger-ui textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}.swagger-ui button,.swagger-ui input{overflow:visible}.swagger-ui button,.swagger-ui select{text-transform:none}.swagger-ui [type=reset],.swagger-ui [type=submit],.swagger-ui button,.swagger-ui html [type=button]{-webkit-appearance:button}.swagger-ui [type=button]::-moz-focus-inner,.swagger-ui [type=reset]::-moz-focus-inner,.swagger-ui [type=submit]::-moz-focus-inner,.swagger-ui button::-moz-focus-inner{border-style:none;padding:0}.swagger-ui [type=button]:-moz-focusring,.swagger-ui [type=reset]:-moz-focusring,.swagger-ui [type=submit]:-moz-focusring,.swagger-ui button:-moz-focusring{outline:1px dotted ButtonText}.swagger-ui fieldset{padding:.35em .75em .625em}.swagger-ui legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}.swagger-ui progress{display:inline-block;vertical-align:baseline}.swagger-ui textarea{overflow:auto}.swagger-ui [type=checkbox],.swagger-ui [type=radio]{box-sizing:border-box;padding:0}.swagger-ui [type=number]::-webkit-inner-spin-button,.swagger-ui [type=number]::-webkit-outer-spin-button{height:auto}.swagger-ui [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.swagger-ui [type=search]::-webkit-search-cancel-button,.swagger-ui [type=search]::-webkit-search-decoration{-webkit-appearance:none}.swagger-ui ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.swagger-ui details,.swagger-ui menu{display:block}.swagger-ui summary{display:list-item}.swagger-ui canvas{display:inline-block}.swagger-ui [hidden],.swagger-ui template{display:none}.swagger-ui .debug *{outline:1px solid gold}.swagger-ui .debug-white *{outline:1px solid #fff}.swagger-ui .debug-black *{outline:1px solid #000}.swagger-ui .debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) repeat 0 0}.swagger-ui .debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eLine truncated
/*# sourceMappingURL=swagger-ui.css.map*/
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["webpack:///./src/style/main.scss","webpack:///./src/style/_type.scss","webpack:///./src/style/_variables.scss","webpack:///./node_modules/tachyons-sass/scss/_normalize.scss","webpack:///./node_modules/tachyons-sass/scss/_debug-children.scss","webpack:///./node_modules/tachyons-sass/scss/_debug-grid.scss","webpack:///./node_modules/tachyons-sass/scss/_box-sizing.scss","webpack:///./node_modules/tachyons-sass/scss/_aspect-ratios.scss","webpack:///./node_modules/tachyons-sass/scss/_images.scss","webpack:///./node_modules/tachyons-sass/scss/_background-size.scss","webpack:///./node_modules/tachyons-sass/scss/_background-position.scss","webpack:///./node_modules/tachyons-sass/scss/_outlines.scss","webpack:///./node_modules/tachyons-sass/scss/_borders.scss","webpack:///./node_modules/tachyons-sass/scss/_border-colors.scss","webpack:///./node_modules/tachyons-sass/scss/_border-radius.scss","webpack:///./node_modules/tachyons-sass/scss/_border-style.scss","webpack:///./node_modules/tachyons-sass/scss/_border-widths.scss","webpack:///./node_modules/tachyons-sass/scss/_box-shadow.scss","webpack:///./node_modules/tachyons-sass/scss/_code.scss","webpack:///./node_modules/tachyons-sass/scss/_coordinates.scss","webpack:///./node_modules/tachyons-sass/scss/_clears.scss","webpack:///./node_modules/tachyons-sass/scss/_flexbox.scss","webpack:///./node_modules/tachyons-sass/scss/_display.scss","webpack:///./node_modules/tachyons-sass/scss/_floats.scss","webpack:///./node_modules/tachyons-sass/scss/_font-family.scss","webpack:///./node_modules/tachyons-sass/scss/_font-style.scss","webpack:///./node_modules/tachyons-sass/scss/_font-weight.scss","webpack:///./node_modules/tachyons-sass/scss/_forms.scss","webpack:///./node_modules/tachyons-sass/scss/_heights.scss","webpack:///./node_modules/tachyons-sass/scss/_letter-spacing.scss","webpack:///./node_modules/tachyons-sass/scss/_line-height.scss","webpack:///./node_modules/tachyons-sass/scss/_links.scss","webpack:///./node_modules/tachyons-sass/scss/_lists.scss","webpack:///./node_modules/tachyons-sass/scss/_max-widths.scss","webpack:///./node_modules/tachyons-sass/scss/_widths.scss","webpack:///./node_modules/tachyons-sass/scss/_overflow.scss","webpack:///./node_modules/tachyons-sass/scss/_position.scss","webpack:///./node_modules/tachyons-sass/scss/_opacity.scss","webpack:///./node_modules/tachyons-sass/scss/_rotations.scss","webpack:///./node_modules/tachyons-sass/scss/_skins.scss","webpack:///./node_modules/tachyons-sass/scss/_skins-pseudo.scss","webpack:///./node_modules/tachyons-sass/scss/_spacing.scss","webpack:///./node_modules/tachyons-sass/scss/_variables.scss","webpack:///./node_modules/tachyons-sass/scss/_negative-margins.scss","webpack:///./node_modules/tachyons-sass/scss/_tables.scss","webpack:///./node_modules/tachyons-sass/scss/_text-decoration.scss","webpack:///./node_modules/tachyons-sass/scss/_text-align.scss","webpack:///./node_modules/tachyons-sass/scss/_text-transform.scss","webpack:///./node_modules/tachyons-sass/scss/_type-scale.scss","webpack:///./node_modules/tachyons-sass/scss/_typography.scss","webpack:///./node_modules/tachyons-sass/scss/_utilities.scss","webpack:///./node_modules/tachyons-sass/scss/_visibility.scss","webpack:///./node_modules/tachyons-sass/scss/_white-space.scss","webpack:///./node_modules/tachyons-sass/scss/_vertical-align.scss","webpack:///./node_modules/tachyons-sass/scss/_hovers.scss","webpack:///./node_modules/tachyons-sass/scss/_z-index.scss","webpack:///./node_modules/tachyons-sass/scss/_nested.scss","webpack:///./src/style/_layout.scss","webpack:///./src/style/_buttons.scss","webpack:///./src/style/_mixins.scss","webpack:///./src/style/_form.scss","webpack:///./src/style/_modal.scss","webpack:///./src/style/_models.scss","webpack:///./src/style/_servers.scss","webpack:///./src/style/_table.scss","webpack:///./src/style/_topbar.scss","webpack:///./src/style/_information.scss","webpack:///./src/style/_authorize.scss","webpack:///./src/style/_errors.scss","webpack:///./src/style/_split-pane-mode.scss","webpack:///./src/style/_markdown.scss"],"names":[],"mappings":"AAAA,YCII,aCYU;ECTd,4EFLI,sBDqhCJ,CGrgCA,iBAEE,0BACA,8BAFA,gBH4gCF,CGhgCA,iBACE,QHkgCF,CG3/BA,gHAME,aH6/BF,CGr/BA,eACE,cACA,cHu/BF,CG5+BA,2DAGE,aH++BF,CGx+BA,mBACE,eH0+BF,CGl+BA,eACE,uBACA,SACA,gBHu+BF,CG/9BA,gBACE,gCACA,aHm+BF,CGx9BA,cAEE,qCADA,4BH69BF,CGp9BA,wBACE,mBACA,0BACA,yEHy9BF,CGl9BA,iCAEE,oBASA,kBH28BF,CGn8BA,kDAGE,gCACA,aH28BF,CGp8BA,gBACE,iBHs8BF,CG/7BA,iBACE,sBACA,UHi8BF,CG17BA,kBACE,aH47BF,CGp7BA,gCAEE,cACA,cACA,kBACA,uBHs7BF,CGn7BA,gBACE,aHq7BF,CGl7BA,gBACE,SHo7BF,CG16BA,oCAEE,oBH46BF,CGr6BA,kCACE,aACA,QHu6BF,CGh6BA,gBACE,iBHk6BF,CG35BA,2BACE,eH65BF,CGl5BA,kGAKE,uBACA,eACA,iBACA,QHw5BF,CGh5BA,qCAEE,gBHm5BF,CG34BA,sCAEE,mBH84BF,CGr4BA,qGAIE,yBHw4BF,CGj4BA,wKAIE,kBACA,SHm4BF,CG53BA,4JAIE,6BH83BF,CGv3BA,qBACE,0BHy3BF,CG/2BA,mBACE,sBACA,cACA,cACA,eACA,UACA,kBHu3BF,CG/2BA,qBACE,qBACA,uBHm3BF,CG52BA,qBACE,aH82BF,CGt2BA,qDAEE,sBACA,SH02BLine truncated
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff. Show more