mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-06-22 04:11:32 +10:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6394331492 |
@@ -3,5 +3,3 @@ DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop"
|
||||
GIANT_BOMB_API_KEY=""
|
||||
|
||||
EXTERNAL_URL="http://localhost:3000"
|
||||
|
||||
NUXT_PORT=4000
|
||||
|
||||
@@ -21,20 +21,17 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: yarn install --immutable --network-timeout 1000000
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
run: yarn typecheck
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -45,17 +42,14 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: yarn install --immutable --network-timeout 1000000
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
run: yarn lint
|
||||
|
||||
@@ -8,20 +8,13 @@ on:
|
||||
schedule:
|
||||
- cron: "0 2 * * *" # run at 2 AM UTC
|
||||
|
||||
env:
|
||||
REGISTRY_IMAGE: ghcr.io/drop-oss/drop
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
web:
|
||||
name: Push website Docker image to registry
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
@@ -35,30 +28,6 @@ jobs:
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Determine final version
|
||||
id: get_final_ver
|
||||
run: |
|
||||
@@ -77,58 +46,22 @@ jobs:
|
||||
echo "Drop's release tag will be: $FINAL_VER"
|
||||
echo "final_ver=$FINAL_VER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ env.REGISTRY_IMAGE }}
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
build-args: |
|
||||
BUILD_DROP_VERSION=${{ steps.get_final_ver.outputs.final_ver }}
|
||||
BUILD_GIT_REF=${{ github.sha }}
|
||||
buildkitd-flags: --debug
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -147,12 +80,33 @@ jobs:
|
||||
# set latest tag for stable releases
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: cache-mount
|
||||
key: cache-mount-${{ hashFiles('Dockerfile') }}
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
- name: Restore Docker cache mounts
|
||||
uses: reproducible-containers/buildkit-cache-dance@v3
|
||||
with:
|
||||
builder: ${{ steps.setup-buildx.outputs.name }}
|
||||
cache-dir: cache-mount
|
||||
dockerfile: Dockerfile
|
||||
skip-extraction: ${{ steps.cache.outputs.cache-hit }}
|
||||
|
||||
- name: Build and push image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BUILD_DROP_VERSION=${{ steps.get_final_ver.outputs.final_ver }}
|
||||
|
||||
+1
-3
@@ -34,6 +34,4 @@ deploy-template/*
|
||||
|
||||
# generated prisma client
|
||||
/prisma/client
|
||||
/prisma/validate
|
||||
|
||||
/server/internal/proto
|
||||
/prisma/validate
|
||||
@@ -0,0 +1,54 @@
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
services:
|
||||
- docker:24.0.5-dind
|
||||
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:latest
|
||||
variables:
|
||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA
|
||||
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest
|
||||
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
|
||||
script:
|
||||
- docker build -t $IMAGE_NAME .
|
||||
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
|
||||
- docker push $IMAGE_NAME
|
||||
- docker push $LATEST_IMAGE_NAME
|
||||
- |
|
||||
if [ $CI_COMMIT_TAG ]; then
|
||||
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
|
||||
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
docker push $PUBLISH_IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
fi
|
||||
|
||||
build-arm64:
|
||||
stage: build
|
||||
image: arm64v8/docker:latest
|
||||
tags:
|
||||
- aarch64
|
||||
variables:
|
||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA-arm64
|
||||
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest-arm64
|
||||
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-arm64
|
||||
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest-arm64
|
||||
script:
|
||||
- docker build -t $IMAGE_NAME . --platform=linux/arm64
|
||||
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
|
||||
- docker push $IMAGE_NAME
|
||||
- docker push $LATEST_IMAGE_NAME
|
||||
- |
|
||||
if [ $CI_COMMIT_TAG ]; then
|
||||
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
|
||||
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
docker push $PUBLISH_IMAGE_NAME
|
||||
docker push $PUBLISH_LATEST_IMAGE_NAME
|
||||
fi
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "drop-base"]
|
||||
path = drop-base
|
||||
url = https://github.com/Drop-OSS/drop-base.git
|
||||
[submodule "torrential"]
|
||||
path = torrential
|
||||
url = https://github.com/Drop-OSS/torrential.git
|
||||
|
||||
+1
-7
@@ -1,7 +1 @@
|
||||
drop-base/
|
||||
# file is fully managed by pnpm, no reason to break it
|
||||
pnpm-lock.yaml
|
||||
|
||||
/torrential/
|
||||
.data/**
|
||||
**/.data/**
|
||||
drop-base/
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"jsonRecursiveSort": true,
|
||||
"jsonSortOrder": "{\"/.*/\": \"lexical\"}",
|
||||
"plugins": ["prettier-plugin-sort-json"]
|
||||
}
|
||||
Vendored
+28
-26
@@ -1,35 +1,37 @@
|
||||
{
|
||||
"spellchecker.ignoreWordsList": ["mTLS", "Wireguard"],
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 5432,
|
||||
"driver": "PostgreSQL",
|
||||
"name": "drop",
|
||||
"database": "drop",
|
||||
"username": "drop",
|
||||
"password": "drop"
|
||||
}
|
||||
],
|
||||
// allow autocomplete for ArkType expressions like "string | num"
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
"i18n-ally.extract.autoDetect": true,
|
||||
"i18n-ally.extract.ignored": ["string >= 14", "string.alphanumeric >= 5"],
|
||||
"i18n-ally.extract.ignoredByFiles": {
|
||||
"components/NewsArticleCreateButton.vue": ["[", "`", "Enter"],
|
||||
"pages/admin/library/sources/index.vue": ["Filesystem"],
|
||||
"server/api/v1/auth/signin/simple.post.ts": ["boolean | undefined"]
|
||||
},
|
||||
"i18n-ally.keepFulfilled": true,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": ["i18n", "i18n/locales"],
|
||||
// prioritize ArkType's "type" for autoimports
|
||||
"typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"],
|
||||
// i18n Ally settings
|
||||
"i18n-ally.sortKeys": true,
|
||||
"prisma.pinToPrisma6": false,
|
||||
"spellchecker.ignoreWordsList": ["mTLS", "Wireguard"],
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"database": "drop",
|
||||
"driver": "PostgreSQL",
|
||||
"name": "drop",
|
||||
"password": "drop",
|
||||
"port": 5432,
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"username": "drop"
|
||||
}
|
||||
"i18n-ally.keepFulfilled": true,
|
||||
"i18n-ally.extract.autoDetect": true,
|
||||
"i18n-ally.localesPaths": ["i18n", "i18n/locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.extract.ignored": [
|
||||
"string >= 14",
|
||||
"string.alphanumeric >= 5",
|
||||
"/api/v1/admin/import/version/preload?id=${encodeURIComponent(\n gameId,\n )}&version=${encodeURIComponent(version)}"
|
||||
],
|
||||
"typescript.experimental.useTsgo": false,
|
||||
// prioritize ArkType's "type" for autoimports
|
||||
"typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"]
|
||||
"i18n-ally.extract.ignoredByFiles": {
|
||||
"pages/admin/library/sources/index.vue": ["Filesystem"],
|
||||
"components/NewsArticleCreateButton.vue": ["[", "`", "Enter"],
|
||||
"server/api/v1/auth/signin/simple.post.ts": ["boolean | undefined"]
|
||||
}
|
||||
}
|
||||
|
||||
+241
-2
@@ -1,3 +1,242 @@
|
||||
# Contributing
|
||||
# CONTRIBUTING GUIDELINES
|
||||
|
||||
Check out our contributing guidelines on our developer docs: [https://developer.droposs.org/contributing](https://developer.droposs.org/contributing).
|
||||
Drop is a community-driven project. Contribution is welcome, encouraged, and appreciated.
|
||||
It is also essential for the development of the project.
|
||||
|
||||
First, please take a moment to review our [code of conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
These guidelines are an attempt at better addressing pending
|
||||
issues and pull requests. Please read them closely.
|
||||
|
||||
Foremost, be so kind as to [search](#use-the-search-luke). This ensures any contribution
|
||||
you would make is not already covered.
|
||||
|
||||
<!-- TOC updateonsave:true depthfrom:2 -->
|
||||
|
||||
- [Reporting Issues](#reporting-issues)
|
||||
- [You have a problem](#you-have-a-problem)
|
||||
- [You have a suggestion](#you-have-a-suggestion)
|
||||
- [Submitting Pull Requests](#submitting-pull-requests)
|
||||
- [Getting started](#getting-started)
|
||||
- [You have a solution](#you-have-a-solution)
|
||||
- [You have an addition](#you-have-an-addition)
|
||||
- [Use the Search, Luke](#use-the-search-luke)
|
||||
- [Commit Guidelines](#commit-guidelines)
|
||||
- [Format](#format)
|
||||
- [Style](#style)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
### You have a problem
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any open issue already covering
|
||||
your problem.
|
||||
|
||||
If you find one, comment on it, so we know more people are experiencing it.
|
||||
|
||||
<!--
|
||||
TODO: Add Troubleshooting
|
||||
If not, look at the [Troubleshooting](https://github.com/Drop-OSS/docs/Troubleshooting)
|
||||
page for instructions on how to gather data to better debug your problem.
|
||||
-->
|
||||
|
||||
If you cannot find an existing issue, you can go ahead and create an issue with as much
|
||||
detail as you can provide.
|
||||
It should include the data gathered as indicated above, along with the following:
|
||||
|
||||
1. How to reproduce the problem
|
||||
2. What the correct behavior should be
|
||||
3. What the actual behavior is
|
||||
|
||||
Please copy to anyone relevant (e.g. plugin maintainers) by mentioning their GitHub handle
|
||||
(starting with `@`) in your message.
|
||||
|
||||
We will do our very best to help you.
|
||||
|
||||
### You have a suggestion
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any open issue already covering
|
||||
your suggestion.
|
||||
|
||||
If you find one, comment on it, so we know more people are supporting it.
|
||||
|
||||
If not, you can go ahead and create an issue. Please copy to anyone relevant (e.g. plugin
|
||||
maintainers) by mentioning their GitHub handle (starting with `@`) in your message.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
### Getting started
|
||||
|
||||
You should be familiar with the basics of
|
||||
[contributing on GitHub](https://help.github.com/articles/using-pull-requests)
|
||||
|
||||
<!--and have a fork
|
||||
[properly set up](https://github.com/drop/docs/Contribution-Technical-Practices).
|
||||
-->
|
||||
|
||||
You MUST always create PRs with _a dedicated branch_ based on the latest upstream tree.
|
||||
|
||||
If you create your own PR, please make sure you do it right. Also be so kind as to reference
|
||||
any issue that would be solved in the PR description body,
|
||||
[for instance](https://help.github.com/articles/closing-issues-via-commit-messages/)
|
||||
_"Fixes #XXXX"_ for issue number XXXX.
|
||||
|
||||
### You have a solution
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any open issue already covering
|
||||
your [problem](#you-have-a-problem), and any pending/merged/rejected PR covering your solution.
|
||||
|
||||
If the solution is already reported, try it out and +1 the pull request if the
|
||||
solution works ok. On the other hand, if you think your solution is better, post
|
||||
it with reference to the other one so we can have both solutions to compare.
|
||||
|
||||
If not, then go ahead and submit a PR. Please copy to anyone relevant (e.g. plugin
|
||||
maintainers) by mentioning their GitHub handle (starting with `@`) in your message.
|
||||
|
||||
### You have an addition
|
||||
|
||||
We are absolutely accepting more contributions or features to drop, but please, make sure
|
||||
that it is reasonable. Contributions that only cover a very small niche are likely to not
|
||||
be added.
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any pending, merged or rejected Pull Requests
|
||||
covering or related to what you want to add.
|
||||
|
||||
If you find one, try it out and work with the author on a common solution.
|
||||
|
||||
If not, then go ahead and submit a PR. Please copy to anyone relevant (e.g. plugin
|
||||
maintainers) by mentioning their GitHub handle (starting with `@`) in your message.
|
||||
|
||||
For any extensive change, such as API changes, you will have to find testers to +1 your PR.
|
||||
|
||||
---
|
||||
|
||||
## Use the Search, Luke
|
||||
|
||||
_May the Force (of past experiences) be with you_
|
||||
|
||||
GitHub offers [many search features](https://help.github.com/articles/searching-github/)
|
||||
to help you check whether a similar contribution to yours already exists. Please search
|
||||
before making any contribution, it avoids duplicates and eases maintenance. Trust me,
|
||||
that works 90% of the time.
|
||||
|
||||
You can also take a look at the [FAQ](https://github.com/Drop-OSS/docs/wiki/FAQ)
|
||||
to be sure your contribution has not already come up.
|
||||
|
||||
If all fails, your thing has probably not been reported yet, so you can go ahead
|
||||
and [create an issue](#reporting-issues) or [submit a PR](#submitting-pull-requests).
|
||||
|
||||
---
|
||||
|
||||
## Translation
|
||||
|
||||
If you want to help translate Drop, we would love to have your help! You can do so on our weblate instance. Please make sure to read the [message format syntax](https://vue-i18n.intlify.dev/guide/essentials/syntax.html) page before starting. Failure to do so may result in your translations causing errors in Drop.
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
Drop uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||
specification. The automatic changelog tool uses these to automatically generate
|
||||
a changelog based on the commit messages. Here's a guide to writing a commit message
|
||||
to allow this:
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
type(scope)!: subject
|
||||
```
|
||||
|
||||
- `type`: the type of the commit is one of the following:
|
||||
- `feat`: new features.
|
||||
- `fix`: bug fixes.
|
||||
- `docs`: documentation changes.
|
||||
- `refactor`: refactor of a particular code section without introducing
|
||||
new features or bug fixes.
|
||||
- `style`: code style improvements.
|
||||
- `perf`: performance improvements.
|
||||
- `test`: changes to the test suite.
|
||||
- `ci`: changes to the CI system.
|
||||
- `build`: changes to the build system.
|
||||
- `chore`: for other changes that don't match previous types. This doesn't appear
|
||||
in the changelog.
|
||||
|
||||
- `scope`: section of the codebase that the commit makes changes to. If it makes changes to
|
||||
many sections, or if no section in particular is modified, leave blank without the parentheses.
|
||||
Examples:
|
||||
- Commit that changes the `git` plugin:
|
||||
|
||||
```
|
||||
feat(git): add alias for `git commit`
|
||||
```
|
||||
|
||||
- Commit that changes many plugins:
|
||||
|
||||
```
|
||||
style: fix inline declaration of arrays
|
||||
```
|
||||
|
||||
For changes to plugins or themes, the scope should be the plugin or theme name:
|
||||
- ✅ `fix(agnoster): commit subject`
|
||||
- ❌ `fix(theme/agnoster): commit subject`
|
||||
|
||||
- `!`: this goes after the `scope` (or the `type` if scope is empty), to indicate that the commit
|
||||
introduces breaking changes.
|
||||
|
||||
Optionally, you can specify a message that the changelog tool will display to the user to indicate
|
||||
what's changed and what they can do to deal with it. You can use multiple lines to type this message;
|
||||
the changelog parser will keep reading until the end of the commit message or until it finds an empty
|
||||
line.
|
||||
|
||||
Example (made up):
|
||||
|
||||
```
|
||||
style(agnoster)!: change dirty git repo glyph
|
||||
|
||||
BREAKING CHANGE: the glyph to indicate when a git repository is dirty has
|
||||
changed from a Powerline character to a standard UTF-8 emoji. You can
|
||||
change it back by setting `ZSH_THEME_DIRTY_GLYPH`.
|
||||
|
||||
Fixes #420
|
||||
|
||||
Co-authored-by: Username <email>
|
||||
```
|
||||
|
||||
- `subject`: a brief description of the changes. This will be displayed in the changelog. If you need
|
||||
to specify other details, you can use the commit body, but it won't be visible.
|
||||
|
||||
Formatting tricks: the commit subject may contain:
|
||||
- Links to related issues or PRs by writing `#issue`. This will be highlighted by the changelog tool:
|
||||
|
||||
```
|
||||
feat(archlinux): add support for aura AUR helper (#9467)
|
||||
```
|
||||
|
||||
- Formatted inline code by using backticks: the text between backticks will also be highlighted by
|
||||
the changelog tool:
|
||||
```
|
||||
feat(shell-proxy): enable unexported `DEFAULT_PROXY` setting (#9774)
|
||||
```
|
||||
|
||||
### Style
|
||||
|
||||
Try to keep the first commit line short. It's harder to do using this commit style but try to be
|
||||
concise, and if you need more space, you can use the commit body. Try to make sure that the commit
|
||||
subject is clear and precise enough that users will know what changed by just looking at the changelog.
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
## Volunteer
|
||||
|
||||
Very nice!! :)
|
||||
|
||||
Please have a look at the [Volunteer](https://github.com/ohmyzsh/ohmyzsh/wiki/Volunteers)
|
||||
page for instructions on where to start and more.
|
||||
-->
|
||||
|
||||
## Reference
|
||||
|
||||
This contributing guide is adapted from the
|
||||
[oh-my-zsh contribution guide](https://github.com/ohmyzsh/ohmyzsh/blob/master/CONTRIBUTING.md).
|
||||
If there are any issues with this, please email admin@deepcore.dev.
|
||||
|
||||
+26
-47
@@ -1,75 +1,54 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:lts-alpine AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
### Unified deps builder
|
||||
# FROM node:lts-alpine AS deps
|
||||
# WORKDIR /app
|
||||
# COPY package.json yarn.lock ./
|
||||
# RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --network-timeout 1000000 --ignore-scripts
|
||||
|
||||
### Build for app
|
||||
FROM node:lts-alpine AS build-system
|
||||
# setup workdir - has to be the same filepath as app because fuckin' Prisma
|
||||
WORKDIR /app
|
||||
|
||||
## so corepack knows pnpm's version
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
## prevent prompt to download
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
## setup for offline
|
||||
RUN corepack pack
|
||||
## don't call out to network anymore
|
||||
ENV COREPACK_ENABLE_NETWORK=0
|
||||
|
||||
### INSTALL DEPS ONCE
|
||||
FROM base AS deps
|
||||
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
### BUILD TORRENTIAL
|
||||
FROM rustlang/rust:nightly-alpine AS torrential-build
|
||||
RUN apk add musl-dev
|
||||
WORKDIR /build
|
||||
COPY torrential .
|
||||
RUN apk add protoc
|
||||
RUN cargo build --release
|
||||
|
||||
### BUILD APP
|
||||
FROM base AS build-system
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NUXT_TELEMETRY_DISABLED=1
|
||||
# ENV YARN_CACHE_FOLDER=/root/.yarn
|
||||
|
||||
## add git so drop can determine its git ref at build
|
||||
RUN apk add --no-cache git
|
||||
# add git so drop can determine its git ref at build
|
||||
# pnpm for build
|
||||
RUN apk add --no-cache git pnpm
|
||||
|
||||
## copy deps and rest of project files
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
# copy deps and rest of project files
|
||||
# COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ARG BUILD_DROP_VERSION
|
||||
ARG BUILD_GIT_REF
|
||||
|
||||
## build
|
||||
RUN pnpm run postinstall && pnpm run build
|
||||
# build
|
||||
RUN pnpm import
|
||||
RUN pnpm install --shamefully-hoist
|
||||
RUN pnpm run build
|
||||
# RUN --mount=type=cache,target=/root/.yarn yarn postinstall && yarn build
|
||||
|
||||
|
||||
# create run environment for Drop
|
||||
FROM base AS run-system
|
||||
### create run environment for Drop
|
||||
FROM node:lts-alpine AS run-system
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NUXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn add --network-timeout 1000000 --no-lockfile --ignore-scripts prisma@6.11.1
|
||||
RUN apk add --no-cache pnpm 7zip nginx
|
||||
RUN pnpm install prisma@7.3.0
|
||||
# init prisma to download all required files
|
||||
RUN pnpm prisma init
|
||||
RUN apk add --no-cache pnpm
|
||||
RUN pnpm install prisma@6.11.1
|
||||
|
||||
COPY --from=build-system /app/prisma.config.ts ./
|
||||
COPY --from=build-system /app/package.json ./
|
||||
COPY --from=build-system /app/.output ./app
|
||||
COPY --from=build-system /app/prisma ./prisma
|
||||
COPY --from=build-system /app/build ./startup
|
||||
COPY --from=build-system /app/build/nginx.conf /nginx.conf
|
||||
COPY --from=torrential-build /build/target/release/torrential /usr/bin/
|
||||
|
||||
ENV LIBRARY="/library"
|
||||
ENV DATA="/data"
|
||||
ENV NGINX_CONFIG="/nginx.conf"
|
||||
# NGINX's port
|
||||
ENV PORT=4000
|
||||
|
||||
CMD ["sh", "/app/startup/launch.sh"]
|
||||
|
||||
@@ -6,32 +6,73 @@
|
||||
# Drop
|
||||
|
||||
[](https://droposs.org)
|
||||
[](https://docs.droposs.org/)
|
||||
[](https://forum.droposs.org)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/ACq4qZp4a9)
|
||||
[](https://opencollective.com/drop-oss)
|
||||
[
|
||||
](https://translate.droposs.org/engage/drop/)
|
||||
|
||||
Drop is an open-source game distribution platform, similar to GameVault or Steam. It's designed to distribute and share DRM-free games quickly, all while being incredibly flexible, beautiful, and fast.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://droposs.org/_ipx/f_webp&q_80/images/carousel/store.png" alt="Drop Screenshot" width="900rem"/>
|
||||
</div>
|
||||
Drop is an open-source game distribution platform, like GameVault or Steam. It's designed to distribute and shared DRM-free game quickly, all while being incredibly flexible, beautiful and fast.
|
||||
|
||||
## Philosophy
|
||||
|
||||
1. Drop is flexible. While abstractions and interfaces can complicate the codebase, the flexibility is worth it.
|
||||
2. Drop is secure. The nature of Drop means an instance can never be accessible without authentication. In line with #1, Drop also supports a huge variety of authentication mechanisms, from username/password to SSO.
|
||||
3. Drop is user-friendly. The interface is designed to be clean and simple to use, with advanced features available to users who want them.
|
||||
1. Drop is flexible. While abstractions and interfaces can make the codebase more complicated, the flexibility is worth it.
|
||||
2. Drop is secure. The nature of Drop means an instance can never be accessible without authentication. In line with #1, Drop also supports a huge variety of authentication mechanisms, from a username/password to SSO.
|
||||
3. Drop is user-friendly. The interface is designed to be clean and simple to use, with complexity available to the users who want it.
|
||||
|
||||
## Deployment
|
||||
|
||||
See our documentation on how to [deploy Drop](https://docs.droposs.org/docs/guides/quickstart) for more information.
|
||||
To just deploy Drop, we've set up a simple docker compose file in deploy-template.
|
||||
|
||||
1. Generate a [GiantBomb API Key](https://www.giantbomb.com/api/)
|
||||
2. Navigate to the deploy-template directory in your terminal (`cd deploy-template`)
|
||||
3. Edit the compose.yml file (`nano compose.yml`) and copy your GiamtBomb API Key into the GIANT_BOMB_API_KEY environment variable
|
||||
4. Run `docker compose up -d`
|
||||
|
||||
Your drop server should now be running. To register the admin user, navigate to http://your.drop.server.ip:3000/register?id=admin
|
||||
and fill in the required forms
|
||||
|
||||
### Adding a game
|
||||
|
||||
To add a game to the drop library, do as follows:
|
||||
|
||||
1. Ensure that the current user owns the library folder with `sudo chown -R $(id -u $(whoami)) library`
|
||||
2. `cd library`
|
||||
3. `mkdir <GAME_NAME>` with the name of the game which you would like to register
|
||||
4. `cd <GAME_NAME>`
|
||||
5. `mkdir <VERSION_NAME>` Upload files for the specific game version to this folder
|
||||
6. Navigate to http://your.drop.server.ip:3000/
|
||||
7. Import game metadata (uses GiantBomb API Key) by selecting the game and specifying which entry to import
|
||||
8. Navigate to http://your.drop.server.ip:3000/admin/library
|
||||
9. You should see the game which you have just imported listed in this menu. There should be a notification that "Drop has detected you have new verions of this game to import". Select import here.
|
||||
10. Select the game version to import and thus fill in fields as required.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
This repo uses the Nuxt 3 + TailwindCSS stack, with the `yarn` package manager.
|
||||
|
||||
For the database, Drop uses Prisma connected to PostgreSQL.
|
||||
|
||||
## Development
|
||||
|
||||
To get started with development, you need `yarn --optional` and `docker compose` installed (or know how to set up a PostgreSQL database).
|
||||
|
||||
### Note: `--optional` flag is **REQUIRED**
|
||||
|
||||
Drop uses a utility package called droplet that's written in Rust. It has builts for Linux (GNU) and Windows, and they are set up as optional packages. `npm` installs these by default, but `yarn` needs the `--optional` flag.
|
||||
|
||||
Steps:
|
||||
|
||||
1. Run `git submodule update --init --recursive` to setup submodules
|
||||
1. Copy the `.env.example` to `.env` and add your GiantBomb metadata key (more metadata providers coming)
|
||||
1. Create the `.data` directory with `mkdir .data`
|
||||
1. Ensure that your user owns the `.data` directory with `sudo chown -R $(id -u $(whoami))`
|
||||
1. Open up a terminal and navigate to `dev-tools`, and run `docker compose up`
|
||||
1. Open up another terminal in the root directory of the project and run `yarn` and then `yarn dev` to start the dev server
|
||||
|
||||
As part of the first-time bootstrap, Drop creates an invitation with the fixed id of 'admin'. So, to create an admin account, go to:
|
||||
|
||||
http://localhost:3000/auth/register?id=admin
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see the [in-depth contributing guide](CONTRIBUTING.md). The guide includes information on how to set up the project, how to contribute code, how to report issues, and even how to effectively translate Drop.
|
||||
|
||||
[](https://translate.droposs.org/engage/drop/)
|
||||
Please see the [in-depth contributing guide](CONTRIBUTING.md)
|
||||
|
||||
@@ -4,51 +4,20 @@
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<ModalStack />
|
||||
<div
|
||||
v-if="showExternalUrlWarning"
|
||||
class="fixed flex flex-row gap-x-2 right-0 bottom-0 m-2 px-2 py-2 z-50 text-right bg-red-700/90 rounded-lg"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-zinc-200 font-bold font-display">{{
|
||||
$t("errors.externalUrl.title")
|
||||
}}</span>
|
||||
<span class="text-xs text-red-400">{{
|
||||
$t("errors.externalUrl.subtitle")
|
||||
}}</span>
|
||||
</div>
|
||||
<button class="text-red-200" @click="() => hideExternalURL()">
|
||||
<XMarkIcon class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XMarkIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
await updateUser();
|
||||
|
||||
const user = useUser();
|
||||
const apiDetails = await $dropFetch("/api/v1");
|
||||
const clientMode = isClientRequest();
|
||||
|
||||
const showExternalUrlWarning = ref(false);
|
||||
function checkExternalUrl() {
|
||||
if (!import.meta.client || clientMode) return;
|
||||
const realOrigin = window.location.origin.trim();
|
||||
const chosenOrigin = apiDetails.external.trim();
|
||||
const ignore = window.localStorage.getItem("ignoreExternalUrl");
|
||||
if (ignore && ignore == "true") return;
|
||||
showExternalUrlWarning.value = !(realOrigin == chosenOrigin);
|
||||
}
|
||||
|
||||
function hideExternalURL() {
|
||||
window.localStorage.setItem("ignoreExternalUrl", "true");
|
||||
showExternalUrlWarning.value = false;
|
||||
}
|
||||
|
||||
if (user.value?.admin) {
|
||||
onMounted(() => {
|
||||
checkExternalUrl();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* You can customise the default animation here. */
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
version: v2
|
||||
plugins:
|
||||
- local: protoc-gen-es
|
||||
out: server/internal/proto
|
||||
opt: target=ts
|
||||
@@ -1,41 +0,0 @@
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
pid nginx.pid;
|
||||
error_log stderr;
|
||||
daemon off;
|
||||
|
||||
http {
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
server_tokens off;
|
||||
|
||||
access_log nginx_host.access.log;
|
||||
client_body_temp_path client_body;
|
||||
fastcgi_temp_path fastcgi_temp;
|
||||
proxy_temp_path proxy_temp;
|
||||
scgi_temp_path scgi_temp;
|
||||
uwsgi_temp_path uwsgi_temp;
|
||||
|
||||
server {
|
||||
listen 3000;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:4000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
location /api/v1/depot/ {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
LockClosedIcon,
|
||||
DevicePhoneMobileIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
CodeBracketIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import { UserIcon } from "@heroicons/vue/24/solid";
|
||||
import type { Component } from "vue";
|
||||
@@ -53,17 +52,10 @@ import type { Component } from "vue";
|
||||
const notifications = useNotifications();
|
||||
const { t } = useI18n();
|
||||
|
||||
const navigation: Ref<
|
||||
(NavigationItem & { icon: Component; count?: number })[]
|
||||
> = computed(() => [
|
||||
const navigation: (NavigationItem & { icon: Component; count?: number })[] = [
|
||||
{ label: t("home"), route: "/account", icon: HomeIcon, prefix: "/account" },
|
||||
{
|
||||
label: t("account.home.title"),
|
||||
route: "/account",
|
||||
icon: HomeIcon,
|
||||
prefix: "/account",
|
||||
},
|
||||
{
|
||||
label: t("account.security.title"),
|
||||
label: t("security"),
|
||||
route: "/account/security",
|
||||
prefix: "/account/security",
|
||||
icon: LockClosedIcon,
|
||||
@@ -74,12 +66,6 @@ const navigation: Ref<
|
||||
prefix: "/account/devices",
|
||||
icon: DevicePhoneMobileIcon,
|
||||
},
|
||||
{
|
||||
label: t("account.token.title"),
|
||||
route: "/account/tokens",
|
||||
prefix: "/account/tokens",
|
||||
icon: CodeBracketIcon,
|
||||
},
|
||||
{
|
||||
label: t("account.notifications.notifications"),
|
||||
route: "/account/notifications",
|
||||
@@ -93,7 +79,7 @@ const navigation: Ref<
|
||||
prefix: "/account/settings",
|
||||
icon: WrenchScrewdriverIcon,
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation.value);
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
</script>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<template v-if="!mLogoObjectId">
|
||||
<DropLogo />
|
||||
</template>
|
||||
<template v-else>
|
||||
<img :src="useObject(mLogoObjectId)" :alt="`${serverName} logo`" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { serverName, mLogoObjectId } = await $dropFetch("/api/v1");
|
||||
</script>
|
||||
@@ -4,14 +4,7 @@
|
||||
:href="`/auth/oidc?redirect=${route.query.redirect ?? '/'}`"
|
||||
class="transition rounded-md grow inline-flex items-center justify-center bg-white/10 px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-white/20"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="auth.signin.signinWithExternalProvider"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #externalProvider>{{
|
||||
providerName || $t("auth.signin.externalProvider")
|
||||
}}</template>
|
||||
<i18n-t keypath="auth.signin.externalProvider" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
@@ -22,6 +15,4 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const { providerName = undefined } = defineProps<{ providerName?: string }>();
|
||||
</script>
|
||||
|
||||
+18
-64
@@ -12,7 +12,7 @@
|
||||
v-model="username"
|
||||
name="username"
|
||||
type="username"
|
||||
autocomplete="username webauthn"
|
||||
autocomplete="username"
|
||||
required
|
||||
class="block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
@@ -86,78 +86,36 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/20/solid";
|
||||
import {
|
||||
startAuthentication,
|
||||
browserSupportsWebAuthn,
|
||||
} from "@simplewebauthn/browser";
|
||||
import { FetchError } from "ofetch";
|
||||
import type { UserModel } from "~/prisma/client/models";
|
||||
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const rememberMe = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
async function passkeyAutofill() {
|
||||
let silentWebauthnOptions;
|
||||
try {
|
||||
silentWebauthnOptions = await $dropFetch("/api/v1/auth/passkey/start", {
|
||||
method: "POST",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await startAuthentication({
|
||||
optionsJSON: silentWebauthnOptions,
|
||||
useBrowserAutofill: true,
|
||||
});
|
||||
|
||||
loading.value = true;
|
||||
|
||||
await $dropFetch("/api/v1/auth/passkey/finish", {
|
||||
method: "POST",
|
||||
body: result,
|
||||
});
|
||||
|
||||
await completeSignin();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (browserSupportsWebAuthn()) {
|
||||
try {
|
||||
await passkeyAutofill();
|
||||
} catch (response) {
|
||||
const message = (response as FetchError).message || t("errors.unknown");
|
||||
error.value = message;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const error = ref<string | undefined>();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
async function signin_wrapper() {
|
||||
function signin_wrapper() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await signin();
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError) {
|
||||
error.value = e.data.message || t("errors.unknown");
|
||||
} else {
|
||||
error.value = e as string;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
signin()
|
||||
.then(() => {
|
||||
router.push(route.query.redirect?.toString() ?? "/");
|
||||
})
|
||||
.catch((response) => {
|
||||
const message = response.statusMessage || t("errors.unknown");
|
||||
error.value = message;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function signin() {
|
||||
const { result } = await $dropFetch("/api/v1/auth/signin/simple", {
|
||||
await $dropFetch("/api/v1/auth/signin/simple", {
|
||||
method: "POST",
|
||||
body: {
|
||||
username: username.value,
|
||||
@@ -165,11 +123,7 @@ async function signin() {
|
||||
rememberMe: rememberMe.value,
|
||||
},
|
||||
});
|
||||
if (result == "2fa") {
|
||||
router.push({ query: route.query, path: "/auth/mfa" });
|
||||
return;
|
||||
}
|
||||
|
||||
await completeSignin();
|
||||
const user = useUser();
|
||||
user.value = await $dropFetch<UserModel | null>("/api/v1/user");
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
v-for="(_, i) in amount"
|
||||
:key="i"
|
||||
:class="[
|
||||
carousel.currentSlide === i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
|
||||
carousel.currentSlide == i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
|
||||
'transition-all cursor-pointer h-2 rounded-full',
|
||||
]"
|
||||
@click="slideTo(i)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -19,8 +18,8 @@ const carousel = inject(injectCarousel)!;
|
||||
|
||||
const amount = carousel.maxSlide - carousel.minSlide + 1;
|
||||
|
||||
function slideTo(index: number) {
|
||||
const offsetIndex = index + carousel.minSlide;
|
||||
carousel.nav.slideTo(offsetIndex);
|
||||
}
|
||||
// function slideTo(index: number) {
|
||||
// const offsetIndex = index + carousel.minSlide;
|
||||
// carousel.nav.slideTo(offsetIndex);
|
||||
// }
|
||||
</script>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<input
|
||||
v-for="i in length"
|
||||
ref="codeElements"
|
||||
:key="i"
|
||||
v-model="code[i - 1]"
|
||||
:class="[
|
||||
size,
|
||||
'uppercase appearance-none text-center bg-zinc-900 rounded-xl border-zinc-700 focus:border-blue-600 text-bold font-display text-zinc-100',
|
||||
]"
|
||||
type="text"
|
||||
pattern="\d*"
|
||||
:placeholder="placeholder[i - 1]"
|
||||
@keydown="(v) => keydown(i - 1, v)"
|
||||
@input="() => input(i - 1)"
|
||||
@focusin="() => select(i - 1)"
|
||||
@paste="(v) => paste(i - 1, v)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
length = 7,
|
||||
placeholder = "1A2B3C4",
|
||||
size = "w-16 h-16 text-2xl",
|
||||
} = defineProps<{
|
||||
length?: number;
|
||||
placeholder?: string;
|
||||
size?: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "complete", code: string): void;
|
||||
}>();
|
||||
|
||||
const codeElements = useTemplateRef("codeElements");
|
||||
const code = ref<string[]>([]);
|
||||
|
||||
function keydown(index: number, event: KeyboardEvent) {
|
||||
if (event.key === "Backspace" && !code.value[index] && index > 0) {
|
||||
codeElements.value![index - 1].focus();
|
||||
}
|
||||
}
|
||||
|
||||
function input(index: number) {
|
||||
if (codeElements.value === null) return;
|
||||
const v = code.value[index] ?? "";
|
||||
if (v.length > 1) code.value[index] = v[0];
|
||||
|
||||
if (!(index + 1 >= codeElements.value.length) && v) {
|
||||
codeElements.value[index + 1].focus();
|
||||
}
|
||||
|
||||
if (!(index - 1 < 0) && !v) {
|
||||
codeElements.value[index - 1].focus();
|
||||
}
|
||||
|
||||
if (index == length - 1) {
|
||||
const assembledCode = code.value.join("");
|
||||
if (assembledCode.length == length) {
|
||||
complete(assembledCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function select(index: number) {
|
||||
if (!codeElements.value) return;
|
||||
if (index >= codeElements.value.length) return;
|
||||
codeElements.value[index].select();
|
||||
}
|
||||
|
||||
function paste(index: number, event: ClipboardEvent) {
|
||||
const newCode = event.clipboardData!.getData("text/plain");
|
||||
for (let i = 0; i < newCode.length && i < length; i++) {
|
||||
code.value[i] = newCode[i];
|
||||
codeElements.value![i].focus();
|
||||
if (i + 1 == length) {
|
||||
complete(code.value.join(""));
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
async function complete(completedCode: string) {
|
||||
emit("complete", completedCode);
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<svg
|
||||
aria-label="Drop Logo"
|
||||
class="text-blue-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -10,16 +9,6 @@
|
||||
d="M4 13.5C4 11.0008 5.38798 8.76189 7.00766 7C8.43926 5.44272 10.0519 4.25811 11.0471 3.5959C11.6287 3.20893 12.3713 3.20893 12.9529 3.5959C13.9481 4.25811 15.5607 5.44272 16.9923 7C18.612 8.76189 20 11.0008 20 13.5C20 17.9183 16.4183 21.5 12 21.5C7.58172 21.5 4 17.9183 4 13.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="100"
|
||||
:stroke-dashoffset="dashArray"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ progress?: number }>();
|
||||
|
||||
const dashArray = computed(() =>
|
||||
props.progress === undefined ? 0 : ((100 - props.progress) / 100) * 50 + 50,
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -10,18 +10,9 @@
|
||||
d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z"
|
||||
/>
|
||||
</svg>
|
||||
<ApplicationLogo aria-hidden="true" class="h-6" />
|
||||
<DropLogo class="h-6" />
|
||||
<span class="text-blue-400 font-display font-bold text-xl uppercase">
|
||||
<template v-if="serverName">
|
||||
{{ serverName }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("drop.drop") }}
|
||||
</template>
|
||||
{{ $t("drop.drop") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { serverName } = await $dropFetch("/api/v1");
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,6 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const url = computed(() => {
|
||||
return `/api/v1/emoji/${twemoji.convert.toCodePoint(props.emoji)}`;
|
||||
return `/twemoji/${twemoji.convert.toCodePoint(props.emoji)}.svg`;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="emulator"
|
||||
class="flex space-x-4 rounded-md bg-zinc-900/50 px-6 outline -outline-offset-1 outline-white/10 w-fit text-xs font-bold text-zinc-100"
|
||||
>
|
||||
<div class="inline-flex gap-x-2 items-center">
|
||||
<img :src="useObject(emulator.gameIcon)" class="size-6" />
|
||||
<span>{{ emulator.gameName }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="h-full w-6 shrink-0 text-white/10"
|
||||
viewBox="0 0 24 44"
|
||||
preserveAspectRatio="none"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z" />
|
||||
</svg>
|
||||
<span class="ml-4">{{ emulator.versionName }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="h-full w-6 shrink-0 text-white/10"
|
||||
viewBox="0 0 24 44"
|
||||
preserveAspectRatio="none"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z" />
|
||||
</svg>
|
||||
<span class="ml-4 truncate">{{ emulator.launchName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { EmulatorLaunchObject } from "~/composables/frontend";
|
||||
|
||||
defineProps<{ emulator: EmulatorLaunchObject }>();
|
||||
</script>
|
||||
@@ -44,9 +44,7 @@ const props = defineProps<{
|
||||
width?: number;
|
||||
}>();
|
||||
|
||||
const {
|
||||
store: { showGamePanelTextDecoration },
|
||||
} = await $dropFetch(`/api/v1/settings`);
|
||||
const { showGamePanelTextDecoration } = await $dropFetch(`/api/v1/settings`);
|
||||
|
||||
const currentComponent = ref<HTMLDivElement>();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div v-if="game!">
|
||||
<div class="grow flex flex-col xl:flex-row gap-y-8">
|
||||
<div class="grow flex flex-row gap-y-8">
|
||||
<div class="grow w-full h-full px-6 py-4 flex flex-col">
|
||||
<div
|
||||
class="flex flex-col lg:flex-row lg:justify-between items-start lg:items-center gap-2"
|
||||
@@ -10,12 +10,10 @@
|
||||
<!-- icon image -->
|
||||
<img :src="coreMetadataIconUrl" class="size-20" />
|
||||
<div>
|
||||
<h1
|
||||
class="text-2xl xl:text-5xl font-bold font-display text-zinc-100"
|
||||
>
|
||||
<h1 class="text-5xl font-bold font-display text-zinc-100">
|
||||
{{ game.mName }}
|
||||
</h1>
|
||||
<p class="mt-1 text-sm xl:text-lg text-zinc-400">
|
||||
<p class="mt-1 text-lg text-zinc-400">
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -30,28 +28,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-8">
|
||||
<SelectorMultiItem
|
||||
v-model="currentTags"
|
||||
:items="tags"
|
||||
:create="createTag"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
for="releaseDate"
|
||||
class="text-sm/6 font-medium text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.game.editReleaseDate") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="releaseDate"
|
||||
v-model="releaseDate"
|
||||
type="date"
|
||||
name="releaseDate"
|
||||
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MultiItemSelector v-model="currentTags" :items="tags" />
|
||||
</div>
|
||||
|
||||
<!-- image carousel pick -->
|
||||
@@ -467,7 +444,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GameModel } from "~/prisma/client/models";
|
||||
import type { GameModel, GameTagModel } from "~/prisma/client/models";
|
||||
import { micromark } from "micromark";
|
||||
import {
|
||||
CheckIcon,
|
||||
@@ -477,7 +454,6 @@ import {
|
||||
} from "@heroicons/vue/24/solid";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { H3Error } from "h3";
|
||||
import type { AdminFetchGameType } from "~/server/api/v1/admin/game/[id]/index.get";
|
||||
|
||||
const showUploadModal = ref(false);
|
||||
const showAddCarouselModal = ref(false);
|
||||
@@ -485,9 +461,8 @@ const showAddImageDescriptionModal = ref(false);
|
||||
const showEditCoreMetadata = ref(false);
|
||||
const mobileShowFinalDescription = ref(true);
|
||||
|
||||
const game = defineModel<SerializeObject<AdminFetchGameType>>({
|
||||
required: true,
|
||||
});
|
||||
type ModelType = SerializeObject<GameModel & { tags: Array<GameTagModel> }>;
|
||||
const game = defineModel<ModelType>() as Ref<ModelType>;
|
||||
if (!game.value)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
@@ -497,9 +472,8 @@ if (!game.value)
|
||||
const currentTags = ref<{ [key: string]: boolean }>(
|
||||
Object.fromEntries(game.value.tags.map((e) => [e.id, true])),
|
||||
);
|
||||
const rawTags = await $dropFetch("/api/v1/admin/tags");
|
||||
const tags = ref(
|
||||
rawTags.map((e) => ({ name: e.name, param: e.id }) satisfies StoreSortOption),
|
||||
const tags = (await $dropFetch("/api/v1/admin/tags")).map(
|
||||
(e) => ({ name: e.name, param: e.id }) satisfies StoreSortOption,
|
||||
);
|
||||
|
||||
watch(
|
||||
@@ -510,49 +484,18 @@ watch(
|
||||
params: {
|
||||
id: game.value.id,
|
||||
},
|
||||
body: {
|
||||
tags: Object.entries(v)
|
||||
.filter((v) => v[1])
|
||||
.map((v) => v[0]),
|
||||
},
|
||||
body: { tags: Object.keys(v) },
|
||||
failTitle: "Failed to update game tags",
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const releaseDate = ref(
|
||||
game.value.mReleased
|
||||
? new Date(game.value.mReleased).toISOString().substring(0, 10)
|
||||
: "",
|
||||
);
|
||||
|
||||
watch(releaseDate, async (newDate) => {
|
||||
const body: PatchGameBody = {};
|
||||
|
||||
if (newDate) {
|
||||
const parsed = new Date(newDate);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
body.mReleased = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
await $dropFetch(`/api/v1/admin/game/:id`, {
|
||||
method: "PATCH",
|
||||
params: {
|
||||
id: game.value.id,
|
||||
},
|
||||
body,
|
||||
failTitle: "Failed to update release date",
|
||||
});
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// I don't know why I split these fields off.
|
||||
const coreMetadataName = ref(game.value.mName);
|
||||
const coreMetadataDescription = ref(game.value.mShortDescription);
|
||||
|
||||
const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId));
|
||||
const coreMetadataIconFileUpload = ref<FileList | undefined>();
|
||||
const coreMetadataLoading = ref(false);
|
||||
@@ -618,6 +561,7 @@ function coreMetadataUpdate_wrapper() {
|
||||
);
|
||||
})
|
||||
.then((newGame) => {
|
||||
console.log(newGame);
|
||||
if (!newGame) return;
|
||||
Object.assign(game.value, newGame);
|
||||
coreMetadataIconUrl.value = useObject(newGame.mIconObjectId);
|
||||
@@ -825,15 +769,4 @@ async function updateImageCarousel() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createTag(value: string): Promise<string> {
|
||||
const tag = await $dropFetch(`/api/v1/admin/tags`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
name: value,
|
||||
},
|
||||
});
|
||||
tags.value.push({ name: tag.name, param: tag.id });
|
||||
return tag.id;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,155 +1,96 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div v-if="game && unimportedVersions" class="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-white">
|
||||
{{ $t("library.admin.version.title") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-300">
|
||||
{{ $t("library.admin.version.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<NuxtLink
|
||||
:href="canImport ? `/admin/library/${game.id}/import` : ''"
|
||||
type="button"
|
||||
:class="[
|
||||
canImport ? 'bg-blue-600 hover:bg-blue-700' : 'bg-blue-800/50',
|
||||
'inline-flex w-fit items-center gap-x-2 rounded-md px-3 py-1 text-sm font-semibold font-display text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
|
||||
]"
|
||||
>
|
||||
{{
|
||||
canImport
|
||||
? $t("library.admin.import.version.import")
|
||||
: $t("library.admin.import.version.noVersions")
|
||||
}}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="relative min-w-full divide-y divide-white/15">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 pr-3 pl-4 text-left text-xs font-medium tracking-wide text-gray-400 uppercase sm:pl-0"
|
||||
<div v-if="game && unimportedVersions">
|
||||
<div class="grow flex flex-row gap-y-8">
|
||||
<div class="grow w-full h-full px-6 py-4 flex flex-col"></div>
|
||||
<div
|
||||
class="lg:overflow-y-auto lg:border-l lg:border-zinc-800 lg:block lg:inset-y-0 lg:z-50 lg:w-[30vw] flex flex-col gap-y-8 px-6 py-4"
|
||||
>
|
||||
<!-- version manager -->
|
||||
<div>
|
||||
<!-- version priority -->
|
||||
<div>
|
||||
<div class="border-b border-zinc-800 pb-3">
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between sm:flex-nowrap"
|
||||
>
|
||||
<h3
|
||||
class="text-base font-semibold font-display leading-6 text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.version.table.name") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t("library.admin.version.table.path") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t("library.admin.version.table.delta") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t("library.admin.version.table.setup") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t("library.admin.version.table.launch") }}
|
||||
</th>
|
||||
<th scope="col" class="py-3 pr-4 pl-3 sm:pr-0">
|
||||
<span class="sr-only">{{ $t("common.edit") }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{ $t("library.admin.versionPriority") }}
|
||||
|
||||
<!-- import games button -->
|
||||
|
||||
<NuxtLink
|
||||
:href="
|
||||
unimportedVersions.length > 0
|
||||
? `/admin/library/${game.id}/import`
|
||||
: ''
|
||||
"
|
||||
type="button"
|
||||
:class="[
|
||||
unimportedVersions.length > 0
|
||||
? 'bg-blue-600 hover:bg-blue-700'
|
||||
: 'bg-blue-800/50',
|
||||
'inline-flex w-fit items-center gap-x-2 rounded-md px-3 py-1 text-sm font-semibold font-display text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
|
||||
]"
|
||||
>
|
||||
{{
|
||||
unimportedVersions.length > 0
|
||||
? $t("library.admin.import.version.import")
|
||||
: $t("library.admin.import.version.noVersions")
|
||||
}}
|
||||
</NuxtLink>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center w-full text-sm text-zinc-600">
|
||||
{{ $t("lowest") }}
|
||||
</div>
|
||||
<draggable
|
||||
:list="game.versions"
|
||||
handle=".handle"
|
||||
class="divide-y divide-white/10"
|
||||
tag="tbody"
|
||||
class="mt-2 space-y-4"
|
||||
@update="() => updateVersionOrder()"
|
||||
>
|
||||
<template #item="{ element: version }: { element: VersionType }">
|
||||
<tr :key="version.versionId">
|
||||
<td>
|
||||
<template
|
||||
#item="{ element: item }: { element: GameVersionModel }"
|
||||
>
|
||||
<div
|
||||
class="w-full inline-flex items-center px-4 py-2 bg-zinc-800 rounded justify-between"
|
||||
>
|
||||
<div class="text-zinc-100 font-semibold">
|
||||
{{ item.versionName }}
|
||||
</div>
|
||||
<div class="text-zinc-400">
|
||||
{{ item.delta ? $t("library.admin.version.delta") : "" }}
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-x-2">
|
||||
<component
|
||||
:is="PLATFORM_ICONS[item.platform]"
|
||||
class="size-6 text-blue-600"
|
||||
/>
|
||||
<Bars3Icon
|
||||
class="cursor-move w-6 h-6 text-zinc-400 handle"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-4 pr-3 pl-4 sm:pl-0">
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm font-medium whitespace-nowrap text-white"
|
||||
>{{ version.displayName ?? version.versionPath }}</span
|
||||
>
|
||||
<span class="text-xs text-zinc-500 mono">{{
|
||||
version.versionId
|
||||
}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
|
||||
{{ version.versionPath }}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
|
||||
{{ version.delta }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
|
||||
<ul class="space-y-2">
|
||||
<GameEditorVersionConfig
|
||||
v-for="config in version.setups"
|
||||
:key="config.setupId"
|
||||
:config="config"
|
||||
/>
|
||||
<li
|
||||
v-if="version.setups.length == 0"
|
||||
class="text-xs uppercase font-display text-zinc-700 font-semibold"
|
||||
>
|
||||
{{ $t("library.admin.version.noSetups") }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
|
||||
<div v-if="version.onlySetup">
|
||||
{{ $t("library.admin.version.setupOnly") }}
|
||||
</div>
|
||||
<ul v-else class="space-y-2">
|
||||
<GameEditorVersionConfig
|
||||
v-for="config in version.launches"
|
||||
:key="config.launchId"
|
||||
:config="config"
|
||||
/>
|
||||
</ul>
|
||||
</td>
|
||||
<td
|
||||
class="py-4 pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0 space-x-2"
|
||||
>
|
||||
<!--
|
||||
<button class="text-blue-400 hover:text-blue-300">
|
||||
Edit<span class="sr-only"
|
||||
>,
|
||||
{{ version.displayName ?? version.versionPath }}</span
|
||||
>
|
||||
<button @click="() => deleteVersion(item.versionName)">
|
||||
<TrashIcon class="w-5 h-5 text-red-600" />
|
||||
</button>
|
||||
-->
|
||||
<button
|
||||
class="text-red-400 hover:text-red-300"
|
||||
@click="() => deleteVersion(version.versionId)"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr></template
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
<div
|
||||
v-if="game.versions.length == 0"
|
||||
class="text-center font-bold text-zinc-400 my-3"
|
||||
>
|
||||
{{ $t("library.admin.version.noVersionsAdded") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center w-full text-sm text-zinc-600">
|
||||
{{ $t("highest") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,51 +116,37 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GameModel, GameVersionModel } from "~/prisma/client/models";
|
||||
import { Bars3Icon, TrashIcon } from "@heroicons/vue/24/solid";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { H3Error } from "h3";
|
||||
import { ExclamationCircleIcon, Bars3Icon } from "@heroicons/vue/24/outline";
|
||||
import type { AdminFetchGameType } from "~/server/api/v1/admin/game/[id]/index.get";
|
||||
import { ExclamationCircleIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
// TODO implement UI for this page
|
||||
|
||||
const props = defineProps<{ unimportedVersions: string[] }>();
|
||||
defineProps<{ unimportedVersions: string[] }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const hasDeleted = ref(false);
|
||||
|
||||
const canImport = computed(
|
||||
() => hasDeleted.value || props.unimportedVersions.length > 0,
|
||||
);
|
||||
|
||||
const game = defineModel<SerializeObject<AdminFetchGameType>>({
|
||||
required: true,
|
||||
});
|
||||
type GameAndVersions = GameModel & { versions: GameVersionModel[] };
|
||||
const game = defineModel<SerializeObject<GameAndVersions>>() as Ref<
|
||||
SerializeObject<GameAndVersions>
|
||||
>;
|
||||
if (!game.value)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Game not provided to editor component",
|
||||
});
|
||||
|
||||
type VersionType = (typeof game.value.versions)[number];
|
||||
|
||||
async function updateVersionOrder() {
|
||||
try {
|
||||
const newVersionOrder = await $dropFetch(
|
||||
"/api/v1/admin/game/:id/versions",
|
||||
{
|
||||
method: "PATCH",
|
||||
body: {
|
||||
versions: game.value.versions.map((e) => e.versionId),
|
||||
},
|
||||
params: {
|
||||
id: game.value.id,
|
||||
},
|
||||
const newVersions = await $dropFetch("/api/v1/admin/game/version", {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
id: game.value.id,
|
||||
versions: game.value.versions.map((e) => e.versionName),
|
||||
},
|
||||
);
|
||||
const newVersions = newVersionOrder.map(
|
||||
(id) => game.value.versions.find((k) => k.versionId == id)!,
|
||||
);
|
||||
});
|
||||
game.value.versions = newVersions;
|
||||
} catch (e) {
|
||||
createModal(
|
||||
@@ -236,22 +163,19 @@ async function updateVersionOrder() {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVersion(versionId: string) {
|
||||
async function deleteVersion(versionName: string) {
|
||||
try {
|
||||
await $dropFetch("/api/v1/admin/game/:id/versions", {
|
||||
await $dropFetch("/api/v1/admin/game/version", {
|
||||
method: "DELETE",
|
||||
body: {
|
||||
version: versionId,
|
||||
},
|
||||
params: {
|
||||
id: game.value.id,
|
||||
versionName: versionName,
|
||||
},
|
||||
});
|
||||
game.value.versions.splice(
|
||||
game.value.versions.findIndex((e) => e.versionId === versionId),
|
||||
game.value.versions.findIndex((e) => e.versionName === versionName),
|
||||
1,
|
||||
);
|
||||
hasDeleted.value = true;
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<li class="p-3 bg-zinc-800 ring-1 ring-zinc-700 shadow rounded-lg space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<h1
|
||||
v-if="!isSetup(props.config)"
|
||||
class="font-semibold text-zinc-300 text-md"
|
||||
>
|
||||
{{ props.config.name }}
|
||||
</h1>
|
||||
<span class="flex items-center">
|
||||
<component
|
||||
:is="PLATFORM_ICONS[props.config.platform]"
|
||||
alt=""
|
||||
class="size-5 flex-shrink-0 text-blue-600"
|
||||
/>
|
||||
<span class="ml-2 block truncate text-zinc-100 text-sm font-bold">{{
|
||||
props.config.platform
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="inline-flex gap-x-1 items-center bg-zinc-950 text-zinc-400 mono rounded-md p-2"
|
||||
>
|
||||
<p>{{ props.config.command }}</p>
|
||||
</div>
|
||||
<EmulatorWidget
|
||||
v-if="!isSetup(props.config) && props.config.emulator"
|
||||
:emulator="{
|
||||
launchId: props.config.launchId,
|
||||
gameName: props.config.emulator.gameVersion.game.mName,
|
||||
gameIcon: props.config.emulator.gameVersion.game.mIconObjectId,
|
||||
versionName: (props.config.emulator.gameVersion.displayName ??
|
||||
props.config.emulator.gameVersion.versionPath)!,
|
||||
launchName: props.config.emulator.name,
|
||||
platform: props.config.emulator.platform,
|
||||
}"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AdminFetchGameType } from "~/server/api/v1/admin/game/[id]/index.get";
|
||||
|
||||
const props = defineProps<{
|
||||
config:
|
||||
| AdminFetchGameType["versions"][number]["setups"][number]
|
||||
| AdminFetchGameType["versions"][number]["launches"][number];
|
||||
}>();
|
||||
|
||||
function isSetup(
|
||||
v: typeof props.config,
|
||||
): v is AdminFetchGameType["versions"][number]["setups"][number] {
|
||||
return Object.prototype.hasOwnProperty.call(v, "setupId");
|
||||
}
|
||||
</script>
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<img
|
||||
:src="rawIcon ? game.icon : useObject(game.icon)"
|
||||
class="w-12 h-12 rounded-sm object-cover"
|
||||
/>
|
||||
<img :src="game.icon" class="w-12 h-12 rounded-sm object-cover" />
|
||||
<div class="flex flex-col items-left">
|
||||
<h1 class="font-semibold font-display text-lg text-zinc-100">
|
||||
{{ game.name }}
|
||||
@@ -21,8 +18,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
|
||||
const { game, rawIcon = true } = defineProps<{
|
||||
const { game } = defineProps<{
|
||||
game: Omit<GameMetadataSearchResult, "year"> & { sourceName?: string };
|
||||
rawIcon?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="6" y1="11" x2="10" y2="11" />
|
||||
<line x1="8" y1="9" x2="8" y2="13" />
|
||||
<line x1="15" y1="12" x2="15.01" y2="12" />
|
||||
<line x1="18" y1="10" x2="18.01" y2="10" />
|
||||
<path
|
||||
d="M17.32 5H6.68a4 4 0 00-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 003 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 019.828 16h4.344a2 2 0 011.414.586L17 18c.5.5 1 1 2 1a3 3 0 003-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0017.32 5z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative group/iconupload rounded-xl overflow-hidden w-20 mx-auto"
|
||||
>
|
||||
<img v-if="objectId" :src="useObject(objectId)" :alt="imageAlt" />
|
||||
<ArrowUpTrayIcon v-else />
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl transition duration-200 absolute inset-0 opacity-0 group-hover/iconupload:opacity-100 focus-visible/iconupload:opacity-100 cursor-pointer bg-zinc-900/80 text-zinc-100 flex flex-col items-center justify-center text-center text-xs font-semibold ring-1 ring-inset ring-zinc-800 px-2"
|
||||
@click="openModal"
|
||||
>
|
||||
<ArrowUpTrayIcon class="size-5" />
|
||||
<span>{{ hoverText }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowUpTrayIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const { objectId, openModal, hoverText, imageAlt } = defineProps<{
|
||||
objectId: string | null;
|
||||
openModal: () => void;
|
||||
hoverText: string;
|
||||
imageAlt: string;
|
||||
}>();
|
||||
</script>
|
||||
@@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div v-if="needsName" class="mb-2">
|
||||
<div
|
||||
class="flex w-full rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
|
||||
>
|
||||
<input
|
||||
id="startup"
|
||||
v-model="launchConfiguration.name"
|
||||
type="text"
|
||||
name="startup"
|
||||
class="block flex-1 border-0 py-1.5 px-3 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
placeholder="Launch name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div
|
||||
class="flex w-full rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
|
||||
>
|
||||
<span
|
||||
class="flex select-none items-center gap-x-0.5 pl-3 text-zinc-500 sm:text-sm"
|
||||
>
|
||||
<div class="relative">
|
||||
<InformationCircleIcon class="peer size-4" />
|
||||
<div
|
||||
class="z-50 w-64 transition duration-100 opacity-0 shadow peer-hover:opacity-100 absolute left-0 p-2 bg-zinc-900 rounded text-xs text-zinc-300"
|
||||
>
|
||||
{{ $t("library.admin.launchRow.currentDirHint") }}
|
||||
</div>
|
||||
</div>
|
||||
{{ $t("library.admin.import.version.installDir") }}
|
||||
</span>
|
||||
<Combobox
|
||||
as="div"
|
||||
:value="launchConfiguration.launch"
|
||||
nullable
|
||||
class="w-full"
|
||||
@update:model-value="(v) => updateLaunchCommand(v)"
|
||||
>
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
class="block flex-1 border-0 py-1.5 pl-1 w-full bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
:placeholder="
|
||||
$t('library.admin.import.version.launchPlaceholder')
|
||||
"
|
||||
@change="launchProcessQuery = $event.target.value"
|
||||
@blur="launchProcessQuery = ''"
|
||||
/>
|
||||
<ComboboxButton
|
||||
v-if="launchFilteredVersionGuesses?.length ?? 0 > 0"
|
||||
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="size-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</ComboboxButton>
|
||||
|
||||
<ComboboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="guess in launchFilteredVersionGuesses"
|
||||
:key="guess.filename"
|
||||
v-slot="{ active, selected }"
|
||||
:value="guess.filename"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-x-2 block truncate',
|
||||
selected && 'font-semibold',
|
||||
]"
|
||||
>
|
||||
{{ guess.filename }}
|
||||
<component
|
||||
:is="PLATFORM_ICONS[guess.platform]"
|
||||
class="size-5"
|
||||
/>
|
||||
<img
|
||||
v-if="guess.type === 'emulator'"
|
||||
:src="useObject(guess.icon)"
|
||||
class="size-5"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<ComboboxOption
|
||||
v-if="
|
||||
launchProcessQuery &&
|
||||
launchConfiguration.launch !== launchProcessQuery
|
||||
"
|
||||
v-slot="{ active, selected }"
|
||||
:value="launchProcessQuery"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="['block truncate', selected && 'font-semibold']"
|
||||
>
|
||||
{{ launchProcessQuery }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.type && props.type === 'Emulator'"
|
||||
class="ml-1 mt-2 rounded-lg bg-blue-900/10 p-1 outline outline-blue-900"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0">
|
||||
<InformationCircleIcon
|
||||
class="size-5 text-blue-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-2 inline-flex items-center">
|
||||
<p class="text-sm text-blue-200">
|
||||
<i18n-t
|
||||
keypath="library.admin.launchRow.emulatorHint"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #rom>
|
||||
<span
|
||||
class="font-mono bg-zinc-950 text-zinc-100 py-1 px-0.5 rounded-xl"
|
||||
>{{
|
||||
// eslint-disable-next-line @intlify/vue-i18n/no-raw-text
|
||||
"{rom}"
|
||||
}}</span
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SelectorPlatform
|
||||
:model-value="launchConfiguration.platform"
|
||||
class="mb-2"
|
||||
@update:model-value="updatePlatform"
|
||||
>
|
||||
{{ $t("library.admin.import.version.platform") }}
|
||||
</SelectorPlatform>
|
||||
<div v-if="props.type && props.type === 'Game' && props.allowEmulator">
|
||||
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
|
||||
{{ $t("library.admin.launchRow.emulatorTitle") }}
|
||||
</h1>
|
||||
<div class="relative mt-2 space-x-1 inline-flex items-center w-full">
|
||||
<EmulatorWidget v-if="emulator" :emulator="emulator" />
|
||||
<div
|
||||
v-else
|
||||
class="font-bold uppercase font-display text-zinc-500 text-sm"
|
||||
>
|
||||
{{ $t("library.admin.launchRow.noEmulatorSelected") }}
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<LoadingButton :loading="false" @click="selectLaunchOpen = true">{{
|
||||
$t("library.admin.launchRow.emulatorSelect")
|
||||
}}</LoadingButton>
|
||||
<button
|
||||
:disabled="!emulator"
|
||||
class="transition rounded p-2 bg-zinc-900/30 group hover:enabled:bg-red-600/10 text-zinc-400 hover:enabled:text-red-600 disabled:bg-zinc-900/80 disabled:text-zinc-700"
|
||||
@click="() => (emulator = undefined)"
|
||||
>
|
||||
<TrashIcon class="transition size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.type && props.type === 'Emulator'">
|
||||
<p class="block text-sm font-medium leading-6 text-zinc-100">
|
||||
{{ $t("library.admin.launchRow.autosuggestHint") }}
|
||||
</p>
|
||||
<SelectorFileExtension
|
||||
v-model="launchConfiguration.suggestions!"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<ModalSelectLaunch
|
||||
v-model="selectLaunchOpen"
|
||||
class="-mt-2"
|
||||
:filter-platform="launchConfiguration.platform"
|
||||
@select="(v) => (emulator = v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { InformationCircleIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import type { EmulatorLaunchObject } from "~/composables/frontend";
|
||||
import type { GameType, Platform } from "~/prisma/client/enums";
|
||||
|
||||
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
|
||||
import type { VersionGuess } from "~/server/internal/library";
|
||||
|
||||
const launchProcessQuery = ref("");
|
||||
|
||||
const launchConfiguration = defineModel<
|
||||
Omit<(typeof ImportVersion.infer)["launches"][number], "name"> & {
|
||||
name?: string;
|
||||
}
|
||||
>({ required: true });
|
||||
const _emulatorMetadata = ref<EmulatorLaunchObject | undefined>(undefined);
|
||||
const emulator = computed({
|
||||
get() {
|
||||
return _emulatorMetadata.value;
|
||||
},
|
||||
set(v) {
|
||||
_emulatorMetadata.value = v;
|
||||
if (v) {
|
||||
launchConfiguration.value.emulatorId = v.launchId;
|
||||
} else {
|
||||
launchConfiguration.value.emulatorId = undefined;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function updatePlatform(v: Platform | undefined) {
|
||||
if (!v) return;
|
||||
launchConfiguration.value.platform = v;
|
||||
if (emulator.value) {
|
||||
if (emulator.value.platform !== v) {
|
||||
emulator.value = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
versionGuesses: Array<VersionGuess> | undefined;
|
||||
needsName: boolean;
|
||||
allowEmulator?: boolean;
|
||||
type?: GameType;
|
||||
}>();
|
||||
|
||||
if (props.type && props.type === "Emulator")
|
||||
launchConfiguration.value.suggestions ??= [];
|
||||
|
||||
const selectLaunchOpen = ref(false);
|
||||
|
||||
const launchFilteredVersionGuesses = computed(() =>
|
||||
props.versionGuesses?.filter((e) =>
|
||||
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase()),
|
||||
),
|
||||
);
|
||||
|
||||
function updateLaunchCommand(command: string) {
|
||||
launchConfiguration.value.launch = command;
|
||||
if (launchConfiguration.value.platform === undefined) {
|
||||
const autosetGuess = props.versionGuesses?.find(
|
||||
(v) => v.filename == command,
|
||||
);
|
||||
if (autosetGuess) {
|
||||
if (autosetGuess.type === "platform") {
|
||||
launchConfiguration.value.platform = autosetGuess.platform;
|
||||
} else if (autosetGuess.type === "emulator") {
|
||||
emulator.value = {
|
||||
launchId: autosetGuess.emulatorId,
|
||||
gameName: autosetGuess.gameName,
|
||||
gameIcon: autosetGuess.icon,
|
||||
versionName: autosetGuess.launchName,
|
||||
launchName: autosetGuess.launchName,
|
||||
platform: autosetGuess.platform,
|
||||
} satisfies EmulatorLaunchObject;
|
||||
launchConfiguration.value.platform = autosetGuess.platform;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<SelectorLanguageListbox />
|
||||
<LanguageSelectorListbox />
|
||||
<NuxtLink
|
||||
class="mt-1 transition text-blue-500 hover:text-blue-600 text-sm"
|
||||
to="https://translate.droposs.org/engage/drop/"
|
||||
to="https://translate.droposs.org/projects/drop/"
|
||||
target="_blank"
|
||||
>
|
||||
<i18n-t
|
||||
@@ -18,12 +18,8 @@
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
|
||||
<DevOnly>
|
||||
<h1 class="mt-3 text-sm text-gray-500">{{ $t("welcome") }}</h1>
|
||||
<DevOnly
|
||||
><h1 class="mt-3 text-sm text-gray-500">{{ $t("welcome") }}</h1>
|
||||
</DevOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<span class="text-xs font-mono text-zinc-400 inline-flex items-top gap-x-2"
|
||||
><span v-if="!short" class="text-zinc-500">{{ log.time }}</span>
|
||||
<span
|
||||
:class="[
|
||||
colours[log.level] || 'text-green-400',
|
||||
'uppercase font-display font-semibold',
|
||||
]"
|
||||
>{{ log.level }}</span
|
||||
>
|
||||
<span v-if="log.prefix" class="text-zinc-200"> {{ log.prefix }}</span>
|
||||
<pre :class="[short ? 'line-clamp-1' : '', 'mt-[1px]']">{{ log.msg }}</pre>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TaskLog } from "~/server/internal/tasks";
|
||||
|
||||
defineProps<{ log: typeof TaskLog.infer; short?: boolean }>();
|
||||
|
||||
const colours: { [key: string]: string } = {
|
||||
info: "text-blue-400",
|
||||
warn: "text-yellow-400",
|
||||
error: "text-red-400",
|
||||
};
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<TileWithLink>
|
||||
<div class="h-full flex gap-4">
|
||||
<div class="flex-1 my-auto">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<div
|
||||
class="md:flex-8 flex-6 lg:flex-2 my-auto text-center flex md:flex-row-reverse lg:inline"
|
||||
>
|
||||
<div class="md:text-2xl text-3xl flex-1 font-bold self-center">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div
|
||||
class="text-2xl xl:text-xs flex-1 md:flex-auto text-left md:text-center lg:text-center self-center"
|
||||
>
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TileWithLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { label, value } = defineProps<{
|
||||
label: string;
|
||||
value: string | number;
|
||||
}>();
|
||||
</script>
|
||||
@@ -11,7 +11,66 @@
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<form @submit.prevent="() => addGame()">
|
||||
<SelectorGame v-model="currentGame" :search="search" />
|
||||
<Listbox v-model="currentGame" as="div">
|
||||
<ListboxLabel
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>{{ $t("library.admin.import.selectGameSearch") }}</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<GameSearchResultWidget
|
||||
v-if="currentGame"
|
||||
:game="currentGame"
|
||||
/>
|
||||
<span v-else class="block truncate text-zinc-600">
|
||||
{{ $t("library.admin.import.selectGamePlaceholder") }}
|
||||
</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="result in metadataGames"
|
||||
:key="result.id"
|
||||
v-slot="{ active }"
|
||||
as="template"
|
||||
:value="result"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<GameSearchResultWidget :game="result" />
|
||||
</li>
|
||||
</ListboxOption>
|
||||
<p
|
||||
v-if="metadataGames.length == 0"
|
||||
class="w-full text-center p-2 uppercase font-display text-zinc-700 font-bold"
|
||||
>
|
||||
{{ $t("library.admin.metadata.companies.addGame.noGames") }}
|
||||
</p>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
<div class="mt-6 flex items-center justify-between gap-3">
|
||||
<label
|
||||
id="published-label"
|
||||
@@ -104,11 +163,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import type { GameModel } from "~/prisma/client/models";
|
||||
import { DialogTitle } from "@headlessui/vue";
|
||||
import {
|
||||
DialogTitle,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
import { FetchError } from "ofetch";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
|
||||
const props = defineProps<{
|
||||
companyId: string;
|
||||
@@ -123,11 +189,26 @@ const emit = defineEmits<{
|
||||
];
|
||||
}>();
|
||||
|
||||
const games = await $dropFetch("/api/v1/admin/game");
|
||||
const metadataGames = computed(() =>
|
||||
games
|
||||
.filter((e) => !(props.exclude ?? []).includes(e.id))
|
||||
.map(
|
||||
(e) =>
|
||||
({
|
||||
id: e.id,
|
||||
name: e.mName,
|
||||
icon: useObject(e.mIconObjectId),
|
||||
description: e.mShortDescription,
|
||||
}) satisfies Omit<GameMetadataSearchResult, "year">,
|
||||
),
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const open = defineModel<boolean>({ required: true });
|
||||
|
||||
const currentGame = ref<GameMetadataSearchResult>();
|
||||
const currentGame = ref<(typeof metadataGames.value)[number]>();
|
||||
const developed = ref(false);
|
||||
const published = ref(false);
|
||||
const addGameLoading = ref(false);
|
||||
@@ -162,10 +243,4 @@ async function addGame() {
|
||||
open.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function search(query: string) {
|
||||
return await $dropFetch("/api/v1/admin/search/game?type=Game", {
|
||||
query: { q: query },
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -59,6 +59,7 @@ const emit = defineEmits<{
|
||||
|
||||
const open = defineModel<boolean>({ required: true });
|
||||
|
||||
const { t } = useI18n();
|
||||
const collectionName = ref("");
|
||||
const createCollectionLoading = ref(false);
|
||||
const collections = await useCollections();
|
||||
@@ -73,7 +74,6 @@ async function createCollection() {
|
||||
const response = await $dropFetch("/api/v1/collection", {
|
||||
method: "POST",
|
||||
body: { name: collectionName.value },
|
||||
failTitle: "Failed to create collection",
|
||||
});
|
||||
|
||||
// Add the game if provided
|
||||
@@ -83,7 +83,6 @@ async function createCollection() {
|
||||
>(`/api/v1/collection/${response.id}/entry`, {
|
||||
method: "POST",
|
||||
body: { id: props.gameId },
|
||||
failTitle: "Failed to add game to collection",
|
||||
});
|
||||
response.entries.push(entry);
|
||||
}
|
||||
@@ -95,6 +94,20 @@ async function createCollection() {
|
||||
open.value = false;
|
||||
|
||||
emit("created", response.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to create collection:", error);
|
||||
|
||||
const err = error as { statusMessage?: string };
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: t("errors.library.collection.create.title"),
|
||||
description: t("errors.library.collection.create.desc", [
|
||||
err?.statusMessage ?? t("errors.unknown"),
|
||||
]),
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
} finally {
|
||||
createCollectionLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<ModalTemplate v-model="open">
|
||||
<template #default>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-white">
|
||||
{{ $t("library.admin.metadata.companies.modals.createTitle") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-zinc-400 text-sm">
|
||||
{{ $t("library.admin.metadata.companies.modals.createDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<form class="space-y-4" @submit.prevent="() => createCompany()">
|
||||
<div>
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm/6 font-medium text-zinc-100"
|
||||
>{{
|
||||
$t("library.admin.metadata.companies.modals.createFieldName")
|
||||
}}</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="name"
|
||||
v-model="companyName"
|
||||
type="text"
|
||||
name="name"
|
||||
:placeholder="
|
||||
$t(
|
||||
'library.admin.metadata.companies.modals.createFieldNamePlaceholder',
|
||||
)
|
||||
"
|
||||
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="description"
|
||||
class="block text-sm/6 font-medium text-zinc-100"
|
||||
>{{
|
||||
$t(
|
||||
"library.admin.metadata.companies.modals.createFieldDescription",
|
||||
)
|
||||
}}</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="description"
|
||||
v-model="companyDescription"
|
||||
type="text"
|
||||
name="description"
|
||||
:placeholder="
|
||||
$t(
|
||||
'library.admin.metadata.companies.modals.createFieldDescriptionPlaceholder',
|
||||
)
|
||||
"
|
||||
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="website"
|
||||
class="block text-sm/6 font-medium text-zinc-100"
|
||||
>{{
|
||||
$t("library.admin.metadata.companies.modals.createFieldWebsite")
|
||||
}}</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="website"
|
||||
v-model="companyWebsite"
|
||||
type="text"
|
||||
name="website"
|
||||
:placeholder="
|
||||
$t(
|
||||
'library.admin.metadata.companies.modals.createFieldWebsitePlaceholder',
|
||||
)
|
||||
"
|
||||
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="hidden" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #buttons="{ close }">
|
||||
<LoadingButton
|
||||
:loading="loading"
|
||||
:disabled="!companyValid"
|
||||
class="w-full sm:w-fit"
|
||||
@click="() => createCompany()"
|
||||
>
|
||||
{{ $t("common.create") }}
|
||||
</LoadingButton>
|
||||
<button
|
||||
ref="cancelButtonRef"
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="() => close()"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CompanyModel } from "~/prisma/client/models";
|
||||
|
||||
const open = defineModel<boolean>({ required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
created: [company: CompanyModel];
|
||||
}>();
|
||||
|
||||
const companyName = ref("");
|
||||
const companyDescription = ref("");
|
||||
const companyWebsite = ref("");
|
||||
|
||||
const loading = ref(false);
|
||||
const companyValid = computed(
|
||||
() => companyName.value && companyDescription.value,
|
||||
);
|
||||
async function createCompany() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const newCompany = await $dropFetch("/api/v1/admin/company", {
|
||||
method: "POST",
|
||||
body: {
|
||||
name: companyName.value,
|
||||
description: companyDescription.value,
|
||||
website: companyWebsite.value,
|
||||
},
|
||||
failTitle: "Failed to create new company",
|
||||
});
|
||||
open.value = false;
|
||||
emit("created", newCompany);
|
||||
} finally {
|
||||
/* empty */
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -1,267 +0,0 @@
|
||||
<template>
|
||||
<ModalTemplate v-model="model" size-class="max-w-3xl">
|
||||
<template #default>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>{{ $t("account.token.name") }}</label
|
||||
>
|
||||
<p class="text-zinc-400 block text-xs font-medium leading-6">
|
||||
{{ $t("account.token.nameDesc") }}
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="name"
|
||||
v-model="name"
|
||||
name="name"
|
||||
type="text"
|
||||
autocomplete="name"
|
||||
:placeholder="
|
||||
props.suggestedName ?? $t('account.token.namePlaceholder')
|
||||
"
|
||||
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Listbox v-model="expiryKey" as="div">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100">{{
|
||||
$t("users.admin.simple.inviteExpiryLabel")
|
||||
}}</ListboxLabel>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate">{{ expiryKey }}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="[label] in Object.entries(expiry)"
|
||||
:key="label"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
:value="label"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ label }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>{{ $t("account.token.acls") }}</label
|
||||
>
|
||||
<p class="text-zinc-400 block text-xs font-medium leading-6">
|
||||
{{ $t("account.token.aclsDesc") }}
|
||||
</p>
|
||||
<fieldset class="divide-y divide-zinc-700">
|
||||
<div
|
||||
v-for="[sectionName, sectionAcls] in Object.entries(
|
||||
aclsBySection,
|
||||
)"
|
||||
:key="sectionName"
|
||||
class="grid lg:grid-cols-3 gap-1 py-3"
|
||||
>
|
||||
<div
|
||||
v-for="[acl, description] in Object.entries(sectionAcls)"
|
||||
:key="acl"
|
||||
class="flex gap-3"
|
||||
>
|
||||
<div class="flex h-6 shrink-0 items-center">
|
||||
<div class="group grid size-4 grid-cols-1">
|
||||
<input
|
||||
:id="`acl-${acl}`"
|
||||
v-model="currentACLs[acl]"
|
||||
aria-describedby="acl-description"
|
||||
:name="`acl-${acl}`"
|
||||
type="checkbox"
|
||||
class="col-start-1 row-start-1 appearance-none rounded-sm border checked:border-blue-600 checked:bg-blue-600 indeterminate:border-blue-600 indeterminate:bg-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 border-white/10 bg-white/5 dark:checked:border-blue-500 dark:checked:bg-blue-500 dark:indeterminate:border-blue-500 dark:indeterminate:bg-blue-500 dark:focus-visible:outline-blue-500 dark:disabled:border-white/5 dark:disabled:bg-white/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
||||
/>
|
||||
<svg
|
||||
class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-disabled:stroke-white/25"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
class="opacity-0 group-has-checked:opacity-100"
|
||||
d="M3 8L6 11L11 3.5"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
class="opacity-0 group-has-indeterminate:opacity-100"
|
||||
d="M3 7H11"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm/6">
|
||||
<label
|
||||
:for="`acl-${acl}`"
|
||||
class="font-display font-medium text-white"
|
||||
>{{ acl }}</label
|
||||
>
|
||||
{{ " " }}
|
||||
<span id="acl-description" class="text-xs text-zinc-400"
|
||||
><span class="sr-only">{{ acl }} </span
|
||||
>{{ description }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<LoadingButton :loading="props.loading" @click="() => createToken()">
|
||||
{{ $t("common.create") }}
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
@click="() => cancel()"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/24/outline";
|
||||
import type { DurationLike } from "luxon";
|
||||
|
||||
// Reuse for both admin and user tokens
|
||||
|
||||
const model = defineModel<boolean>({ required: true });
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
acls: { [key: string]: string };
|
||||
loading?: boolean;
|
||||
suggestedAcls?: string[];
|
||||
suggestedName?: string;
|
||||
}>();
|
||||
|
||||
// Label to parameters to moment.js .add()
|
||||
const expiry: Record<string, DurationLike | undefined> = {
|
||||
[t("account.token.expiryMonth")]: {
|
||||
month: 1,
|
||||
},
|
||||
[t("account.token.expiry3Month")]: {
|
||||
month: 3,
|
||||
},
|
||||
[t("account.token.expiry6Month")]: {
|
||||
month: 6,
|
||||
},
|
||||
[t("account.token.expiryYear")]: {
|
||||
year: 1,
|
||||
},
|
||||
[t("account.token.expiry5Year")]: {
|
||||
year: 5,
|
||||
},
|
||||
[t("account.token.noExpiry")]: undefined,
|
||||
};
|
||||
const expiryKey = ref<keyof typeof expiry>(Object.keys(expiry)[0]); // Cast to any because we just know it's okay
|
||||
const name = ref(props.suggestedName ?? "");
|
||||
const currentACLs = ref<{ [key: string]: boolean }>(
|
||||
Object.fromEntries((props.suggestedAcls ?? []).map((v) => [v, true])),
|
||||
);
|
||||
|
||||
const aclsBySection = computed(() => {
|
||||
const sections: { [key: string]: { [key: string]: string } } = {};
|
||||
for (const [acl, description] of Object.entries(props.acls)) {
|
||||
const section = acl.split(":")[0];
|
||||
sections[section] ??= {};
|
||||
sections[section][acl] = description;
|
||||
}
|
||||
return sections;
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
create: [name: string, acls: string[], expiry: DurationLike | undefined];
|
||||
}>();
|
||||
|
||||
function createToken() {
|
||||
emit(
|
||||
"create",
|
||||
name.value,
|
||||
Object.entries(currentACLs.value)
|
||||
.filter(([_acl, enabled]) => enabled)
|
||||
.map(([acl, _enabled]) => acl),
|
||||
expiry[expiryKey.value],
|
||||
);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
model.value = false;
|
||||
}
|
||||
|
||||
watch(model, (c) => {
|
||||
if (!c) {
|
||||
name.value = "";
|
||||
currentACLs.value = {};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -22,7 +22,7 @@
|
||||
class="bg-red-600 text-white hover:bg-red-500"
|
||||
@click="() => deleteCollection()"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
{{ $t("delete") }}
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
class="bg-red-600 text-white hover:bg-red-500"
|
||||
@click="() => deleteArticle()"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
{{ $t("delete") }}
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
class="bg-red-600 text-white hover:bg-red-500"
|
||||
@click="() => deleteUser()"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
{{ $t("delete") }}
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
<template>
|
||||
<ModalTemplate v-model="open">
|
||||
<template #default>
|
||||
<div>
|
||||
<h1 as="h3" class="text-lg font-medium leading-6 text-white">
|
||||
{{ $t("library.admin.launchSelector.title") }}
|
||||
</h1>
|
||||
<p class="mt-1 text-zinc-400 text-sm">
|
||||
{{ $t("library.admin.launchSelector.description") }}
|
||||
</p>
|
||||
<div
|
||||
v-if="props.filterPlatform"
|
||||
class="inline-flex items-center mt-2 gap-x-4"
|
||||
>
|
||||
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
|
||||
{{ $t("library.admin.launchSelector.platformFilterHint") }}
|
||||
</h1>
|
||||
<span class="flex items-center">
|
||||
<component
|
||||
:is="PLATFORM_ICONS[props.filterPlatform]"
|
||||
alt=""
|
||||
class="size-5 flex-shrink-0 text-blue-600"
|
||||
/>
|
||||
<span class="ml-2 block truncate text-zinc-100 text-sm font-bold">{{
|
||||
props.filterPlatform
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 space-y-4">
|
||||
<div>
|
||||
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
|
||||
{{ $t("library.admin.launchSelector.search") }}
|
||||
</h1>
|
||||
<SelectorGame
|
||||
:search="search"
|
||||
:model-value="game"
|
||||
class="w-full mt-2"
|
||||
@update:model-value="(value) => updateGame(value)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="versions !== undefined && Object.entries(versions).length == 0"
|
||||
class="text-zinc-300 text-sm font-bold font-display uppercase text-center w-full"
|
||||
>
|
||||
{{ $t("library.admin.launchSelector.noVersions") }}
|
||||
</div>
|
||||
<div v-else-if="versions !== undefined">
|
||||
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
|
||||
{{ $t("library.admin.launchSelector.selectVersions") }}
|
||||
</h1>
|
||||
<SelectorCombox
|
||||
:search="
|
||||
(v) =>
|
||||
Object.values(versions!)
|
||||
.filter((k) =>
|
||||
(k.displayName || k.versionPath)!
|
||||
.toLowerCase()
|
||||
.includes(v.toLowerCase()),
|
||||
)
|
||||
.map((v) => ({
|
||||
id: v.versionId,
|
||||
name: (v.displayName ?? v.versionPath)!,
|
||||
}))
|
||||
"
|
||||
:display="(v) => v.name"
|
||||
:model-value="version"
|
||||
class="w-full mt-2"
|
||||
@update:model-value="updateVersion"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
{{ value.name }}
|
||||
</template>
|
||||
</SelectorCombox>
|
||||
</div>
|
||||
<div v-if="versions && version">
|
||||
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
|
||||
{{ $t("library.admin.launchSelector.selectCommand") }}
|
||||
</h1>
|
||||
<SelectorCombox
|
||||
:search="
|
||||
(v) =>
|
||||
versions![version!.id].launches
|
||||
.filter(
|
||||
(k) =>
|
||||
(k.name || k.command)
|
||||
.toLowerCase()
|
||||
.includes(v.toLowerCase()) &&
|
||||
(props.filterPlatform
|
||||
? k.platform == props.filterPlatform
|
||||
: true),
|
||||
)
|
||||
.map((v) => ({
|
||||
id: v.launchId,
|
||||
...v,
|
||||
}))
|
||||
"
|
||||
:display="(v) => v.name"
|
||||
:model-value="launchId"
|
||||
class="w-full mt-2"
|
||||
@update:model-value="(v) => (launchId = v)"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-zinc-300 text-sm">
|
||||
{{ value.name }}
|
||||
</span>
|
||||
<span class="text-zinc-400 text-xs">{{ value.command }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</SelectorCombox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ error }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<LoadingButton :loading="false" :disabled="!launchId" @click="submit">
|
||||
{{ $t("common.select") }}
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
@click="() => (open = false)"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import type { EmulatorLaunchObject } from "~/composables/frontend";
|
||||
import type { Platform } from "~/prisma/client/enums";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
|
||||
const props = defineProps<{ filterPlatform?: Platform }>();
|
||||
|
||||
const open = defineModel<boolean>({ required: true });
|
||||
|
||||
const error = ref<string | undefined>();
|
||||
const game = ref<GameMetadataSearchResult | undefined>(undefined);
|
||||
const version = ref<{ id: string; name: string } | undefined>(undefined);
|
||||
const launchId = ref<
|
||||
{ id: string; name: string; command: string; platform: Platform } | undefined
|
||||
>(undefined);
|
||||
|
||||
const versions = ref<
|
||||
| {
|
||||
[key: string]: {
|
||||
displayName: string | null;
|
||||
launches: {
|
||||
launchId: string;
|
||||
command: string;
|
||||
name: string;
|
||||
platform: Platform;
|
||||
}[];
|
||||
versionId: string;
|
||||
versionPath: string | null;
|
||||
};
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [data: EmulatorLaunchObject];
|
||||
}>();
|
||||
|
||||
async function search(query: string) {
|
||||
return await $dropFetch("/api/v1/admin/search/game", {
|
||||
query: { q: query, type: "Emulator" },
|
||||
});
|
||||
}
|
||||
|
||||
function updateGame(value: GameMetadataSearchResult | undefined) {
|
||||
if (game.value !== value || value == undefined) {
|
||||
version.value = undefined;
|
||||
versions.value = undefined;
|
||||
launchId.value = undefined;
|
||||
}
|
||||
|
||||
game.value = value;
|
||||
|
||||
if (game.value) fetchVersions();
|
||||
}
|
||||
|
||||
async function fetchVersions() {
|
||||
const newVersions = await $dropFetch("/api/v1/admin/game/:id/versions", {
|
||||
params: { id: game.value!.id },
|
||||
failTitle: "Failed to fetch versions for launch picker",
|
||||
});
|
||||
versions.value = Object.fromEntries(newVersions.map((v) => [v.versionId, v]));
|
||||
}
|
||||
|
||||
function updateVersion(v: typeof version.value) {
|
||||
if (version.value !== v || v == undefined) {
|
||||
launchId.value = undefined;
|
||||
}
|
||||
version.value = v;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
emit("select", {
|
||||
launchId: launchId.value!.id,
|
||||
gameName: game.value!.name,
|
||||
gameIcon: game.value!.icon,
|
||||
versionName: version.value!.name,
|
||||
launchName: launchId.value!.name,
|
||||
platform: launchId.value!.platform,
|
||||
});
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
watch(open, () => {
|
||||
game.value = undefined;
|
||||
updateGame(game.value);
|
||||
});
|
||||
</script>
|
||||
@@ -34,12 +34,12 @@
|
||||
<ComboboxInput
|
||||
class="block w-full rounded-md bg-zinc-900 py-1.5 pr-12 pl-3 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-500 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
:display-value="(item) => (item as StoreSortOption)?.name"
|
||||
:placeholder="$t('common.components.multiitem.placeholder')"
|
||||
placeholder="Start typing..."
|
||||
@change="search = $event.target.value"
|
||||
@blur="search = ''"
|
||||
/>
|
||||
<ComboboxButton
|
||||
class="absolute inset-0 flex items-center justify-end rounded-r-md px-2 focus:outline-hidden"
|
||||
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-hidden"
|
||||
>
|
||||
<ChevronDownIcon class="size-5 text-gray-400" aria-hidden="true" />
|
||||
</ComboboxButton>
|
||||
@@ -68,51 +68,7 @@
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<ComboboxOption
|
||||
v-if="$props.create"
|
||||
v-slot="{ active }"
|
||||
:value="CREATE_PREFIX + search"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-hidden'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span class="block truncate">
|
||||
{{ $t("common.components.multiitem.new", [search]) }}
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
|
||||
<div
|
||||
v-if="createLoading"
|
||||
class="absolute inset-0 bg-zinc-950 flex items-center justify-center"
|
||||
>
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-8 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">{{ $t("common.srLoading") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
@@ -129,7 +85,6 @@ import {
|
||||
} from "@headlessui/vue";
|
||||
const props = defineProps<{
|
||||
items: Array<StoreSortOption>;
|
||||
create?: (value: string) => Promise<string>;
|
||||
}>();
|
||||
|
||||
const model = defineModel<{ [key: string]: boolean }>();
|
||||
@@ -147,37 +102,7 @@ const enabledItems = computed(() =>
|
||||
props.items.filter((e) => model.value?.[e.param]),
|
||||
);
|
||||
|
||||
// I do not love how this works, but it's okay for now
|
||||
const CREATE_PREFIX = "CREATE";
|
||||
|
||||
const createLoading = ref(false);
|
||||
function add(item: string) {
|
||||
if (item.startsWith(CREATE_PREFIX)) {
|
||||
if (!props.create) return;
|
||||
const value = item.substring(CREATE_PREFIX.length);
|
||||
createLoading.value = true;
|
||||
props
|
||||
.create(value)
|
||||
.then(
|
||||
(result) => {
|
||||
add(result);
|
||||
},
|
||||
(err) => {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Failed to create value",
|
||||
description: err,
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
},
|
||||
)
|
||||
.finally(() => {
|
||||
createLoading.value = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
search.value = "";
|
||||
model.value ??= {};
|
||||
model.value[item] = true;
|
||||
@@ -24,6 +24,7 @@
|
||||
>
|
||||
{{ name }}
|
||||
</NuxtLink>
|
||||
<!-- todo -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex shrink-0">
|
||||
@@ -43,12 +44,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { NotificationModel } from "~/prisma/client/models";
|
||||
|
||||
const props = defineProps<{
|
||||
notification: SerializeObject<NotificationModel>;
|
||||
}>();
|
||||
const props = defineProps<{ notification: NotificationModel }>();
|
||||
|
||||
async function deleteMe() {
|
||||
await $dropFetch(`/api/v1/notifications/:id`, {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<h2 v-if="title" class="text-lg mb-4 w-full">{{ title }}</h2>
|
||||
<div class="flex">
|
||||
<div class="flex flex-col md:flex-row xl:gap-4 mx-auto">
|
||||
<div class="relative flex max-w-[12rem] my-auto min-w-50">
|
||||
<svg class="aspect-square grow relative inline" viewBox="0 0 100 100">
|
||||
<PieChartPieSlice
|
||||
v-for="slice in slices"
|
||||
:key="`${slice.percentage}-${slice.totalPercentage}`"
|
||||
:slice="slice"
|
||||
/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 bg-zinc-900 rounded-full m-12" />
|
||||
</div>
|
||||
<ul class="flex flex-col gap-y-1 m-auto text-left">
|
||||
<li
|
||||
v-for="slice in slices"
|
||||
:key="slice.value"
|
||||
class="text-sm inline-flex items-center gap-x-1"
|
||||
>
|
||||
<span
|
||||
class="size-3 inline-block rounded-sm"
|
||||
:class="CHART_COLOURS[slice.color].bg"
|
||||
/>
|
||||
{{
|
||||
$t("common.labelValueColon", {
|
||||
label: slice.label,
|
||||
value: $n(slice.value),
|
||||
})
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { generateSlices } from "~/components/PieChart/utils";
|
||||
import type { SliceData } from "~/components/PieChart/types";
|
||||
|
||||
const { data, title = undefined } = defineProps<{
|
||||
data: SliceData[];
|
||||
title?: string | undefined;
|
||||
}>();
|
||||
|
||||
const slices = generateSlices(data);
|
||||
</script>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<path
|
||||
v-if="slice.percentage !== 0 && slice.percentage !== 100"
|
||||
:class="[CHART_COLOURS[slice.color].fill]"
|
||||
:d="`
|
||||
M ${slice.start}
|
||||
A ${slice.radius},${slice.radius} 0 ${getFlags(slice.percentage)} ${polarToCartesian(slice.center, slice.radius, percent2Degrees(slice.totalPercentage))}
|
||||
L ${slice.center}
|
||||
z
|
||||
`"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<circle
|
||||
v-if="slice.percentage === 100"
|
||||
:r="slice.radius"
|
||||
:cx="slice.center.x"
|
||||
:cy="slice.center.y"
|
||||
:class="[CHART_COLOURS[slice.color].fill]"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Slice } from "~/components/PieChart/types";
|
||||
import {
|
||||
getFlags,
|
||||
percent2Degrees,
|
||||
polarToCartesian,
|
||||
} from "~/components/PieChart/utils";
|
||||
import { CHART_COLOURS } from "~/utils/colors";
|
||||
|
||||
const { slice } = defineProps<{
|
||||
slice: Slice;
|
||||
}>();
|
||||
</script>
|
||||
Vendored
-19
@@ -1,19 +0,0 @@
|
||||
import type Tuple from "~/utils/tuple";
|
||||
import type { ChartColour } from "~/utils/colors";
|
||||
|
||||
export type Slice = {
|
||||
start: Tuple;
|
||||
center: Tuple;
|
||||
percentage: number;
|
||||
totalPercentage: number;
|
||||
radius: number;
|
||||
color: ChartColour;
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type SliceData = {
|
||||
value: number;
|
||||
color?: ChartColour;
|
||||
label: string;
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import Tuple from "~/utils/tuple";
|
||||
import type { Slice, SliceData } from "~/components/PieChart/types";
|
||||
import { sum, lastItem } from "~/utils/array";
|
||||
|
||||
export const START = new Tuple(50, 10);
|
||||
export const CENTER = new Tuple(50, 50);
|
||||
export const RADIUS = 40;
|
||||
|
||||
export const polarToCartesian = (
|
||||
center: Tuple,
|
||||
radius: number,
|
||||
angleInDegrees: number,
|
||||
) => {
|
||||
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180;
|
||||
const x = center.x + radius * Math.cos(angleInRadians);
|
||||
const y = center.y + radius * Math.sin(angleInRadians);
|
||||
return new Tuple(x, y);
|
||||
};
|
||||
|
||||
export const percent2Degrees = (percentage: number) => (360 * percentage) / 100;
|
||||
|
||||
export function generateSlices(data: SliceData[]): Slice[] {
|
||||
return data.reduce((accumulator, currentValue, index, array) => {
|
||||
const percentage =
|
||||
(currentValue.value * 100) / sum(array.map((slice) => slice.value));
|
||||
return [
|
||||
...accumulator,
|
||||
{
|
||||
start: accumulator.length
|
||||
? polarToCartesian(
|
||||
CENTER,
|
||||
RADIUS,
|
||||
percent2Degrees(lastItem(accumulator).totalPercentage),
|
||||
)
|
||||
: START,
|
||||
radius: RADIUS,
|
||||
percentage: percentage,
|
||||
totalPercentage:
|
||||
sum(accumulator.map((element) => element.percentage)) + percentage,
|
||||
center: CENTER,
|
||||
color: PIE_COLOURS[index % PIE_COLOURS.length],
|
||||
label: currentValue.label,
|
||||
value: currentValue.value,
|
||||
},
|
||||
];
|
||||
}, [] as Slice[]);
|
||||
}
|
||||
|
||||
export const getFlags = (percentage: number) =>
|
||||
percentage > 50 ? new Tuple(1, 1) : new Tuple(0, 1);
|
||||
@@ -32,7 +32,7 @@
|
||||
class="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-950 ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="[name, value] in values"
|
||||
v-for="[name, value] in Object.entries(values)"
|
||||
:key="value"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
@@ -82,11 +82,10 @@ import {
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { Platform } from "~/prisma/client/enums";
|
||||
|
||||
const model = defineModel<Platform | undefined>();
|
||||
const model = defineModel<PlatformClient | undefined>();
|
||||
|
||||
const typedModel = computed<Platform | null>({
|
||||
const typedModel = computed<PlatformClient | null>({
|
||||
get() {
|
||||
return model.value || null;
|
||||
},
|
||||
@@ -96,5 +95,5 @@ const typedModel = computed<Platform | null>({
|
||||
},
|
||||
});
|
||||
|
||||
const values = Object.entries(Platform);
|
||||
const values = Object.fromEntries(Object.entries(PlatformClient));
|
||||
</script>
|
||||
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'relative h-5 rounded-xl overflow-hidden',
|
||||
CHART_COLOURS[backgroundColor].bg,
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:style="{ width: `${percentage}%` }"
|
||||
:class="['transition-all h-full', CHART_COLOURS[color].bg]"
|
||||
/>
|
||||
<span
|
||||
class="absolute inset-0 flex items-center justify-center text-blue-200 text-sm font-bold font-display"
|
||||
>
|
||||
<!-- {{ $t("tasks.admin.progress", [Math.round(percentage * 10) / 10]) }} -->
|
||||
{{ $n(Math.round(percentage * 100) / 10000, "percent") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type ChartColour, CHART_COLOURS } from "~/utils/colors";
|
||||
const {
|
||||
percentage,
|
||||
color = "blue",
|
||||
backgroundColor = "zinc",
|
||||
} = defineProps<{
|
||||
percentage: number;
|
||||
color?: ChartColour;
|
||||
backgroundColor?: ChartColour;
|
||||
}>();
|
||||
</script>
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<table v-if="items.length > 0" class="w-full mt-4 space-y-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/10">
|
||||
<tr v-for="item in items" :key="`${item.rank}-${item.name}`">
|
||||
<td
|
||||
class="my-2 size-7 rounded-sm bg-zinc-950 ring ring-zinc-800 inline-flex items-center justify-center font-bold font-display text-blue-500"
|
||||
>
|
||||
{{ item.rank }}
|
||||
</td>
|
||||
<td class="w-full font-bold px-2">{{ item.name }}</td>
|
||||
<td
|
||||
class="text-right text-sm font-semibold text-zinc-500 whitespace-nowrap"
|
||||
>
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
v-else
|
||||
class="w-full p-2 text-center uppercase text-sm font-display font-bold text-zinc-700"
|
||||
>
|
||||
{{ $t("common.noData") }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
export type RankItem = {
|
||||
rank: number;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
const { items } = defineProps<{
|
||||
items: RankItem[];
|
||||
}>();
|
||||
</script>
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<Combobox
|
||||
as="div"
|
||||
nullable
|
||||
:immediate="true"
|
||||
:model-value="model"
|
||||
class="bg-zinc-800 rounded"
|
||||
@update:model-value="updateModelValue"
|
||||
>
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
:key="model?.id ?? 'off'"
|
||||
class="block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
placeholder="Start typing..."
|
||||
:display-value="(v) => (v ? props.display(v as T) : '')"
|
||||
@change="query = $event.target.value"
|
||||
@blur="query = ''"
|
||||
/>
|
||||
<ComboboxButton
|
||||
class="absolute inset-0 right-0 flex items-center justify-end rounded-r-md px-2 focus:outline-none"
|
||||
>
|
||||
<ChevronUpDownIcon class="size-5 text-gray-400" aria-hidden="true" />
|
||||
</ComboboxButton>
|
||||
|
||||
<ComboboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<div
|
||||
v-if="results.length == 0"
|
||||
class="text-zinc-300 uppercase font-display font-bold text-center p-4"
|
||||
>
|
||||
{{ $t("common.noResults") }}
|
||||
</div>
|
||||
<ComboboxOption
|
||||
v-for="result in results"
|
||||
v-else
|
||||
:key="result.id"
|
||||
v-slot="{ active, selected }"
|
||||
:value="result"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active ? 'bg-blue-600 text-white outline-none' : 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span>
|
||||
<slot :value="result" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends { id: string }">
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const props = defineProps<{
|
||||
search: (query: string) => T[];
|
||||
display: (value: T) => string;
|
||||
}>();
|
||||
|
||||
const model = defineModel<T | undefined>();
|
||||
const query = ref("");
|
||||
|
||||
const results = computed(() => props.search(query.value));
|
||||
|
||||
function updateModelValue(v: T) {
|
||||
model.value = v;
|
||||
}
|
||||
</script>
|
||||
@@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex gap-1 flex-wrap">
|
||||
<span
|
||||
v-for="extension in model"
|
||||
:key="extension"
|
||||
class="inline-flex items-center gap-x-0.5 rounded-md bg-blue-400/10 px-2 py-1 text-xs font-medium text-blue-400 inset-ring inset-ring-blue-400/30"
|
||||
>
|
||||
{{ extension }}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative -mr-1 size-3.5 rounded-xs hover:bg-blue-500/30"
|
||||
@click="() => removeFileExtension(extension)"
|
||||
>
|
||||
<span class="sr-only">{{ $t("common.remove") }}</span>
|
||||
<svg
|
||||
viewBox="0 0 14 14"
|
||||
class="size-3.5 stroke-blue-400 group-hover:stroke-blue-300"
|
||||
>
|
||||
<path d="M4 4l6 6m0-6l-6 6" />
|
||||
</svg>
|
||||
<span class="absolute -inset-1"></span>
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="model.length == 0" class="text-zinc-500 text-xs">{{
|
||||
$t("library.admin.fileExtSelector.noSelected")
|
||||
}}</span>
|
||||
</div>
|
||||
<Combobox
|
||||
as="div"
|
||||
nullable
|
||||
:immediate="true"
|
||||
:model-value="model"
|
||||
class="mt-2 bg-zinc-800 rounded"
|
||||
@update:model-value="addFileExtension"
|
||||
>
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
class="block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6 w-full"
|
||||
placeholder="Start typing..."
|
||||
:display-value="(_) => ''"
|
||||
@change="query = $event.target.value"
|
||||
@blur="query = ''"
|
||||
/>
|
||||
<ComboboxButton
|
||||
class="absolute inset-0 right-0 flex items-center justify-end rounded-r-md px-2 focus:outline-none"
|
||||
>
|
||||
<ChevronUpDownIcon class="size-5 text-gray-400" aria-hidden="true" />
|
||||
</ComboboxButton>
|
||||
|
||||
<ComboboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-if="query"
|
||||
v-slot="{ active, selected }"
|
||||
:value="query"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span>
|
||||
{{
|
||||
$t("library.admin.fileExtSelector.add", [normalize(query)])
|
||||
}}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const model = defineModel<string[]>({ required: true });
|
||||
|
||||
const query = ref("");
|
||||
|
||||
function normalize(v: string) {
|
||||
const k = v.toLowerCase().replaceAll(/[^a-zA-Z0-9]*/g, "");
|
||||
if (k.startsWith(".")) return k;
|
||||
return `.${k}`;
|
||||
}
|
||||
|
||||
function addFileExtension(raw: string) {
|
||||
const value = normalize(raw);
|
||||
if (model.value.includes(value)) return;
|
||||
model.value.push(value);
|
||||
}
|
||||
|
||||
function removeFileExtension(extension: string) {
|
||||
const index = model.value.findIndex((v) => v === extension);
|
||||
if (index == -1) return;
|
||||
model.value.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<Combobox
|
||||
v-model="currentResult"
|
||||
as="div"
|
||||
nullable
|
||||
class="bg-zinc-800 rounded"
|
||||
>
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
class="block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
placeholder="Start typing..."
|
||||
:display-value="(game) => (game as GameMetadataSearchResult)?.name"
|
||||
@change="gameSearchQuery = $event.target.value"
|
||||
@blur="gameSearchQuery = ''"
|
||||
/>
|
||||
<ComboboxButton
|
||||
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
||||
>
|
||||
<ChevronUpDownIcon class="size-5 text-gray-400" aria-hidden="true" />
|
||||
</ComboboxButton>
|
||||
|
||||
<ComboboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<div
|
||||
v-if="gameSearchQuery.length < 4"
|
||||
class="text-zinc-300 uppercase font-display font-bold text-center p-4"
|
||||
>
|
||||
{{ $t("library.admin.gameSelector.hint") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="resultsLoading || results === undefined"
|
||||
class="flex items-center justify-center p-2"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-8 h-8 text-transparent animate-spin fill-zinc-100"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="results.length == 0"
|
||||
class="text-zinc-500 uppercase font-display font-bold text-center p-4"
|
||||
>
|
||||
{{ $t("common.noResults") }}
|
||||
</div>
|
||||
<ComboboxOption
|
||||
v-for="result in results"
|
||||
v-else
|
||||
:key="result.id"
|
||||
v-slot="{ active, selected }"
|
||||
:value="result"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active ? 'bg-blue-600 text-white outline-none' : 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span>
|
||||
<GameSearchResultWidget :game="result" :raw-icon="false" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { ChevronUpDownIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
|
||||
const props = defineProps<{
|
||||
search: (query: string) => Promise<Array<GameMetadataSearchResult>>;
|
||||
}>();
|
||||
|
||||
const currentResult = defineModel<GameMetadataSearchResult | undefined>();
|
||||
const gameSearchQuery = ref("");
|
||||
|
||||
const resultsLoading = ref(false);
|
||||
const results = ref<Array<GameMetadataSearchResult>>();
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
watch(gameSearchQuery, async (v) => {
|
||||
if (v.length < 4) {
|
||||
results.value = [];
|
||||
resultsLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeout) clearTimeout(timeout);
|
||||
resultsLoading.value = true;
|
||||
timeout = setTimeout(async () => {
|
||||
results.value = await props.search(v);
|
||||
resultsLoading.value = false;
|
||||
timeout = undefined;
|
||||
}, 600);
|
||||
});
|
||||
</script>
|
||||
@@ -1,187 +0,0 @@
|
||||
<template>
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="min-w-full divide-y divide-zinc-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-3"
|
||||
>
|
||||
{{ $t("common.name") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("type") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.sources.working") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("options") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.sources.totalSpace") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.sources.freeSpace") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.sources.utilizationPercentage") }}
|
||||
</th>
|
||||
<th
|
||||
v-if="editSource || deleteSource"
|
||||
scope="col"
|
||||
class="relative py-3.5 pl-3 pr-4 sm:pr-3"
|
||||
>
|
||||
<span class="sr-only">{{ $t("actions") }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(source, sourceIdx) in sources" :key="source.id">
|
||||
<td
|
||||
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
|
||||
>
|
||||
{{ source.name }}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400 flex gap-x-1 items-center"
|
||||
>
|
||||
<component
|
||||
:is="optionsMetadata[source.backend].icon"
|
||||
class="size-5 text-zinc-400"
|
||||
/>
|
||||
{{ optionsMetadata[source.backend].title }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
<CheckIcon v-if="source.working" class="size-5 text-green-500" />
|
||||
<XMarkIcon v-else class="size-5 text-red-500" />
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
{{ source.options }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
{{ source.fsStats && formatBytes(source.fsStats.totalSpace) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
{{ source.fsStats && formatBytes(source.fsStats.freeSpace) }}
|
||||
</td>
|
||||
<td
|
||||
class="align-middle flex flex-cols-5 whitespace-nowrap px-3 py-4 text-sm text-zinc-400"
|
||||
>
|
||||
<div class="flex-auto content-right">
|
||||
<ProgressBar
|
||||
v-if="source.fsStats"
|
||||
:percentage="
|
||||
getPercentage(
|
||||
source.fsStats.totalSpace - source.fsStats.freeSpace,
|
||||
source.fsStats.totalSpace,
|
||||
)
|
||||
"
|
||||
:color="
|
||||
getBarColor(
|
||||
getPercentage(
|
||||
source.fsStats.totalSpace - source.fsStats.freeSpace,
|
||||
source.fsStats.totalSpace,
|
||||
),
|
||||
)
|
||||
"
|
||||
background-color="slate"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="editSource || deleteSource"
|
||||
class="relative whitespace-nowrap py-4 pl-3 pr-3 text-right text-sm font-medium space-x-2"
|
||||
>
|
||||
<button
|
||||
v-if="editSource"
|
||||
class="text-blue-500 hover:text-blue-400"
|
||||
@click="() => editSource(sourceIdx)"
|
||||
>
|
||||
{{ $t("common.edit") }}
|
||||
<span class="sr-only">
|
||||
{{ $t("chars.srComma", [source.name]) }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="deleteSource"
|
||||
class="text-red-500 hover:text-red-400"
|
||||
@click="() => deleteSource(sourceIdx)"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
<span class="sr-only">
|
||||
{{ $t("chars.srComma", [source.name]) }}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
|
||||
import type { LibraryBackend } from "~/prisma/client/enums";
|
||||
import { BackwardIcon, CheckIcon, XMarkIcon } from "@heroicons/vue/24/outline";
|
||||
import { DropLogo } from "#components";
|
||||
import { formatBytes } from "~/server/internal/utils/files";
|
||||
import { getBarColor } from "~/utils/colors";
|
||||
import { getPercentage } from "~/utils/utils";
|
||||
|
||||
const {
|
||||
sources,
|
||||
deleteSource = undefined,
|
||||
editSource = undefined,
|
||||
} = defineProps<{
|
||||
sources: WorkingLibrarySource[];
|
||||
summaryMode?: boolean;
|
||||
deleteSource?: (id: number) => void;
|
||||
editSource?: (id: number) => void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const optionsMetadata: {
|
||||
[key in LibraryBackend]: {
|
||||
title: string;
|
||||
description: string;
|
||||
docsLink: string;
|
||||
icon: Component;
|
||||
};
|
||||
} = {
|
||||
Filesystem: {
|
||||
title: t("library.admin.sources.fsTitle"),
|
||||
description: t("library.admin.sources.fsDesc"),
|
||||
docsLink: "https://docs.droposs.org/docs/library#drop-style",
|
||||
icon: DropLogo,
|
||||
},
|
||||
FlatFilesystem: {
|
||||
title: t("library.admin.sources.fsFlatTitle"),
|
||||
description: t("library.admin.sources.fsFlatDesc"),
|
||||
docsLink: "https://docs.droposs.org/docs/library#flat-style-or-compat",
|
||||
icon: BackwardIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,12 +1,3 @@
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"↓": "↓",
|
||||
"↑": "↑"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
@@ -127,7 +118,7 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<SelectorMultiItem
|
||||
<MultiItemSelector
|
||||
v-else
|
||||
v-model="[optionValues[section.param] as any][0]"
|
||||
:items="section.options"
|
||||
@@ -185,16 +176,9 @@
|
||||
active ? 'bg-zinc-900 outline-hidden' : '',
|
||||
'w-full text-left block px-4 py-2 text-sm',
|
||||
]"
|
||||
@click.prevent="handleSortClick(option, $event)"
|
||||
@click="() => (currentSort = option.param)"
|
||||
>
|
||||
{{ option.name }}
|
||||
<span v-if="currentSort === option.param">
|
||||
{{
|
||||
sortOrder === "asc"
|
||||
? $t("chars.arrowUp")
|
||||
: $t("chars.arrowDown")
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
@@ -295,7 +279,7 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<SelectorMultiItem
|
||||
<MultiItemSelector
|
||||
v-else
|
||||
v-model="[optionValues[section.param] as any][0]"
|
||||
:items="section.options"
|
||||
@@ -308,7 +292,7 @@
|
||||
<div
|
||||
v-if="games?.length ?? 0 > 0"
|
||||
ref="product-grid"
|
||||
class="col-span-4 grid gap-5 grid-cols-[repeat(auto-fill,minmax(150px,auto))]"
|
||||
class="relative lg:col-span-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4"
|
||||
>
|
||||
<!-- Your content -->
|
||||
<GamePanel
|
||||
@@ -376,10 +360,8 @@ import {
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { GameModel, GameTagModel } from "~/prisma/client/models";
|
||||
import { Platform } from "~/prisma/client/enums";
|
||||
const {
|
||||
store: { showGamePanelTextDecoration },
|
||||
} = await $dropFetch(`/api/v1/settings`);
|
||||
import MultiItemSelector from "./MultiItemSelector.vue";
|
||||
const { showGamePanelTextDecoration } = await $dropFetch(`/api/v1/settings`);
|
||||
|
||||
const mobileFiltersOpen = ref(false);
|
||||
|
||||
@@ -407,13 +389,8 @@ const sorts: Array<StoreSortOption> = [
|
||||
name: "Recently Added",
|
||||
param: "recent",
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
param: "name",
|
||||
},
|
||||
];
|
||||
const currentSort = ref(sorts[0].param);
|
||||
const sortOrder = ref<"asc" | "desc">("desc");
|
||||
|
||||
const options: Array<StoreFilterOption> = [
|
||||
...(tags.length > 0
|
||||
@@ -430,7 +407,7 @@ const options: Array<StoreFilterOption> = [
|
||||
name: "Platform",
|
||||
param: "platform",
|
||||
multiple: true,
|
||||
options: Object.values(Platform).map((e) => ({ name: e, param: e })),
|
||||
options: Object.values(PlatformClient).map((e) => ({ name: e, param: e })),
|
||||
},
|
||||
...(props.extraOptions ?? []),
|
||||
];
|
||||
@@ -489,7 +466,7 @@ async function updateGames(query: string, resetGames: boolean) {
|
||||
results: Array<SerializeObject<GameModel>>;
|
||||
count: number;
|
||||
}>(
|
||||
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}&order=${sortOrder.value}${query ? "&" + query : ""}`,
|
||||
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}${query ? "&" + query : ""}`,
|
||||
);
|
||||
if (resetGames) {
|
||||
games.value = newValues.results;
|
||||
@@ -506,19 +483,6 @@ watch(filterQuery, (newUrl) => {
|
||||
watch(currentSort, (_) => {
|
||||
updateGames(filterQuery.value, true);
|
||||
});
|
||||
watch(sortOrder, (_) => {
|
||||
updateGames(filterQuery.value, true);
|
||||
});
|
||||
|
||||
await updateGames(filterQuery.value, true);
|
||||
|
||||
function handleSortClick(option: StoreSortOption, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
if (currentSort.value === option.param) {
|
||||
sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
currentSort.value = option.param;
|
||||
sortOrder.value = option.param === "name" ? "asc" : "desc";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="task"
|
||||
class="flex w-full items-center justify-between space-x-6 p-6"
|
||||
>
|
||||
<div class="flex-1 truncate">
|
||||
<div class="flex items-center space-x-1">
|
||||
<div>
|
||||
<CheckCircleIcon v-if="task.success" class="size-5 text-green-600" />
|
||||
<XMarkIcon v-else-if="task.error" class="size-5 text-red-600" />
|
||||
<div
|
||||
v-else
|
||||
class="size-2 bg-blue-600 rounded-full animate-pulse m-1"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="truncate text-sm font-medium text-zinc-100">
|
||||
{{ task.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="active"
|
||||
class="mt-2 w-full rounded-full overflow-hidden bg-zinc-900"
|
||||
>
|
||||
<div
|
||||
:style="{ width: `${task.progress}%` }"
|
||||
class="bg-blue-600 h-[3px] transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 bg-zinc-950 px-2 pb-1 rounded-sm">
|
||||
<LogLine :short="true" :log="parseTaskLog(task.log.at(-1))" />
|
||||
</div>
|
||||
<ul v-if="task.actions" class="mt-1 flex flex-row gap-x-2">
|
||||
<NuxtLink
|
||||
v-for="[name, link] in task.actions.map((v) => v.split(':'))"
|
||||
:key="link"
|
||||
:href="link"
|
||||
class="text-xs text-zinc-100 bg-blue-900 p-1 rounded"
|
||||
>{{ name }}</NuxtLink
|
||||
>
|
||||
</ul>
|
||||
<NuxtLink
|
||||
type="button"
|
||||
:href="`/admin/task/${task.id}`"
|
||||
class="mt-3 ml-1 rounded-md text-xs font-medium text-zinc-100 hover:text-zinc-300 focus:outline-none focus:ring-2 focus:ring-zinc-100 focus:ring-offset-2"
|
||||
>
|
||||
<i18n-t keypath="tasks.admin.viewTask" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- renders server side when we don't want to access the current tasks -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
import type { TaskMessage } from "~/server/internal/tasks";
|
||||
|
||||
defineProps<{ task: TaskMessage | undefined; active?: boolean }>();
|
||||
</script>
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'border border-zinc-800 rounded-xl h-full px-6 py-4 relative bg-zinc-950/30',
|
||||
{ 'min-h-50 pb-15': link, 'lg:pb-4': !link },
|
||||
]"
|
||||
>
|
||||
<h1
|
||||
v-if="props.title"
|
||||
:class="[
|
||||
'font-semibold text-lg w-full',
|
||||
{ 'mb-3': !props.subtitle && link },
|
||||
]"
|
||||
>
|
||||
{{ props.title }}
|
||||
<div v-if="rightTitle" class="float-right">{{ props.rightTitle }}</div>
|
||||
</h1>
|
||||
<h2
|
||||
v-if="props.subtitle"
|
||||
:class="['text-zinc-400 text-sm w-full', { 'mb-3': link }]"
|
||||
>
|
||||
{{ props.subtitle }}
|
||||
<div v-if="rightTitle" class="float-right">{{ props.rightTitle }}</div>
|
||||
</h2>
|
||||
|
||||
<slot />
|
||||
|
||||
<div v-if="props.link" class="absolute bottom-5 right-5">
|
||||
<NuxtLink
|
||||
:to="props.link.url"
|
||||
class="transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
|
||||
>
|
||||
{{ props.link.label }}
|
||||
<ArrowRightIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArrowRightIcon } from "@heroicons/vue/20/solid";
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
rightTitle?: string;
|
||||
link?: {
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
}>();
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
{{ $t("drop.desc") }}
|
||||
</p>
|
||||
|
||||
<SelectorLanguage />
|
||||
<LanguageSelector />
|
||||
|
||||
<div class="flex space-x-6">
|
||||
<NuxtLink
|
||||
@@ -91,11 +91,10 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center xl:col-span-3 mt-8">
|
||||
<NuxtLink
|
||||
:to="`https://github.com/Drop-OSS/drop/releases/tag/${versionInfo.version}`"
|
||||
<p
|
||||
class="text-xs text-zinc-700 hover:text-zinc-400 transition-colors duration-200 cursor-default select-none"
|
||||
>
|
||||
<i18n-t keypath="footer.version" tag="span" scope="global">
|
||||
<i18n-t keypath="footer.version" tag="p" scope="global">
|
||||
<template #version>
|
||||
<span>{{ versionInfo.version }}</span>
|
||||
</template>
|
||||
@@ -103,7 +102,7 @@
|
||||
<span>{{ versionInfo.gitRef }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
>
|
||||
<div class="flex shrink-0 h-16 items-center justify-between">
|
||||
<NuxtLink :to="homepageURL">
|
||||
<ApplicationLogo class="h-8 w-auto" />
|
||||
<DropLogo class="h-8 w-auto" />
|
||||
</NuxtLink>
|
||||
|
||||
<UserHeaderUserWidget />
|
||||
|
||||
@@ -46,10 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { NotificationModel } from "~/prisma/client/models";
|
||||
|
||||
const props = defineProps<{
|
||||
notifications: Array<SerializeObject<NotificationModel>>;
|
||||
}>();
|
||||
const props = defineProps<{ notifications: Array<NotificationModel> }>();
|
||||
</script>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full bg-zinc-950 p-1 inline-flex items-center gap-x-2 fixed inset-x-0 top-0 z-100"
|
||||
>
|
||||
<button
|
||||
class="p-1 text-zinc-300 hover:text-zinc-100 hover:bg-zinc-900 transition-all rounded"
|
||||
@click="() => router.back()"
|
||||
>
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1 text-zinc-300 hover:text-zinc-100 hover:bg-zinc-900 transition-all rounded"
|
||||
@click="() => router.forward()"
|
||||
>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</button>
|
||||
<span class="text-zinc-400 text-sm">
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref("Loading...");
|
||||
|
||||
onMounted(() => {
|
||||
title.value = document.title;
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
title.value = "Loading...";
|
||||
// TODO: more robust after-render "detection"
|
||||
setTimeout(() => {
|
||||
title.value = document.title;
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { SystemData } from "~/server/internal/system-data";
|
||||
|
||||
const ws = new WebSocketHandler("/api/v1/admin/system-data/ws");
|
||||
|
||||
export const useSystemData = () =>
|
||||
useState<SerializeObject<SystemData>>(
|
||||
"system-data",
|
||||
(): SystemData => ({
|
||||
totalRam: 0,
|
||||
freeRam: 0,
|
||||
cpuLoad: 0,
|
||||
cpuCores: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
ws.listen((systemDataString) => {
|
||||
const data = JSON.parse(systemDataString) as SerializeObject<SystemData>;
|
||||
const systemData = useSystemData();
|
||||
systemData.value = data;
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
import type { NavigationItem } from "./types";
|
||||
|
||||
export const useCurrentNavigationIndex = (
|
||||
@@ -8,7 +9,7 @@ export const useCurrentNavigationIndex = (
|
||||
|
||||
const currentNavigation = ref(-1);
|
||||
|
||||
function calculateCurrentNavIndex(to: typeof route) {
|
||||
function calculateCurrentNavIndex(to: RouteLocationNormalized) {
|
||||
const validOptions = navigation
|
||||
.map((e, i) => ({ ...e, index: i }))
|
||||
.filter((e) => to.fullPath.startsWith(e.prefix));
|
||||
|
||||
Vendored
-22
@@ -1,22 +0,0 @@
|
||||
import type {
|
||||
ComponentCustomOptions as _ComponentCustomOptions,
|
||||
ComponentCustomProperties as _ComponentCustomProperties,
|
||||
} from "vue";
|
||||
import type { Platform } from "~/prisma/client/enums";
|
||||
|
||||
declare module "@vue/runtime-core" {
|
||||
interface ComponentCustomProperties extends _ComponentCustomProperties {
|
||||
$t: (key: string, ...args: unknown[]) => string;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface ComponentCustomOptions extends _ComponentCustomOptions {}
|
||||
}
|
||||
|
||||
export interface EmulatorLaunchObject {
|
||||
launchId: string;
|
||||
gameName: string;
|
||||
gameIcon: string;
|
||||
versionName: string;
|
||||
launchName: string;
|
||||
platform: Platform;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IconsLinuxLogo, IconsWindowsLogo, IconsMacLogo } from "#components";
|
||||
import { Platform } from "~/prisma/client/enums";
|
||||
import { PlatformClient } from "./types";
|
||||
|
||||
export const PLATFORM_ICONS = {
|
||||
[Platform.Linux]: IconsLinuxLogo,
|
||||
[Platform.Windows]: IconsWindowsLogo,
|
||||
[Platform.macOS]: IconsMacLogo,
|
||||
[PlatformClient.Linux]: IconsLinuxLogo,
|
||||
[PlatformClient.Windows]: IconsWindowsLogo,
|
||||
[PlatformClient.macOS]: IconsMacLogo,
|
||||
};
|
||||
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
declare module "kjua";
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { NotificationModel } from "~/prisma/client/models";
|
||||
|
||||
const ws = new WebSocketHandler("/api/v1/notifications/ws");
|
||||
|
||||
export const useNotifications = () =>
|
||||
useState<Array<SerializeObject<NotificationModel>>>(
|
||||
"notifications",
|
||||
() => [],
|
||||
);
|
||||
useState<Array<NotificationModel>>("notifications", () => []);
|
||||
|
||||
ws.listen((e) => {
|
||||
const notification = JSON.parse(e) as SerializeObject<NotificationModel>;
|
||||
const notification = JSON.parse(e) as NotificationModel;
|
||||
const notifications = useNotifications();
|
||||
notifications.value.push(notification);
|
||||
});
|
||||
|
||||
+26
-40
@@ -16,7 +16,7 @@ interface DropFetch<
|
||||
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
|
||||
>(
|
||||
request: R,
|
||||
opts?: O & { failTitle?: string; params?: { [key: string]: string } },
|
||||
opts?: O & { failTitle?: string },
|
||||
): Promise<
|
||||
// sometimes there is an error, other times there isn't
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -46,28 +46,10 @@ export const $dropFetch: DropFetch = async (rawRequest, opts) => {
|
||||
});
|
||||
const request = requestParts.join("/");
|
||||
|
||||
// If not in setup
|
||||
if (!getCurrentInstance()?.proxy) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Excessive stack depth comparing types
|
||||
return await $fetch(request, opts);
|
||||
} catch (e) {
|
||||
if (import.meta.client && opts?.failTitle) {
|
||||
console.warn(e);
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: opts.failTitle,
|
||||
description:
|
||||
(e as FetchError)?.data?.message ?? (e as string).toString(),
|
||||
//buttonText: $t("common.close"),
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Excessive stack depth comparing types
|
||||
return await $fetch(request, opts);
|
||||
}
|
||||
|
||||
const id = request.toString();
|
||||
@@ -82,22 +64,26 @@ export const $dropFetch: DropFetch = async (rawRequest, opts) => {
|
||||
}
|
||||
|
||||
const headers = useRequestHeaders(["cookie", "authorization"]);
|
||||
const data = await $fetch(request, {
|
||||
...opts,
|
||||
headers: { ...headers, ...opts?.headers },
|
||||
});
|
||||
if (import.meta.server) state.value = data;
|
||||
return data;
|
||||
};
|
||||
|
||||
export function isClientRequest() {
|
||||
const existingState = useState("clientMode", () => false);
|
||||
if (import.meta.server) {
|
||||
const headers = useRequestHeaders(["User-Agent"]);
|
||||
const calculatedClientRequest =
|
||||
headers["user-agent"] == "Drop Desktop Client";
|
||||
existingState.value = calculatedClientRequest;
|
||||
try {
|
||||
const data = await $fetch(request, {
|
||||
...opts,
|
||||
headers: { ...headers, ...opts?.headers },
|
||||
});
|
||||
if (import.meta.server) state.value = data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
if (import.meta.client && opts?.failTitle) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: opts.failTitle,
|
||||
description:
|
||||
(e as FetchError)?.statusMessage ?? (e as string).toString(),
|
||||
buttonText: $t("common.close"),
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return existingState.value;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,7 +52,6 @@ websocketHandler.listen((message) => {
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
log: [],
|
||||
actions: [],
|
||||
};
|
||||
state.value.error = { title, description };
|
||||
break;
|
||||
|
||||
@@ -11,3 +11,9 @@ export type QuickActionNav = {
|
||||
notifications?: Ref<number>;
|
||||
action: () => Promise<void>;
|
||||
};
|
||||
|
||||
export enum PlatformClient {
|
||||
Windows = "Windows",
|
||||
Linux = "Linux",
|
||||
macOS = "macOS",
|
||||
}
|
||||
|
||||
@@ -11,12 +11,3 @@ export const updateUser = async () => {
|
||||
|
||||
user.value = await $dropFetch<UserModel | null>("/api/v1/user");
|
||||
};
|
||||
|
||||
export async function completeSignin() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const user = useUser();
|
||||
user.value = await $dropFetch<UserModel | null>("/api/v1/user");
|
||||
router.push(route.query.redirect?.toString() ?? "/");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ services:
|
||||
postgres:
|
||||
# using alpine image to reduce image size
|
||||
image: postgres:alpine
|
||||
ports:
|
||||
- 5432:5432
|
||||
healthcheck:
|
||||
test: pg_isready -d drop -U drop
|
||||
interval: 30s
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
user: "1000:1000"
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- ../.data/db:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=drop
|
||||
- POSTGRES_USER=drop
|
||||
- POSTGRES_DB=drop
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
||||
+1
-1
Submodule drop-base updated: dad3487be6...04125e89be
@@ -10,10 +10,10 @@ const props = defineProps({
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const user = useUser();
|
||||
const statusCode = props.error?.statusCode;
|
||||
const message = props.error?.data
|
||||
? JSON.parse(props.error.data as string).message
|
||||
: props.error.cause || props.error?.statusMessage || t("errors.unknown");
|
||||
const message =
|
||||
props.error?.message || props.error?.statusMessage || t("errors.unknown");
|
||||
const showSignIn = statusCode ? statusCode == 403 || statusCode == 401 : false;
|
||||
|
||||
async function signIn() {
|
||||
@@ -21,6 +21,11 @@ async function signIn() {
|
||||
redirect: `/auth/signin?redirect=${encodeURIComponent(route.fullPath)}`,
|
||||
});
|
||||
}
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
case 403:
|
||||
await signIn();
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: t("errors.pageTitle", [statusCode ?? message]),
|
||||
@@ -38,13 +43,13 @@ if (import.meta.client) {
|
||||
<header
|
||||
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
|
||||
>
|
||||
<ApplicationLogo class="h-10 w-auto sm:h-12" />
|
||||
<DropLogo class="h-10 w-auto sm:h-12" />
|
||||
</header>
|
||||
<main
|
||||
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
|
||||
>
|
||||
<div class="max-w-lg">
|
||||
<p class="text-base font-semibold leading-8 text-red-600">
|
||||
<p class="text-base font-semibold leading-8 text-blue-600">
|
||||
{{ error?.statusCode }}
|
||||
</p>
|
||||
<h1
|
||||
@@ -58,16 +63,15 @@ if (import.meta.client) {
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
|
||||
<p class="mt-6 text-base leading-7 text-zinc-400">
|
||||
{{ $t("errors.occurred") }}
|
||||
</p>
|
||||
<!-- <p>{{ error. }}</p> -->
|
||||
<div class="mt-10">
|
||||
<!-- clearError is inconsistent so reload app to clear erro -->
|
||||
<a
|
||||
v-if="!showSignIn"
|
||||
href="/"
|
||||
<!-- full app reload to fix errors -->
|
||||
<NuxtLink
|
||||
v-if="user && !showSignIn"
|
||||
to="/"
|
||||
class="text-sm font-semibold leading-7 text-blue-600"
|
||||
>
|
||||
<i18n-t keypath="errors.backHome" tag="span" scope="global">
|
||||
@@ -75,7 +79,7 @@ if (import.meta.client) {
|
||||
<span aria-hidden="true">{{ $t("chars.arrowBack") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</a>
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else
|
||||
class="text-sm font-semibold leading-7 text-blue-600"
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
// @ts-check
|
||||
import { globalIgnores } from "eslint/config";
|
||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||
import vueI18n from "@intlify/eslint-plugin-vue-i18n";
|
||||
import noPrismaDelete from "./rules/no-prisma-delete.mts";
|
||||
|
||||
export default withNuxt([
|
||||
globalIgnores([".data/*"]),
|
||||
|
||||
eslintConfigPrettier,
|
||||
|
||||
// vue-i18n plugin
|
||||
@@ -23,7 +19,6 @@ export default withNuxt([
|
||||
},
|
||||
],
|
||||
"@intlify/vue-i18n/no-missing-keys": "error",
|
||||
"drop/no-prisma-delete": "error",
|
||||
},
|
||||
settings: {
|
||||
"vue-i18n": {
|
||||
@@ -34,8 +29,5 @@ export default withNuxt([
|
||||
messageSyntaxVersion: "^11.0.0",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
drop: { rules: { "no-prisma-delete": noPrismaDelete } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -15,13 +15,6 @@ export default defineI18nConfig(() => {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const defaultNumberFormat = {
|
||||
percent: {
|
||||
style: "percent",
|
||||
useGrouping: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
return {
|
||||
// https://i18n.nuxtjs.org/docs/guide/locale-fallback
|
||||
fallbackLocale: "en-us",
|
||||
@@ -38,17 +31,5 @@ export default defineI18nConfig(() => {
|
||||
zh: defaultDateTimeFormat,
|
||||
"zh-tw": defaultDateTimeFormat,
|
||||
},
|
||||
numberFormats: {
|
||||
"en-us": defaultNumberFormat,
|
||||
"en-gb": defaultNumberFormat,
|
||||
"en-au": defaultNumberFormat,
|
||||
"en-pirate": defaultNumberFormat,
|
||||
fr: defaultNumberFormat,
|
||||
de: defaultNumberFormat,
|
||||
it: defaultNumberFormat,
|
||||
es: defaultNumberFormat,
|
||||
zh: defaultNumberFormat,
|
||||
"zh-tw": defaultNumberFormat,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
+1
-733
@@ -1,733 +1 @@
|
||||
{
|
||||
"account": {
|
||||
"devices": {
|
||||
"capabilities": "Funktionen",
|
||||
"lastConnected": "Zuletzt verbunden",
|
||||
"noDevices": "Keine Geräte sind mit deinem Konto verbunden.",
|
||||
"platform": "Plattform",
|
||||
"revoke": "Wiederrufen",
|
||||
"subheader": "Geräte verwalten, die auf Ihr Drop Konto zugreifen dürfen.",
|
||||
"title": "Geräte"
|
||||
},
|
||||
"home": {
|
||||
"title": "Startseite"
|
||||
},
|
||||
"notifications": {
|
||||
"all": "Alles anzeigen {arrow}",
|
||||
"clear": "Benachrichtigungen entfernen",
|
||||
"desc": "Anzeigen und Verwalten deiner Benachrichtigung.",
|
||||
"markAllAsRead": "Markiere alle als gelesen",
|
||||
"markAsRead": "Als gelesen Markieren",
|
||||
"none": "Keine Benachrichtigungen",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"title": "Benachrichtigungen",
|
||||
"unread": "Ungelesene Benachrichtigungen"
|
||||
},
|
||||
"security": {
|
||||
"title": "Sicherheit"
|
||||
},
|
||||
"settings": "Einstellungen",
|
||||
"title": "Kontoeinstellungen",
|
||||
"token": {
|
||||
"acls": "Berechtigungen (ACLs/Scopes)",
|
||||
"aclsDesc": "Definiert, wozu dieses Schlüssel berechtigt ist. Du solltest vermeiden, alle ACLs auszuwählen, wenn dies nicht notwendig ist.",
|
||||
"expiry": "Ablaufdatum",
|
||||
"expiry3Month": "3 Monate",
|
||||
"expiry5Year": "5 Jahre",
|
||||
"expiry6Month": "6 Monate",
|
||||
"expiryMonth": "Ein Monat",
|
||||
"expiryYear": "Ein Jahr",
|
||||
"name": "API-Schlüssel Name",
|
||||
"nameDesc": "Der Name des Schlüssels, als Referenz.",
|
||||
"namePlaceholder": "Mein neuer Schlüssel",
|
||||
"noExpiry": "Unbegrenzt gültig",
|
||||
"noTokens": "Keine Schlüssel mit deinem Konto verbunden.",
|
||||
"revoke": "Wiederrufen",
|
||||
"subheader": "Verwalte deine API-Schlüssel und deren Zugriffsrechte.",
|
||||
"success": "Schlüssel erfolgreich erstellt.",
|
||||
"successNote": "Bitte jetzt kopieren, da es nicht noch einmal angezeigt wird.",
|
||||
"title": "API-Schlüssel"
|
||||
}
|
||||
},
|
||||
"actions": "Aktionen",
|
||||
"add": "Hinzufügen",
|
||||
"adminTitle": "Admin Dashboard - Drop",
|
||||
"adminTitleTemplate": "{0} - Admin - Drop",
|
||||
"auth": {
|
||||
"callback": {
|
||||
"authClient": "Client autorisieren?",
|
||||
"authorize": "Autorisieren",
|
||||
"authorizedClient": "Drop hat den Client erfolgreich autorisiert. Du kannst dieses Fenster nun schließen.",
|
||||
"issues": "Probleme?",
|
||||
"learn": "Mehr erfahren {arrow}",
|
||||
"paste": "Füge diesen Code in den Client ein, um fortzufahren:",
|
||||
"permWarning": "Das akzeptieren dieser Anfrage erlaubt \"{name}\" auf \"{plattform}\" folgende Berechtigungen:",
|
||||
"requestedAccess": "\"{name}\" hat Zugriff auf dein Drop Konto angefordert.",
|
||||
"success": "Erfolgreich!"
|
||||
},
|
||||
"code": {
|
||||
"description": "Verwende einen Code, um dein Drop Client zu verbinden, wenn dein Gerät kein Webbrowser öffnen kann.",
|
||||
"title": "Verbinde deinen Drop Client"
|
||||
},
|
||||
"confirmPassword": "Bestätige @:auth.password",
|
||||
"displayName": "Anzeigename",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"register": {
|
||||
"confirmPasswordFormat": "Muss mit oben genanntem übereinstimmen",
|
||||
"emailFormat": "Muss im Format nutzer{'@'}beispiel.de sein",
|
||||
"passwordFormat": "Muss mindestens 8 Zeichen enthalten",
|
||||
"subheader": "Gebe unten deine Daten ein, um dein Konto zu erstellen.",
|
||||
"title": "Erstelle dein Drop Konto",
|
||||
"usernameFormat": "Muss mindestens 5 Zeichen enthalten und aus Kleinbuchstaben bestehen"
|
||||
},
|
||||
"signin": {
|
||||
"externalProvider": "Externer Anbieter",
|
||||
"forgot": "Passwort vergessen?",
|
||||
"noAccount": "Noch kein Konto? Bitte den Admin, eines für dich zu erstellen.",
|
||||
"or": "ODER",
|
||||
"pageTitle": "Bei Drop anmelden",
|
||||
"rememberMe": "Erinnere mich",
|
||||
"signin": "Anmelden",
|
||||
"signinWithExternalProvider": "Bei externem Anbieter anmelden {arrow}",
|
||||
"title": "Melde dich bei deinem Konto an"
|
||||
},
|
||||
"signout": "Ausloggen",
|
||||
"username": "Nutzername"
|
||||
},
|
||||
"cancel": "Abbrechen",
|
||||
"chars": {
|
||||
"arrow": "→",
|
||||
"arrowBack": "←",
|
||||
"arrowDown": "↓",
|
||||
"arrowUp": "↑",
|
||||
"quoted": "\"\"",
|
||||
"srComma": ", {0}"
|
||||
},
|
||||
"common": {
|
||||
"add": "Hinzufügen",
|
||||
"cannotUndo": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"close": "Schließen",
|
||||
"create": "Erstellen",
|
||||
"date": "Datum",
|
||||
"delete": "Löschen",
|
||||
"deleteConfirm": "Möchtest du \"{0}\" wirklich löschen?",
|
||||
"divider": "{'|'}",
|
||||
"edit": "Bearbeiten",
|
||||
"friends": "Freunde",
|
||||
"groups": "Gruppen",
|
||||
"insert": "Einfügen",
|
||||
"labelValueColon": "{label}: {value}",
|
||||
"name": "Name",
|
||||
"noData": "Keine Daten",
|
||||
"noResults": "Keine Ergebnisse",
|
||||
"noSelected": "Keine Elemente ausgewählt.",
|
||||
"remove": "Entfernen",
|
||||
"save": "Speichern",
|
||||
"saved": "Gespeichert",
|
||||
"servers": "Server",
|
||||
"srLoading": "Lade…",
|
||||
"tags": "Tags",
|
||||
"today": "Heute"
|
||||
},
|
||||
"drop": {
|
||||
"desc": "Eine Open-Source-Plattform für die Verteilung von Spielen, die auf Geschwindigkeit, Flexibilität und Ästhetik ausgelegt ist.",
|
||||
"drop": "Drop"
|
||||
},
|
||||
"editor": {
|
||||
"bold": "Fett",
|
||||
"boldPlaceholder": "fettgedruckter Text",
|
||||
"code": "Code",
|
||||
"codePlaceholder": "code",
|
||||
"heading": "Überschrift",
|
||||
"headingPlaceholder": "überschrift",
|
||||
"italic": "Kursiv",
|
||||
"italicPlaceholder": "kursiver Text",
|
||||
"link": "Link",
|
||||
"linkPlaceholder": "link Text",
|
||||
"listItem": "Listenelement",
|
||||
"listItemPlaceholder": "listenelement"
|
||||
},
|
||||
"errors": {
|
||||
"admin": {
|
||||
"user": {
|
||||
"delete": {
|
||||
"desc": "Drop konnte diesen Benutzer nicht löschen: {0}",
|
||||
"title": "Benutzer konnte nicht gelöscht werden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"disabled": "Ungültiges oder deaktiviertes Konto. Bitte kontaktiere einen Server Admin.",
|
||||
"invalidInvite": "Ungültige oder abgelaufene Einladung",
|
||||
"invalidPassState": "Ungültiger Passwortzustand. Bitte kontaktiere einen Server Admin.",
|
||||
"invalidUserOrPass": "Ungültiger Nutzername oder Passwort.",
|
||||
"inviteIdRequired": "id erforderlich beim Abrufen der Einladung",
|
||||
"method": {
|
||||
"signinDisabled": "Anmeldemethode nicht aktiviert"
|
||||
},
|
||||
"usernameTaken": "Nutzername bereits vergeben."
|
||||
},
|
||||
"backHome": "{arrow} Zurück zur Startseite",
|
||||
"externalUrl": {
|
||||
"subtitle": "Diese Nachricht ist nur sichtbar für Admins.",
|
||||
"title": "Zugriff über eine andere EXTERNAL_URL. Bitte die Dokumentation prüfen."
|
||||
},
|
||||
"game": {
|
||||
"banner": {
|
||||
"description": "Das Aktualisieren des Banners ist fehlgeschlagen: {0}",
|
||||
"title": "Das Aktualisieren des Banners ist fehlgeschlagen"
|
||||
},
|
||||
"carousel": {
|
||||
"description": "Das Aktualisieren des Bildkarussells ist fehlgeschlagen: {0}",
|
||||
"title": "Das Aktualisieren des Bildkarussells ist fehlgeschlagen"
|
||||
},
|
||||
"cover": {
|
||||
"description": "Das Aktualisieren des Titelbildes ist fehlgeschlagen: {0}",
|
||||
"title": "Das Aktualisieren des Titelbildes ist fehlgeschlagen"
|
||||
},
|
||||
"deleteImage": {
|
||||
"description": "Das Löschen des Bildes ist fehlgeschlagen: {0}",
|
||||
"title": "Das Löschen des Bildes ist fehlgeschlagen"
|
||||
},
|
||||
"description": {
|
||||
"description": "Das Aktualisieren der Spielbeschreibung ist fehlgeschlagen: {0}",
|
||||
"title": "Das Aktualisieren der Spielbeschreibung ist fehlgeschlagen"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Das Aktualisieren der Spielmetadaten ist fehlgeschlagen: {0}",
|
||||
"title": "Das Aktualisieren der Spielmetadaten ist fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"invalidBody": "Ungültiger Anfragenkörper: {0}",
|
||||
"inviteRequired": "Registrierung nur mit Einladung möglich.",
|
||||
"library": {
|
||||
"add": {
|
||||
"desc": "Drop konnte dieses Spiel nicht zu deiner Bibliothek hinzufügen: {0}",
|
||||
"title": "Das Spiel konnte nicht zur Bibliothek hinzugefügt werden"
|
||||
},
|
||||
"collection": {
|
||||
"create": {
|
||||
"desc": "Das Erstellen der Sammlung ist fehlgeschlagen: {0}",
|
||||
"title": "Das Erstellen der Sammlung ist fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"delete": {
|
||||
"desc": "Das Löschen der Quelle ist fehlgeschlagen: {0}",
|
||||
"title": "Das Löschen der Quellbibliothek ist fehlgeschlagen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"article": {
|
||||
"delete": {
|
||||
"desc": "Das Löschen des Artikels ist fehlgeschlagen: {0}",
|
||||
"title": "Das Löschen des Artikels ist fehlgeschlagen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"occurred": "Bei der Bearbeitung deiner Anfrage ist ein Fehler aufgetreten. Wenn du glaubst, dass es sich um einen Bug handelt, melde diesen bitte. Versuche dich anzumelden, um zu sehen, ob dadurch das Problem behoben wird.",
|
||||
"ohNo": "Oh nein!",
|
||||
"pageTitle": "{0} | Drop",
|
||||
"revokeClient": "Client konnte nicht widerrufen werden",
|
||||
"revokeClientFull": "Client konnte nicht widerrufen werden {0}",
|
||||
"signIn": "Anmelden {arrow}",
|
||||
"support": "Support Discord",
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten",
|
||||
"upload": {
|
||||
"description": "Drop konnte die Datei nicht hochladen: {0}",
|
||||
"title": "Das hochladen der Datei ist Fehlgeschlagen"
|
||||
},
|
||||
"version": {
|
||||
"delete": {
|
||||
"desc": "Beim Löschen der Version ist ein Fehler aufgetreten: {error}",
|
||||
"title": "Beim Löschen der Version ist ein Fehler aufgetreten"
|
||||
},
|
||||
"order": {
|
||||
"desc": "Beim Aktualisieren der Version ist ein Fehler aufgetreten: {error}",
|
||||
"title": "Beim Aktualisieren der Versionsreihenfolge ist ein Fehler aufgetreten"
|
||||
}
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"about": "Über",
|
||||
"aboutDrop": "Über Drop",
|
||||
"api": "API Dokumentation",
|
||||
"comparison": "Vergleich",
|
||||
"docs": {
|
||||
"client": "Client Dokumentation",
|
||||
"server": "Server Dokumentation"
|
||||
},
|
||||
"documentation": "Dokumentation",
|
||||
"findGame": "Finde ein Spiel",
|
||||
"footer": "Fußzeile",
|
||||
"games": "Spiele",
|
||||
"social": {
|
||||
"discord": "Discord",
|
||||
"github": "GitHub"
|
||||
},
|
||||
"topSellers": "Bestseller",
|
||||
"version": "Drop {version} {gitRef}"
|
||||
},
|
||||
"header": {
|
||||
"admin": {
|
||||
"admin": "Admin",
|
||||
"home": "Startseite",
|
||||
"library": "Bibliothek",
|
||||
"metadata": "Metadaten",
|
||||
"settings": {
|
||||
"store": "Store",
|
||||
"title": "Einstellungen",
|
||||
"tokens": "API-Schlüssel"
|
||||
},
|
||||
"tasks": "Aufgaben",
|
||||
"users": "Benutzer"
|
||||
},
|
||||
"back": "Zurück",
|
||||
"openSidebar": "Öffne Seitenleiste"
|
||||
},
|
||||
"helpUsTranslate": "Hilf uns Drop zu übersetzen {arrow}",
|
||||
"highest": "Höchste",
|
||||
"home": {
|
||||
"admin": {
|
||||
"activeInactiveUsers": "Aktive/inaktive Benutzer",
|
||||
"activeUsers": "Aktive Benutzer",
|
||||
"allVersionsCombined": "Alle Versionen zusammen",
|
||||
"availableRam": "({usedRam} / {totalRam})",
|
||||
"biggestGamesOnServer": "Größte Spiele auf dem Server",
|
||||
"biggestGamesToDownload": "Die größten Spiele zum Herunterladen",
|
||||
"cpuUsage": "CPU Nutzung",
|
||||
"games": "Spiele",
|
||||
"goToUsers": "Zu den Benutzern",
|
||||
"inactiveUsers": "Inaktive Benutzer",
|
||||
"latestVersionOnly": "Nur die neueste Version",
|
||||
"librarySources": "Bibliotheksquellen",
|
||||
"numberCores": "({count} Kerne) | ({count} Kerne) | ({count} Kerne)",
|
||||
"ramUsage": "RAM Nutzung",
|
||||
"subheader": "Übersicht",
|
||||
"title": "Startseite",
|
||||
"users": "Benutzer",
|
||||
"version": "Version"
|
||||
}
|
||||
},
|
||||
"library": {
|
||||
"addGames": "Alle Spiele",
|
||||
"addToLib": "Zur Bibliothek hinzufügen",
|
||||
"admin": {
|
||||
"detectedGame": "Drop hat erkannt, dass du ein neues Spiel importieren kannst.",
|
||||
"detectedVersion": "Drop hat erkannt, dass du eine neue Version dieses Spiels importieren kannst.",
|
||||
"game": {
|
||||
"addCarouselNoImages": "Keine Bilder zum hinzufügen.",
|
||||
"addDescriptionNoImages": "Keine Bilder zum hinzufügen.",
|
||||
"addImageCarousel": "Aus der Bilderbibliothek hinzufügen",
|
||||
"currentBanner": "Banner",
|
||||
"currentCover": "Cover",
|
||||
"deleteImage": "Bild löschen",
|
||||
"editGameDescription": "Spielbeschreibung",
|
||||
"editGameName": "Spielname",
|
||||
"editReleaseDate": "Veröffentlichungsdatum",
|
||||
"imageCarousel": "Bilderkarussell",
|
||||
"imageCarouselDescription": "Anpassen, welche Bilder und in welcher Reihenfolge sie auf der Shop-Seite angezeigt werden.",
|
||||
"imageCarouselEmpty": "Es wurden noch keine Bilder zum Karussell hinzugefügt.",
|
||||
"imageLibrary": "Bilderbibliothek",
|
||||
"imageLibraryDescription": "Bitte beachten: Alle hochgeladenen Bilder sind für alle Nutzer über die Browser-Entwicklertools zugänglich.",
|
||||
"removeImageCarousel": "Bild entfernen",
|
||||
"setBanner": "Als Banner festlegen",
|
||||
"setCover": "Als Cover festlegen"
|
||||
},
|
||||
"gameLibrary": "Spielebibliothek",
|
||||
"import": {
|
||||
"bulkImportDescription": "Auf dieser Seite wirst du nicht zur Importaufgabe weitergeleitet, sodass du mehrere Spiele nacheinander importieren kannst.",
|
||||
"bulkImportTitle": "Massenimport Modus",
|
||||
"import": "Import",
|
||||
"link": "Import {arrow}",
|
||||
"loading": "Spieldaten werden geladen…",
|
||||
"search": "Suche",
|
||||
"searchPlaceholder": "Fallout 4",
|
||||
"selectDir": "Bitte wähle ein Verzeichnis aus…",
|
||||
"selectGame": "Spiel zum Import auswählen",
|
||||
"selectGamePlaceholder": "Bitte wähle ein Spiel aus…",
|
||||
"selectGameSearch": "Spiel auswählen",
|
||||
"selectPlatform": "Bitte wähle eine Plattform aus…",
|
||||
"version": {
|
||||
"advancedOptions": "Erweiterte Optionen",
|
||||
"import": "Version Importieren",
|
||||
"installDir": "(Installationsverzeichnis)/",
|
||||
"launchCmd": "Programm/Befehl starten",
|
||||
"launchDesc": "Ausführbare Datei zum starten des Spiels",
|
||||
"launchPlaceholder": "spiel.exe --args",
|
||||
"loadingVersion": "Lade Versionsmetadaten…",
|
||||
"noAdv": "Keine erweiterten Optionen für diese Konfiguration.",
|
||||
"noLaunches": "Keine Startkonfigurationen hinzugefügt.",
|
||||
"noSetups": "Keine Einrichtungskonfigurationen hinzugefügt.",
|
||||
"noVersions": "Keine Version zum importieren",
|
||||
"platform": "Plattformversion",
|
||||
"setupCmd": "Installationsprogramm oder Befehl ausführen",
|
||||
"setupDesc": "Wird einmal ausgeführt, wenn das Spiel installiert wird",
|
||||
"setupMode": "Einrichtungsmodus",
|
||||
"setupModeDesc": "Wenn aktiviert, hat diese Version keinen Startbefehl und führt einfach die ausführbare Datei auf dem Computer des Nutzers aus. Nützlich für Spiele, die nur einen Installer bereitstellen und keine portablen Dateien.",
|
||||
"setupPlaceholder": "setup.exe",
|
||||
"umuLauncherId": "UMU Launcher ID",
|
||||
"umuOverride": "Überschreibe UMU Launcher Spiel ID",
|
||||
"umuOverrideDesc": "Standardmäßig verwendet Drop beim Start über den UMU Launcher eine Nicht-ID. Um die richtigen Patches für manche Spiele zu erhalten, musst du dieses Feld eventuell manuell setzen.",
|
||||
"updateMode": "Aktualisierungsmodus",
|
||||
"updateModeDesc": "Wenn aktiviert, werden diese Dateien über die vorherige Version installiert (überschrieben). Werden mehrere ‚Update-Modi‘ hintereinander verwendet, werden sie in der angegebenen Reihenfolge angewendet.",
|
||||
"version": "Wähle die Version für den Import aus"
|
||||
},
|
||||
"withoutMetadata": "Ohne Metadaten importieren"
|
||||
},
|
||||
"libraryHint": "Keine Bibliotheken konfiguriert.",
|
||||
"libraryHintDocsLink": "Was bedeutet das? {arrow}",
|
||||
"metadata": {
|
||||
"companies": {
|
||||
"action": "Verwalte {arrow}",
|
||||
"addGame": {
|
||||
"description": "Wähle ein Spiel aus, das dem Unternehmen hinzugefügt werden soll, und lege fest, ob es als Entwickler, Publisher oder beides geführt werden soll.",
|
||||
"developer": "Entwickler?",
|
||||
"noGames": "Keine Spiele zum hinzufügen",
|
||||
"publisher": "Publisher?",
|
||||
"title": "Verbinde das Spiel mit diesem Unternehmen"
|
||||
},
|
||||
"description": "Unternehmen organisieren Spiele danach, wer sie entwickelt oder veröffentlicht hat.",
|
||||
"editor": {
|
||||
"action": "Spiel hinzufügen {plus}",
|
||||
"descriptionPlaceholder": "{'<'}Beschreibung{'>'}",
|
||||
"developed": "Entwickelt",
|
||||
"libraryDescription": "Hinzufügen, bearbeiten oder entfernen, was diese Firma entwickelt und/oder veröffentlicht hat.",
|
||||
"libraryTitle": "Spielebibliothek",
|
||||
"noDescription": "(Keine Beschreibung)",
|
||||
"published": "Veröffentlicht",
|
||||
"uploadBanner": "Banner hochladen",
|
||||
"uploadIcon": "Icon hochladen",
|
||||
"websitePlaceholder": "{'<'}Webseite{'>'}"
|
||||
},
|
||||
"modals": {
|
||||
"createDescription": "Erstelle ein Unternehmen, um deine Spiele besser zu organisieren.",
|
||||
"createFieldDescription": "Unternehmensbeschreibung",
|
||||
"createFieldDescriptionPlaceholder": "Ein kleines Indie-Studio, das…",
|
||||
"createFieldName": "Unternehmensname",
|
||||
"createFieldNamePlaceholder": "Mein neues Unternehmen…",
|
||||
"createFieldWebsite": "Unternehmenswebseite",
|
||||
"createFieldWebsitePlaceholder": "https://beispiel.de/",
|
||||
"createTitle": "Unternehmen erstellen",
|
||||
"nameDescription": "Bearbeite den Namen des Unternehmens. Wird verwendet, um neue Spielimporte zuzuordnen.",
|
||||
"nameTitle": "Bearbeite Firmenname",
|
||||
"shortDeckDescription": "Bearbeite die Firmenbeschreibung. Beeinträchtigt nicht die Lange (markdown) Beschreibung.",
|
||||
"shortDeckTitle": "Bearbeite Firmenbeschreibung",
|
||||
"websiteDescription": "„Bearbeite die Webseite des Unternehmens. Hinweis: Dies wird ein Link sein und bietet keinen Redirect-Schutz.",
|
||||
"websiteTitle": "Unternehmenswebseite bearbeiten"
|
||||
},
|
||||
"noCompanies": "Keine Unternehmen",
|
||||
"noGames": "Keine Spiele",
|
||||
"search": "Suche Unternehmen…",
|
||||
"searchGames": "Unternehmensspiele durchsuchen…",
|
||||
"title": "Unternehmen"
|
||||
},
|
||||
"tags": {
|
||||
"action": "Verwalte {arrow}",
|
||||
"create": "Erstellen",
|
||||
"description": "Tags werden automatisch aus importierten Genres erstellt. Du kannst eigene Tags hinzufügen, um deine Spielbibliothek zu kategorisieren.",
|
||||
"modal": {
|
||||
"description": "Erstelle einen Tag, um deine Bibliothek zu organisieren.",
|
||||
"title": "Tag erstellen"
|
||||
},
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
"metadataProvider": "Metadatenanbieter",
|
||||
"noGames": "Keine Spiele importiert",
|
||||
"offline": "Drop konnte auf dieses Spiel nicht zugreifen.",
|
||||
"offlineTitle": "Spiel offline",
|
||||
"openEditor": "Im Editor öffnen {arrow}",
|
||||
"openStore": "Im Store öffnen",
|
||||
"shortDesc": "Kurzbeschreibung",
|
||||
"sources": {
|
||||
"create": "Quelle erstellen",
|
||||
"createDesc": "Drop wird diese Quelle verwenden, um auf deine Spielbibliothek zuzugreifen und die Spiele verfügbar zu machen.",
|
||||
"desc": "Konfiguriere deine Bibliotheksquellen, wo Drop nach neuen Spielen und Versionen zum Import suchen wird.",
|
||||
"documentationLink": "Dokumentation {arrow}",
|
||||
"edit": "Quelle bearbeiten",
|
||||
"freeSpace": "Freier Speicher",
|
||||
"fsDesc": "Importiert Spiele von einem Pfad auf der Festplatte. Benötigt eine versionsbasierte Ordnerstruktur und unterstützt archivierte Spiele.",
|
||||
"fsFlatDesc": "Importiert Spiele von einem Pfad auf der Festplatte, jedoch ohne separate Unterordner für Versionen. Nützlich beim Migrieren einer bestehenden Bibliothek zu Drop.",
|
||||
"fsFlatTitle": "Kompatibilität",
|
||||
"fsPath": "Pfad",
|
||||
"fsPathDesc": "Absoluter Pfad zur Spielebibliothek.",
|
||||
"fsPathPlaceholder": "/mnt/spiele",
|
||||
"fsTitle": "Drop-Stil",
|
||||
"link": "Quellen {arrow}",
|
||||
"nameDesc": "Der Name deiner Quelle, als Referenz.",
|
||||
"namePlaceholder": "Meine neue Quelle",
|
||||
"percentage": "{number}%",
|
||||
"sources": "Bibliotheksquellen",
|
||||
"totalSpace": "Gesamtspeicherplatz",
|
||||
"typeDesc": "Der Typ deiner Quelle. Ändert die erforderlichen Optionen.",
|
||||
"utilizationPercentage": "Nutzungsgrad",
|
||||
"working": "Funktioniert es?"
|
||||
},
|
||||
"subheader": "Wenn du Ordner zu deinen Bibliotheksquellen hinzufügst, erkennt Drop diese und fordert dich auf, sie zu importieren. Jedes Spiel muss importiert werden, bevor du eine Version importieren kannst.",
|
||||
"title": "Bibliotheken",
|
||||
"version": {
|
||||
"delta": "Upgrade Modus",
|
||||
"noVersions": "Du hast keine verfügbare Version dieses Spiels.",
|
||||
"noVersionsAdded": "keine Versionen hinzugefügt"
|
||||
},
|
||||
"versionPriority": "Versions Priorität"
|
||||
},
|
||||
"back": "Zurück zur Bibliothek",
|
||||
"collection": {
|
||||
"addToNew": "Zur neuen Sammlung hinzufügen",
|
||||
"collections": "Sammlungen",
|
||||
"create": "Sammlung erstellen",
|
||||
"createDesc": "Sammlungen können genutzt werden, um deine Spiele zu organisieren und sie einfacher zu finden. Besonders bei großen Bibliotheken.",
|
||||
"delete": "Sammlung löschen",
|
||||
"namePlaceholder": "Sammlungsname",
|
||||
"noCollections": "Keine Sammlungen",
|
||||
"notFound": "Sammlung nicht gefunden",
|
||||
"subheader": "Füge eine neue Sammlung hinzu, um deine Spiele zu organisieren",
|
||||
"title": "Sammlung"
|
||||
},
|
||||
"gameCount": "{0} Spiele | {0} Spiele | {0} Spiele",
|
||||
"inLib": "In der Bibliothek",
|
||||
"launcherOpen": "Im Launcher öffnen",
|
||||
"noGames": "Keine Spiele in der Bibliothek",
|
||||
"notFound": "Spiel nicht gefunden",
|
||||
"search": "Durchsuche Bibliothek…",
|
||||
"subheader": "Verwalte deine Spiele in Sammlungen für einen einfacheren Zugriff auf alle deine Spiele."
|
||||
},
|
||||
"news": {
|
||||
"article": {
|
||||
"add": "Hinzufügen",
|
||||
"content": "Inhalt (Markdown)",
|
||||
"create": "Neuen Artikel erstellen",
|
||||
"editor": "Editor",
|
||||
"editorGuide": "Verwende die obigen Shortcuts oder schreibe direkt in Markdown. Unterstützt **fett**, *kursiv*, [Links](URL) und mehr.",
|
||||
"new": "Neuer Artikel",
|
||||
"preview": "Vorschau",
|
||||
"shortDesc": "Kurzbeschreibung",
|
||||
"submit": "Absenden",
|
||||
"tagPlaceholder": "Tag hinzufügen…",
|
||||
"titles": "Titel",
|
||||
"uploadCover": "Cover hochladen"
|
||||
},
|
||||
"back": "Zurück zu Neuigkeiten",
|
||||
"checkLater": "Schaue später für Updates vorbei.",
|
||||
"delete": "Artikel löschen",
|
||||
"filter": {
|
||||
"all": "Gesamt",
|
||||
"month": "Diesen Monat",
|
||||
"week": "Diese Woche",
|
||||
"year": "Dieses Jahr"
|
||||
},
|
||||
"none": "Keine Artikel",
|
||||
"notFound": "Artikel nicht gefunden",
|
||||
"search": "Suche Artikel",
|
||||
"searchPlaceholder": "Suche Artikel…",
|
||||
"subheader": "Bleibe auf dem Laufenden über die neuesten Updates und Ankündigungen.",
|
||||
"title": "Neueste Neuigkeiten"
|
||||
},
|
||||
"options": "Einstellungen",
|
||||
"selectLanguage": "Sprache auswählen",
|
||||
"services": {
|
||||
"nginx": {
|
||||
"description": "Integrierter einfacher Reverse-Proxy, um alle Drop-Komponenten miteinander zu verbinden.",
|
||||
"title": "NGINX"
|
||||
},
|
||||
"torrential": {
|
||||
"description": "Der interne Download-Server für Drop.",
|
||||
"title": "Torrential"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"admin": {
|
||||
"description": "Konfiguriere Drop Einstellungen",
|
||||
"store": {
|
||||
"dropGameAltPlaceholder": "Beispiel Spielsymbol",
|
||||
"dropGameDescriptionPlaceholder": "Dies ist ein exemplarisches Spiel. Es wird ersetzt wenn du ein Spiel importierst.",
|
||||
"dropGameNamePlaceholder": "Beispielspiel",
|
||||
"showGamePanelTextDecoration": "Zeige Titel und Beschreibung auf den Spielkacheln (Standard: an)",
|
||||
"title": "Store"
|
||||
},
|
||||
"title": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"auth": {
|
||||
"description": "Die Authentifizierung in Drop erfolgt über mehrere konfigurierte ‚Provider‘. Jeder Provider ermöglicht es Nutzern, sich über seine Methode anzumelden. Um zu starten, sollte mindestens ein Authentifizierungs-Provider aktiviert sein und ein Konto über diesen erstellt werden.",
|
||||
"docs": "Dokumentation {arrow}",
|
||||
"enabled": "Aktiviert?",
|
||||
"openid": {
|
||||
"description": "OpenID Connect (OIDC) ist eine oft unterstützte OAuth2 Erweiterung. Drop erfordert die Konfiguration von OIDC über Umgebungsvariablen.",
|
||||
"skip": "Ich habe ein OIDC Nutzer",
|
||||
"title": "OpenID Connect"
|
||||
},
|
||||
"simple": {
|
||||
"description": "Die einfache Authentifizierung verwendet Nutzername und Password zur Authentifizierung von Benutzern. Sie ist standartmäßig aktiviert, wenn kein anderer Authentifizierungsanbieter aktiviert ist.",
|
||||
"register": "Als Admin registrieren {arrow}",
|
||||
"title": "Einfache Authentifizierung"
|
||||
},
|
||||
"title": "Authentifizierung"
|
||||
},
|
||||
"finish": "Los geht's {arrow}",
|
||||
"noPage": "keine Seite",
|
||||
"stages": {
|
||||
"account": {
|
||||
"description": "Du benötigst mindestens ein Konto, um Drop zu benutzen.",
|
||||
"name": "Richte dein Administratorkonto ein."
|
||||
},
|
||||
"library": {
|
||||
"description": "Füge mindestens eine Bibliotheksquelle hinzu, um Drop zu nutzen.",
|
||||
"name": "Erstelle eine Bibliothek."
|
||||
}
|
||||
},
|
||||
"welcome": "Hallo.",
|
||||
"welcomeDescription": "Willkommen zum Drop Einrichtungsassistenten. Er führt dich durch die erstmalige Konfiguration von Drop und erklärt dir, wie es funktioniert."
|
||||
},
|
||||
"store": {
|
||||
"about": "Über",
|
||||
"commingSoon": "Demnächst verfügbar",
|
||||
"developers": "Entwickler | Entwickler | Entwickler",
|
||||
"exploreMore": "Mehr entdecken {arrow}",
|
||||
"featured": "Empfohlen",
|
||||
"images": "Spielbilder",
|
||||
"lookAt": "Schau es dir an",
|
||||
"noDevelopers": "Keine Entwickler",
|
||||
"noFeatured": "KEINE HERVORGEHOBENEN SPIELE",
|
||||
"noGame": "KEIN SPIEL",
|
||||
"noImages": "Keine Bilder",
|
||||
"noPublishers": "Kein Publisher.",
|
||||
"noTags": "Keine Tags",
|
||||
"openAdminDashboard": "Im Admin Dashboard öffnen",
|
||||
"openFeatured": "Spiele in der Admin-Bibliothek markieren {arrow}",
|
||||
"platform": "Plattform | Plattform | Plattform",
|
||||
"publishers": "Publisher | Publisher | Publisher",
|
||||
"rating": "Bewertung",
|
||||
"readLess": "Weniger anzeigen",
|
||||
"readMore": "Mehr anzeigen",
|
||||
"recentlyAdded": "Kürzlich hinzugefügt",
|
||||
"recentlyReleased": "Kürzlich veröffentlicht",
|
||||
"recentlyUpdated": "Kürzlich aktualisiert",
|
||||
"released": "Veröffentlicht",
|
||||
"reviews": "({0} Bewertungen)",
|
||||
"size": "Größe",
|
||||
"tags": "Tags",
|
||||
"title": "Store",
|
||||
"view": {
|
||||
"sort": "Sortieren",
|
||||
"srFilters": "Filter",
|
||||
"srGames": "Spiele",
|
||||
"srViewGrid": "Raster anzeigen"
|
||||
},
|
||||
"viewInStore": "Im Store ansehen",
|
||||
"website": "Webseite"
|
||||
},
|
||||
"tasks": {
|
||||
"admin": {
|
||||
"back": "{arrow} Zurück zu den Aufgaben",
|
||||
"completedTasksTitle": "Abgeschlossene Aufgaben",
|
||||
"dailyScheduledTitle": "Tägliche Aufgaben",
|
||||
"execute": "{arrow} Ausführen",
|
||||
"noTasksRunning": "Keine laufenden Aufgaben",
|
||||
"progress": "{0}%",
|
||||
"runningTasksTitle": "Laufende Aufgaben",
|
||||
"scheduled": {
|
||||
"checkUpdateDescription": "Drop auf Updates überprüfen.",
|
||||
"checkUpdateName": "Auf Updates prüfen.",
|
||||
"cleanupInvitationsDescription": "Bereinigt abgelaufene Einladungen aus der Datenbank, um Speicherplatz zu sparen.",
|
||||
"cleanupInvitationsName": "Einladungen bereinigen",
|
||||
"cleanupObjectsDescription": "Erkennt und löscht nicht referenzierte und ungenutzte Objekte, um Speicherplatz zu sparen.",
|
||||
"cleanupObjectsName": "Objekte bereinigen",
|
||||
"cleanupSessionsDescription": "Bereinigt abgelaufene Sitzungen, um Speicherplatz zu sparen und die Sicherheit zu gewährleisten.",
|
||||
"cleanupSessionsName": "Sitzungen bereinigen."
|
||||
},
|
||||
"viewTask": "Ansehen {arrow}",
|
||||
"weeklyScheduledTitle": "Wöchentliche Aufgaben"
|
||||
}
|
||||
},
|
||||
"title": "Drop",
|
||||
"titleTemplate": "{0} - Drop",
|
||||
"todo": "Todo",
|
||||
"type": "Typ",
|
||||
"upload": "Hochladen",
|
||||
"uploadFile": "Datei hochladen",
|
||||
"user": {
|
||||
"editProfile": "Profil bearbeiten",
|
||||
"noActivity": "Keine aktuellen Ereignisse",
|
||||
"notFound": "Nutzer nicht gefunden",
|
||||
"recent": "Kürzliche Aktivitäten (TODO)",
|
||||
"recentSub": "Kürzliche Aktivitäten von diesem Nutzer",
|
||||
"unknown": "Unbekannter Nutzer"
|
||||
},
|
||||
"userHeader": {
|
||||
"closeSidebar": "Seitenleiste schließen",
|
||||
"links": {
|
||||
"community": "Community",
|
||||
"library": "Bibliothek",
|
||||
"news": "Neuigkeiten"
|
||||
},
|
||||
"profile": {
|
||||
"admin": "Admin Dashboard",
|
||||
"settings": "Kontoeinstellungen"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"admin": {
|
||||
"adminHeader": "Administrator?",
|
||||
"adminUserLabel": "Admin",
|
||||
"authLink": "Authentifizierung {arrow}",
|
||||
"authentication": {
|
||||
"configure": "Konfigurieren",
|
||||
"description": "Drop unterstützt eine Vielzahl von Authentifizierungsmechanismen. Wenn du sie aktivierst oder deaktivierst, werden sie auf dem Anmeldebildschirm angezeigt, damit Benutzer sie auswählen können. Klicke auf die Drei-Punkte, um den Authentifizierungsmechanismus zu konfigurieren.",
|
||||
"disabled": "Deaktiviert",
|
||||
"enabled": "Aktiviert",
|
||||
"enabledKey": "Aktiviert?",
|
||||
"oidc": "OpenID Connect",
|
||||
"simple": "Einfach (Nutzername/Passwort)",
|
||||
"srOpenOptions": "Einstellungen öffnen",
|
||||
"title": "Authentifizierung"
|
||||
},
|
||||
"authoptionsHeader": "Authentifizierungseinstellungen",
|
||||
"delete": "Löschen",
|
||||
"deleteUser": "Benutzer löschen {0}",
|
||||
"description": "Verwalte Benutzer auf deiner Drop-Instanz und konfiguriere deine Authentifizierungsmethode.",
|
||||
"displayNameHeader": "Anzeigename",
|
||||
"emailHeader": "E-Mail",
|
||||
"normalUserLabel": "Normaler Benutzer",
|
||||
"simple": {
|
||||
"adminInvitation": "Admin Einladung",
|
||||
"createInvitation": "Einladung erstellen",
|
||||
"description": "Die einfache Authentifizierung verwendet ein Einladungssystem zur Erstellung von Benutzern. Du kannst eine Einladung erstellen und optional einen Benutzernamen oder eine E-Mail-Adresse für den Benutzer angeben. Daraufhin wird eine magische URL generiert, mit der ein Konto erstellt werden kann.",
|
||||
"expires": "Läuft ab: {expiry}",
|
||||
"invitationTitle": "Einladungen",
|
||||
"invite3Days": "3 Tage",
|
||||
"invite6Months": "6 Monate",
|
||||
"inviteAdminSwitchDescription": "Erstelle diesen Benutzer als Administrator",
|
||||
"inviteAdminSwitchLabel": "Admin Einladung",
|
||||
"inviteButton": "Einladung",
|
||||
"inviteDescription": "Drop erstellt eine URL, die du an die Person senden kannst, die du einladen möchtest. Du kannst optional einen Benutzernamen oder eine E-Mail-Adresse angeben, die sie verwenden soll.",
|
||||
"inviteEmailDescription": "Muss im Format nutzer{'@'}beispiel.de sein",
|
||||
"inviteEmailLabel": "E-Mail-Adresse (optional)",
|
||||
"inviteEmailPlaceholder": "ich{'@'}beispiel.de",
|
||||
"inviteExpiryLabel": "Läuft ab",
|
||||
"inviteMonth": "1 Monat",
|
||||
"inviteNever": "Niemals",
|
||||
"inviteTitle": "Ein Benutzer zu Drop einladen",
|
||||
"inviteUsernameFormat": "Muss mindestens 5 Zeichen lang sein",
|
||||
"inviteUsernameLabel": "Nutzername (optional)",
|
||||
"inviteUsernamePlaceholder": "meinNutzername",
|
||||
"inviteWeek": "1 Woche",
|
||||
"inviteYear": "1 Jahr",
|
||||
"neverExpires": "Läuft niemals ab.",
|
||||
"noEmailEnforced": "Keine E-Mail erforderlich.",
|
||||
"noInvitations": "Keine Einladungen.",
|
||||
"noUsernameEnforced": "Kein Nutzername erforderlich.",
|
||||
"title": "Einfache Authentifizierung",
|
||||
"userInvitation": "Benutzereinladung"
|
||||
},
|
||||
"srEditLabel": "Bearbeiten",
|
||||
"usernameHeader": "Nutzername"
|
||||
}
|
||||
},
|
||||
"welcome": "Deutsche, willkommen!"
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"setup": {
|
||||
"welcome": "G'day."
|
||||
},
|
||||
"account": {
|
||||
"devices": {
|
||||
"subheader": "Manage the devices authorised to access your Drop account."
|
||||
@@ -16,8 +19,5 @@
|
||||
"subheader": "Add a new collection to organise your games"
|
||||
},
|
||||
"subheader": "Organise your games into collections for easy access, and access all your games."
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "G'day."
|
||||
}
|
||||
}
|
||||
|
||||
+17
-44
@@ -11,7 +11,6 @@
|
||||
},
|
||||
"notifications": {
|
||||
"all": "Gaze upon all {arrow}",
|
||||
"clear": "Notifications walk the plank, eh?",
|
||||
"desc": "View and manage yer messages from the crows' nest.",
|
||||
"markAllAsRead": "Mark all as read, aye!",
|
||||
"markAsRead": "Mark as read, matey!",
|
||||
@@ -20,14 +19,8 @@
|
||||
"title": "Messages from the Crows' Nest",
|
||||
"unread": "Unread Messages"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"title": "Yer Own Coffer",
|
||||
"token": {
|
||||
"name": "Key engraving",
|
||||
"nameDesc": "What here be inscribed on this key.",
|
||||
"subheader": "Keep 'yer keys to your treasure close.",
|
||||
"title": "Treasure keys"
|
||||
}
|
||||
"settings": "Account Settings, savvy?",
|
||||
"title": "Yer Own Coffer"
|
||||
},
|
||||
"actions": "Deeds",
|
||||
"add": "Add",
|
||||
@@ -45,10 +38,6 @@
|
||||
"requestedAccess": "\"{name}\" has requested passage to yer Drop coffer.",
|
||||
"success": "Shiver me timbers, it worked!"
|
||||
},
|
||||
"code": {
|
||||
"description": "Use the secret map to dock ye ship when lacking a web surfer.",
|
||||
"title": "Dock ye ship"
|
||||
},
|
||||
"confirmPassword": "Confirm @:auth.password",
|
||||
"displayName": "Yer Scallywag Name",
|
||||
"email": "Salty Mail",
|
||||
@@ -56,7 +45,7 @@
|
||||
"register": {
|
||||
"confirmPasswordFormat": "Must be the same as above, savvy?",
|
||||
"emailFormat": "Must be in the fashion of a true scallywag {'@'} example.com",
|
||||
"passwordFormat": "Must be 8 or more marks, ye landlubber!",
|
||||
"passwordFormat": "Must be 14 or more marks, ye landlubber!",
|
||||
"subheader": "Fill in yer details below to make yer mark.",
|
||||
"title": "Forge yer Drop Mark",
|
||||
"usernameFormat": "Must be 5 or more marks, and all lowercase, argh!"
|
||||
@@ -82,12 +71,10 @@
|
||||
"srComma": ", {0}"
|
||||
},
|
||||
"common": {
|
||||
"add": "Append",
|
||||
"cannotUndo": "This deed cannot be undone, ye hear!",
|
||||
"close": "Shut yer trap!",
|
||||
"create": "Forge!",
|
||||
"date": "Date",
|
||||
"delete": "Scuttle!",
|
||||
"deleteConfirm": "Are ye sure ye want to scuttle \"{0}\", ye rogue?",
|
||||
"divider": "{'|'}",
|
||||
"edit": "Amend",
|
||||
@@ -96,15 +83,13 @@
|
||||
"insert": "Insert",
|
||||
"name": "Name, argh!",
|
||||
"noResults": "No plunder found!",
|
||||
"noSelected": "No cargo selected.",
|
||||
"remove": "Walk the plank",
|
||||
"save": "Stow it!",
|
||||
"saved": "Preserved",
|
||||
"servers": "Ships",
|
||||
"srLoading": "Loading, loading, argh…",
|
||||
"srLoading": "Loading, loading, argh...",
|
||||
"tags": "Marks",
|
||||
"today": "Today"
|
||||
},
|
||||
"delete": "Scuttle!",
|
||||
"drop": {
|
||||
"desc": "An open-source game distribution platform built for speed, flexibility and beauty, like a swift brigantine!",
|
||||
"drop": "Drop"
|
||||
@@ -243,8 +228,6 @@
|
||||
"header": {
|
||||
"admin": {
|
||||
"admin": "Cap'n",
|
||||
"metadata": "Meta argh",
|
||||
"settings": "Shape",
|
||||
"tasks": "Duties",
|
||||
"users": "Crew"
|
||||
},
|
||||
@@ -280,26 +263,24 @@
|
||||
},
|
||||
"gameLibrary": "Game Treasure Hoard",
|
||||
"import": {
|
||||
"bulkImportDescription": "When importing ye versions, ye won't be sent to the import duty.",
|
||||
"bulkImportTitle": "Plunder the imports",
|
||||
"import": "Import, ye dog!",
|
||||
"link": "Import {arrow}",
|
||||
"loading": "Loadin' plunder results, arrr…",
|
||||
"loading": "Loadin' plunder results, arrr...",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Fallout 4, savvy?",
|
||||
"selectDir": "Pick a directory, ye landlubber…",
|
||||
"selectDir": "Pick a directory, ye landlubber...",
|
||||
"selectGame": "Pick plunder to import",
|
||||
"selectGamePlaceholder": "Pick a game, ye dog…",
|
||||
"selectGamePlaceholder": "Pick a game, ye dog...",
|
||||
"selectGameSearch": "Pick game",
|
||||
"selectPlatform": "Pick a ship, ye scallywag…",
|
||||
"selectPlatform": "Pick a ship, ye scallywag...",
|
||||
"version": {
|
||||
"advancedOptions": "Advanced options, savvy?",
|
||||
"import": "Import version",
|
||||
"installDir": "(install_dir)/",
|
||||
"launchCmd": "Launch executable/command, argh!",
|
||||
"launchDesc": "Executable to launch the game, matey!",
|
||||
"launchPlaceholder": "game.exe --args",
|
||||
"loadingVersion": "Loading version charts…",
|
||||
"launchPlaceholder": "game.exe, aye!",
|
||||
"loadingVersion": "Loading version charts...",
|
||||
"noAdv": "No advanced options for this rig, argh.",
|
||||
"noVersions": "No versions to import, savvy!",
|
||||
"platform": "Ship type",
|
||||
@@ -317,16 +298,6 @@
|
||||
},
|
||||
"withoutMetadata": "Import without charts"
|
||||
},
|
||||
"metadata": {
|
||||
"companies": {
|
||||
"action": "Shape {arrow}",
|
||||
"addGame": {
|
||||
"developer": "Creator?",
|
||||
"noGames": "No games to plunder",
|
||||
"publisher": "Distributor?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadataProvider": "Charts Provider",
|
||||
"noGames": "No plunder imported, savvy!",
|
||||
"openEditor": "Open in Editor {arrow}",
|
||||
@@ -374,9 +345,10 @@
|
||||
"launcherOpen": "Open in Launcher, argh!",
|
||||
"noGames": "No plunder in treasure hoard, savvy!",
|
||||
"notFound": "Plunder not found, matey!",
|
||||
"search": "Search treasure hoard, ye dog…",
|
||||
"search": "Search treasure hoard, ye dog...",
|
||||
"subheader": "Sort yer plunder into collections for easy access, and get to all yer plunder, savvy!"
|
||||
},
|
||||
"lowest": "lowest",
|
||||
"news": {
|
||||
"article": {
|
||||
"add": "Add, ye dog!",
|
||||
@@ -388,7 +360,7 @@
|
||||
"preview": "Preview, matey!",
|
||||
"shortDesc": "Short description",
|
||||
"submit": "Submit, ye scurvy dog!",
|
||||
"tagPlaceholder": "Add a mark, ye dog…",
|
||||
"tagPlaceholder": "Add a mark, ye dog...",
|
||||
"titles": "Title, argh!",
|
||||
"uploadCover": "Hoist cover image"
|
||||
},
|
||||
@@ -404,11 +376,12 @@
|
||||
"none": "No articles, savvy!",
|
||||
"notFound": "Article not found, matey!",
|
||||
"search": "Search articles, ye dog!",
|
||||
"searchPlaceholder": "Search articles, argh…",
|
||||
"searchPlaceholder": "Search articles, argh...",
|
||||
"subheader": "Stay up to date with the latest charts and announcements, savvy!",
|
||||
"title": "Latest News from the High Seas"
|
||||
},
|
||||
"options": "Options, matey!",
|
||||
"security": "Safety",
|
||||
"selectLanguage": "Pick yer tongue",
|
||||
"settings": "Settings",
|
||||
"store": {
|
||||
@@ -453,7 +426,7 @@
|
||||
}
|
||||
},
|
||||
"title": "Drop",
|
||||
"titleTemplate": "{0} - Drop",
|
||||
"titleTemplate": "{0} | Drop",
|
||||
"todo": "Todo, argh!",
|
||||
"type": "Type",
|
||||
"upload": "Hoist!",
|
||||
|
||||
+131
-352
@@ -9,12 +9,8 @@
|
||||
"subheader": "Manage the devices authorized to access your Drop account.",
|
||||
"title": "Devices"
|
||||
},
|
||||
"home": {
|
||||
"title": "Home"
|
||||
},
|
||||
"notifications": {
|
||||
"all": "View all {arrow}",
|
||||
"clear": "Clear notifications",
|
||||
"desc": "View and manage your notifications.",
|
||||
"markAllAsRead": "Mark all as read",
|
||||
"markAsRead": "Mark as read",
|
||||
@@ -23,88 +19,14 @@
|
||||
"title": "Notifications",
|
||||
"unread": "Unread Notifications"
|
||||
},
|
||||
"security": {
|
||||
"2fa": {
|
||||
"superlevelHint": {
|
||||
"signin": "Sign in {arrow}",
|
||||
"success": "You have access to these protected actions.",
|
||||
"title": "Sign in again to access these settings."
|
||||
},
|
||||
"title": "Two-factor authentication",
|
||||
"totp": {
|
||||
"description": "TOTP generates one-time codes, completely offline. You can use any TOTP authenticator you like.",
|
||||
"disableButton": "Disable",
|
||||
"title": "TOTP"
|
||||
},
|
||||
"webauthn": {
|
||||
"bypassHint": "Also lets you bypass signing in with compatible devices.",
|
||||
"description": "Otherwise known as passkeys. Authenticate using biometrics, a device, YubiKeys, or any compatible FIDO2 device.",
|
||||
"manage": "Manage",
|
||||
"modal": {
|
||||
"description": "Create new keys or remove existing keys from your account.",
|
||||
"new": "New key",
|
||||
"tableCreated": "Created",
|
||||
"tableName": "Name",
|
||||
"title": "WebAuthn Keys"
|
||||
},
|
||||
"title": "WebAuthn"
|
||||
}
|
||||
},
|
||||
"title": "Security"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"title": "Account Settings",
|
||||
"token": {
|
||||
"acls": "ACLs/scopes",
|
||||
"aclsDesc": "Defines what this token has the authority to do. You should avoid selecting all ACLs, if they are not necessary.",
|
||||
"expiry": "Expiry",
|
||||
"expiry3Month": "3 months",
|
||||
"expiry5Year": "5 years",
|
||||
"expiry6Month": "6 months",
|
||||
"expiryMonth": "A month",
|
||||
"expiryYear": "A year",
|
||||
"name": "API token name",
|
||||
"nameDesc": "The name of the token, for reference.",
|
||||
"namePlaceholder": "My New Token",
|
||||
"noExpiry": "No expiry",
|
||||
"noTokens": "No tokens connected to your account.",
|
||||
"revoke": "Revoke",
|
||||
"subheader": "Manage your API tokens, and what they can access.",
|
||||
"success": "Successfully created token.",
|
||||
"successNote": "Make sure to copy it now, as it won't be shown again.",
|
||||
"title": "API Tokens"
|
||||
}
|
||||
"title": "Account Settings"
|
||||
},
|
||||
"actions": "Actions",
|
||||
"add": "Add",
|
||||
"adminTitle": "Admin Dashboard - {0}",
|
||||
"adminTitleTemplate": "{0} - Admin - {1}",
|
||||
"adminTitle": "Admin Dashboard - Drop",
|
||||
"adminTitleTemplate": "{0} - Admin - Drop",
|
||||
"auth": {
|
||||
"2fa": {
|
||||
"backToOptions": "{arrow} Back to options",
|
||||
"description": "Two-factor authentication is enabled on your account. Choose one of the options below to continue.",
|
||||
"passkey": {
|
||||
"createDescription": "WebAuthn, or passkeys, allow you to sign in or complete 2FA with biometrics or hardware security devices.",
|
||||
"createTitle": "Create a passkey",
|
||||
"description": "Use a passkey, like biometrics, a hardware security device, or other compatible device to sign in to your Drop account.",
|
||||
"passkeyNameTag": "Name",
|
||||
"signinButton": "Sign in with WebAuthn",
|
||||
"title": "WebAuthn"
|
||||
},
|
||||
"success": {
|
||||
"back": "{arrow} Back to account security",
|
||||
"description": "Drop has successfully created and added your 2FA method. If this is your first time configuring 2FA, your account now requires it to sign in.",
|
||||
"title": "Added your 2FA method!"
|
||||
},
|
||||
"title": "Two-factor authentication",
|
||||
"totp": {
|
||||
"createDescription": "Use your TOTP authenticator, like Google Authenticator, Aegis, or Bitwarden, to add 2FA to your Drop account.",
|
||||
"createHint": "Enter the generated code to enable TOTP",
|
||||
"createTitle": "Set up your authenticator",
|
||||
"description": "Use a one-time code to sign in to your Drop account.",
|
||||
"title": "TOTP"
|
||||
}
|
||||
},
|
||||
"callback": {
|
||||
"authClient": "Authorize client?",
|
||||
"authorize": "Authorize",
|
||||
@@ -117,8 +39,8 @@
|
||||
"success": "Successful!"
|
||||
},
|
||||
"code": {
|
||||
"description": "Use a code to connect your Drop client if you are unable to open a web browser on your device.",
|
||||
"title": "Connect your Drop client"
|
||||
"title": "Connect your Drop client",
|
||||
"description": "Use a code to connect your Drop client if you are unable to open a web browser on your device."
|
||||
},
|
||||
"confirmPassword": "Confirm @:auth.password",
|
||||
"displayName": "Display Name",
|
||||
@@ -127,68 +49,87 @@
|
||||
"register": {
|
||||
"confirmPasswordFormat": "Must be the same as above",
|
||||
"emailFormat": "Must be in the format user{'@'}example.com",
|
||||
"passwordFormat": "Must be 8 or more characters",
|
||||
"passwordFormat": "Must be 14 or more characters",
|
||||
"subheader": "Fill in your details below to create your account.",
|
||||
"title": "Create your Drop account",
|
||||
"usernameFormat": "Must be 5 or more characters, and lowercase"
|
||||
},
|
||||
"signin": {
|
||||
"externalProvider": "external provider",
|
||||
"externalProvider": "Sign in with external provider {arrow}",
|
||||
"forgot": "Forgot password?",
|
||||
"noAccount": "Don't have an account? Ask an admin to create one for you.",
|
||||
"noAccountProtected": "We need you to sign in again for security reasons while attempting to access more sensitive actions.",
|
||||
"or": "OR",
|
||||
"pageTitle": "Sign in to Drop",
|
||||
"rememberMe": "Remember me",
|
||||
"signin": "Sign in",
|
||||
"signinWithExternalProvider": "Sign in with {externalProvider} {arrow}",
|
||||
"title": "Sign in to your account",
|
||||
"titleProtected": "Sign in to access protected action"
|
||||
"title": "Sign in to your account"
|
||||
},
|
||||
"signout": "Signout",
|
||||
"username": "Username"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Hey there.",
|
||||
"welcomeDescription": "Welcome to Drop setup wizard. It will walk you through configuring Drop for the first time, and how it works.",
|
||||
"finish": "Let's go {arrow}",
|
||||
"noPage": "no page",
|
||||
"auth": {
|
||||
"title": "Authentication",
|
||||
"description": "Authentication in Drop happens through multiple configured 'providers'. Each one can allow users to sign-in through their method. To get started, have at least one authentication provider enabled, and create an account through it.",
|
||||
"docs": "Documentation {arrow}",
|
||||
"enabled": "Enabled?",
|
||||
"simple": {
|
||||
"title": "Simple authentication",
|
||||
"description": "Simple authentication uses username/password to authentication users. It is enabled by default if no other authentication provider is enabled.",
|
||||
"register": "Register as admin {arrow}"
|
||||
},
|
||||
"openid": {
|
||||
"title": "OpenID Connect",
|
||||
"description": "OpenID Connect (OIDC) is an OAuth2 extension commonly supported. Drop requires OIDC configuration to be done via environment variables.",
|
||||
"skip": "I have a user with OIDC"
|
||||
}
|
||||
},
|
||||
"stages": {
|
||||
"account": {
|
||||
"name": "Setup your admin account.",
|
||||
"description": "You need at least one account to start using Drop."
|
||||
},
|
||||
"library": {
|
||||
"name": "Create a library.",
|
||||
"description": "Add at least one library source to use Drop."
|
||||
}
|
||||
}
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"chars": {
|
||||
"arrow": "→",
|
||||
"arrowBack": "←",
|
||||
"arrowDown": "↓",
|
||||
"arrowUp": "↑",
|
||||
"quoted": "\"\"",
|
||||
"srComma": ", {0}"
|
||||
},
|
||||
"common": {
|
||||
"add": "Add",
|
||||
"cannotUndo": "This action cannot be undone.",
|
||||
"close": "Close",
|
||||
"components": {
|
||||
"multiitem": {
|
||||
"new": "Create new: \"{0}\"",
|
||||
"placeholder": "Start typing..."
|
||||
}
|
||||
},
|
||||
"create": "Create",
|
||||
"date": "Date",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Are you sure you want to delete \"{0}\"?",
|
||||
"divider": "{'|'}",
|
||||
"edit": "Edit",
|
||||
"friends": "Friends",
|
||||
"groups": "Groups",
|
||||
"insert": "Insert",
|
||||
"labelValueColon": "{label}: {value}",
|
||||
"name": "Name",
|
||||
"noData": "No data",
|
||||
"noResults": "No results",
|
||||
"noSelected": "No items selected.",
|
||||
"remove": "Remove",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"select": "Select",
|
||||
"servers": "Servers",
|
||||
"srLoading": "Loading…",
|
||||
"srLoading": "Loading...",
|
||||
"tags": "Tags",
|
||||
"today": "Today"
|
||||
"today": "Today",
|
||||
"add": "Add"
|
||||
},
|
||||
"delete": "Delete",
|
||||
"drop": {
|
||||
"desc": "An open-source game distribution platform built for speed, flexibility and beauty.",
|
||||
"drop": "Drop"
|
||||
@@ -228,10 +169,6 @@
|
||||
"usernameTaken": "Username already taken."
|
||||
},
|
||||
"backHome": "{arrow} Back to home",
|
||||
"externalUrl": {
|
||||
"subtitle": "This message is only visible to admins.",
|
||||
"title": "Accessing over different EXTERNAL_URL. Please check the docs."
|
||||
},
|
||||
"game": {
|
||||
"banner": {
|
||||
"description": "Drop failed to update the banner image: {0}",
|
||||
@@ -273,7 +210,7 @@
|
||||
},
|
||||
"source": {
|
||||
"delete": {
|
||||
"desc": "Drop couldn't delete this source: {0}",
|
||||
"desc": "Drop couldn't add delete this source: {0}",
|
||||
"title": "Failed to delete library source"
|
||||
}
|
||||
}
|
||||
@@ -312,7 +249,6 @@
|
||||
"footer": {
|
||||
"about": "About",
|
||||
"aboutDrop": "About Drop",
|
||||
"api": "API documentation",
|
||||
"comparison": "Comparison",
|
||||
"docs": {
|
||||
"client": "Client Docs",
|
||||
@@ -332,15 +268,8 @@
|
||||
"header": {
|
||||
"admin": {
|
||||
"admin": "Admin",
|
||||
"home": "Home",
|
||||
"library": "Library",
|
||||
"metadata": "Meta",
|
||||
"settings": {
|
||||
"general": "General Settings",
|
||||
"store": "Store",
|
||||
"title": "Settings",
|
||||
"tokens": "API tokens"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"tasks": "Tasks",
|
||||
"users": "Users"
|
||||
},
|
||||
@@ -348,38 +277,16 @@
|
||||
"openSidebar": "Open sidebar"
|
||||
},
|
||||
"helpUsTranslate": "Help us translate Drop {arrow}",
|
||||
"home": {
|
||||
"admin": {
|
||||
"activeInactiveUsers": "Active/inactive users",
|
||||
"activeUsers": "Active users",
|
||||
"allVersionsCombined": "All versions combined",
|
||||
"availableRam": "({usedRam} / {totalRam})",
|
||||
"biggestGamesOnServer": "Biggest games on server",
|
||||
"biggestGamesToDownload": "Biggest games to download",
|
||||
"cpuUsage": "CPU usage",
|
||||
"games": "Games",
|
||||
"goToUsers": "Go to users",
|
||||
"inactiveUsers": "Inactive users",
|
||||
"latestVersionOnly": "Latest version only",
|
||||
"librarySources": "Library sources",
|
||||
"numberCores": "({count} cores) | ({count} core) | ({count} cores)",
|
||||
"ramUsage": "RAM usage",
|
||||
"subheader": "Instance summary",
|
||||
"title": "Home",
|
||||
"users": "Users",
|
||||
"version": "Version"
|
||||
}
|
||||
},
|
||||
"highest": "highest",
|
||||
"home": "Home",
|
||||
"library": {
|
||||
"addGames": "All Games",
|
||||
"addToLib": "Add to Library",
|
||||
"admin": {
|
||||
"detectedGame": "Drop has detected you have new games to import.",
|
||||
"detectedVersion": "Drop has detected you have new versions of this game to import.",
|
||||
"fileExtSelector": {
|
||||
"add": "Add \"{0}\"",
|
||||
"noSelected": "No extensions selected."
|
||||
},
|
||||
"detectedVersion": "Drop has detected you have new verions of this game to import.",
|
||||
"offlineTitle": "Game offline",
|
||||
"offline": "Drop couldn't access this game.",
|
||||
"game": {
|
||||
"addCarouselNoImages": "No images to add.",
|
||||
"addDescriptionNoImages": "No images to add.",
|
||||
@@ -389,7 +296,6 @@
|
||||
"deleteImage": "Delete image",
|
||||
"editGameDescription": "Game Description",
|
||||
"editGameName": "Game Name",
|
||||
"editReleaseDate": "Release Date",
|
||||
"imageCarousel": "Image Carousel",
|
||||
"imageCarouselDescription": "Customise what images and what order are shown on the store page.",
|
||||
"imageCarouselEmpty": "No images added to the carousel yet.",
|
||||
@@ -400,188 +306,119 @@
|
||||
"setCover": "Set as cover"
|
||||
},
|
||||
"gameLibrary": "Game Library",
|
||||
"gameSelector": {
|
||||
"hint": "Type at least 4 characters to get results"
|
||||
},
|
||||
"import": {
|
||||
"bulkImportDescription": "When on this page, you won't be redirect to the import task, so you can import multiple games in succession.",
|
||||
"bulkImportTitle": "Bulk import mode",
|
||||
"import": "Import",
|
||||
"importAs": "Import as",
|
||||
"link": "Import {arrow}",
|
||||
"loading": "Loading game results…",
|
||||
"loading": "Loading game results...",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Fallout 4",
|
||||
"selectDir": "Please select a directory…",
|
||||
"selectDir": "Please select a directory...",
|
||||
"selectGame": "Select game to import",
|
||||
"selectGamePlaceholder": "Please select a game…",
|
||||
"selectGamePlaceholder": "Please select a game...",
|
||||
"selectGameSearch": "Select game",
|
||||
"selectPlatform": "Please select a platform…",
|
||||
"selectPlatform": "Please select a platform...",
|
||||
"bulkImportTitle": "Bulk import mode",
|
||||
"bulkImportDescription": "When on, this page won't redirect you to the import task, so you can import multiple games in succession.",
|
||||
"version": {
|
||||
"displayName": "Display Name",
|
||||
"displayNameDesc": "Optionally, set the display name of the version. If not set, uses the name in the dropdown.",
|
||||
"displayNamePlaceholder": "My New Version",
|
||||
"advancedOptions": "Advanced options",
|
||||
"import": "Import version",
|
||||
"installDir": "(install_dir)/",
|
||||
"launchCmd": "Launch executable/command",
|
||||
"launchDesc": "Executable to launch the game",
|
||||
"launchPlaceholder": "game.exe --args",
|
||||
"loadingVersion": "Loading version metadata…",
|
||||
"noLaunches": "No launch configurations added.",
|
||||
"noNameProvided": "No name provided.",
|
||||
"noSetups": "No setup configurations added.",
|
||||
"launchPlaceholder": "game.exe",
|
||||
"loadingVersion": "Loading version metadata...",
|
||||
"noAdv": "No advanced options for this configuration.",
|
||||
"noVersions": "No versions to import",
|
||||
"platform": "Version platform",
|
||||
"setupCmd": "Setup executable/command",
|
||||
"setupDesc": "Ran once when the game is installed",
|
||||
"setupMode": "Setup mode",
|
||||
"setupModeDesc": "When enabled, this version does not have a launch command, and simply runs the executable on the user's computer. Useful for games that only distribute installer and not portable files.",
|
||||
"setupPlaceholder": "setup.exe",
|
||||
"umuLauncherId": "UMU Launcher ID",
|
||||
"umuOverride": "Override UMU Launcher Game ID",
|
||||
"umuOverrideDesc": "By default, Drop uses a non-ID when launching with UMU Launcher. In order to get the right patches for some games, you may have to manually set this field.",
|
||||
"updateMode": "Update mode",
|
||||
"updateModeDesc": "When enabled, these files will be installed on top of (overwriting) the previous version's. If multiple \"update modes\" are chained together, they are applied in order.",
|
||||
"version": "Select version to import"
|
||||
},
|
||||
"withoutMetadata": "Import without metadata"
|
||||
},
|
||||
"launchRow": {
|
||||
"autosuggestHint": "Auto-suggest extensions",
|
||||
"currentDirHint": "The installation directory is set as the current directory when launching. It is not prepended to your command.",
|
||||
"emulatorHint": "{rom} is replaced with the game's launch command for emulators.",
|
||||
"emulatorSelect": "Select new emulator",
|
||||
"emulatorTitle": "Emulator",
|
||||
"noEmulatorSelected": "No emulator selected"
|
||||
},
|
||||
"launchSelector": {
|
||||
"description": "Select a launch option as an emulator for your new launch option.",
|
||||
"noVersions": "No versions imported.",
|
||||
"platformFilterHint": "Only showing launches for:",
|
||||
"search": "Search for an emulator",
|
||||
"selectCommand": "Select a launch command",
|
||||
"selectVersions": "Select a version",
|
||||
"title": "Select a launch option"
|
||||
},
|
||||
"libraryHint": "No libraries configured.",
|
||||
"libraryHintDocsLink": "What does this mean? {arrow}",
|
||||
"massImportTool": "Mass Import Tool",
|
||||
"metadata": {
|
||||
"companies": {
|
||||
"action": "Manage {arrow}",
|
||||
"addGame": {
|
||||
"description": "Pick a game to add to the company, and whether it should be listed as a developer, publisher, or both.",
|
||||
"developer": "Developer?",
|
||||
"publisher": "Publisher?",
|
||||
"title": "Connect game to this company"
|
||||
},
|
||||
"description": "Companies organize games by who they were developed or published by.",
|
||||
"editor": {
|
||||
"action": "Add Game {plus}",
|
||||
"developed": "Developed",
|
||||
"libraryDescription": "Add, remove, or customise what this company has developed and/or published.",
|
||||
"libraryTitle": "Game Library",
|
||||
"noDescription": "(no description)",
|
||||
"published": "Published",
|
||||
"uploadBanner": "Upload banner",
|
||||
"uploadIcon": "Upload icon",
|
||||
"websitePlaceholder": "{'<'}website{'>'}"
|
||||
},
|
||||
"modals": {
|
||||
"createDescription": "Create a company to further organize your games.",
|
||||
"createFieldDescription": "Company Description",
|
||||
"createFieldDescriptionPlaceholder": "A small indie studio that...",
|
||||
"createFieldName": "Company Name",
|
||||
"createFieldNamePlaceholder": "My New Company...",
|
||||
"createFieldWebsite": "Company Website",
|
||||
"createFieldWebsitePlaceholder": "https://example.com/",
|
||||
"createTitle": "Create a company",
|
||||
"nameDescription": "Edit the company's name. Used to match to new game imports.",
|
||||
"nameTitle": "Edit company name",
|
||||
"shortDeckDescription": "Edit the company's description. Doesn't affect long (markdown) description.",
|
||||
"shortDeckTitle": "Edit company description",
|
||||
"websiteDescription": "Edit the company's website. Note: this will be a link, and won't have redirect protection.",
|
||||
"websiteTitle": "Edit company website"
|
||||
},
|
||||
"noCompanies": "No companies",
|
||||
"noGames": "No games",
|
||||
"search": "Search companies…",
|
||||
"searchGames": "Search company games…",
|
||||
"title": "Companies"
|
||||
},
|
||||
"tags": {
|
||||
"action": "Manage {arrow}",
|
||||
"create": "Create",
|
||||
"description": "Tags are automatically created from imported genres. You can add custom tags to add categorisation to your game library.",
|
||||
"modal": {
|
||||
"description": "Create a tag to organize your library.",
|
||||
"title": "Create Tag"
|
||||
},
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
"metadataProvider": "Metadata provider",
|
||||
"nav": {
|
||||
"backPagination": "Previous",
|
||||
"clearAllFilters": "Clear all",
|
||||
"filterCount": "{0} filters",
|
||||
"filterLabel": "Filters",
|
||||
"filters": {
|
||||
"metadata": {
|
||||
"emptyDescription": "Empty description",
|
||||
"featured": "Featured",
|
||||
"noCarousel": "No images in carousel",
|
||||
"title": "Metadata"
|
||||
},
|
||||
"version": {
|
||||
"available": "Available to import",
|
||||
"none": "No versions imported",
|
||||
"title": "Versions"
|
||||
}
|
||||
},
|
||||
"nextPagination": "Next",
|
||||
"sortLabel": "Sort"
|
||||
},
|
||||
"noGames": "No games imported",
|
||||
"offline": "Drop couldn't access this game.",
|
||||
"offlineTitle": "Game offline",
|
||||
"openEditor": "Open in Editor {arrow}",
|
||||
"openStore": "Open in Store",
|
||||
"shortDesc": "Short Description",
|
||||
"sources": {
|
||||
"create": "Create source",
|
||||
"edit": "Edit source",
|
||||
"createDesc": "Drop will use this source to access your game library, and make them available.",
|
||||
"desc": "Configure your library sources, where Drop will look for new games and versions to import.",
|
||||
"documentationLink": "Documentation {arrow}",
|
||||
"edit": "Edit source",
|
||||
"freeSpace": "Free space",
|
||||
"fsDesc": "Imports games from a path on disk. Requires version-based folder structure, and supports archived games.",
|
||||
"fsFlatDesc": "Imports games from a path on disk, but without a separate version subfolder. Useful when migrating an existing library to Drop.",
|
||||
"fsFlatTitle": "Compatibility",
|
||||
"fsPath": "Path",
|
||||
"fsPathDesc": "An absolute path to your game library.",
|
||||
"fsPathPlaceholder": "/mnt/games",
|
||||
"fsTitle": "Drop-style",
|
||||
"link": "Sources {arrow}",
|
||||
"nameDesc": "The name of your source, for reference.",
|
||||
"namePlaceholder": "My New Source",
|
||||
"sources": "Library Sources",
|
||||
"totalSpace": "Total space",
|
||||
"typeDesc": "The type of your source. Changes the required options.",
|
||||
"utilizationPercentage": "Utilization percentage",
|
||||
"working": "Working?"
|
||||
},
|
||||
"subheader": "As you add folders to your library sources, Drop will detect it and prompt you to import it. Each game needs to be imported before you can import a version.",
|
||||
"title": "Libraries",
|
||||
"version": {
|
||||
"description": "All versions imported for your game.",
|
||||
"noSetups": "No setups configured.",
|
||||
"delta": "Upgrade mode",
|
||||
"noVersions": "You have no versions of this game available.",
|
||||
"setupOnly": "Version configured as in setup-only mode.",
|
||||
"table": {
|
||||
"delta": "Update mode",
|
||||
"launch": "Launch Configurations",
|
||||
"name": "Name (ID)",
|
||||
"path": "Path",
|
||||
"setup": "Setup Configurations"
|
||||
"noVersionsAdded": "no versions added"
|
||||
},
|
||||
"versionPriority": "Version priority",
|
||||
"metadata": {
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"description": "Tags are automatically created from imported genres. You can add custom tags to add categorisation to your game library.",
|
||||
"action": "Manage {arrow}",
|
||||
"create": "Create",
|
||||
"modal": {
|
||||
"title": "Create Tag",
|
||||
"description": "Create a tag to organize your library."
|
||||
}
|
||||
},
|
||||
"title": "Versions"
|
||||
"companies": {
|
||||
"title": "Companies",
|
||||
"description": "Companies organize games by who they were developed or published by.",
|
||||
"action": "Manage {arrow}",
|
||||
"search": "Search companies...",
|
||||
"searchGames": "Search company games...",
|
||||
"noCompanies": "No companies",
|
||||
"noGames": "No games",
|
||||
"editor": {
|
||||
"libraryTitle": "Game Library",
|
||||
"libraryDescription": "Add, remove, or customise what this company has developed and/or published.",
|
||||
"action": "Add Game {plus}",
|
||||
"published": "Published",
|
||||
"developed": "Developed",
|
||||
"uploadIcon": "Upload icon",
|
||||
"uploadBanner": "Upload banner",
|
||||
"noDescription": "(no description)"
|
||||
},
|
||||
"addGame": {
|
||||
"title": "Connect game to this company",
|
||||
"description": "Pick a game to add to the company, and whether it should be listed as a developer, publisher, or both.",
|
||||
"publisher": "Publisher?",
|
||||
"developer": "Developer?",
|
||||
"noGames": "No games to add"
|
||||
},
|
||||
"modals": {
|
||||
"nameTitle": "Edit company name",
|
||||
"nameDescription": "Edit the company's name. Used to match to new game imports.",
|
||||
"shortDeckTitle": "Edit company description",
|
||||
"shortDeckDescription": "Edit the company's description. Doesn't affect long (markdown) description.",
|
||||
"websiteTitle": "Edit company website",
|
||||
"websiteDescription": "Edit the company's website. Note: this will be a link, and won't have redirect protection."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"back": "Back to Library",
|
||||
@@ -602,9 +439,10 @@
|
||||
"launcherOpen": "Open in Launcher",
|
||||
"noGames": "No games in library",
|
||||
"notFound": "Game not found",
|
||||
"search": "Search library…",
|
||||
"search": "Search library...",
|
||||
"subheader": "Organize your games into collections for easy access, and access all your games."
|
||||
},
|
||||
"lowest": "lowest",
|
||||
"news": {
|
||||
"article": {
|
||||
"add": "Add",
|
||||
@@ -616,7 +454,7 @@
|
||||
"preview": "Preview",
|
||||
"shortDesc": "Short description",
|
||||
"submit": "Submit",
|
||||
"tagPlaceholder": "Add a tag…",
|
||||
"tagPlaceholder": "Add a tag...",
|
||||
"titles": "Title",
|
||||
"uploadCover": "Upload cover image"
|
||||
},
|
||||
@@ -632,35 +470,16 @@
|
||||
"none": "No articles",
|
||||
"notFound": "Article not found",
|
||||
"search": "Search articles",
|
||||
"searchPlaceholder": "Search articles…",
|
||||
"searchPlaceholder": "Search articles...",
|
||||
"subheader": "Stay up to date with the latest updates and announcements.",
|
||||
"title": "Latest News"
|
||||
},
|
||||
"options": "Options",
|
||||
"security": "Security",
|
||||
"selectLanguage": "Select language",
|
||||
"services": {
|
||||
"nginx": {
|
||||
"description": "Built-in simple reverse proxy to connect all the Drop components together.",
|
||||
"title": "NGINX"
|
||||
},
|
||||
"torrential": {
|
||||
"description": "The internal download server for Drop.",
|
||||
"title": "Torrential"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"admin": {
|
||||
"general": {
|
||||
"applicationLogo": "Application logo",
|
||||
"customLogo": "Custom logo",
|
||||
"defaultLogo": "Default logo",
|
||||
"logo": "Logo",
|
||||
"serverName": "Server name",
|
||||
"serverNameDescription": "The name of the server",
|
||||
"serverNamePlaceholder": "My Drop Instance",
|
||||
"title": "General settings",
|
||||
"uploadLogo": "Upload logo"
|
||||
},
|
||||
"description": "Configure Drop settings",
|
||||
"store": {
|
||||
"dropGameAltPlaceholder": "Example Game icon",
|
||||
"dropGameDescriptionPlaceholder": "This is an example game. It will be replaced if you import a game.",
|
||||
@@ -671,60 +490,30 @@
|
||||
"title": "Settings"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"auth": {
|
||||
"description": "Authentication in Drop happens through multiple configured 'providers'. Each one can allow users to sign-in through their method. To get started, have at least one authentication provider enabled, and create an account through it.",
|
||||
"docs": "Documentation {arrow}",
|
||||
"enabled": "Enabled?",
|
||||
"openid": {
|
||||
"description": "OpenID Connect (OIDC) is an OAuth2 extension commonly supported. Drop requires OIDC configuration to be done via environment variables.",
|
||||
"skip": "I have a user with OIDC",
|
||||
"title": "OpenID Connect"
|
||||
},
|
||||
"simple": {
|
||||
"description": "Simple authentication uses username/password to authentication users. It is enabled by default if no other authentication provider is enabled.",
|
||||
"register": "Register as admin {arrow}",
|
||||
"title": "Simple authentication"
|
||||
},
|
||||
"title": "Authentication"
|
||||
},
|
||||
"finish": "Let's go {arrow}",
|
||||
"noPage": "no page",
|
||||
"stages": {
|
||||
"account": {
|
||||
"description": "You need at least one account to start using Drop.",
|
||||
"name": "Setup your admin account."
|
||||
},
|
||||
"library": {
|
||||
"description": "Add at least one library source to use Drop.",
|
||||
"name": "Create a library."
|
||||
}
|
||||
},
|
||||
"welcome": "Hey there.",
|
||||
"welcomeDescription": "Welcome to Drop setup wizard. It will walk you through configuring Drop for the first time, and how it works."
|
||||
},
|
||||
"store": {
|
||||
"about": "About",
|
||||
"commingSoon": "coming soon",
|
||||
"developers": "Developers | Developer | Developers",
|
||||
"exploreMore": "Explore more {arrow}",
|
||||
"featured": "Featured",
|
||||
"images": "Game Images",
|
||||
"lookAt": "Check it out",
|
||||
"noDevelopers": "No developers",
|
||||
"noFeatured": "NO FEATURED GAMES",
|
||||
"noGame": "NO GAME",
|
||||
"noGame": "no game",
|
||||
"noImages": "No images",
|
||||
"noPublishers": "No publishers.",
|
||||
"noTags": "No tags",
|
||||
"openAdminDashboard": "Open in Admin Dashboard",
|
||||
"openFeatured": "Star games in Admin Library {arrow}",
|
||||
"platform": "Platform | Platform | Platforms",
|
||||
"publishers": "Publishers | Publisher | Publishers",
|
||||
"rating": "Rating",
|
||||
"readLess": "Click to read less",
|
||||
"readMore": "Click to read more",
|
||||
"recentlyAdded": "Recently Added",
|
||||
"recentlyReleased": "Recently released",
|
||||
"recentlyUpdated": "Recently Updated",
|
||||
"released": "Released",
|
||||
"reviews": "({0} Reviews)",
|
||||
"size": "Size",
|
||||
"tags": "Tags",
|
||||
"title": "Store",
|
||||
"view": {
|
||||
@@ -733,17 +522,15 @@
|
||||
"srGames": "Games",
|
||||
"srViewGrid": "View grid"
|
||||
},
|
||||
"viewInStore": "View in Store"
|
||||
"viewInStore": "View in Store",
|
||||
"website": "Website"
|
||||
},
|
||||
"tasks": {
|
||||
"admin": {
|
||||
"back": "{arrow} Back to Tasks",
|
||||
"completedTasksTitle": "Completed tasks",
|
||||
"dailyScheduledTitle": "Daily scheduled tasks",
|
||||
"execute": "{arrow} Execute",
|
||||
"noActions": "No actions",
|
||||
"noTasksRunning": "No tasks currently running",
|
||||
"progress": "{0}%",
|
||||
"runningTasksTitle": "Running tasks",
|
||||
"scheduled": {
|
||||
"checkUpdateDescription": "Check if Drop has an update.",
|
||||
@@ -765,14 +552,6 @@
|
||||
"type": "Type",
|
||||
"upload": "Upload",
|
||||
"uploadFile": "Upload file",
|
||||
"user": {
|
||||
"editProfile": "Edit profile",
|
||||
"noActivity": "No recent activity",
|
||||
"notFound": "User not found",
|
||||
"recent": "Recent activity (TODO)",
|
||||
"recentSub": "Recent activity by this user",
|
||||
"unknown": "Unknown user"
|
||||
},
|
||||
"userHeader": {
|
||||
"closeSidebar": "Close sidebar",
|
||||
"links": {
|
||||
@@ -789,7 +568,6 @@
|
||||
"admin": {
|
||||
"adminHeader": "Admin?",
|
||||
"adminUserLabel": "Admin user",
|
||||
"authLink": "Authentication {arrow}",
|
||||
"authentication": {
|
||||
"configure": "Configure",
|
||||
"description": "Drop supports a variety of \"authentication mechanisms\". As you enable or disable them, they are shown on the sign in screen for users to select from. Click the dot menu to configure the authentication mechanism.",
|
||||
@@ -801,6 +579,7 @@
|
||||
"srOpenOptions": "Open options",
|
||||
"title": "Authentication"
|
||||
},
|
||||
"authLink": "Authentication {arrow}",
|
||||
"authoptionsHeader": "Auth Options",
|
||||
"delete": "Delete",
|
||||
"deleteUser": "Delete user {0}",
|
||||
@@ -813,7 +592,7 @@
|
||||
"createInvitation": "Create invitation",
|
||||
"description": "Simple authentication uses a system of 'invitations' to create users. You can create an invitation, and optionally specify a username or email for the user, and then it will generate a magic URL that can be used to create an account.",
|
||||
"expires": "Expires: {expiry}",
|
||||
"invitationTitle": "Invitations",
|
||||
"invitationTitle": "invitations",
|
||||
"invite3Days": "3 days",
|
||||
"invite6Months": "6 months",
|
||||
"inviteAdminSwitchDescription": "Create this user as an administrator",
|
||||
|
||||
+1
-733
@@ -1,733 +1 @@
|
||||
{
|
||||
"account": {
|
||||
"devices": {
|
||||
"capabilities": "Capacités",
|
||||
"lastConnected": "Dernière Connexion",
|
||||
"noDevices": "Aucun appareil connecté à vôtre compte.",
|
||||
"platform": "Plateforme",
|
||||
"revoke": "Révoquer",
|
||||
"subheader": "Gérer les appareils authorisés à accéder à votre compte Drop.",
|
||||
"title": "Appareils"
|
||||
},
|
||||
"home": {
|
||||
"title": "Accueil"
|
||||
},
|
||||
"notifications": {
|
||||
"all": "Voir tout {arrow}",
|
||||
"clear": "Effacer les notifications",
|
||||
"desc": "Voir et gérer vos notifications.",
|
||||
"markAllAsRead": "Marquer tout comme lu",
|
||||
"markAsRead": "Marquer comme lu",
|
||||
"none": "Pas de notification",
|
||||
"notifications": "Notifications",
|
||||
"title": "Notifications",
|
||||
"unread": "Notifications Non Lues"
|
||||
},
|
||||
"security": {
|
||||
"title": "Sécurité"
|
||||
},
|
||||
"settings": "Paramètres",
|
||||
"title": "Paramètres du Compte",
|
||||
"token": {
|
||||
"acls": "ACLs/scopes",
|
||||
"aclsDesc": "Définir les permissions du Token. Il n'est pas recommandé de sélectionner toutes les ACLs, à moins que ce soit nécessaire.",
|
||||
"expiry": "Expiration",
|
||||
"expiry3Month": "3 mois",
|
||||
"expiry5Year": "5 Années",
|
||||
"expiry6Month": "6 mois",
|
||||
"expiryMonth": "Un mois",
|
||||
"expiryYear": "Une année",
|
||||
"name": "Nom du Token API",
|
||||
"nameDesc": "Le nom du Token, comme référence.",
|
||||
"namePlaceholder": "Mon nouveau Token",
|
||||
"noExpiry": "Pas d'expiration",
|
||||
"noTokens": "Aucun Token connecté à votre compte.",
|
||||
"revoke": "Révoquer",
|
||||
"subheader": "Gérer vos Tokens et leurs permissions associées.",
|
||||
"success": "Token créé avec succès.",
|
||||
"successNote": "Assurez vous de le sauvegarder maintenant, il ne sera plus disponible après.",
|
||||
"title": "API Tokens"
|
||||
}
|
||||
},
|
||||
"actions": "Actions",
|
||||
"add": "Ajouter",
|
||||
"adminTitle": "Tableau de Bord Administratif - Drop",
|
||||
"adminTitleTemplate": "{0} - Administration - Drop",
|
||||
"auth": {
|
||||
"callback": {
|
||||
"authClient": "Authoriser le client ?",
|
||||
"authorize": "Authoriser",
|
||||
"authorizedClient": "Drop a réussi a autoriser le client. Vous pouvez fermer cette fenêtre.",
|
||||
"issues": "Vous avez des problèmes ?",
|
||||
"learn": "En savoir plus {arrow}",
|
||||
"paste": "Collez ce code dans le client pour continuer :",
|
||||
"permWarning": "Accepter cette requête autorisera \"{name}\" sur \"{plateform} à :",
|
||||
"requestedAccess": "\"{name} a demandé accès à votre compte Drop.",
|
||||
"success": "Réussi !"
|
||||
},
|
||||
"code": {
|
||||
"description": "Utiliser un code pour vous connecter à votre client Drop si vous ne pouvez pas ouvrir un navigateur web sur votre appareil.",
|
||||
"title": "Connecter votre client Drop"
|
||||
},
|
||||
"confirmPassword": "Confirmez @:auth.password",
|
||||
"displayName": "Nom d'Affichage",
|
||||
"email": "Email",
|
||||
"password": "Mot de passe",
|
||||
"register": {
|
||||
"confirmPasswordFormat": "Doit être pareil qu'au dessus",
|
||||
"emailFormat": "Doit être au format utilisateur{'@'}exemple.com",
|
||||
"passwordFormat": "Doit être au moins 8 caractères ou plus",
|
||||
"subheader": "Remplissez vos coordonnées pour créer votre compte.",
|
||||
"title": "Créer votre compte Drop",
|
||||
"usernameFormat": "Doit être au moins 5 caractères et en minuscules"
|
||||
},
|
||||
"signin": {
|
||||
"externalProvider": "un fournisseur externe",
|
||||
"forgot": "Mot de passe oublié ?",
|
||||
"noAccount": "Pas de compte ? Demandez à un administrateur d'en créer un pour vous.",
|
||||
"or": "OU",
|
||||
"pageTitle": "Se connecter à Drop",
|
||||
"rememberMe": "Se souvenir de moi",
|
||||
"signin": "Se connecter",
|
||||
"signinWithExternalProvider": "Connectez vous avec {externalProvider} {arrow}",
|
||||
"title": "Se connecter à votre compte"
|
||||
},
|
||||
"signout": "Déconnexion",
|
||||
"username": "Nom d'utilisateur"
|
||||
},
|
||||
"cancel": "Annuler",
|
||||
"chars": {
|
||||
"arrow": "→",
|
||||
"arrowBack": "←",
|
||||
"arrowDown": "↓",
|
||||
"arrowUp": "↑",
|
||||
"quoted": "\"\"",
|
||||
"srComma": ", {0}"
|
||||
},
|
||||
"common": {
|
||||
"add": "Ajouter",
|
||||
"cannotUndo": "Cette action ne peut pas être défaite.",
|
||||
"close": "Fermer",
|
||||
"create": "Créer",
|
||||
"date": "Date",
|
||||
"delete": "Supprimer",
|
||||
"deleteConfirm": "Êtes vous sûr de vouloir supprimer \"{0}\" ?",
|
||||
"divider": "{'|'}",
|
||||
"edit": "Éditer",
|
||||
"friends": "Amis",
|
||||
"groups": "Groupes",
|
||||
"insert": "Insérer",
|
||||
"labelValueColon": "{label} : {value}",
|
||||
"name": "Nom",
|
||||
"noData": "Pas de donnée",
|
||||
"noResults": "Pas de résultat",
|
||||
"noSelected": "Pas d'élément sélectionné.",
|
||||
"remove": "Retirer",
|
||||
"save": "Sauvegarder",
|
||||
"saved": "Sauvegardé",
|
||||
"servers": "Serveurs",
|
||||
"srLoading": "Chargement…",
|
||||
"tags": "Étiquettes",
|
||||
"today": "Aujourd'hui"
|
||||
},
|
||||
"drop": {
|
||||
"desc": "Une plateforme de distribution libre conçue pour être rapide, flexible et belle.",
|
||||
"drop": "Drop"
|
||||
},
|
||||
"editor": {
|
||||
"bold": "Gras",
|
||||
"boldPlaceholder": "Caractères gras",
|
||||
"code": "Code",
|
||||
"codePlaceholder": "code",
|
||||
"heading": "En-tête",
|
||||
"headingPlaceholder": "en-tête",
|
||||
"italic": "Italique",
|
||||
"italicPlaceholder": "texte italique",
|
||||
"link": "Lien",
|
||||
"linkPlaceholder": "texte du lien",
|
||||
"listItem": "Élement de liste",
|
||||
"listItemPlaceholder": "élément de liste"
|
||||
},
|
||||
"errors": {
|
||||
"admin": {
|
||||
"user": {
|
||||
"delete": {
|
||||
"desc": "Drop n'a pas pu supprimer cet utilisateur : {0}",
|
||||
"title": "Échec de la suppression de l'utilisateur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"disabled": "Compte invalide or désactivé. Merci de contacter l'administrateur du serveur.",
|
||||
"invalidInvite": "Invitation invalide ou expirée",
|
||||
"invalidPassState": "Le mot de passe enregistré est invalide. Merci de contacter l'administrateur du serveur.",
|
||||
"invalidUserOrPass": "Nom d'utilisateur ou password invalide.",
|
||||
"inviteIdRequired": "id est requis pour récupérer l'invitation",
|
||||
"method": {
|
||||
"signinDisabled": "Méthode de connexion non activée"
|
||||
},
|
||||
"usernameTaken": "Nom d'utilisateur déjà pris."
|
||||
},
|
||||
"backHome": "{arrow} Retour a l'accueil",
|
||||
"externalUrl": {
|
||||
"subtitle": "Ce message n'est visible qu'aux administrateurs.",
|
||||
"title": "Accès via une EXTERNAL_URL différente. Veuillez consulter la documentation."
|
||||
},
|
||||
"game": {
|
||||
"banner": {
|
||||
"description": "Drop a échoué a mettre à jour l'image de la bannière : {0}",
|
||||
"title": "Échec de la mise à jour de l'image de la bannière"
|
||||
},
|
||||
"carousel": {
|
||||
"description": "Drop a échoué a mettre a jour le carrousel à images : {0}",
|
||||
"title": "Échec de la mise à jour du carrousel à images"
|
||||
},
|
||||
"cover": {
|
||||
"description": "Drop a échoué à mettre à jour l'image de couverture : {0}",
|
||||
"title": "Échec de la mise à jour de l'image de couverture"
|
||||
},
|
||||
"deleteImage": {
|
||||
"description": "Drop a échoué à supprimer l'image : {0}",
|
||||
"title": "Échec de la suppression de l'image"
|
||||
},
|
||||
"description": {
|
||||
"description": "Drop a échoué à mettre à jour la description du jeu : {0}",
|
||||
"title": "Échec de la mise à jour de la description du jeu"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Drop a échoué à mettre à jour les données méta : {0}",
|
||||
"title": "Échec de la mise à jour des données méta"
|
||||
}
|
||||
},
|
||||
"invalidBody": "Corps de requête non valide : {0}",
|
||||
"inviteRequired": "Invitation requise pour créer un compte.",
|
||||
"library": {
|
||||
"add": {
|
||||
"desc": "Drop n'a pas pu ajouter ce jeu à votre bibliothèque : {0}",
|
||||
"title": "Échec de l'ajout du jeu à la bibliothèque"
|
||||
},
|
||||
"collection": {
|
||||
"create": {
|
||||
"desc": "Drop n'a pas pu créer votre collection : {0}",
|
||||
"title": "Échec de la création de la collection"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"delete": {
|
||||
"desc": "Drop n'a pas pu supprimer cette source : {0}",
|
||||
"title": "Échec de la suppression de la source de bibliothèque"
|
||||
}
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"article": {
|
||||
"delete": {
|
||||
"desc": "Drop n'a pas pu supprimer cet article : {0}",
|
||||
"title": "Échec de la suppression de l'article"
|
||||
}
|
||||
}
|
||||
},
|
||||
"occurred": "Une erreur s'est produite en réponse à vôtre requête. Si vous pensez que c'est un bug, merci de le rapporter. Essayer de vous connecter et voyez si cela résoud le problème.",
|
||||
"ohNo": "Oh non !",
|
||||
"pageTitle": "{0} | Drop",
|
||||
"revokeClient": "Échec de la révocation du client",
|
||||
"revokeClientFull": "Échec de la revocation du client {0}",
|
||||
"signIn": "Se connecter {arrow}",
|
||||
"support": "Assistance Discord",
|
||||
"unknown": "Une erreur inconnue est survenue",
|
||||
"upload": {
|
||||
"description": "Drop n'a pas pu uploader le fichier : {0}",
|
||||
"title": "Échec de l'upload du fichier"
|
||||
},
|
||||
"version": {
|
||||
"delete": {
|
||||
"desc": "Drop a rencontré une erreur pendant la suppression de la version : {error}",
|
||||
"title": "Une erreur est survenue pendant la supression de la version"
|
||||
},
|
||||
"order": {
|
||||
"desc": "Drop a rencontré une erreur pendant la mise a jour de la version : {error}",
|
||||
"title": "Une erreur est survenue pendant la mise a jour de l'ordre des versions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"about": "À propos",
|
||||
"aboutDrop": "À propos de Drop",
|
||||
"api": "Documentation de l'API",
|
||||
"comparison": "Comparaison",
|
||||
"docs": {
|
||||
"client": "Documentation du client",
|
||||
"server": "Documentation du serveur"
|
||||
},
|
||||
"documentation": "Documentation",
|
||||
"findGame": "Trouver un jeu",
|
||||
"footer": "Pied de page",
|
||||
"games": "Jeux",
|
||||
"social": {
|
||||
"discord": "Discord",
|
||||
"github": "GitHub"
|
||||
},
|
||||
"topSellers": "Meilleures Ventes",
|
||||
"version": "Drop {version} {gitRef}"
|
||||
},
|
||||
"header": {
|
||||
"admin": {
|
||||
"admin": "Administration",
|
||||
"home": "Accueil",
|
||||
"library": "Bibliothèque",
|
||||
"metadata": "Méta",
|
||||
"settings": {
|
||||
"store": "Store",
|
||||
"title": "Paramètres",
|
||||
"tokens": "API tokens"
|
||||
},
|
||||
"tasks": "Tâches",
|
||||
"users": "Utilisateurs"
|
||||
},
|
||||
"back": "Retour",
|
||||
"openSidebar": "Ouvrir la barre latérale"
|
||||
},
|
||||
"helpUsTranslate": "Aidez nous à traduire Drop {arrow}",
|
||||
"highest": "le plus haut",
|
||||
"home": {
|
||||
"admin": {
|
||||
"activeInactiveUsers": "Utilisateurs actifs/inactifs",
|
||||
"activeUsers": "Utilisateurs actifs",
|
||||
"allVersionsCombined": "Toutes les versions combinées",
|
||||
"availableRam": "({usedRam} / {totalRam})",
|
||||
"biggestGamesOnServer": "Les plus gros jeux sur le serveur",
|
||||
"biggestGamesToDownload": "Les plus gros jeux à télécharger",
|
||||
"cpuUsage": "Utilisation du processeur",
|
||||
"games": "Jeux",
|
||||
"goToUsers": "Aller aux utilisateurs",
|
||||
"inactiveUsers": "Utilisateurs inactifs",
|
||||
"latestVersionOnly": "Dernière version seulement",
|
||||
"librarySources": "Sources de bibliothèques",
|
||||
"numberCores": "({count} cœur) | ({count} cœur) | ({count} cœurs)",
|
||||
"ramUsage": "Utilisation de la mémoire vive",
|
||||
"subheader": "Résumé de l'instance",
|
||||
"title": "Accueil",
|
||||
"users": "Utilisateurs",
|
||||
"version": "Version"
|
||||
}
|
||||
},
|
||||
"library": {
|
||||
"addGames": "Tous les jeux",
|
||||
"addToLib": "Ajouter à la bibliothèque",
|
||||
"admin": {
|
||||
"detectedGame": "Drop a détecté que vous avez des nouveaux jeux a importer.",
|
||||
"detectedVersion": "Drop a détecté que vous avez des nouvelles versions de ce jeu à importer.",
|
||||
"game": {
|
||||
"addCarouselNoImages": "Pas d'image a ajouter.",
|
||||
"addDescriptionNoImages": "Pas d'image à ajouter.",
|
||||
"addImageCarousel": "Ajouter à partir d'une bibliothèque d'images",
|
||||
"currentBanner": "bannière",
|
||||
"currentCover": "couverture",
|
||||
"deleteImage": "Supprimer l'image",
|
||||
"editGameDescription": "Description du jeu",
|
||||
"editGameName": "Nom du jeu",
|
||||
"editReleaseDate": "Date de sortie",
|
||||
"imageCarousel": "Carrousel d'images",
|
||||
"imageCarouselDescription": "Personnaliser quelles images et dans quel ordre elles sont affichées sur la page du Store.",
|
||||
"imageCarouselEmpty": "Aucune image n'a encore été ajoutée au carousel.",
|
||||
"imageLibrary": "Bibliothèque d'images",
|
||||
"imageLibraryDescription": "Veuillez noter que toutes les images uploadées sont accessible a tous les utilisateurs via des outils de développement des navigateurs.",
|
||||
"removeImageCarousel": "Retirer l'image",
|
||||
"setBanner": "Définir comme bannière",
|
||||
"setCover": "Définir comme couverture"
|
||||
},
|
||||
"gameLibrary": "Bibliothèque de jeux",
|
||||
"import": {
|
||||
"bulkImportDescription": "Lorsque vous êtes sur cette page, vous ne serez pas redirigé sur la tâche d'importation, pour que vous puissiez importer plusieurs jeux successivement.",
|
||||
"bulkImportTitle": "Mode d'importation de masse",
|
||||
"import": "Importer",
|
||||
"link": "Imported {arrow}",
|
||||
"loading": "Chargement des résultats des jeux…",
|
||||
"search": "Rechercher",
|
||||
"searchPlaceholder": "Fallout 4",
|
||||
"selectDir": "Merci de choisir un dossier…",
|
||||
"selectGame": "Sélectionnez le jeu à importer",
|
||||
"selectGamePlaceholder": "Merci de sélectionner un jeu…",
|
||||
"selectGameSearch": "Sélectionner un jeu",
|
||||
"selectPlatform": "Merci de sélectionner une plateforme…",
|
||||
"version": {
|
||||
"advancedOptions": "Options avancées",
|
||||
"import": "Importer une version",
|
||||
"installDir": "(install_dir)/",
|
||||
"launchCmd": "Lancer l'exécutable/commande",
|
||||
"launchDesc": "Exécutable pour lancer le jeu",
|
||||
"launchPlaceholder": "jeu.exe --args",
|
||||
"loadingVersion": "Chargement des métadonnées de la version…",
|
||||
"noAdv": "Pas d'option avancée pour cette configuration.",
|
||||
"noLaunches": "Aucune configuration de lancement ajoutée.",
|
||||
"noSetups": "Aucune configuration d'installation ajoutée.",
|
||||
"noVersions": "Pas de version à importer",
|
||||
"platform": "Version de la plateforme",
|
||||
"setupCmd": "Exécutable/commande d'installation",
|
||||
"setupDesc": "Exécuté une fois lorsque le jeu a été installé",
|
||||
"setupMode": "Mode de configuration",
|
||||
"setupModeDesc": "Lorsqu'elle est activée, cette version n'a pas de commande de lancement, et exécute simplement l'exécutable sur l'ordinateur de l'utilisateur. Utile pour les jeux qui distribue uniquement des fichiers d'installation et non les fichiers portables.",
|
||||
"setupPlaceholder": "setup.exe",
|
||||
"umuLauncherId": "UMU Launcher ID",
|
||||
"umuOverride": "Remplacer l'ID de jeu du lanceur UMU",
|
||||
"umuOverrideDesc": "Par défaut, Drop utilise un non-ID pour lancer les jeux avec UMU Launcher. Pour récupérer les bons patchs pour certains jeux, vous pourriez avoir besoin de changer ce champ manuellement.",
|
||||
"updateMode": "Mode de mise à jour",
|
||||
"updateModeDesc": "Lorsqu'ils sont activés, ces fichiers seront installés par-dessus (remplaçant) la version précédente. Si plusieurs \"modes de mise à jour\" sont enchaînés, ils sont appliqués dans l'ordre.",
|
||||
"version": "Sélectionner la version à importer"
|
||||
},
|
||||
"withoutMetadata": "Importer sans les données méta"
|
||||
},
|
||||
"libraryHint": "Pas de bibliothèque configurée.",
|
||||
"libraryHintDocsLink": "Qu'est-ce que cela veut dire ? {arrow}",
|
||||
"metadata": {
|
||||
"companies": {
|
||||
"action": "Gérer {arrow}",
|
||||
"addGame": {
|
||||
"description": "Choisissez un jeu à ajouter à la société, et si il faudrait la lister en tant que développeur, éditeur, ou les deux.",
|
||||
"developer": "Développeur ?",
|
||||
"noGames": "Pas de jeu à ajouter",
|
||||
"publisher": "Éditeur ?",
|
||||
"title": "Connecter le jeu a cette société"
|
||||
},
|
||||
"description": "Les sociétés organisent les jeux par qui les a développer ou éditer.",
|
||||
"editor": {
|
||||
"action": "Ajouter un jeu {plus}",
|
||||
"descriptionPlaceholder": "{'<'}description{'>'}",
|
||||
"developed": "Développé",
|
||||
"libraryDescription": "Ajouter, supprimer ou personnaliser ce que cette société a développé et/ou publié.",
|
||||
"libraryTitle": "Bibliothèque de jeux",
|
||||
"noDescription": "(pas de description)",
|
||||
"published": "Publié",
|
||||
"uploadBanner": "Uploader bannière",
|
||||
"uploadIcon": "Uplader icône",
|
||||
"websitePlaceholder": "{'<'}site web{'>'}"
|
||||
},
|
||||
"modals": {
|
||||
"createDescription": "Créez une société pour mieux organizer vos jeux.",
|
||||
"createFieldDescription": "Description de la Société",
|
||||
"createFieldDescriptionPlaceholder": "Un petit studio indépendant qui...",
|
||||
"createFieldName": "Nom de la société",
|
||||
"createFieldNamePlaceholder": "Ma nouvelle société...",
|
||||
"createFieldWebsite": "Site web de la société",
|
||||
"createFieldWebsitePlaceholder": "https://exemple com/",
|
||||
"createTitle": "Créer une société",
|
||||
"nameDescription": "Éditer le nom de la société. Ce nom est utilisé pour trouver les jeux nouvellement importés.",
|
||||
"nameTitle": "Éditer le nom de la société",
|
||||
"shortDeckDescription": "Éditer la description de la company. Cela n'affecte pas la description longue (markdown).",
|
||||
"shortDeckTitle": "Éditer la description de la société",
|
||||
"websiteDescription": "Éditer le site internet de la société. Note : cela sera un lien, et ne bénéficiera pas de la protection aux redirects.",
|
||||
"websiteTitle": "Éditer le site internet de la société"
|
||||
},
|
||||
"noCompanies": "Pas de société",
|
||||
"noGames": "Pas de jeu",
|
||||
"search": "Chercher des sociétés…",
|
||||
"searchGames": "Chercher les jeux de l'entreprise…",
|
||||
"title": "Sociétés"
|
||||
},
|
||||
"tags": {
|
||||
"action": "Gérer {arrow}",
|
||||
"create": "Créer",
|
||||
"description": "Les tags sont automatiquement créés à partir des genres importés. Vous pouvez ajouter des tags personnalisés pour ajouter la catégorisation de votre bibliothèque de jeux.",
|
||||
"modal": {
|
||||
"description": "Créer un tag pour organiser votre bibliothèque.",
|
||||
"title": "Créer un tag"
|
||||
},
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
"metadataProvider": "Fournisseur de données méta",
|
||||
"noGames": "Pas de jeu importé",
|
||||
"offline": "Drop n'a pas pu accéder à ce jeu.",
|
||||
"offlineTitle": "Jeu hors-ligne",
|
||||
"openEditor": "Ouvrir dans l'éditeur {arrow}",
|
||||
"openStore": "Ouvrir dans le Store",
|
||||
"shortDesc": "Description Courte",
|
||||
"sources": {
|
||||
"create": "Créer une source",
|
||||
"createDesc": "Drop va utiliser cette source pour accéder à votre bibliothèque de jeux, et les rendre disponible.",
|
||||
"desc": "Configurer vos sources de bibliothèques où Drop va regarder pour les nouveaux jeux et versions à importer.",
|
||||
"documentationLink": "Documentation {arrow}",
|
||||
"edit": "Éditer la source",
|
||||
"freeSpace": "Espace disponible",
|
||||
"fsDesc": "Importe les jeux à partir d'un chemin d'accès sur le disque. Cela requière une structure des dossiers basées sur la version, et qui supporte les jeux archivés.",
|
||||
"fsFlatDesc": "Importe les jeux à partir d'un chemin d’accès sur le disque, mais sans le sous-dossier version séparé. Utile pour migrer une bibliothèque vers Drop.",
|
||||
"fsFlatTitle": "Compatibilité",
|
||||
"fsPath": "Chemin d’accès",
|
||||
"fsPathDesc": "Un chemin d’accès absolu à votre bibliothèque de jeux.",
|
||||
"fsPathPlaceholder": "/mnt/jeux",
|
||||
"fsTitle": "Drop-style",
|
||||
"link": "Sources {arrow}",
|
||||
"nameDesc": "Le nom de votre source, pour référence.",
|
||||
"namePlaceholder": "Mes Nouvelle Source",
|
||||
"percentage": "{number}%",
|
||||
"sources": "Sources de Bibliothèques",
|
||||
"totalSpace": "Espace total",
|
||||
"typeDesc": "Le type de source. Affecte les options requises.",
|
||||
"utilizationPercentage": "Pourcentage d'utilisation",
|
||||
"working": "Marche ?"
|
||||
},
|
||||
"subheader": "Lorsque que vous rajoutez des dossiers à vos sources de bibliothèques, Drop le détectera et vous demandera de les importer. Chaque jeu a besoin d’être importé avant que vous puissiez importer une version.",
|
||||
"title": "Bibliothèques",
|
||||
"version": {
|
||||
"delta": "Mode de mise à jour",
|
||||
"noVersions": "Vous n'avez aucune version de ce jeu de disponible.",
|
||||
"noVersionsAdded": "pas de version ajoutée"
|
||||
},
|
||||
"versionPriority": "Priorité des versions"
|
||||
},
|
||||
"back": "Retour à la Bibliothèque",
|
||||
"collection": {
|
||||
"addToNew": "Ajouter à une nouvelle collection",
|
||||
"collections": "Collections",
|
||||
"create": "Créer une Collection",
|
||||
"createDesc": "Les collections peuvent être utilisées pour organiser vos jeux et vous permettre de les trouver plus facilement, surtout si vous possédez une grosse bibliothèque.",
|
||||
"delete": "Supprimer la Collection",
|
||||
"namePlaceholder": "Nom de la collection",
|
||||
"noCollections": "Pas de collection",
|
||||
"notFound": "Collection non trouvée",
|
||||
"subheader": "Ajouter une nouvelle collection pour organiser vos jeux",
|
||||
"title": "Collection"
|
||||
},
|
||||
"gameCount": "{0} jeux | {0} jeu | {0} jeux",
|
||||
"inLib": "Dans la Bibliothèque",
|
||||
"launcherOpen": "Ouvrir dans le Launcher",
|
||||
"noGames": "Pas de jeu dans la bibliothèque",
|
||||
"notFound": "Jeu non trouvé",
|
||||
"search": "Chercher bibliothèque…",
|
||||
"subheader": "Organiser vos jeux en collections pour un accès facile, et accéder à tous vos jeux."
|
||||
},
|
||||
"news": {
|
||||
"article": {
|
||||
"add": "Ajouter",
|
||||
"content": "Contenu (Markdown)",
|
||||
"create": "Créer un Nouvel Article",
|
||||
"editor": "Éditeur",
|
||||
"editorGuide": "Utilisez les raccourcis ci-dessus ou écrivez en Markdown directement. Supporte **gras**, *italique*, [lines](adresse), et plus.",
|
||||
"new": "Nouvel article",
|
||||
"preview": "Aperçu",
|
||||
"shortDesc": "Description courte",
|
||||
"submit": "Soumettre",
|
||||
"tagPlaceholder": "Ajouter un tag…",
|
||||
"titles": "Titre",
|
||||
"uploadCover": "Uploader l'image de couverture"
|
||||
},
|
||||
"back": "Retour aux Nouvelles",
|
||||
"checkLater": "Vérifier plus tard pour les mises à jour.",
|
||||
"delete": "Supprimer l'Article",
|
||||
"filter": {
|
||||
"all": "Tous les temps",
|
||||
"month": "Ce mois",
|
||||
"week": "Cette semaine",
|
||||
"year": "Cette année"
|
||||
},
|
||||
"none": "Pas d'article",
|
||||
"notFound": "Article non trouvé",
|
||||
"search": "Chercher des articles",
|
||||
"searchPlaceholder": "Chercher des articles…",
|
||||
"subheader": "Rester à jour avec les dernières mises à et annonces.",
|
||||
"title": "Dernières Nouvelles"
|
||||
},
|
||||
"options": "Options",
|
||||
"selectLanguage": "Sélectionner la langue",
|
||||
"services": {
|
||||
"nginx": {
|
||||
"description": "Proxy inverse simple intégré pour connecter tous les composants Drop.",
|
||||
"title": "NGINX"
|
||||
},
|
||||
"torrential": {
|
||||
"description": "Le server de téléchargement interne de Drop.",
|
||||
"title": "Torrential"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"admin": {
|
||||
"description": "Configurer les paramètres de Drop",
|
||||
"store": {
|
||||
"dropGameAltPlaceholder": "Exemple d'icône de Jeu",
|
||||
"dropGameDescriptionPlaceholder": "Ceci est un jeu exemple. Il sera remplacé si vous importez un jeu.",
|
||||
"dropGameNamePlaceholder": "Jeu Exemple",
|
||||
"showGamePanelTextDecoration": "Afficher le titre et la description sur les tuiles de jeu (par défaut : activé)",
|
||||
"title": "Store"
|
||||
},
|
||||
"title": "Paramètres"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"auth": {
|
||||
"description": "Authentification sur Drop se passe à travers multiple 'fournisseurs' pré-configuré. Chaque fournisseur peut autoriser des utilisateurs à se connecter via leurs méthodes. Pour commencer, aillez au moins un fournisseur d'authentification d'activé, et créer un compte via celui-ci.",
|
||||
"docs": "Documentation {arrow}",
|
||||
"enabled": "Activé ?",
|
||||
"openid": {
|
||||
"description": "OpenID Connect (OIDC) est une extension OAuth2 communément supportée. Drop requière que la configuration OIDC se fasse via des variables d'environnement.",
|
||||
"skip": "J'ai un utiliser avec OIDC",
|
||||
"title": "OpenID Connect"
|
||||
},
|
||||
"simple": {
|
||||
"description": "L'authentification simple utilise le nom d'utiliser et mot de passe pour authentifier les utilisateurs. Elle est activée par défaut si aucun autre fournisseur d'authentification est activé.",
|
||||
"register": "Créer un compte administrateur",
|
||||
"title": "Authentification simple"
|
||||
},
|
||||
"title": "Authentification"
|
||||
},
|
||||
"finish": "C'est parti {arrow}",
|
||||
"noPage": "Pas de page",
|
||||
"stages": {
|
||||
"account": {
|
||||
"description": "Vous avez besoin d'au moins un compte pour démarrer Drop.",
|
||||
"name": "Configurez votre compte administrateur."
|
||||
},
|
||||
"library": {
|
||||
"description": "Ajouter au moins une source de bibliothèques pour utiliser Drop.",
|
||||
"name": "Créer une bibliothèque."
|
||||
}
|
||||
},
|
||||
"welcome": "Salut.",
|
||||
"welcomeDescription": "Bienvenue dans l'assistant de configuration de Drop. Il va aidera configurer Drop pour la première fois, et vous expliquera son fonctionnement."
|
||||
},
|
||||
"store": {
|
||||
"about": "À propos",
|
||||
"commingSoon": "prochainement",
|
||||
"developers": "Développeurs | Développeur | Développeurs",
|
||||
"exploreMore": "Explorer plus {arrow}",
|
||||
"featured": "Mis en avant",
|
||||
"images": "Images de Jeux",
|
||||
"lookAt": "Découvrez le maintenant",
|
||||
"noDevelopers": "Pas de développeur",
|
||||
"noFeatured": "PAS DE JEU MIS EN AVANT",
|
||||
"noGame": "PAS DE JEU",
|
||||
"noImages": "Pas d'image",
|
||||
"noPublishers": "Pas d'éditeur.",
|
||||
"noTags": "Pas de tag",
|
||||
"openAdminDashboard": "Ouvrir dans le Tableau de Bord d'Administration",
|
||||
"openFeatured": "Mettez des étoiles aux jeux dans l'administration de la bibliothèque {arrow}",
|
||||
"platform": "Plateforme | Plateforme | Plateformes",
|
||||
"publishers": "Éditeurs | Éditeur | Éditeurs",
|
||||
"rating": "Note",
|
||||
"readLess": "Cliquez pour lire moins",
|
||||
"readMore": "Clique pour lire plus",
|
||||
"recentlyAdded": "Ajouté Récemment",
|
||||
"recentlyReleased": "Récemment publié",
|
||||
"recentlyUpdated": "Récemment Mis à Jour",
|
||||
"released": "Publié",
|
||||
"reviews": "({0} Avis)",
|
||||
"size": "Taille",
|
||||
"tags": "Tags",
|
||||
"title": "Store",
|
||||
"view": {
|
||||
"sort": "Trier",
|
||||
"srFilters": "Filtres",
|
||||
"srGames": "Jeux",
|
||||
"srViewGrid": "Voir grille"
|
||||
},
|
||||
"viewInStore": "Voir dans le Store",
|
||||
"website": "Site internet"
|
||||
},
|
||||
"tasks": {
|
||||
"admin": {
|
||||
"back": "{arrow} Retour aux Tâches",
|
||||
"completedTasksTitle": "Tâches complétées",
|
||||
"dailyScheduledTitle": "Tâches quotidiennes planifiées",
|
||||
"execute": "{arrow} Exécuter",
|
||||
"noTasksRunning": "Pas de tâche en cours",
|
||||
"progress": "{0}%",
|
||||
"runningTasksTitle": "Tâches en cours d'exécution",
|
||||
"scheduled": {
|
||||
"checkUpdateDescription": "Vérifier si Drop a une mise à jour.",
|
||||
"checkUpdateName": "Vérifier la mise à jour.",
|
||||
"cleanupInvitationsDescription": "Nettoie les invitations expirées de la base de données pour économiser de l'espace.",
|
||||
"cleanupInvitationsName": "Nettoie les invitations",
|
||||
"cleanupObjectsDescription": "Détecte et supprime les objets non référencés et non utilisés pour économiser de l'espace.",
|
||||
"cleanupObjectsName": "Nettoyer les objets",
|
||||
"cleanupSessionsDescription": "Nettoie les sessions expirées pour économiser de l'espace et assurer la sécurité.",
|
||||
"cleanupSessionsName": "Nettoie les sessions."
|
||||
},
|
||||
"viewTask": "Voir {arrow}",
|
||||
"weeklyScheduledTitle": "Tâches hebdomadaires planifiées"
|
||||
}
|
||||
},
|
||||
"title": "Drop",
|
||||
"titleTemplate": "{0} - Drop",
|
||||
"todo": "À faire",
|
||||
"type": "Type",
|
||||
"upload": "Uploader",
|
||||
"uploadFile": "Uploader fichier",
|
||||
"user": {
|
||||
"editProfile": "Éditer profil",
|
||||
"noActivity": "Pas d'activité récente",
|
||||
"notFound": "Utilisateur introuvable",
|
||||
"recent": "Activité récente (À faire)",
|
||||
"recentSub": "Activité récente de cet utilisateur",
|
||||
"unknown": "Utilisateur inconnu"
|
||||
},
|
||||
"userHeader": {
|
||||
"closeSidebar": "Fermer la barre latérale",
|
||||
"links": {
|
||||
"community": "Communauté",
|
||||
"library": "Bibliothèque",
|
||||
"news": "Nouvelles"
|
||||
},
|
||||
"profile": {
|
||||
"admin": "Tableau de Bord Administratif",
|
||||
"settings": "Paramètres du compte"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"admin": {
|
||||
"adminHeader": "Administrateur ?",
|
||||
"adminUserLabel": "Administrateur",
|
||||
"authLink": "Authentification {arrow}",
|
||||
"authentication": {
|
||||
"configure": "Configurer",
|
||||
"description": "Drop supporte une variété de \"mécanismes d'authentification\". Lorsque vous les activez ou les désactivez, ils sont affichés sur la page de connection pour que les utilisateurs puissent les sélectionner. Cliquer sur le menu à points pour configurer le mécanisme d'authentification.",
|
||||
"disabled": "Désactivé",
|
||||
"enabled": "Activé",
|
||||
"enabledKey": "Activée ?",
|
||||
"oidc": "OpenID Connect",
|
||||
"simple": "Simple (nom d'utilisateur/mot de passe)",
|
||||
"srOpenOptions": "Ouvrir les options",
|
||||
"title": "Authentification"
|
||||
},
|
||||
"authoptionsHeader": "Options Auth",
|
||||
"delete": "Supprimer",
|
||||
"deleteUser": "Supprimer l'utilisateur {0}",
|
||||
"description": "Gérer les utilisateurs sur votre instance Drop, et configurer vos méthodes d'authentification.",
|
||||
"displayNameHeader": "Nom d'affichage",
|
||||
"emailHeader": "Email",
|
||||
"normalUserLabel": "Utilisateur normal",
|
||||
"simple": {
|
||||
"adminInvitation": "Invitation adminstrateur",
|
||||
"createInvitation": "Créer invitation",
|
||||
"description": "L'authentification simple utilise un système d'invitations pour créer les utilisateurs. Tu peux créer une invitation et optionnellement spécifier le nom d'utilisateur ou email de cet utilisateur, et un lien magique sera généré un lien magique qui peut être utilisé pour créer le compte.",
|
||||
"expires": "Expire : {expiry}",
|
||||
"invitationTitle": "Invitations",
|
||||
"invite3Days": "3 jours",
|
||||
"invite6Months": "6 mois",
|
||||
"inviteAdminSwitchDescription": "Créer cet utilisateur en tant qu'adminstrateur",
|
||||
"inviteAdminSwitchLabel": "Invitation adminstrateur",
|
||||
"inviteButton": "Invitation",
|
||||
"inviteDescription": "Drop va générer une adresse que vous pouvez envoyer à la personne que vous voulez inviter. Vous pouvez optionnellement spécifier un nom d'utilisateur ou une adresse e-mail qu'elle pourra utiliser.",
|
||||
"inviteEmailDescription": "Doit être dans le format utilisateur{'@'}exemple.com",
|
||||
"inviteEmailLabel": "E-mail adresse (optionnel)",
|
||||
"inviteEmailPlaceholder": "moi{'@'}exemple.com",
|
||||
"inviteExpiryLabel": "Expire",
|
||||
"inviteMonth": "1 mois",
|
||||
"inviteNever": "Jamais",
|
||||
"inviteTitle": "Inviter l'utilisateur sur Drop",
|
||||
"inviteUsernameFormat": "Doit être 5 caractères ou plus",
|
||||
"inviteUsernameLabel": "Nom d'utilisateur (optionnel)",
|
||||
"inviteUsernamePlaceholder": "monNomDUtilisateur",
|
||||
"inviteWeek": "1 semaine",
|
||||
"inviteYear": "1 an",
|
||||
"neverExpires": "N'expire jamais.",
|
||||
"noEmailEnforced": "Pas d'e-mail imposé.",
|
||||
"noInvitations": "Pas d'invitation.",
|
||||
"noUsernameEnforced": "Pas de nom d'utilisateur imposé.",
|
||||
"title": "Authentication simple",
|
||||
"userInvitation": "Invitation utilisateur"
|
||||
},
|
||||
"srEditLabel": "Éditer",
|
||||
"usernameHeader": "Nom d'utilisateur"
|
||||
}
|
||||
},
|
||||
"welcome": "Américain, bienvenue !"
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -1,709 +0,0 @@
|
||||
{
|
||||
"account": {
|
||||
"devices": {
|
||||
"capabilities": "Możliwości",
|
||||
"lastConnected": "Ostatnio Połączone",
|
||||
"noDevices": "Brak urządzeń połączonych z Twoim kontem.",
|
||||
"platform": "Platforma",
|
||||
"revoke": "Usuń",
|
||||
"subheader": "Zarządzaj urządzeniami z dostępem do Twojego konta Drop.",
|
||||
"title": "Urządzenia"
|
||||
},
|
||||
"notifications": {
|
||||
"all": "Zobacz wszystkie {arrow}",
|
||||
"clear": "Wyczyść powiadomienia",
|
||||
"desc": "Przeglądaj i zarządzaj powiadomieniami.",
|
||||
"markAllAsRead": "Oznacz wszystkie jako przeczytane",
|
||||
"markAsRead": "Oznacz jako przeczytane",
|
||||
"none": "Brak powiadomień",
|
||||
"notifications": "Powiadomienia",
|
||||
"title": "Powiadomienia",
|
||||
"unread": "Nieprzeczytane Powiadomienia"
|
||||
},
|
||||
"settings": "Ustawienia",
|
||||
"title": "Ustawienia Konta",
|
||||
"token": {
|
||||
"acls": "ACLe/zakresy",
|
||||
"aclsDesc": "Definiuje zakres uprawnień tego tokena. Unikaj zaznaczania wszystkich uprawnień ACL, jeśli nie są one potrzebne.",
|
||||
"expiry": "Data Ważności",
|
||||
"expiry3Month": "3 miesiące",
|
||||
"expiry5Year": "5 lat",
|
||||
"expiry6Month": "6 miesięcy",
|
||||
"expiryMonth": "Miesiąc",
|
||||
"expiryYear": "Rok",
|
||||
"name": "Nazwa tokena API",
|
||||
"nameDesc": "Nazwa tokena (do identyfikacji).",
|
||||
"namePlaceholder": "Mój Nowy Token",
|
||||
"noExpiry": "Bezterminowy",
|
||||
"noTokens": "Brak tokenów podłączonych do Twojego konta.",
|
||||
"revoke": "Usuń",
|
||||
"subheader": "Zarządzaj swoimi tokenami API oraz tym, do czego mają dostęp.",
|
||||
"success": "Pomyślnie utworzono token.",
|
||||
"successNote": "Upewnij się aby skopiować go teraz, ponieważ nie będzie wyświetlany ponownie.",
|
||||
"title": "Tokeny API"
|
||||
}
|
||||
},
|
||||
"actions": "Akcje",
|
||||
"add": "Dodaj",
|
||||
"adminTitle": "Panel Administratora - Drop",
|
||||
"adminTitleTemplate": "{0} - Administrator - Drop",
|
||||
"auth": {
|
||||
"callback": {
|
||||
"authClient": "Autoryzować klienta?",
|
||||
"authorize": "Autoryzuj",
|
||||
"authorizedClient": "Drop pomyślnie autoryzował klienta. Możesz teraz zamknąć to okno.",
|
||||
"issues": "Masz problemy?",
|
||||
"learn": "Dowiedz się więcej {arrow}",
|
||||
"paste": "Wklej ten kod do klienta aby kontynuować:",
|
||||
"permWarning": "Akceptowanie tego żądania pozwoli \"{name}\" na platformie \"{platform}\" na:",
|
||||
"requestedAccess": "\"{name}\" poprosił o dostęp do twojego konta Drop.",
|
||||
"success": "Pomyślnie!"
|
||||
},
|
||||
"code": {
|
||||
"description": "Użyj kodu aby połączyć twojego klienta Drop jeżeli nie jesteś w stanie otworzyć przeglądarki na swoim urządzeniu.",
|
||||
"title": "Połącz swojego klienta Drop"
|
||||
},
|
||||
"confirmPassword": "Potwierdź @:auth.password",
|
||||
"displayName": "Nazwa Wyświetlana",
|
||||
"email": "Email",
|
||||
"password": "Hasło",
|
||||
"register": {
|
||||
"confirmPasswordFormat": "Musi być takie samo jak powyżej",
|
||||
"emailFormat": "Musi być w formacie uzytkownik{'@'}example.com",
|
||||
"passwordFormat": "Musi mieć conajmniej 8 znaków",
|
||||
"subheader": "Wpisz poniżej swoje dane, aby utworzyć swoje konto.",
|
||||
"title": "Stwórz swoje konto Drop",
|
||||
"usernameFormat": "Musi mieć co najmniej 5 znaków i małe litery"
|
||||
},
|
||||
"signin": {
|
||||
"externalProvider": "Zaloguj się za pomocą zewnętrznego dostawcy {arrow}",
|
||||
"forgot": "Zapomniałeś hasła?",
|
||||
"noAccount": "Nie posiadasz konta? Poproś administratora żeby ci je stworzył.",
|
||||
"or": "LUB",
|
||||
"pageTitle": "Zaloguj się do Drop",
|
||||
"rememberMe": "Zapamiętaj mnie",
|
||||
"signin": "Zaloguj się",
|
||||
"title": "Zaloguj się do swojego konta"
|
||||
},
|
||||
"signout": "Wyloguj się",
|
||||
"username": "Nazwa Użytkownika"
|
||||
},
|
||||
"cancel": "Anuluj",
|
||||
"chars": {
|
||||
"arrow": "→",
|
||||
"arrowBack": "←",
|
||||
"quoted": "\"\"",
|
||||
"srComma": ", {0}"
|
||||
},
|
||||
"common": {
|
||||
"add": "Dodaj",
|
||||
"cannotUndo": "Ta czynność nie może zostać cofnięta.",
|
||||
"close": "Zamknij",
|
||||
"create": "Utwórz",
|
||||
"date": "Data",
|
||||
"delete": "Usuń",
|
||||
"deleteConfirm": "Czy jesteś pewny że chcesz usunąć \"{0}\"?",
|
||||
"divider": "{'|'}",
|
||||
"edit": "Edytuj",
|
||||
"friends": "Znajomi",
|
||||
"groups": "Grupy",
|
||||
"insert": "Wstaw",
|
||||
"labelValueColon": "{label}: {value}",
|
||||
"name": "Nazwa",
|
||||
"noData": "Brak danych",
|
||||
"noResults": "Brak wyników",
|
||||
"noSelected": "Nie wybrano żadnych elementów.",
|
||||
"remove": "Usuń",
|
||||
"save": "Zapisz",
|
||||
"saved": "Zapisano",
|
||||
"servers": "Serwery",
|
||||
"srLoading": "Ładowanie…",
|
||||
"tags": "Tagi",
|
||||
"today": "Dzisiaj"
|
||||
},
|
||||
"drop": {
|
||||
"desc": "Platforma typu open source do dystrybucji gier, stworzona z myślą o szybkości, elastyczności i estetyce.",
|
||||
"drop": "Drop"
|
||||
},
|
||||
"editor": {
|
||||
"bold": "Pogrubienie",
|
||||
"boldPlaceholder": "pogrubiony tekst",
|
||||
"code": "Kod",
|
||||
"codePlaceholder": "kod",
|
||||
"heading": "Nagłówek",
|
||||
"headingPlaceholder": "nagłówek",
|
||||
"italic": "Kursywa",
|
||||
"italicPlaceholder": "tekst kursywny",
|
||||
"link": "Link",
|
||||
"linkPlaceholder": "tekst linku",
|
||||
"listItem": "Element listy",
|
||||
"listItemPlaceholder": "element listy"
|
||||
},
|
||||
"errors": {
|
||||
"admin": {
|
||||
"user": {
|
||||
"delete": {
|
||||
"desc": "Drop nie mógł usunąć tego użytkownika: {0}",
|
||||
"title": "Nie udało się usunąć użytkownika"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"disabled": "Nieprawidłowe lub wyłączone konto. Prosze skontaktuj się z administratorem serwera.",
|
||||
"invalidInvite": "Nieprawidłowe lub wygasłe zaproszenie",
|
||||
"invalidPassState": "Nieprawidłowe hasło. Skontaktuj się z administratorem serwera.",
|
||||
"invalidUserOrPass": "Nieprawidłowa nazwa użytkownika i hasło.",
|
||||
"inviteIdRequired": "identyfikator wymagany do pobrania zaproszenia",
|
||||
"method": {
|
||||
"signinDisabled": "Metoda logowania nie jest włączona"
|
||||
},
|
||||
"usernameTaken": "Nazwa użytkownika jest już zajęta."
|
||||
},
|
||||
"backHome": "{arrow} Powrót do domu",
|
||||
"externalUrl": {
|
||||
"subtitle": "Ta wiadomość jest widoczna tylko dla administratorów.",
|
||||
"title": "Dostęp poprzez inny EXTERNAL_URL. Proszę sprawdzić dokumentację."
|
||||
},
|
||||
"game": {
|
||||
"banner": {
|
||||
"description": "Drop nie udało się zaktualizować obrazu banera: {0}",
|
||||
"title": "Nie udało się zaktualizować obrazu banera"
|
||||
},
|
||||
"carousel": {
|
||||
"description": "Drop nie udało się zaktualizować karuzeli obrazów: {0}",
|
||||
"title": "Nie udało się zaktualizować karuzeli obrazów"
|
||||
},
|
||||
"cover": {
|
||||
"description": "Drop nie udało się zaktualizować obrazu okładki: {0}",
|
||||
"title": "Nie udało się zaktualizować obrazu okładki"
|
||||
},
|
||||
"deleteImage": {
|
||||
"description": "Drop nie udało się usunąć obrazu: {0}",
|
||||
"title": "Nie udało się usunąć obrazu"
|
||||
},
|
||||
"description": {
|
||||
"description": "Drop nie udało się zaktualizować opisu gry: {0}",
|
||||
"title": "Nie udało się zaktualizować opisu gry"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Drop nie udało się zaktualizować metadanych gry: {0}",
|
||||
"title": "Nie udało się zaktualizować metadanych"
|
||||
}
|
||||
},
|
||||
"invalidBody": "Nieprawidłowa treść żądania: {0}",
|
||||
"inviteRequired": "Do rejestracji wymagane jest zaproszenie.",
|
||||
"library": {
|
||||
"add": {
|
||||
"desc": "Drop nie mógł dodać tej gry do twojej biblioteki: {0}",
|
||||
"title": "Nie udało się dodać gry do biblioteki"
|
||||
},
|
||||
"collection": {
|
||||
"create": {
|
||||
"desc": "Drop nie mógł utworzyć twojej kolekcji: {0}",
|
||||
"title": "Nie udało się utworzyć kolekcji"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"delete": {
|
||||
"desc": "Drop nie mógł usunąć tego źródła: {0}",
|
||||
"title": "Nie udało się usunąć źródła biblioteki"
|
||||
}
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"article": {
|
||||
"delete": {
|
||||
"desc": "Drop nie mógł usunąć tego artykułu: {0}",
|
||||
"title": "Nie udało się usunąć artykułu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"occurred": "Wystąpił błąd podczas odpowiadania na Twoje żądanie. Jeśli uważasz, że jest to błąd, zgłoś go. Spróbuj się zalogować i sprawdź, czy to rozwiąże problem.",
|
||||
"ohNo": "O nie!",
|
||||
"pageTitle": "{0} | Drop",
|
||||
"revokeClient": "Nie udało się usunąć klienta",
|
||||
"revokeClientFull": "Nie udało się usunąć klienta {0}",
|
||||
"signIn": "Zaloguj się {arrow}",
|
||||
"support": "Discord Wsparcia",
|
||||
"unknown": "Wystąpił nieznany błąd",
|
||||
"upload": {
|
||||
"description": "Drop nie mógł przesłać pliku: {0}",
|
||||
"title": "Nie udało się przesłać pliku"
|
||||
},
|
||||
"version": {
|
||||
"delete": {
|
||||
"desc": "Drop napotkał błąd podczas usuwania wersji: {error}",
|
||||
"title": "Wystąpił błąd podczas usuwania wersji"
|
||||
},
|
||||
"order": {
|
||||
"desc": "Drop napotkał błąd podczas aktualizowania wersji: {error}",
|
||||
"title": "Wystąpił błąd podczas aktualizacji kolejności wersji"
|
||||
}
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"about": "O",
|
||||
"aboutDrop": "O Drop",
|
||||
"comparison": "Porównanie",
|
||||
"docs": {
|
||||
"client": "Dokumentacja Klienta",
|
||||
"server": "Dokumentacja Serwera"
|
||||
},
|
||||
"documentation": "Dokumentacja",
|
||||
"findGame": "Znajdź grę",
|
||||
"footer": "Stopka",
|
||||
"games": "Gry",
|
||||
"social": {
|
||||
"discord": "Discord",
|
||||
"github": "GitHub"
|
||||
},
|
||||
"topSellers": "Bestsellery",
|
||||
"version": "Drop {version} {gitRef}"
|
||||
},
|
||||
"header": {
|
||||
"admin": {
|
||||
"admin": "Administrator",
|
||||
"home": "Strona Główna",
|
||||
"library": "Biblioteka",
|
||||
"metadata": "Metadane",
|
||||
"settings": {
|
||||
"store": "Sklep",
|
||||
"title": "Ustawienia",
|
||||
"tokens": "Tokeny API"
|
||||
},
|
||||
"tasks": "Zadania",
|
||||
"users": "Użytkownicy"
|
||||
},
|
||||
"back": "Wróć",
|
||||
"openSidebar": "Otwórz menu boczne"
|
||||
},
|
||||
"helpUsTranslate": "Pomóż nam tłumaczyć Drop {arrow}",
|
||||
"highest": "najwyższe",
|
||||
"home": {
|
||||
"admin": {
|
||||
"activeInactiveUsers": "Aktywni/nieaktywni użytkownicy",
|
||||
"activeUsers": "Aktywni użytkownicy",
|
||||
"allVersionsCombined": "Wszystkie wersje łącznie",
|
||||
"biggestGamesOnServer": "Największe gry na serwerze",
|
||||
"biggestGamesToDownload": "Największe gry do pobrania",
|
||||
"games": "Gry",
|
||||
"goToUsers": "Idź do użytkowników",
|
||||
"inactiveUsers": "Nieaktywni użytkownicy",
|
||||
"latestVersionOnly": "Tylko najnowsza wersja",
|
||||
"librarySources": "Źródła biblioteki",
|
||||
"subheader": "Podsumowanie Instancji",
|
||||
"title": "Strona Główna",
|
||||
"users": "Użytkownicy",
|
||||
"version": "Wersja"
|
||||
}
|
||||
},
|
||||
"library": {
|
||||
"addGames": "Wszystkie Gry",
|
||||
"addToLib": "Dodaj do Biblioteki",
|
||||
"admin": {
|
||||
"detectedGame": "Drop wykrył że masz nowe gry do zaimportowania.",
|
||||
"detectedVersion": "Drop wykrył że masz nowe wersje tej gry do zaimportowania.",
|
||||
"game": {
|
||||
"addCarouselNoImages": "Brak obrazów do dodania.",
|
||||
"addDescriptionNoImages": "Brak obrazów do dodania.",
|
||||
"addImageCarousel": "Dodaj z galerii obrazów",
|
||||
"currentBanner": "baner",
|
||||
"currentCover": "okładka",
|
||||
"deleteImage": "Usuń obraz",
|
||||
"editGameDescription": "Opis Gry",
|
||||
"editGameName": "Nazwa Gry",
|
||||
"editReleaseDate": "Data Wydania",
|
||||
"imageCarousel": "Karuzela Obrazów",
|
||||
"imageCarouselDescription": "Dostosuj, jakie zdjęcia i w jakiej kolejności są wyświetlane na stronie sklepu.",
|
||||
"imageCarouselEmpty": "Nie dodano jeszcze żadnych zdjęć do karuzeli.",
|
||||
"imageLibrary": "Biblioteka Obrazów",
|
||||
"imageLibraryDescription": "Należy pamiętać, że wszystkie przesłane obrazy są dostępne dla wszystkich użytkowników za pośrednictwem narzędzi programistycznych przeglądarki.",
|
||||
"removeImageCarousel": "Usuń zdjęcie",
|
||||
"setBanner": "Ustaw jako baner",
|
||||
"setCover": "Ustaw jako okładke"
|
||||
},
|
||||
"gameLibrary": "Biblioteka Gier",
|
||||
"import": {
|
||||
"bulkImportDescription": "Na tej stronie nie zostaniesz przekierowany do zadania importowania, więc możesz importować wiele gier po kolei.",
|
||||
"bulkImportTitle": "Tryb importu zbiorczego",
|
||||
"import": "Importuj",
|
||||
"link": "Importuj {arrow}",
|
||||
"loading": "Ładowanie wyników gry…",
|
||||
"search": "Szukaj",
|
||||
"searchPlaceholder": "Fallout 4",
|
||||
"selectDir": "Wybierz katalog…",
|
||||
"selectGame": "Wybierz grę do zaimportowania",
|
||||
"selectGamePlaceholder": "Wybierz grę…",
|
||||
"selectGameSearch": "Wybierz grę",
|
||||
"selectPlatform": "Wybierz platformę…",
|
||||
"version": {
|
||||
"advancedOptions": "Zaawansowane opcje",
|
||||
"import": "Zaimportuj wersje",
|
||||
"installDir": "(katalog_instalacji)/",
|
||||
"launchCmd": "Plik wykonywalny/polecenie uruchomienia gry",
|
||||
"launchDesc": "Plik wykonywalny do uruchomienia gry",
|
||||
"launchPlaceholder": "gra.exe",
|
||||
"loadingVersion": "Ładowanie metadanych wersji…",
|
||||
"noAdv": "Brak zaawansowanych opcji dla tej konfiguracji.",
|
||||
"noVersions": "Brak wersji do zaimportowania",
|
||||
"platform": "Platforma wersji",
|
||||
"setupCmd": "Plik wykonywalny/polecenie instalacji gry",
|
||||
"setupDesc": "Uruchamiany raz po zainstalowaniu gry",
|
||||
"setupMode": "Tryb instalacji",
|
||||
"setupModeDesc": "Po włączeniu, ta wersja nie posiada polecenia uruchamiania i po prostu uruchamia plik wykonywalny na komputerze użytkownika. Przydatne w przypadku gier, które dystrybuują tylko instalator, a nie pliki przenośne.",
|
||||
"setupPlaceholder": "instalator.exe",
|
||||
"umuLauncherId": "ID UMU Launcher",
|
||||
"umuOverride": "Nadpisz ID Gry UMU Launcher",
|
||||
"umuOverrideDesc": "Domyślnie Drop używa identyfikatora non-ID podczas uruchamiania z pomocą UMU Launcher. Aby uzyskać odpowiednie poprawki dla niektórych gier, może być konieczne ręczne ustawienie tego pola.",
|
||||
"updateMode": "Tryb aktualizacji",
|
||||
"updateModeDesc": "Po włączeniu, te pliki zostaną zainstalowane nad (nadpisując) poprzednią wersją. Jeśli połączonych jest kilka \"trybów aktualizacji\", są one stosowane w kolejności.",
|
||||
"version": "Wybierz wersję do zaimportowania"
|
||||
},
|
||||
"withoutMetadata": "Importuj bez metadanych"
|
||||
},
|
||||
"libraryHint": "Brak skonfigurowanych bibliotek.",
|
||||
"libraryHintDocsLink": "Co to oznacza? {arrow}",
|
||||
"metadata": {
|
||||
"companies": {
|
||||
"action": "Zarządzaj {arrow}",
|
||||
"addGame": {
|
||||
"description": "Wybierz grę, którą chcesz dodać do firmy, i zdecyduj, czy powinna być wymieniona jako producent, wydawca, czy jedno i drugie.",
|
||||
"developer": "Deweloper?",
|
||||
"noGames": "Brak gier do dodania",
|
||||
"publisher": "Wydawca?",
|
||||
"title": "Połącz grę do tej firmy"
|
||||
},
|
||||
"description": "Firmy organizują gry według tego, kto je stworzył lub wydał.",
|
||||
"editor": {
|
||||
"action": "Dodaj Grę {plus}",
|
||||
"descriptionPlaceholder": "{'<'}opis{'>'}",
|
||||
"developed": "Stworzone przez",
|
||||
"libraryDescription": "Dodawaj, usuwaj lub dostosowuj treści stworzone i/lub wydane przez tę firmę.",
|
||||
"libraryTitle": "Biblioteka Gier",
|
||||
"noDescription": "(brak opisu)",
|
||||
"published": "Wydane przez",
|
||||
"uploadBanner": "Prześlij baner",
|
||||
"uploadIcon": "Prześlij ikonę",
|
||||
"websitePlaceholder": "{'<'}strona{'>'}"
|
||||
},
|
||||
"modals": {
|
||||
"createDescription": "Stwórz firmę, aby lepiej organizować swoje gry.",
|
||||
"createFieldDescription": "Opis Firmy",
|
||||
"createFieldDescriptionPlaceholder": "Małe studio indie które...",
|
||||
"createFieldName": "Nazwa Firmy",
|
||||
"createFieldNamePlaceholder": "Moja Nowa Firma...",
|
||||
"createFieldWebsite": "Strona Firmy",
|
||||
"createFieldWebsitePlaceholder": "https://example.com/",
|
||||
"createTitle": "Utwórz firmę",
|
||||
"nameDescription": "Edytuj nazwę firmy. Służy do dopasowania do nowych importów gier.",
|
||||
"nameTitle": "Edytuj nazwę firmy",
|
||||
"shortDeckDescription": "Edytuj opis firmy. Nie wpływa na długi opis (markdown).",
|
||||
"shortDeckTitle": "Edytuj opis firmy",
|
||||
"websiteDescription": "Edytuj stronę internetową firmy. Uwaga: będzie to link i nie będzie miał zabezpieczenia przed przekierowaniem.",
|
||||
"websiteTitle": "Edytuj stronę firmy"
|
||||
},
|
||||
"noCompanies": "Brak firm",
|
||||
"noGames": "Brak gier",
|
||||
"search": "Szukaj firm…",
|
||||
"searchGames": "Szukaj gier firmy…",
|
||||
"title": "Firmy"
|
||||
},
|
||||
"tags": {
|
||||
"action": "Zarządzaj {arrow}",
|
||||
"create": "Utwórz",
|
||||
"description": "Tagi są tworzone automatycznie na podstawie importowanych gatunków. Możesz dodawać własne tagi, aby kategoryzować swoją bibliotekę gier.",
|
||||
"modal": {
|
||||
"description": "Utwórz tag, aby uporządkować swoją bibliotekę.",
|
||||
"title": "Utwórz Tag"
|
||||
},
|
||||
"title": "Tagi"
|
||||
}
|
||||
},
|
||||
"metadataProvider": "Dostawca Metadanych",
|
||||
"noGames": "Brak zaimportowanych gier",
|
||||
"offline": "Drop nie mógł uzyskać dostępu do tej gry.",
|
||||
"offlineTitle": "Gra Offline",
|
||||
"openEditor": "Otwórz w Edytorze {arrow}",
|
||||
"openStore": "Otwórz w Sklepie",
|
||||
"shortDesc": "Krótki Opis",
|
||||
"sources": {
|
||||
"create": "Utwórz źródło",
|
||||
"createDesc": "Drop użyje tego źródła, aby uzyskać dostęp do Twojej biblioteki gier i je udostępnić.",
|
||||
"desc": "Skonfiguruj źródła biblioteki, w których Drop będzie wyszukiwał nowe gry i wersje do zaimportowania.",
|
||||
"documentationLink": "Dokumentacja {arrow}",
|
||||
"edit": "Edytuj źródło",
|
||||
"freeSpace": "Wolna przestrzeń",
|
||||
"fsDesc": "Importuje gry ze ścieżki na dysku. Wymaga struktury folderów opartej na wersjach, i obsługuje gry zarchiwizowane.",
|
||||
"fsFlatDesc": "Importuje gry ze ścieżki na dysku, ale bez osobnego podfolderu wersji. Przydatne podczas migracji istniejącej biblioteki do Drop.",
|
||||
"fsFlatTitle": "Kompatybilność",
|
||||
"fsPath": "Ścieżka",
|
||||
"fsPathDesc": "Ścieżka absolutna do twojej biblioteki gier.",
|
||||
"fsPathPlaceholder": "/mnt/games",
|
||||
"fsTitle": "Styl Drop",
|
||||
"link": "Źródła {arrow}",
|
||||
"nameDesc": "Nazwa źródła, do celów referencyjnych.",
|
||||
"namePlaceholder": "Moje Nowe Źródło",
|
||||
"percentage": "{number}%",
|
||||
"sources": "Źródła Biblioteki",
|
||||
"totalSpace": "Całkowita przestrzeń",
|
||||
"typeDesc": "Typ twojego źródła. Zmienia wymagane opcje.",
|
||||
"utilizationPercentage": "Procent wykorzystania",
|
||||
"working": "Działa?"
|
||||
},
|
||||
"subheader": "Gdy dodasz foldery do źródeł biblioteki, Drop je wykryje i poprosi o zaimportowanie. Każda gra musi zostać zaimportowana, zanim będzie można zaimportować jej wersję.",
|
||||
"title": "Biblioteki",
|
||||
"version": {
|
||||
"delta": "Tryb uaktualnienia",
|
||||
"noVersions": "Nie masz dostępnych wersji tej gry.",
|
||||
"noVersionsAdded": "nie dodano żadnych wersji"
|
||||
},
|
||||
"versionPriority": "Priorytet wersji"
|
||||
},
|
||||
"back": "Wróć do Biblioteki",
|
||||
"collection": {
|
||||
"addToNew": "Dodaj do nowej kolekcji",
|
||||
"collections": "Kolekcje",
|
||||
"create": "Stwórz Kolekcję",
|
||||
"createDesc": "Kolekcje mogą pomóc Ci uporządkować swoje gry i ułatwić ich odnajdywanie, zwłaszcza jeśli masz dużą bibliotekę.",
|
||||
"delete": "Usuń Kolekcję",
|
||||
"namePlaceholder": "Nazwa Kolekcji",
|
||||
"noCollections": "Brak kolekcji",
|
||||
"notFound": "Kolekcja nie znaleziona",
|
||||
"subheader": "Dodaj nową kolekcję aby zorganizować swoje gry",
|
||||
"title": "Kolekcja"
|
||||
},
|
||||
"gameCount": "{0} gry | {0} gra | {0} gier",
|
||||
"inLib": "W Bibliotece",
|
||||
"launcherOpen": "Otwórz w Programie Uruchamiającym",
|
||||
"noGames": "Brak gier w bibliotece",
|
||||
"notFound": "Gra nie znaleziona",
|
||||
"search": "Przeszukaj bibliotekę…",
|
||||
"subheader": "Zorganizuj swoje gry w kolekcje, aby mieć do nich łatwy dostęp i mieć dostęp do wszystkich swoich gier."
|
||||
},
|
||||
"lowest": "najniższy",
|
||||
"news": {
|
||||
"article": {
|
||||
"add": "Dodaj",
|
||||
"content": "Treść (Markdown)",
|
||||
"create": "Utwórz Nowy Artykuł",
|
||||
"editor": "Edytor",
|
||||
"editorGuide": "Użyj powyższych skrótów lub napisz bezpośrednio w Markdown. Obsługuje **pogrubienie**, *kursywę*, [linki](url) i wiele innych.",
|
||||
"new": "Nowy artykuł",
|
||||
"preview": "Podgląd",
|
||||
"shortDesc": "Krótki opis",
|
||||
"submit": "Zatwierdź",
|
||||
"tagPlaceholder": "Dodaj tag…",
|
||||
"titles": "Tytuł",
|
||||
"uploadCover": "Prześlij zdjęcie okładki"
|
||||
},
|
||||
"back": "Wróć do Wiadomości",
|
||||
"checkLater": "Sprawdź później, czy są jakieś aktualizacje.",
|
||||
"delete": "Usuń Artykuł",
|
||||
"filter": {
|
||||
"all": "Od początku",
|
||||
"month": "W tym miesiącu",
|
||||
"week": "W tym tygodniu",
|
||||
"year": "W tym roku"
|
||||
},
|
||||
"none": "Brak artykułów",
|
||||
"notFound": "Artykuł nie znaleziony",
|
||||
"search": "Szukaj artykułów",
|
||||
"searchPlaceholder": "Przeszukaj artykuły…",
|
||||
"subheader": "Bądź na bieżąco z najnowszymi aktualizacjami i ogłoszeniami.",
|
||||
"title": "Najnowsze Wiadomości"
|
||||
},
|
||||
"options": "Opcje",
|
||||
"security": "Bezpieczeństwo",
|
||||
"selectLanguage": "Wybierz język",
|
||||
"settings": {
|
||||
"admin": {
|
||||
"description": "Skonfiguruj ustawienia Drop",
|
||||
"store": {
|
||||
"dropGameAltPlaceholder": "Przykładowa ikonka Gry",
|
||||
"dropGameDescriptionPlaceholder": "To przykładowa gra. Będzie zastąpiona kiedy zaimportujesz grę.",
|
||||
"dropGameNamePlaceholder": "Przykładowa Gra",
|
||||
"showGamePanelTextDecoration": "Pokaż tytuł i opis na kafelkach gry (domyślnie: włączone)",
|
||||
"title": "Sklep"
|
||||
},
|
||||
"title": "Ustawienia"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"auth": {
|
||||
"description": "Uwierzytelnianie w Drop odbywa się za pośrednictwem wielu skonfigurowanych \"dostawców\". Każdy z nich może umożliwiać użytkownikom logowanie się za pomocą wybranej metody. Aby rozpocząć, należy włączyć co najmniej jednego dostawcę uwierzytelniania i utworzyć u niego konto.",
|
||||
"docs": "Dokumentacja {arrow}",
|
||||
"enabled": "Włączone?",
|
||||
"openid": {
|
||||
"description": "OpenID Connect (OIDC) to powszechnie obsługiwane rozszerzenie OAuth2. Drop wymaga konfiguracji OIDC za pomocą zmiennych środowiskowych.",
|
||||
"skip": "Mam użytkownika z OIDC",
|
||||
"title": "OpenID Connect"
|
||||
},
|
||||
"simple": {
|
||||
"description": "Proste uwierzytelnianie wykorzystuje nazwę użytkownika i hasło do uwierzytelniania użytkowników. Jest ono domyślnie włączone, jeśli żaden inny dostawca uwierzytelniania nie jest włączony.",
|
||||
"register": "Zarejestruj się jako administrator {arrow}",
|
||||
"title": "Proste uwierzytelnianie"
|
||||
},
|
||||
"title": "Uwierzytelnianie"
|
||||
},
|
||||
"finish": "Chodźmy {arrow}",
|
||||
"noPage": "brak strony",
|
||||
"stages": {
|
||||
"account": {
|
||||
"description": "Potrzebujesz co najmniej jednego konta żeby zacząć używać Drop.",
|
||||
"name": "Skonfiguruj swoje konto administratora."
|
||||
},
|
||||
"library": {
|
||||
"description": "Dodaj co najmniej jedno źródło biblioteki żeby używać Drop.",
|
||||
"name": "Utwórz bibliotekę."
|
||||
}
|
||||
},
|
||||
"welcome": "Cześć.",
|
||||
"welcomeDescription": "Witamy w kreatorze konfiguracji Drop. Poprowadzi Cię on przez proces pierwszej konfiguracji Drop i pokaże, jak to wszystko działa."
|
||||
},
|
||||
"store": {
|
||||
"about": "O",
|
||||
"commingSoon": "wkrótce",
|
||||
"developers": "Producentów | Producent | Producentów",
|
||||
"exploreMore": "Odkryj więcej {arrow}",
|
||||
"featured": "Wyróżnione",
|
||||
"images": "Zdjęcia Gry",
|
||||
"lookAt": "Sprawdź",
|
||||
"noDevelopers": "Brak producentów",
|
||||
"noFeatured": "BRAK WYRÓŻNIONYCH GIER",
|
||||
"noGame": "BRAK GRY",
|
||||
"noImages": "Brak zdjęć",
|
||||
"noPublishers": "Brak wydawców.",
|
||||
"noTags": "Brak tagów",
|
||||
"openAdminDashboard": "Otwórz w Panelu Administracyjnym",
|
||||
"openFeatured": "Oznacz gry w Bibliotece Administracyjnej {arrow}",
|
||||
"platform": "Platforma | Platforma | Platformy",
|
||||
"publishers": "Wydawcy | Wydawca | Wydawcy",
|
||||
"rating": "Ocena",
|
||||
"readLess": "Kliknij aby przeczytać mniej",
|
||||
"readMore": "Kliknij aby przeczytać więcej",
|
||||
"recentlyAdded": "Ostatnio Dodane",
|
||||
"recentlyReleased": "Ostatnio wydane",
|
||||
"recentlyUpdated": "Ostatnio Zaktualizowane",
|
||||
"released": "Wydane",
|
||||
"reviews": "({0} Ocen)",
|
||||
"size": "Rozmiar",
|
||||
"tags": "Tagi",
|
||||
"title": "Sklep",
|
||||
"view": {
|
||||
"sort": "Sortuj",
|
||||
"srFilters": "Filtry",
|
||||
"srGames": "Gry",
|
||||
"srViewGrid": "Zobacz siatkę"
|
||||
},
|
||||
"viewInStore": "Zobacz w Sklepie",
|
||||
"website": "Strona"
|
||||
},
|
||||
"tasks": {
|
||||
"admin": {
|
||||
"back": "{arrow} Wróć do Zadań",
|
||||
"completedTasksTitle": "Ukończone zadania",
|
||||
"dailyScheduledTitle": "Codzienne zaplanowane zadania",
|
||||
"execute": "{arrow} Wykonaj",
|
||||
"noTasksRunning": "Brak aktualnie uruchomionych zadań",
|
||||
"progress": "{0}%",
|
||||
"runningTasksTitle": "Uruchomione zadania",
|
||||
"scheduled": {
|
||||
"checkUpdateDescription": "Sprawdź czy Drop ma aktualizację.",
|
||||
"checkUpdateName": "Sprawdź aktualizację.",
|
||||
"cleanupInvitationsDescription": "Usuwa wygasłe zaproszenia z bazy danych, aby zaoszczędzić miejsce.",
|
||||
"cleanupInvitationsName": "Wyczyść zaproszenia",
|
||||
"cleanupObjectsDescription": "Wykrywa i usuwa nieużywane i nieodwoływane obiekty, aby zaoszczędzić miejsce.",
|
||||
"cleanupObjectsName": "Wyczyść obiekty",
|
||||
"cleanupSessionsDescription": "Usuwa wygasłe sesje, aby zaoszczędzić miejsce i zapewnić bezpieczeństwo.",
|
||||
"cleanupSessionsName": "Wyczyść sesje."
|
||||
},
|
||||
"viewTask": "Zobacz {arrow}",
|
||||
"weeklyScheduledTitle": "Zadania zaplanowane tygodniowo"
|
||||
}
|
||||
},
|
||||
"title": "Drop",
|
||||
"titleTemplate": "{0} - Drop",
|
||||
"todo": "Do zrobienia",
|
||||
"type": "Typ",
|
||||
"upload": "Prześlij",
|
||||
"uploadFile": "Prześlij plik",
|
||||
"user": {
|
||||
"editProfile": "Edytuj profil",
|
||||
"noActivity": "Brak ostatniej aktywności",
|
||||
"notFound": "Użytkownik nie znaleziony",
|
||||
"recent": "Ostatnia aktywność (Do Zrobienia)",
|
||||
"recentSub": "Ostatnia aktywność tego użytkownika",
|
||||
"unknown": "Nieznany użytkownik"
|
||||
},
|
||||
"userHeader": {
|
||||
"closeSidebar": "Zamknij menu boczne",
|
||||
"links": {
|
||||
"community": "Społeczność",
|
||||
"library": "Biblioteka",
|
||||
"news": "Wiadomości"
|
||||
},
|
||||
"profile": {
|
||||
"admin": "Panel Administracyjny",
|
||||
"settings": "Ustawienia konta"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"admin": {
|
||||
"adminHeader": "Administrator?",
|
||||
"adminUserLabel": "Użytkownik administratora",
|
||||
"authLink": "Uwierzytelnianie {arrow}",
|
||||
"authentication": {
|
||||
"configure": "Konfiguruj",
|
||||
"description": "Drop obsługuje różnorodne \"mechanizmy uwierzytelniania\". Po ich włączeniu lub wyłączeniu są one wyświetlane na ekranie logowania, umożliwiając użytkownikom wybór. Kliknij menu z kropkami, aby skonfigurować mechanizm uwierzytelniania.",
|
||||
"disabled": "Wyłączone",
|
||||
"enabled": "Włączone",
|
||||
"enabledKey": "Włączone?",
|
||||
"oidc": "OpenID Connect",
|
||||
"simple": "Proste (nazwa użytkownika/hasło)",
|
||||
"srOpenOptions": "Otwórz opcje",
|
||||
"title": "Uwierzytelnianie"
|
||||
},
|
||||
"authoptionsHeader": "Opcje Uwierzytelniania",
|
||||
"delete": "Usuń",
|
||||
"deleteUser": "Usuń użytkownika {0}",
|
||||
"description": "Zarządzaj użytkownikami na twojej instancji Drop, i skonfiguruj swoje metody uwierzytelniania.",
|
||||
"displayNameHeader": "Nazwa Wyświetlana",
|
||||
"emailHeader": "Email",
|
||||
"normalUserLabel": "Normalny użytkownik",
|
||||
"simple": {
|
||||
"adminInvitation": "Zaproszenie administratora",
|
||||
"createInvitation": "Utwórz zaproszenie",
|
||||
"description": "Proste uwierzytelnianie wykorzystuje system 'zaproszeń' do tworzenia użytkowników. Możesz utworzyć zaproszenie i opcjonalnie podać nazwę użytkownika lub adres e-mail, a następnie system wygeneruje magiczny adres URL, którego można użyć do utworzenia konta.",
|
||||
"expires": "Wygasa: {expiry}",
|
||||
"invitationTitle": "Zaproszenia",
|
||||
"invite3Days": "3 dni",
|
||||
"invite6Months": "6 miesięcy",
|
||||
"inviteAdminSwitchDescription": "Utwórz tego użytkownika jako administratora",
|
||||
"inviteAdminSwitchLabel": "Zaproszenie administratora",
|
||||
"inviteButton": "Zaproś",
|
||||
"inviteDescription": "Drop wygeneruje adres URL, który możesz wysłać osobie, którą chcesz zaprosić. Opcjonalnie możesz podać jej nazwę użytkownika lub adres e-mail.",
|
||||
"inviteEmailDescription": "Musi być w formacie uzytkownik{'@'}example.com",
|
||||
"inviteEmailLabel": "Adres e-mail (opcjonalny)",
|
||||
"inviteEmailPlaceholder": "ja{'@'}example.com",
|
||||
"inviteExpiryLabel": "Wygasa",
|
||||
"inviteMonth": "1 miesiąc",
|
||||
"inviteNever": "Nigdy",
|
||||
"inviteTitle": "Zaproś użytkownika do Drop",
|
||||
"inviteUsernameFormat": "Musi mieć 5 lub więcej znaków",
|
||||
"inviteUsernameLabel": "Nazwa Użytkownika (opcjonalna)",
|
||||
"inviteUsernamePlaceholder": "mojaNazwa",
|
||||
"inviteWeek": "1 tydzień",
|
||||
"inviteYear": "1 rok",
|
||||
"neverExpires": "Nigdy nie wygasa.",
|
||||
"noEmailEnforced": "Nie wymuszono adresu e-mail.",
|
||||
"noInvitations": "Brak zaproszeń.",
|
||||
"noUsernameEnforced": "Nie wymuszono Nazwy Użytkownika.",
|
||||
"title": "Proste uwierzytelnianie",
|
||||
"userInvitation": "Zaproszenie użytkownika"
|
||||
},
|
||||
"srEditLabel": "Edytuj",
|
||||
"usernameHeader": "Nazwa Użytkownika"
|
||||
}
|
||||
},
|
||||
"welcome": "Polaku, Witaj!"
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"account": {
|
||||
"devices": {
|
||||
"capabilities": "Возможности",
|
||||
"lastConnected": "Последнее подключение",
|
||||
"noDevices": "К вашей учетной записи не подключено ни одного устройства.",
|
||||
"platform": "Платформа",
|
||||
"revoke": "Аннулировать",
|
||||
"subheader": "Управляйте устройствами, имеющими доступ к вашей учетной записи Drop.",
|
||||
"title": "Устройства"
|
||||
},
|
||||
"notifications": {
|
||||
"all": "Показать все {arrow}",
|
||||
"desc": "Просмотр и управление уведомлениями.",
|
||||
"markAllAsRead": "Отметить все как прочитанные",
|
||||
"markAsRead": "Отметить как прочитанное",
|
||||
"none": "Нет уведомлений",
|
||||
"notifications": "Уведомления",
|
||||
"title": "Уведомления",
|
||||
"unread": "Непрочитанные уведомления"
|
||||
},
|
||||
"settings": "Настройки",
|
||||
"title": "Настройки учетной записи",
|
||||
"token": {
|
||||
"acls": "Доступ и права",
|
||||
"aclsDesc": "Определяет, какие действия разрешены для этого токена. Не выбирайте все ACL, если это не требуется.",
|
||||
"expiry": "Истечение срока",
|
||||
"expiry3Month": "3 месяца",
|
||||
"expiry5Year": "5 лет",
|
||||
"expiry6Month": "6 месяцев",
|
||||
"expiryMonth": "Месяц",
|
||||
"expiryYear": "Год",
|
||||
"name": "Название API-токена",
|
||||
"nameDesc": "Название токена для справки.",
|
||||
"namePlaceholder": "Мои новые токены",
|
||||
"noExpiry": "Без срока действия",
|
||||
"noTokens": "На вашем аккаунте нет подключённых токенов.",
|
||||
"revoke": "Аннулировать",
|
||||
"subheader": "Управляйте своими API-токенами и их доступом.",
|
||||
"success": "Токен успешно создан.",
|
||||
"successNote": "Скопируйте токен сейчас, позже его не будет видно.",
|
||||
"title": "API-Токены"
|
||||
}
|
||||
},
|
||||
"actions": "Действия",
|
||||
"add": "Добавить",
|
||||
"adminTitle": "Панель администратора - Drop",
|
||||
"adminTitleTemplate": "{0} - Админ - Drop",
|
||||
"auth": {
|
||||
"callback": {
|
||||
"authClient": "Авторизовать клиента?",
|
||||
"authorize": "Авторизовать",
|
||||
"authorizedClient": "Drop успешно авторизовал клиента. Теперь вы можете закрыть это окно.",
|
||||
"issues": "Есть проблемы?",
|
||||
"learn": "Узнать больше {arrow}",
|
||||
"paste": "Вставьте этот код в клиент, чтобы продолжить:",
|
||||
"permWarning": "Принятие этого запроса позволит \"{name}\" на \"{platform}\" выполнять следующие действия:",
|
||||
"requestedAccess": "\"{name}\" запросил доступ к вашей учетной записи Drop.",
|
||||
"success": "Успешно!"
|
||||
},
|
||||
"email": "Элетронная почка",
|
||||
"password": "Пароль",
|
||||
"register": {
|
||||
"confirmPasswordFormat": "Должно совпадать с выше."
|
||||
},
|
||||
"signin": {
|
||||
"forgot": "Забыли пароль?",
|
||||
"noAccount": "Нет аккаунта? Попросите администратора создать его для вас.",
|
||||
"or": "Или",
|
||||
"signin": "Войти",
|
||||
"title": "Войдите в свой аккаунт"
|
||||
},
|
||||
"signout": "Выход",
|
||||
"username": "Имя пользователя"
|
||||
},
|
||||
"cancel": "Отмена",
|
||||
"common": {
|
||||
"add": "Добавить",
|
||||
"close": "Закрыть",
|
||||
"create": "Создать",
|
||||
"date": "Дата",
|
||||
"delete": "Удалить",
|
||||
"deleteConfirm": "Вы точно хотите удалить \"{0}\"?",
|
||||
"edit": "Редактировать",
|
||||
"friends": "Друзья",
|
||||
"groups": "Группы",
|
||||
"name": "Имя",
|
||||
"noResults": "Нет результатов",
|
||||
"noSelected": "Не выбранные предметы.",
|
||||
"remove": "Удалить",
|
||||
"save": "Сохранить",
|
||||
"saved": "Сохранено",
|
||||
"servers": "Сервера",
|
||||
"srLoading": "Загрузка…",
|
||||
"tags": "Теги",
|
||||
"today": "Сегодня"
|
||||
},
|
||||
"drop": {
|
||||
"drop": "Уронить"
|
||||
},
|
||||
"editor": {
|
||||
"link": "Ссылка"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { Localisation } from "./utils";
|
||||
import {
|
||||
allLocalisableFiles,
|
||||
flattenLocalisation,
|
||||
keysFromContent,
|
||||
stripEquivalence,
|
||||
} from "./utils";
|
||||
import fs from "node:fs";
|
||||
|
||||
const files = allLocalisableFiles();
|
||||
|
||||
const keySet = new Map<string, string>();
|
||||
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const keys = keysFromContent(content);
|
||||
keys.forEach((key) => keySet.set(key, file));
|
||||
}
|
||||
|
||||
const localeFile: Localisation = JSON.parse(
|
||||
fs.readFileSync("./i18n/locales/en_us.json", "utf-8"),
|
||||
);
|
||||
const flattenedLocalisation = flattenLocalisation(localeFile);
|
||||
|
||||
for (const [key, file] of keySet.entries()) {
|
||||
console.log(stripEquivalence(flattenedLocalisation.get(key)!));
|
||||
|
||||
if (!flattenedLocalisation.delete(key))
|
||||
throw new Error(
|
||||
`Found key "${key}" in file ${file} that doesn't exist in localisation`,
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import type { Localisation } from "./utils";
|
||||
import {
|
||||
allLocalisableFiles,
|
||||
fetchLocalisation,
|
||||
keysFromContent,
|
||||
} from "./utils";
|
||||
|
||||
const files = allLocalisableFiles();
|
||||
const localeFile: Localisation = JSON.parse(
|
||||
fs.readFileSync("./i18n/locales/en_us.json", "utf-8"),
|
||||
);
|
||||
|
||||
const keepPrefixes = ["error", "common", "chars"];
|
||||
const keyMap: Map<string, string> = new Map();
|
||||
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const keys = keysFromContent(content);
|
||||
|
||||
const fileNoExtension = file.slice(0, file.lastIndexOf("."));
|
||||
|
||||
for (const key of keys) {
|
||||
const _value = fetchLocalisation(localeFile, key);
|
||||
|
||||
const newKeySuffix = key.split(".").slice(-1); /*value
|
||||
.replaceAll(/[^a-zA-Z\s]/g, "")
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.slice(0, 3)
|
||||
.map((v, i) =>
|
||||
v
|
||||
? i > 0
|
||||
? v[0].toUpperCase() + v.slice(1)
|
||||
: v
|
||||
: key.split(".").slice(-1),
|
||||
)
|
||||
.join("");*/
|
||||
|
||||
const newKey = [
|
||||
...fileNoExtension
|
||||
.replaceAll(/[^a-zA-Z0-9/]/g, "")
|
||||
.toLowerCase()
|
||||
.split("/"),
|
||||
newKeySuffix,
|
||||
].join(".");
|
||||
|
||||
const finalKey = keepPrefixes.some((v) => key.startsWith(v)) ? key : newKey;
|
||||
|
||||
keyMap.set(key, finalKey);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(keyMap);
|
||||
@@ -1,122 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import prettier from "prettier";
|
||||
const prettierConfig = JSON.parse(
|
||||
fs.readFileSync("./.prettierrc.json", "utf-8"),
|
||||
);
|
||||
|
||||
const paths = ["./components", "./layouts", "./pages", "./server"];
|
||||
const constPaths = ["error.vue", "app.vue"];
|
||||
const extensions = [".vue", ".ts"];
|
||||
|
||||
function recursiveFindFiles(root: string): string[] {
|
||||
const results = [];
|
||||
const subpaths = fs.readdirSync(root);
|
||||
for (const subpath of subpaths) {
|
||||
const absPath = path.join(root, subpath);
|
||||
if (extensions.some((v) => absPath.endsWith(v))) {
|
||||
results.push(absPath);
|
||||
continue;
|
||||
}
|
||||
const stat = fs.statSync(absPath);
|
||||
if (stat.isDirectory()) {
|
||||
results.push(...recursiveFindFiles(absPath));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return [...results, ...constPaths];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the paths of all files available to be localised
|
||||
*/
|
||||
export function allLocalisableFiles(): string[] {
|
||||
const files = paths.map((k) => recursiveFindFiles(k)).flat();
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const I18N_UTIL_REGEX = /(?<=[^a-zA-Z]t\(\s*?["']).*?(?=["'])/g;
|
||||
const I18N_KEYPATH_REGEX = /(?<=keypath=["']).*?(?=["'])/g;
|
||||
/**
|
||||
* Uses regex to match all i18n keys in content
|
||||
* @param content The file content to match against
|
||||
*/
|
||||
export function keysFromContent(content: string): string[] {
|
||||
const matches = [
|
||||
...content.matchAll(I18N_UTIL_REGEX),
|
||||
...content.matchAll(I18N_KEYPATH_REGEX),
|
||||
];
|
||||
return matches.map((v) => v[0]);
|
||||
}
|
||||
|
||||
export type Localisation = { [key: string]: Localisation | string };
|
||||
|
||||
export function flattenLocalisation(localisation: Localisation) {
|
||||
const map = new Map<string, string>();
|
||||
flattenLocalisationRecursive(map, [], localisation);
|
||||
return map;
|
||||
}
|
||||
|
||||
function flattenLocalisationRecursive(
|
||||
map: Map<string, string>,
|
||||
key: string[],
|
||||
localisationBranch: Localisation | string,
|
||||
) {
|
||||
if (typeof localisationBranch === "string") {
|
||||
map.set(key.join("."), localisationBranch);
|
||||
return;
|
||||
}
|
||||
for (const [subKey, value] of Object.entries(localisationBranch)) {
|
||||
const newKey = [...key, subKey];
|
||||
flattenLocalisationRecursive(map, newKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteLocalisation(localisation: Localisation, key: string) {
|
||||
const parts = key.split(".");
|
||||
let current: Localisation | string = localisation;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
if (typeof current === "string")
|
||||
throw new Error(`${key} not found in localisation`);
|
||||
current = current[part];
|
||||
}
|
||||
if (typeof current === "string")
|
||||
throw new Error(`${key} not found in localisation`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete current[parts.at(-1)!];
|
||||
}
|
||||
|
||||
export function fetchLocalisation(
|
||||
localisation: Localisation,
|
||||
key: string,
|
||||
): string {
|
||||
const parts = key.split(".");
|
||||
let current: Localisation | string = localisation;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
if (typeof current === "string")
|
||||
throw new Error(`${key} not found in localisation`);
|
||||
current = current[part];
|
||||
}
|
||||
if (typeof current === "string")
|
||||
throw new Error(`${key} not found in localisation`);
|
||||
|
||||
return current[parts.at(-1)!] as string;
|
||||
}
|
||||
|
||||
export async function writeJSON<T>(path: string, object: T) {
|
||||
const flatStr = JSON.stringify(object);
|
||||
const formatted = await prettier.format(flatStr, {
|
||||
parser: "json",
|
||||
...prettierConfig,
|
||||
});
|
||||
fs.writeFileSync(path, formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips some sort of English language string down to something that can be compared to be basically equivalent
|
||||
*/
|
||||
export function stripEquivalence(value: string): string {
|
||||
return value.replaceAll(/[.,\s]/g, "").toLowerCase();
|
||||
}
|
||||
+5
-22
@@ -95,7 +95,7 @@
|
||||
class="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:z-50 lg:block lg:w-20 lg:overflow-y-auto lg:bg-zinc-950 lg:pb-4"
|
||||
>
|
||||
<div class="flex flex-col h-24 shrink-0 items-center justify-center">
|
||||
<ApplicationLogo class="h-8 w-auto" />
|
||||
<DropLogo class="h-8 w-auto" />
|
||||
<span
|
||||
class="mt-1 bg-blue-400 px-1 py-0.5 rounded-md text-xs font-bold font-display"
|
||||
>{{ $t("header.admin.admin") }}</span
|
||||
@@ -170,19 +170,13 @@ import type { NavigationItem } from "~/composables/types";
|
||||
import { useCurrentNavigationIndex } from "~/composables/current-page-engine";
|
||||
import { ArrowLeftIcon } from "@heroicons/vue/16/solid";
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
import type { Settings } from "~/server/internal/utils/types";
|
||||
|
||||
const i18nHead = useLocaleHead();
|
||||
|
||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
{ label: $t("home"), route: "/admin", prefix: "/admin", icon: HomeIcon },
|
||||
{
|
||||
label: $t("header.admin.home"),
|
||||
route: "/admin",
|
||||
prefix: "/admin",
|
||||
icon: HomeIcon,
|
||||
},
|
||||
{
|
||||
label: $t("header.admin.library"),
|
||||
label: $t("userHeader.links.library"),
|
||||
route: "/admin/library",
|
||||
prefix: "/admin/library",
|
||||
icon: ServerStackIcon,
|
||||
@@ -206,7 +200,7 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
icon: RectangleStackIcon,
|
||||
},
|
||||
{
|
||||
label: $t("header.admin.settings.title"),
|
||||
label: $t("header.admin.settings"),
|
||||
route: "/admin/settings",
|
||||
prefix: "/admin/settings",
|
||||
icon: Cog6ToothIcon,
|
||||
@@ -232,15 +226,6 @@ router.afterEach(() => {
|
||||
sidebarOpen.value = false;
|
||||
});
|
||||
|
||||
const {
|
||||
generalSettings: { serverName, mLogoObjectId },
|
||||
} = await $dropFetch<Settings>("/api/v1/settings");
|
||||
|
||||
const favicon = mLogoObjectId ? useObject(mLogoObjectId) : "/favicon.ico";
|
||||
useFavicon(favicon, { rel: "icon" });
|
||||
|
||||
const applicationName = serverName || $t("drop.drop");
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: i18nHead.value.htmlAttrs.lang,
|
||||
@@ -248,9 +233,7 @@ useHead({
|
||||
dir: i18nHead.value.htmlAttrs.dir,
|
||||
},
|
||||
titleTemplate(title) {
|
||||
return title
|
||||
? $t("adminTitleTemplate", [title, applicationName])
|
||||
: $t("adminTitle", [applicationName]);
|
||||
return title ? $t("adminTitleTemplate", [title]) : $t("adminTitle");
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
+6
-12
@@ -1,23 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!clientRequest"
|
||||
class="flex flex-col w-full min-h-screen bg-zinc-900"
|
||||
>
|
||||
<LazyUserHeader class="z-50" hydrate-on-idle />
|
||||
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
||||
<UserHeader class="z-50" hydrate-on-idle />
|
||||
<div class="grow flex">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
<LazyUserFooter class="z-50" hydrate-on-interaction />
|
||||
<UserFooter class="z-50" hydrate-on-interaction />
|
||||
</div>
|
||||
<div v-else class="flex flex-col w-full min-h-screen bg-zinc-900">
|
||||
<div v-else class="flex w-full min-h-screen bg-zinc-900">
|
||||
<NuxtPage />
|
||||
<LazyUserHeaderStoreNav />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const clientRequest = isClientRequest();
|
||||
const route = useRoute();
|
||||
const i18nHead = useLocaleHead();
|
||||
const noWrapper = !!route.query.noWrapper;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -34,7 +31,4 @@ useHead({
|
||||
return title ? t("titleTemplate", [title]) : t("title");
|
||||
},
|
||||
});
|
||||
const { mLogoObjectId } = await $dropFetch("/api/v1");
|
||||
const favicon = mLogoObjectId ? useObject(mLogoObjectId) : "/favicon.ico";
|
||||
useFavicon(favicon, { rel: "icon" });
|
||||
</script>
|
||||
|
||||
+34
-37
@@ -1,9 +1,9 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { execSync } from "node:child_process";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { cpSync, readFileSync, existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import module from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import module from "module";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import { type } from "arktype";
|
||||
|
||||
const packageJsonSchema = type({
|
||||
@@ -11,18 +11,13 @@ const packageJsonSchema = type({
|
||||
version: "string",
|
||||
});
|
||||
|
||||
const twemojiPackage = module.findPackageJSON(
|
||||
const twemojiJson = module.findPackageJSON(
|
||||
"@discordapp/twemoji",
|
||||
import.meta.url,
|
||||
);
|
||||
if (!twemojiPackage) {
|
||||
if (!twemojiJson) {
|
||||
throw new Error("Could not find @discordapp/twemoji package.");
|
||||
}
|
||||
const twemojiAssetsPath = path.join(
|
||||
path.dirname(twemojiPackage),
|
||||
"dist",
|
||||
"svg",
|
||||
);
|
||||
|
||||
// get drop version
|
||||
const dropVersion = getDropVersion();
|
||||
@@ -69,8 +64,7 @@ export default defineNuxtConfig({
|
||||
|
||||
experimental: {
|
||||
buildCache: true,
|
||||
viewTransition: false,
|
||||
appManifest: false,
|
||||
viewTransition: true,
|
||||
componentIslands: true,
|
||||
},
|
||||
|
||||
@@ -80,11 +74,32 @@ export default defineNuxtConfig({
|
||||
|
||||
vite: {
|
||||
plugins: [
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tailwindcss() as any,
|
||||
tailwindcss(),
|
||||
// only used in dev server, not build because nitro sucks
|
||||
// see build hook below
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: "node_modules/@discordapp/twemoji/dist/svg/*",
|
||||
dest: "twemoji",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
hooks: {
|
||||
"nitro:build:public-assets": (nitro) => {
|
||||
// this is only run during build, not dev server
|
||||
// https://github.com/nuxt/nuxt/issues/18918#issuecomment-1925774964
|
||||
// copy emojis to .output/public/twemoji
|
||||
const targetDir = path.join(nitro.options.output.publicDir, "twemoji");
|
||||
cpSync(path.join(path.dirname(twemojiJson), "dist", "svg"), targetDir, {
|
||||
recursive: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
gitRef: commitHash,
|
||||
dropVersion: dropVersion,
|
||||
@@ -98,15 +113,6 @@ export default defineNuxtConfig({
|
||||
|
||||
routeRules: {
|
||||
"/api/**": { cors: true },
|
||||
|
||||
// redirect old OIDC callback route
|
||||
"/auth/callback/oidc": {
|
||||
redirect: "/api/v1/auth/oidc/callback",
|
||||
},
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port: 4000,
|
||||
},
|
||||
|
||||
nitro: {
|
||||
@@ -147,14 +153,6 @@ export default defineNuxtConfig({
|
||||
base: "./.data/appCache",
|
||||
},
|
||||
},
|
||||
|
||||
serverAssets: [
|
||||
{
|
||||
baseName: "twemoji",
|
||||
// get path to twemoji svg assets
|
||||
dir: twemojiAssetsPath,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
typescript: {
|
||||
@@ -174,11 +172,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
i18n: {
|
||||
bundle: {
|
||||
optimizeTranslationDirective: false,
|
||||
},
|
||||
defaultLocale: "en-us",
|
||||
lazy: true,
|
||||
strategy: "no_prefix",
|
||||
experimental: {
|
||||
localeDetector: "localeDetector.ts",
|
||||
@@ -259,7 +253,6 @@ export default defineNuxtConfig({
|
||||
"https://www.giantbomb.com",
|
||||
"https://images.pcgamingwiki.com",
|
||||
"https://images.igdb.com",
|
||||
"https://*.steamstatic.com",
|
||||
],
|
||||
},
|
||||
strictTransportSecurity: false,
|
||||
@@ -282,7 +275,11 @@ function getDropVersion(): string {
|
||||
// example nightly: "v0.3.0-nightly.2025.05.28"
|
||||
const defaultVersion = "v0.0.0-alpha.0";
|
||||
|
||||
const packageJsonPath = fileURLToPath(import.meta.resolve("./package.json"));
|
||||
// get path
|
||||
const packageJsonPath = path.join(
|
||||
path.dirname(import.meta.url.replace("file://", "")),
|
||||
"package.json",
|
||||
);
|
||||
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
console.error("Could not find package.json, using default version.");
|
||||
|
||||
+14
-32
@@ -1,70 +1,56 @@
|
||||
{
|
||||
"name": "drop",
|
||||
"version": "0.4.0",
|
||||
"version": "0.3.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=22.16.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare && prisma generate && buf generate",
|
||||
"postinstall": "nuxt prepare && prisma generate",
|
||||
"typecheck": "nuxt typecheck",
|
||||
"lint": "pnpm run lint:eslint && pnpm run lint:prettier",
|
||||
"lint": "yarn lint:eslint && yarn lint:prettier",
|
||||
"lint:eslint": "eslint .",
|
||||
"lint:prettier": "prettier . --check",
|
||||
"lint:fix": "eslint . --fix && prettier --write --list-different ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@discordapp/twemoji": "^16.0.1",
|
||||
"@drop-oss/droplet": "1.6.0",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@lobomfz/prismark": "0.0.3",
|
||||
"@nuxt/fonts": "^0.11.0",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/kit": "^3.20.1",
|
||||
"@nuxtjs/i18n": "^9.5.5",
|
||||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@prisma/client": "^6.11.1",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"@vueuse/nuxt": "13.6.0",
|
||||
"argon2": "^0.43.0",
|
||||
"arktype": "^2.1.10",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cbor2": "^2.0.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"cookie-es": "^2.0.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"file-type-mime": "^0.4.3",
|
||||
"jdenticon": "^3.3.0",
|
||||
"jose": "^6.1.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"kjua": "^0.10.0",
|
||||
"luxon": "^3.6.1",
|
||||
"micromark": "^4.0.1",
|
||||
"normalize-url": "^8.0.2",
|
||||
"nuxt": "^3.20.1",
|
||||
"nuxt": "^3.17.4",
|
||||
"nuxt-security": "2.2.0",
|
||||
"otp-io": "^1.2.7",
|
||||
"parse-cosekey": "^1.0.2",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"prisma": "7.3.0",
|
||||
"prisma": "^6.11.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.7.1",
|
||||
"shescape": "^2.1.8",
|
||||
"stream-mime-type": "^2.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
"unstorage": "^1.15.0",
|
||||
"vite-plugin-static-copy": "^3.0.0",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vue3-carousel": "^0.16.0",
|
||||
@@ -72,28 +58,22 @@
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/buf": "^1.65.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
||||
"@golar/vue": "^0.0.13",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^4.0.1",
|
||||
"@nuxt/eslint": "^1.3.0",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/node": "^22.13.16",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@typescript-eslint/utils": "^8.50.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"golar": "^0.0.13",
|
||||
"h3": "^1.15.5",
|
||||
"h3": "^1.15.3",
|
||||
"nitropack": "^2.11.12",
|
||||
"ofetch": "^1.4.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"sass": "^1.79.4",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
@@ -104,5 +84,7 @@
|
||||
"vue3-carousel": "^0.16.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.29.1+sha512.48dae233635a645768a3028d19545cacc1688639eeb1f3734e42d6d6b971afbf22aa1ac9af52a173d9c3a20c15857cfa400f19994d79a2f626fcc73fccda9bbc"
|
||||
"prisma": {
|
||||
"schema": "./prisma"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="border-b border-zinc-800 pb-4 w-full">
|
||||
<div
|
||||
class="gap-2 flex flex-col lg:flex-row lg:items-center justify-between w-full"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h2
|
||||
class="text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
|
||||
>
|
||||
{{ $t("account.notifications.notifications") }}
|
||||
</h2>
|
||||
<div class="inline-flex gap-x-2">
|
||||
<button
|
||||
:disabled="notifications.length === 0"
|
||||
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none"
|
||||
@click="markAllAsRead"
|
||||
>
|
||||
<CheckIcon class="size-4" />
|
||||
{{ $t("account.notifications.markAllAsRead") }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="notifications.length === 0"
|
||||
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-red-800 px-3 py-2 text-sm font-semibold text-red-100 shadow-sm transition-all duration-200 hover:bg-red-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-red-800 disabled:hover:scale-100 disabled:hover:shadow-none"
|
||||
@click="clearNotifications"
|
||||
>
|
||||
<TrashIcon class="size-4" />
|
||||
{{ $t("account.notifications.clear") }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
:disabled="notifications.length === 0"
|
||||
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none"
|
||||
@click="markAllAsRead"
|
||||
>
|
||||
<CheckIcon class="size-4" />
|
||||
{{ $t("account.notifications.markAllAsRead") }}
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
|
||||
@@ -43,7 +31,7 @@
|
||||
:class="{ 'opacity-75': notification.read }"
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col lg:flex-row items-start justify-between">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-base font-semibold text-zinc-100">
|
||||
{{ notification.title }}
|
||||
@@ -64,9 +52,7 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-4 lg:mt-0 lg:ml-4 flex flex-shrink-0 items-center gap-x-2"
|
||||
>
|
||||
<div class="ml-4 flex flex-shrink-0 items-center gap-x-2">
|
||||
<span class="text-xs text-zinc-500">
|
||||
<RelativeTime :date="notification.created" />
|
||||
</span>
|
||||
@@ -85,7 +71,7 @@
|
||||
@click="deleteNotification(notification.id)"
|
||||
>
|
||||
<TrashIcon class="size-3" />
|
||||
{{ $t("common.delete") }}
|
||||
{{ $t("delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,12 +106,22 @@ useHead({
|
||||
});
|
||||
|
||||
// Fetch notifications
|
||||
const notifications = useNotifications();
|
||||
const notifications = ref<SerializeObject<NotificationModel>[]>([]);
|
||||
|
||||
async function fetchNotifications() {
|
||||
const { data } = await useFetch("/api/v1/notifications");
|
||||
notifications.value = data.value || [];
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
await fetchNotifications();
|
||||
|
||||
// Mark a notification as read
|
||||
async function markAsRead(id: string) {
|
||||
await $dropFetch(`/api/v1/notifications/${id}/read`, { method: "POST" });
|
||||
const notification = notifications.value.find((n) => n.id === id);
|
||||
const notification = notifications.value.find(
|
||||
(n: SerializeObject<NotificationModel>) => n.id === id,
|
||||
);
|
||||
if (notification) {
|
||||
notification.read = true;
|
||||
}
|
||||
@@ -133,21 +129,12 @@ async function markAsRead(id: string) {
|
||||
|
||||
// Mark all notifications as read
|
||||
async function markAllAsRead() {
|
||||
await $dropFetch("/api/v1/notifications/readall", {
|
||||
method: "POST",
|
||||
failTitle: "Failed to read all notifications",
|
||||
});
|
||||
notifications.value.forEach((notification) => {
|
||||
notification.read = true;
|
||||
});
|
||||
}
|
||||
|
||||
async function clearNotifications() {
|
||||
await $dropFetch("/api/v1/notifications/clear", {
|
||||
method: "POST",
|
||||
failTitle: "Failed to clear notifications",
|
||||
});
|
||||
notifications.value = [];
|
||||
await $dropFetch("/api/v1/notifications/readall", { method: "POST" });
|
||||
notifications.value.forEach(
|
||||
(notification: SerializeObject<NotificationModel>) => {
|
||||
notification.read = true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Delete a notification
|
||||
|
||||
+1
-255
@@ -1,257 +1,3 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!superlevel"
|
||||
class="border-l-4 p-4 border-yellow-500 bg-yellow-500/10"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<ExclamationTriangleIcon
|
||||
class="size-5 text-yellow-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-300">
|
||||
{{ $t("account.security.2fa.superlevelHint.title") }}
|
||||
<NuxtLink
|
||||
href="/auth/signin?redirect=/account/security&superlevel=true"
|
||||
class="font-medium underline text-yellow-300 hover:text-yellow-200"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="account.security.2fa.superlevelHint.signin"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="border-l-4 p-4 border-green-500 bg-green-500/10">
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<CheckCircleIcon class="size-5 text-green-500" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-green-300">
|
||||
{{ $t("account.security.2fa.superlevelHint.success") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 relative">
|
||||
<div></div>
|
||||
<div class="mt-8 border-b border-white/10 pb-2">
|
||||
<h3 class="text-base font-semibold text-white">
|
||||
{{ $t("account.security.2fa.title") }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap gap-8">
|
||||
<!-- TOTP -->
|
||||
<div
|
||||
class="group relative border-white/10 bg-zinc-800/50 p-6 rounded-md"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="inline-flex rounded-lg p-3 bg-blue-400/10 text-blue-400"
|
||||
>
|
||||
<ClockIcon class="size-6" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-8 max-w-sm">
|
||||
<h3 class="text-base font-semibold text-white">
|
||||
<NuxtLink
|
||||
:href="mfa.mecs.TOTP?.enabled ? '' : '/mfa/setup/totp'"
|
||||
class="focus:outline-hidden"
|
||||
>
|
||||
<!-- Extend touch target to entire panel -->
|
||||
<span
|
||||
v-if="!mfa.mecs.TOTP?.enabled"
|
||||
class="absolute inset-0"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("account.security.2fa.totp.title") }}
|
||||
</NuxtLink>
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-400">
|
||||
{{ $t("account.security.2fa.totp.description") }}
|
||||
</p>
|
||||
<div v-if="mfa.mecs.TOTP?.enabled" class="mt-3">
|
||||
<LoadingButton :loading="false">{{
|
||||
$t("account.security.2fa.totp.disableButton")
|
||||
}}</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="pointer-events-none absolute top-6 right-6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
v-if="!mfa.mecs.TOTP?.enabled"
|
||||
class="size-6 text-gray-500 group-hover:text-gray-200"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 4h1a1 1 0 00-1-1v1zm-1 12a1 1 0 102 0h-2zM8 3a1 1 0 000 2V3zM3.293 19.293a1 1 0 101.414 1.414l-1.414-1.414zM19 4v12h2V4h-2zm1-1H8v2h12V3zm-.707.293l-16 16 1.414 1.414 16-16-1.414-1.414z"
|
||||
/>
|
||||
</svg>
|
||||
<CheckIcon v-else class="size-6 text-green-600" />
|
||||
</span>
|
||||
</div>
|
||||
<!-- WebAuthn -->
|
||||
<div
|
||||
class="group relative border-white/10 bg-zinc-800/50 p-6 rounded-md"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="inline-flex rounded-lg p-3 bg-blue-400/10 text-blue-400"
|
||||
>
|
||||
<KeyIcon class="size-6" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-8 max-w-sm">
|
||||
<h3 class="text-base font-semibold text-white">
|
||||
{{ $t("account.security.2fa.webauthn.title") }}
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-400">
|
||||
{{ $t("account.security.2fa.webauthn.description") }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs font-bold text-zinc-300">
|
||||
{{ $t("account.security.2fa.webauthn.bypassHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<LoadingButton
|
||||
class="mt-3"
|
||||
:loading="false"
|
||||
@click="() => (webAuthnOpen = true)"
|
||||
>{{ $t("account.security.2fa.webauthn.manage") }}</LoadingButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!superlevel" class="absolute inset-0 bg-zinc-900/50" />
|
||||
</div>
|
||||
<ModalTemplate v-model="webAuthnOpen" size-class="max-w-2xl">
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-white">
|
||||
{{ $t("account.security.2fa.webauthn.modal.title") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-300">
|
||||
{{ $t("account.security.2fa.webauthn.modal.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<NuxtLink
|
||||
to="/mfa/setup/webauthn"
|
||||
class="block rounded-md bg-blue-500 px-3 py-2 text-center text-sm font-semibold text-white shadow-xs hover:bg-blue-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
|
||||
>
|
||||
{{ $t("account.security.2fa.webauthn.modal.new") }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div
|
||||
class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"
|
||||
>
|
||||
<table class="relative min-w-full divide-y divide-white/15">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
|
||||
>
|
||||
{{ $t("account.security.2fa.webauthn.modal.tableName") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
|
||||
>
|
||||
{{
|
||||
$t("account.security.2fa.webauthn.modal.tableCreated")
|
||||
}}
|
||||
</th>
|
||||
|
||||
<th scope="col" class="py-3.5 pr-4 pl-3 sm:pr-0">
|
||||
<span class="sr-only">{{ $t("common.delete") }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/10">
|
||||
<tr
|
||||
v-for="mec in (mfa.mecs.WebAuthn?.credentials as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
created: number;
|
||||
}>) ?? []"
|
||||
:key="mec.id"
|
||||
>
|
||||
<td
|
||||
class="py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-white sm:pl-0"
|
||||
>
|
||||
{{ mec.name }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-white sm:pl-0"
|
||||
>
|
||||
<RelativeTime :date="new Date(mec.created)" />
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="py-4 pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0"
|
||||
>
|
||||
<button
|
||||
class="text-blue-400 hover:text-blue-300"
|
||||
@click="() => deletePasskey(mec.id)"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<button
|
||||
ref="cancelButtonRef"
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-900 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-950 transition-all duration-200 hover:scale-105 hover:shadow-lg active:scale-95 sm:mt-0 sm:w-auto"
|
||||
@click="webAuthnOpen = false"
|
||||
>
|
||||
{{ $t("common.close") }}
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</div>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { CheckIcon, ClockIcon, KeyIcon } from "@heroicons/vue/24/outline";
|
||||
const superlevel = await $dropFetch("/api/v1/user/superlevel");
|
||||
//const auth = await $dropFetch("/api/v1/user/auth");
|
||||
const mfa = await $dropFetch("/api/v1/user/mfa");
|
||||
|
||||
const webAuthnOpen = ref(false);
|
||||
|
||||
async function deletePasskey(id: string) {
|
||||
await $dropFetch("/api/v1/user/mfa/webauthn", {
|
||||
method: "DELETE",
|
||||
body: { id },
|
||||
failTitle: "Failed to delete passkey",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div>
|
||||
<h2
|
||||
class="mt-2 text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
|
||||
>
|
||||
{{ $t("account.token.title") }}
|
||||
</h2>
|
||||
<p
|
||||
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
|
||||
>
|
||||
{{ $t("account.token.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<LoadingButton :loading="false" @click="() => (createOpen = true)">
|
||||
{{ $t("common.create") }}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="newToken"
|
||||
class="mt-4 rounded-md p-4 bg-green-500/10 outline outline-green-500/20"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<CheckCircleIcon class="size-5 text-green-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-green-300">
|
||||
{{ $t("account.token.success") }}
|
||||
</p>
|
||||
<p class="text-xs text-green-300/70">
|
||||
{{ $t("account.token.successNote") }}
|
||||
</p>
|
||||
<p
|
||||
class="mt-2 bg-zinc-950 px-3 py-2 font-mono text-zinc-400 rounded-xl"
|
||||
>
|
||||
{{ newToken }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-auto pl-3">
|
||||
<div class="-mx-1.5 -my-1.5">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex rounded-md bg-green-50 p-1.5 text-green-500 hover:bg-green-100 focus-visible:ring-2 focus-visible:ring-green-600 focus-visible:ring-offset-2 focus-visible:ring-offset-green-50 focus-visible:outline-hidden dark:bg-transparent dark:text-green-400 dark:hover:bg-green-500/10 dark:focus-visible:ring-green-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-green-900"
|
||||
@click="() => (newToken = undefined)"
|
||||
>
|
||||
<span class="sr-only">{{ $t("common.close") }}</span>
|
||||
<XMarkIcon class="size-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-8 overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900 shadow-sm"
|
||||
>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-800">
|
||||
<thead>
|
||||
<tr class="bg-zinc-800/50">
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-6"
|
||||
>
|
||||
{{ $t("common.name") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("account.token.acls") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
{{ $t("account.token.expiry") }}
|
||||
</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">{{ $t("actions") }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-800">
|
||||
<tr
|
||||
v-for="(token, tokenIdx) in tokens"
|
||||
:key="token.id"
|
||||
class="transition-colors duration-150 hover:bg-zinc-800/50"
|
||||
>
|
||||
<td
|
||||
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-6"
|
||||
>
|
||||
{{ token.name }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="acl in token.acls"
|
||||
:key="acl"
|
||||
class="inline-flex items-center gap-x-1 rounded-md bg-blue-400/10 px-2 py-1 text-xs font-medium text-blue-400 ring-1 ring-inset ring-blue-400/20"
|
||||
>
|
||||
{{ acl }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
<RelativeTime v-if="token.expiresAt" :date="token.expiresAt" />
|
||||
<span v-else>{{ $t("account.token.noExpiry") }}</span>
|
||||
</td>
|
||||
<td
|
||||
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
|
||||
>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-red-400/10 px-2 py-1 text-xs font-medium text-red-400 ring-1 ring-inset ring-red-400/20 transition-all duration-200 hover:bg-red-400/20 hover:scale-105 active:scale-95"
|
||||
@click="() => revokeToken(tokenIdx)"
|
||||
>
|
||||
{{ $t("account.token.revoke") }}
|
||||
<span class="sr-only">
|
||||
{{ $t("chars.srComma", [token.name]) }}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="tokens.length === 0">
|
||||
<td colspan="5" class="py-8 text-center text-sm text-zinc-400">
|
||||
{{ $t("account.token.noTokens") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalCreateToken
|
||||
v-model="createOpen"
|
||||
:acls="acls"
|
||||
:loading="createLoading"
|
||||
:suggested-name="suggestedName"
|
||||
:suggested-acls="suggestedAcls"
|
||||
@create="(name, acls, expiry) => createToken(name, acls, expiry)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArkErrors, type } from "arktype";
|
||||
import { DateTime, type DurationLike } from "luxon";
|
||||
import { CheckCircleIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
|
||||
const tokens = ref(await $dropFetch("/api/v1/user/token"));
|
||||
const acls = await $dropFetch("/api/v1/user/token/acls");
|
||||
|
||||
const createOpen = ref(false);
|
||||
const createLoading = ref(false);
|
||||
|
||||
const newToken = ref<string | undefined>();
|
||||
|
||||
const suggestedName = ref();
|
||||
const suggestedAcls = ref<string[]>([]);
|
||||
|
||||
const payloadParser = type({
|
||||
name: "string?",
|
||||
acls: "string[]?",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
if (route.query.payload) {
|
||||
try {
|
||||
const rawPayload = JSON.parse(atob(route.query.payload.toString()));
|
||||
const payload = payloadParser(rawPayload);
|
||||
if (payload instanceof ArkErrors) throw payload;
|
||||
suggestedName.value = payload.name;
|
||||
suggestedAcls.value = payload.acls ?? [];
|
||||
createOpen.value = true;
|
||||
} catch {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Failed to parse the token create payload.",
|
||||
fatal: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createToken(
|
||||
name: string,
|
||||
acls: string[],
|
||||
expiry: DurationLike | undefined,
|
||||
) {
|
||||
createLoading.value = true;
|
||||
try {
|
||||
const result = await $dropFetch("/api/v1/user/token", {
|
||||
method: "POST",
|
||||
body: {
|
||||
name,
|
||||
acls,
|
||||
expiry: expiry ? DateTime.now().plus(expiry) : undefined,
|
||||
},
|
||||
failTitle: "Failed to create API token.",
|
||||
});
|
||||
newToken.value = result.token;
|
||||
tokens.value.push(result);
|
||||
} finally {
|
||||
/* empty */
|
||||
}
|
||||
createOpen.value = false;
|
||||
createLoading.value = false;
|
||||
}
|
||||
|
||||
async function revokeToken(index: number) {
|
||||
const token = tokens.value[index];
|
||||
if (!token) return;
|
||||
|
||||
await $dropFetch("/api/v1/user/token/:id", {
|
||||
method: "DELETE",
|
||||
params: {
|
||||
id: token.id,
|
||||
},
|
||||
failTitle: "Failed to revoke token.",
|
||||
});
|
||||
|
||||
tokens.value.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
+1
-193
@@ -1,183 +1,6 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-zinc-100">
|
||||
{{ t("home.admin.title") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">
|
||||
{{ t("home.admin.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<main
|
||||
class="mx-auto max-w-md lg:max-w-none md:max-w-none w-full py-2 text-zinc-100"
|
||||
>
|
||||
<div class="grid grid-cols-6 gap-4">
|
||||
<div class="col-span-6 lg:col-span-1 md:col-span-3 row-span-1">
|
||||
<MiniTile :value="version" :label="t('home.admin.version')">
|
||||
<template #icon>
|
||||
<ApplicationLogo />
|
||||
</template>
|
||||
</MiniTile>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 lg:col-span-1 md:col-span-3">
|
||||
<MiniTile :label="t('home.admin.games')" :value="$n(gameCount)">
|
||||
<template #icon>
|
||||
<GamepadIcon />
|
||||
</template>
|
||||
</MiniTile>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-span-6 lg:col-span-1 md:col-span-3 row-span-1 lg:col-start-1 lg:row-start-2"
|
||||
>
|
||||
<TileWithLink>
|
||||
<div class="h-full flex">
|
||||
<div class="flex-1 my-auto">
|
||||
<ServerStackIcon />
|
||||
</div>
|
||||
<div
|
||||
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
|
||||
>
|
||||
<div class="text-3xl flex-1 font-bold">
|
||||
{{ $n(sources.length) }}
|
||||
</div>
|
||||
<div class="text-xs flex-1 text-left lg:text-center">
|
||||
{{ t("home.admin.librarySources") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TileWithLink>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-span-6 lg:col-span-1 md:col-span-3 row-span-1 lg:col-start-2 lg:row-start-2"
|
||||
>
|
||||
<TileWithLink>
|
||||
<div class="h-full flex">
|
||||
<div class="flex-1 my-auto">
|
||||
<UserGroupIcon />
|
||||
</div>
|
||||
<div
|
||||
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
|
||||
>
|
||||
<div class="text-3xl flex-1 font-bold">
|
||||
{{ $n(userStats.userCount) }}
|
||||
</div>
|
||||
<div class="text-xs flex-1 text-left lg:text-center">
|
||||
{{ t("home.admin.users") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TileWithLink>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 row-span-1 lg:col-span-2 lg:row-span-2">
|
||||
<TileWithLink
|
||||
:link="{
|
||||
url: '/admin/users',
|
||||
label: t('home.admin.goToUsers'),
|
||||
}"
|
||||
:title="t('home.admin.activeInactiveUsers')"
|
||||
>
|
||||
<PieChart :data="pieChartData" />
|
||||
</TileWithLink>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 row-span-1 lg:col-span-2 lg:row-span-2">
|
||||
<TileWithLink title="System">
|
||||
<div class="h-full pb-15 content-center">
|
||||
<div class="grid grid-cols-1 text-center gap-4">
|
||||
<h3 class="col-span-1 text-lg font-semibold flex">
|
||||
<div class="flex-1 text-left">
|
||||
{{ $t("home.admin.cpuUsage") }}
|
||||
</div>
|
||||
<div class="flex-1 text-sm grow text-right self-center">
|
||||
{{ $t("home.admin.numberCores", systemData.cpuCores) }}
|
||||
</div>
|
||||
</h3>
|
||||
<div class="col-span-1">
|
||||
<ProgressBar
|
||||
:color="getBarColor(systemData.cpuLoad)"
|
||||
:percentage="systemData.cpuLoad"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="col-span-1 text-lg font-semibold my-2 flex">
|
||||
<div class="flex-none text-left">
|
||||
{{ $t("home.admin.ramUsage") }}
|
||||
</div>
|
||||
<div class="flex-1 text-sm grow text-right self-center">
|
||||
{{
|
||||
$t("home.admin.availableRam", {
|
||||
usedRam: formatBytes(
|
||||
systemData.totalRam - systemData.freeRam,
|
||||
),
|
||||
totalRam: formatBytes(systemData.totalRam),
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</h3>
|
||||
<div class="col-span-1">
|
||||
<ProgressBar
|
||||
:color="
|
||||
getBarColor(
|
||||
getPercentage(
|
||||
systemData.totalRam - systemData.freeRam,
|
||||
systemData.totalRam,
|
||||
),
|
||||
)
|
||||
"
|
||||
:percentage="
|
||||
getPercentage(
|
||||
systemData.totalRam - systemData.freeRam,
|
||||
systemData.totalRam,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TileWithLink>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6">
|
||||
<TileWithLink
|
||||
title="Library"
|
||||
:link="{ url: '/admin/library', label: 'Go to library' }"
|
||||
>
|
||||
<SourceTable :sources="sources" />
|
||||
</TileWithLink>
|
||||
</div>
|
||||
<div class="col-span-6 lg:col-span-2">
|
||||
<TileWithLink
|
||||
:title="t('home.admin.biggestGamesToDownload')"
|
||||
:subtitle="t('home.admin.latestVersionOnly')"
|
||||
>
|
||||
<!-- <RankingList :items="biggestGamesLatest.map(gameToRankItem)" />-->
|
||||
</TileWithLink>
|
||||
</div>
|
||||
<div class="col-span-6 lg:col-span-2">
|
||||
<TileWithLink
|
||||
:title="t('home.admin.biggestGamesOnServer')"
|
||||
:subtitle="t('home.admin.allVersionsCombined')"
|
||||
>
|
||||
<!-- <RankingList :items="biggestGamesCombined.map(gameToRankItem)" />-->
|
||||
</TileWithLink>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
<template><div /></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatBytes } from "~/server/internal/utils/files";
|
||||
import GamepadIcon from "~/components/Icons/GamepadIcon.vue";
|
||||
import { ServerStackIcon, UserGroupIcon } from "@heroicons/vue/24/outline";
|
||||
import { getPercentage } from "~/utils/utils";
|
||||
import { getBarColor } from "~/utils/colors";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
@@ -185,19 +8,4 @@ definePageMeta({
|
||||
useHead({
|
||||
title: "Home",
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const systemData = useSystemData();
|
||||
|
||||
const { version, gameCount, sources, userStats } =
|
||||
await $dropFetch("/api/v1/admin/home");
|
||||
|
||||
const pieChartData = [
|
||||
{
|
||||
label: t("home.admin.inactiveUsers"),
|
||||
value: userStats.userCount - userStats.activeSessions,
|
||||
},
|
||||
{ label: t("home.admin.activeUsers"), value: userStats.activeSessions },
|
||||
];
|
||||
</script>
|
||||
|
||||
+475
-240
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-4 sm:max-w-[40rem]">
|
||||
<div class="flex flex-col gap-y-4 max-w-lg">
|
||||
<Listbox
|
||||
as="div"
|
||||
:model-value="currentlySelectedVersion"
|
||||
@@ -13,7 +13,7 @@
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<span v-if="currentlySelectedVersion != -1" class="block truncate">{{
|
||||
versions[currentlySelectedVersion].name
|
||||
versions[currentlySelectedVersion]
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-zinc-600">{{
|
||||
$t("library.admin.import.selectDir")
|
||||
@@ -38,7 +38,7 @@
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="(version, versionIdx) in versions"
|
||||
:key="version.identifier"
|
||||
:key="version"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
:value="versionIdx"
|
||||
@@ -54,7 +54,7 @@
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ version.name }}</span
|
||||
>{{ version }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
@@ -73,198 +73,303 @@
|
||||
</div>
|
||||
</Listbox>
|
||||
|
||||
<div v-if="versionGuesses" class="flex flex-col gap-4">
|
||||
<!-- version display name -->
|
||||
<div class="bg-zinc-800 p-4 rounded-xl relative flex flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium leading-6 text-zinc-100">{{
|
||||
$t("library.admin.import.version.displayName")
|
||||
}}</label>
|
||||
<p class="text-zinc-400 text-xs">
|
||||
{{ $t("library.admin.import.version.displayNameDesc") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id="display-name"
|
||||
v-model="versionSettings.displayName"
|
||||
type="text"
|
||||
class="min-w-48 block w-full rounded-md border-radius-md bg-zinc-950 px-3 py-1.5 text-white outline-1 -outline-offset-1 outline-zinc-800 placeholder:text-zinc-500 focus:outline-1 focus:-outline-offset-1 focus:outline-blue-500 sm:text-sm/6"
|
||||
:placeholder="
|
||||
$t('library.admin.import.version.displayNamePlaceholder')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="versionGuesses" class="flex flex-col gap-8">
|
||||
<!-- setup executable -->
|
||||
<div class="bg-zinc-800 p-4 rounded-xl relative flex flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium leading-6 text-zinc-100">{{
|
||||
$t("library.admin.import.version.setupCmd")
|
||||
}}</label>
|
||||
<p class="text-zinc-400 text-xs">
|
||||
{{ $t("library.admin.import.version.setupDesc") }}
|
||||
</p>
|
||||
</div>
|
||||
<ol
|
||||
v-if="versionSettings.setups.length > 0"
|
||||
class="divide-y-1 divide-zinc-700"
|
||||
<div>
|
||||
<label
|
||||
for="startup"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>{{ $t("library.admin.import.version.setupCmd") }}</label
|
||||
>
|
||||
<li
|
||||
v-for="(launch, launchIdx) in versionSettings.setups"
|
||||
:key="launchIdx"
|
||||
class="py-2 inline-flex items-start gap-x-1 w-full"
|
||||
>
|
||||
<ImportVersionLaunchRow
|
||||
v-model="versionSettings.setups[launchIdx]"
|
||||
:version-guesses="versionGuesses"
|
||||
:needs-name="false"
|
||||
/>
|
||||
<button
|
||||
class="transition rounded p-1 bg-zinc-900/30 group hover:bg-red-600/30"
|
||||
@click="() => versionSettings.setups.splice(launchIdx, 1)"
|
||||
>
|
||||
<TrashIcon
|
||||
class="transition size-5 text-zinc-700 group-hover:text-red-700"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
<span
|
||||
v-else
|
||||
class="text-sm text-zinc-700 uppercase font-display font-bold"
|
||||
>{{ $t("library.admin.import.version.noSetups") }}</span
|
||||
>
|
||||
<LoadingButton
|
||||
:loading="false"
|
||||
class="w-fit"
|
||||
@click="() => versionSettings.setups.push({} as any)"
|
||||
>{{ $t("common.add") }}</LoadingButton
|
||||
>
|
||||
</div>
|
||||
<!-- setup mode -->
|
||||
<div class="relative">
|
||||
<SwitchGroup
|
||||
as="div"
|
||||
class="bg-zinc-800 p-4 rounded-xl flex items-center justify-between gap-4"
|
||||
>
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>{{ $t("library.admin.import.version.setupMode") }}</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400">{{
|
||||
$t("library.admin.import.version.setupModeDesc")
|
||||
}}</SwitchDescription>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="versionSettings.onlySetup"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? 'bg-blue-600' : 'bg-zinc-900',
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
|
||||
]"
|
||||
<p class="text-zinc-400 text-xs">
|
||||
{{ $t("library.admin.import.version.setupDesc") }}
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="flex w-fit rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? 'translate-x-5' : 'translate-x-0',
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
]"
|
||||
/>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
<div
|
||||
v-if="type === GameType.Dependency"
|
||||
class="absolute inset-0 bg-zinc-900/50"
|
||||
/>
|
||||
</div>
|
||||
<!-- launch executables -->
|
||||
<div class="relative flex flex-col gap-y-2 bg-zinc-800 p-4 rounded-xl">
|
||||
<div>
|
||||
<label class="block text-sm font-medium leading-6 text-zinc-100">{{
|
||||
$t("library.admin.import.version.launchCmd")
|
||||
}}</label>
|
||||
<p class="text-zinc-400 text-xs">
|
||||
{{ $t("library.admin.import.version.launchDesc") }}
|
||||
</p>
|
||||
</div>
|
||||
<ol
|
||||
v-if="versionSettings.launches.length > 0"
|
||||
class="divide-y-1 divide-zinc-700"
|
||||
>
|
||||
<li
|
||||
v-for="(launch, launchIdx) in versionSettings.launches"
|
||||
:key="launchIdx"
|
||||
class="py-2 inline-flex items-start gap-x-1 w-full"
|
||||
>
|
||||
<Disclosure
|
||||
v-slot="{ open }"
|
||||
:default-open="true"
|
||||
as="div"
|
||||
class="py-2 px-3 w-full bg-zinc-900 rounded-lg"
|
||||
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
|
||||
>
|
||||
<dt>
|
||||
<DisclosureButton
|
||||
class="flex w-full items-center text-left text-white"
|
||||
>
|
||||
<span v-if="launch.name" class="text-sm font-semibold">{{
|
||||
launch.name
|
||||
}}</span>
|
||||
<span v-else class="text-sm text-zinc-500 italic">{{
|
||||
$t("library.admin.import.version.noNameProvided")
|
||||
}}</span>
|
||||
<span class="ml-auto flex h-7 items-center">
|
||||
<PlusIcon v-if="!open" class="size-6" aria-hidden="true" />
|
||||
<MinusIcon v-else class="size-6" aria-hidden="true" />
|
||||
</span>
|
||||
<button
|
||||
class="ml-1 transition rounded p-1 bg-zinc-900/30 group hover:bg-red-600/30"
|
||||
@click.prevent="
|
||||
() => versionSettings.launches.splice(launchIdx, 1)
|
||||
"
|
||||
>
|
||||
<TrashIcon
|
||||
class="transition size-5 text-zinc-700 group-hover:text-red-700"
|
||||
/>
|
||||
</button>
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" class="mt-2">
|
||||
<ImportVersionLaunchRow
|
||||
v-model="versionSettings.launches[launchIdx]"
|
||||
:version-guesses="versionGuesses"
|
||||
:needs-name="true"
|
||||
:allow-emulator="true"
|
||||
:type="type"
|
||||
{{ $t("library.admin.import.version.installDir") }}
|
||||
</span>
|
||||
<Combobox
|
||||
as="div"
|
||||
:value="versionSettings.setup"
|
||||
nullable
|
||||
@update:model-value="(v) => updateSetupCommand(v)"
|
||||
>
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
:placeholder="
|
||||
$t('library.admin.import.version.setupPlaceholder')
|
||||
"
|
||||
@change="setupProcessQuery = $event.target.value"
|
||||
@blur="setupProcessQuery = ''"
|
||||
/>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</li>
|
||||
</ol>
|
||||
<span
|
||||
v-else
|
||||
class="text-sm text-zinc-700 uppercase font-display font-bold"
|
||||
>{{ $t("library.admin.import.version.noLaunches") }}</span
|
||||
>
|
||||
<LoadingButton
|
||||
:loading="false"
|
||||
class="w-fit"
|
||||
@click="() => versionSettings.launches.push({} as any)"
|
||||
>{{ $t("common.add") }}</LoadingButton
|
||||
>
|
||||
<ComboboxButton
|
||||
v-if="setupFilteredVersionGuesses?.length ?? 0 > 0"
|
||||
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="size-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</ComboboxButton>
|
||||
|
||||
<ComboboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="guess in setupFilteredVersionGuesses"
|
||||
:key="guess.filename"
|
||||
v-slot="{ active, selected }"
|
||||
:value="guess.filename"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-x-2 block truncate',
|
||||
selected && 'font-semibold',
|
||||
]"
|
||||
>
|
||||
{{ guess.filename }}
|
||||
<component
|
||||
:is="PLATFORM_ICONS[guess.platform as PlatformClient]"
|
||||
class="size-5"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<ComboboxOption
|
||||
v-if="setupProcessQuery"
|
||||
v-slot="{ active, selected }"
|
||||
:value="setupProcessQuery"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="['block truncate', selected && 'font-semibold']"
|
||||
>
|
||||
{{ $t("chars.quoted", { text: setupProcessQuery }) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</Combobox>
|
||||
<input
|
||||
id="startup"
|
||||
v-model="versionSettings.setupArgs"
|
||||
type="text"
|
||||
name="startup"
|
||||
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
placeholder="--setup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- setup mode -->
|
||||
<SwitchGroup as="div" class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>{{ $t("library.admin.import.version.setupMode") }}</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400">{{
|
||||
$t("library.admin.import.version.setupModeDesc")
|
||||
}}</SwitchDescription>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="versionSettings.onlySetup"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? 'bg-blue-600' : 'bg-zinc-800',
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? 'translate-x-5' : 'translate-x-0',
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
]"
|
||||
/>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
<div class="relative">
|
||||
<label
|
||||
for="startup"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>{{ $t("library.admin.import.version.launchCmd") }}</label
|
||||
>
|
||||
<p class="text-zinc-400 text-xs">
|
||||
{{ $t("library.admin.import.version.launchDesc") }}
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="flex w-fit rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
|
||||
>
|
||||
<span
|
||||
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
|
||||
>{{ $t("library.admin.import.version.installDir") }}</span
|
||||
>
|
||||
<Combobox
|
||||
as="div"
|
||||
:value="versionSettings.launch"
|
||||
nullable
|
||||
@update:model-value="(v) => updateLaunchCommand(v)"
|
||||
>
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
:placeholder="
|
||||
$t('library.admin.import.version.launchPlaceholder')
|
||||
"
|
||||
@change="launchProcessQuery = $event.target.value"
|
||||
@blur="launchProcessQuery = ''"
|
||||
/>
|
||||
<ComboboxButton
|
||||
v-if="launchFilteredVersionGuesses?.length ?? 0 > 0"
|
||||
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="size-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</ComboboxButton>
|
||||
|
||||
<ComboboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="guess in launchFilteredVersionGuesses"
|
||||
:key="guess.filename"
|
||||
v-slot="{ active, selected }"
|
||||
:value="guess.filename"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-x-2 block truncate',
|
||||
selected && 'font-semibold',
|
||||
]"
|
||||
>
|
||||
{{ guess.filename }}
|
||||
<component
|
||||
:is="PLATFORM_ICONS[guess.platform as PlatformClient]"
|
||||
class="size-5"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<ComboboxOption
|
||||
v-if="launchProcessQuery"
|
||||
v-slot="{ active, selected }"
|
||||
:value="launchProcessQuery"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-zinc-100',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="['block truncate', selected && 'font-semibold']"
|
||||
>
|
||||
{{ $t("chars.quoted", { text: launchProcessQuery }) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</Combobox>
|
||||
<input
|
||||
id="startup"
|
||||
v-model="versionSettings.launchArgs"
|
||||
type="text"
|
||||
name="startup"
|
||||
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
placeholder="--launch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="versionSettings.onlySetup"
|
||||
class="absolute inset-0 bg-zinc-900/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SwitchGroup
|
||||
as="div"
|
||||
class="bg-zinc-800 p-4 rounded-xl flex items-center gap-4 justify-between"
|
||||
>
|
||||
<PlatformSelector v-model="versionSettings.platform">
|
||||
{{ $t("library.admin.import.version.platform") }}
|
||||
</PlatformSelector>
|
||||
<SwitchGroup as="div" class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
@@ -280,7 +385,7 @@
|
||||
<Switch
|
||||
v-model="versionSettings.delta"
|
||||
:class="[
|
||||
versionSettings.delta ? 'bg-blue-600' : 'bg-zinc-900',
|
||||
versionSettings.delta ? 'bg-blue-600' : 'bg-zinc-800',
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
|
||||
]"
|
||||
>
|
||||
@@ -293,11 +398,91 @@
|
||||
/>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
<Disclosure v-slot="{ open }" as="div" class="py-2">
|
||||
<dt>
|
||||
<DisclosureButton
|
||||
class="border-b border-zinc-600 pb-2 flex w-full items-start justify-between text-left text-zinc-100"
|
||||
>
|
||||
<span class="text-base/7 font-semibold">
|
||||
{{ $t("library.admin.import.version.advancedOptions") }}
|
||||
</span>
|
||||
<span class="ml-6 flex h-7 items-center">
|
||||
<ChevronUpIcon v-if="!open" class="size-6" aria-hidden="true" />
|
||||
<ChevronDownIcon v-else class="size-6" aria-hidden="true" />
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel
|
||||
as="dd"
|
||||
class="bg-zinc-950/30 p-3 rounded-b-lg mt-2 flex flex-col gap-y-4"
|
||||
>
|
||||
<!-- UMU launcher configuration -->
|
||||
<div
|
||||
v-if="versionSettings.platform == PlatformClient.Windows"
|
||||
class="flex flex-col gap-y-4"
|
||||
>
|
||||
<SwitchGroup as="div" class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>
|
||||
{{ $t("library.admin.import.version.umuOverride") }}
|
||||
</SwitchLabel>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400">
|
||||
{{ $t("library.admin.import.version.umuOverrideDesc") }}
|
||||
</SwitchDescription>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="umuIdEnabled"
|
||||
:class="[
|
||||
umuIdEnabled ? 'bg-blue-600' : 'bg-zinc-800',
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
umuIdEnabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
]"
|
||||
/>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
<div>
|
||||
<label
|
||||
for="umu-id"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.import.version.umuLauncherId") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="umu-id"
|
||||
v-model="umuId"
|
||||
name="umu-id"
|
||||
type="text"
|
||||
autocomplete="umu-id"
|
||||
required
|
||||
:disabled="!umuIdEnabled"
|
||||
placeholder="umu-starcitizen"
|
||||
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-950 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-zinc-400">
|
||||
{{ $t("library.admin.import.version.noAdv") }}
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
|
||||
<LoadingButton
|
||||
class="w-fit ml-auto"
|
||||
class="w-fit"
|
||||
:loading="importLoading"
|
||||
@click="startImport"
|
||||
@click="startImport_wrapper"
|
||||
>
|
||||
{{ $t("library.admin.import.import") }}
|
||||
</LoadingButton>
|
||||
@@ -354,19 +539,15 @@ import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
TrashIcon,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { FetchError } from "ofetch";
|
||||
import { GameType } from "~/prisma/client/enums";
|
||||
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
|
||||
import type { VersionGuess } from "~/server/internal/library";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
@@ -376,21 +557,80 @@ const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const gameId = route.params.id.toString();
|
||||
const { versions, type } = await $dropFetch(
|
||||
const versions = await $dropFetch(
|
||||
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
|
||||
);
|
||||
const currentlySelectedVersion = ref(-1);
|
||||
const versionSettings = ref<Omit<typeof ImportVersion.infer, "version" | "id">>(
|
||||
{
|
||||
delta: false,
|
||||
onlySetup: type === GameType.Dependency,
|
||||
launches: [],
|
||||
setups: [],
|
||||
requiredContent: [],
|
||||
},
|
||||
const versionSettings = ref<{
|
||||
platform: PlatformClient | undefined;
|
||||
|
||||
onlySetup: boolean;
|
||||
launch: string;
|
||||
launchArgs: string;
|
||||
setup: string;
|
||||
setupArgs: string;
|
||||
|
||||
delta: boolean;
|
||||
umuId: string;
|
||||
}>({
|
||||
platform: undefined,
|
||||
launch: "",
|
||||
launchArgs: "",
|
||||
setup: "",
|
||||
setupArgs: "",
|
||||
delta: false,
|
||||
onlySetup: false,
|
||||
umuId: "",
|
||||
});
|
||||
|
||||
const versionGuesses =
|
||||
ref<Array<{ platform: PlatformClient; filename: string }>>();
|
||||
const launchProcessQuery = ref("");
|
||||
const setupProcessQuery = ref("");
|
||||
|
||||
const launchFilteredVersionGuesses = computed(() =>
|
||||
versionGuesses.value?.filter((e) =>
|
||||
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase()),
|
||||
),
|
||||
);
|
||||
const setupFilteredVersionGuesses = computed(() =>
|
||||
versionGuesses.value?.filter((e) =>
|
||||
e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase()),
|
||||
),
|
||||
);
|
||||
|
||||
const versionGuesses = ref<Array<VersionGuess>>();
|
||||
function updateLaunchCommand(value: string) {
|
||||
versionSettings.value.launch = value;
|
||||
autosetPlatform(value);
|
||||
}
|
||||
|
||||
function updateSetupCommand(value: string) {
|
||||
versionSettings.value.setup = value;
|
||||
autosetPlatform(value);
|
||||
}
|
||||
|
||||
function autosetPlatform(value: string) {
|
||||
if (!versionGuesses.value) return;
|
||||
if (versionSettings.value.platform) return;
|
||||
const guessIndex = versionGuesses.value.findIndex(
|
||||
(e) => e.filename === value,
|
||||
);
|
||||
if (guessIndex == -1) return;
|
||||
versionSettings.value.platform = versionGuesses.value[guessIndex].platform;
|
||||
}
|
||||
|
||||
const umuIdEnabled = ref(false);
|
||||
const umuId = computed({
|
||||
get() {
|
||||
if (umuIdEnabled.value) return versionSettings.value.umuId;
|
||||
return undefined;
|
||||
},
|
||||
set(v) {
|
||||
if (umuIdEnabled.value && v) {
|
||||
versionSettings.value.umuId = v;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const importLoading = ref(false);
|
||||
const importError = ref<string | undefined>();
|
||||
@@ -399,43 +639,38 @@ async function updateCurrentlySelectedVersion(value: number) {
|
||||
if (currentlySelectedVersion.value == value) return;
|
||||
currentlySelectedVersion.value = value;
|
||||
const version = versions[currentlySelectedVersion.value];
|
||||
try {
|
||||
const results = await $dropFetch(`/api/v1/admin/import/version/preload`, {
|
||||
failTitle: "Failed to fetch version information",
|
||||
query: {
|
||||
id: gameId,
|
||||
type: version.type,
|
||||
version: version.identifier,
|
||||
},
|
||||
});
|
||||
versionGuesses.value = results as typeof versionGuesses.value;
|
||||
} catch {
|
||||
currentlySelectedVersion.value = -1;
|
||||
}
|
||||
const results = await $dropFetch(
|
||||
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
|
||||
gameId,
|
||||
)}&version=${encodeURIComponent(version)}`,
|
||||
);
|
||||
versionGuesses.value = results.map((e) => ({
|
||||
...e,
|
||||
platform: e.platform as PlatformClient,
|
||||
}));
|
||||
}
|
||||
|
||||
async function startImport() {
|
||||
importLoading.value = true;
|
||||
|
||||
if (!versionSettings.value) return;
|
||||
try {
|
||||
const taskId = await $dropFetch("/api/v1/admin/import/version", {
|
||||
method: "POST",
|
||||
body: {
|
||||
...versionSettings.value,
|
||||
id: gameId,
|
||||
version: versions[currentlySelectedVersion.value],
|
||||
},
|
||||
const taskId = await $dropFetch("/api/v1/admin/import/version", {
|
||||
method: "POST",
|
||||
body: {
|
||||
id: gameId,
|
||||
version: versions[currentlySelectedVersion.value],
|
||||
...versionSettings.value,
|
||||
},
|
||||
});
|
||||
router.push(`/admin/task/${taskId.taskId}`);
|
||||
}
|
||||
|
||||
function startImport_wrapper() {
|
||||
importLoading.value = true;
|
||||
startImport()
|
||||
.catch((error) => {
|
||||
importError.value = error.statusMessage ?? t("errors.unknown");
|
||||
})
|
||||
.finally(() => {
|
||||
importLoading.value = false;
|
||||
});
|
||||
router.push(`/admin/task/${taskId.taskId}`);
|
||||
} catch (error) {
|
||||
if (error instanceof FetchError) {
|
||||
importError.value = error.data?.message ?? t("errors.unknown");
|
||||
} else {
|
||||
importError.value = (error as string)?.toString();
|
||||
}
|
||||
} finally {
|
||||
importLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user