mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-13 00:02:37 +10:00
Compare commits
17 Commits
5c1b0e6c1e
...
weblate
| Author | SHA1 | Date | |
|---|---|---|---|
| 54801d9448 | |||
| 251ddb8ff8 | |||
| dfa30c8a65 | |||
| 289034d0c8 | |||
| 2a23f4d14c | |||
| b20d355527 | |||
| fa9620eac1 | |||
| a201b62c04 | |||
| 9bf164ab77 | |||
| 97c6f3490c | |||
| f5cb856d3d | |||
| 67de1f6c02 | |||
| 1002265000 | |||
| 37a2dff0dd | |||
| 799cd6c394 | |||
| 2a005a2222 | |||
| 3942d5c442 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -5,8 +5,8 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
# This can be used to automatically publish nightlies at UTC nighttime
|
# This can be used to automatically publish nightlies at UTC nighttime
|
||||||
schedule:
|
#schedule:
|
||||||
- cron: "0 2 * * *" # run at 2 AM UTC
|
# - cron: "0 2 * * *" # run at 2 AM UTC
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
web:
|
web:
|
||||||
|
|||||||
272
CONTRIBUTING.md
272
CONTRIBUTING.md
@ -1,271 +1,3 @@
|
|||||||
# CONTRIBUTING GUIDELINES
|
# Contributing
|
||||||
|
|
||||||
Drop is a community-driven project. Contribution is welcome, encouraged, and appreciated.
|
Check out our contributing guidelines on our developer docs: [https://developer.droposs.org/contributing](https://developer.droposs.org/contributing).
|
||||||
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)
|
|
||||||
- [Development](#development)
|
|
||||||
- [Note: `--optional` flag is **REQUIRED**](#note-optional-flag-is-required)
|
|
||||||
- [Tech Stack](#tech-stack)
|
|
||||||
- [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)
|
|
||||||
- [Translation](#translation)
|
|
||||||
- [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.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
To get started with development, you need `yarn` and `docker compose` installed (or know how to set up a PostgreSQL database).
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
|
|
||||||
1. Run `git submodule update --init --recursive` to setup submodules
|
|
||||||
1. Copy the `.env.example` to `.env` and add any api keys you need to use (e.g. for the Giant Bomb API)
|
|
||||||
- You can find other configuration options in the [documentation](https://docs.droposs.org/)
|
|
||||||
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 prisma migrate dev` to setup the database
|
|
||||||
1. Run `yarn dev` to start the development 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
|
|
||||||
|
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
This repo uses the Nuxt 3 + TailwindCSS stack, with the `yarn` package manager.
|
|
||||||
|
|
||||||
For the database, Drop uses Prisma connected to PostgreSQL.
|
|
||||||
|
|
||||||
## 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](https://translate.droposs.org/engage/drop/). Please make sure to **read** the [message format syntax](https://vue-i18n.intlify.dev/guide/essentials/syntax.html) page before starting. We use this special syntax to enable high quality translations, and 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.
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin "@tailwindcss/typography";
|
@plugin "@tailwindcss/typography";
|
||||||
@plugin "@tailwindcss/forms";
|
@plugin "@tailwindcss/forms";
|
||||||
@config "../tailwind.config.js";
|
@config "../../tailwind.config.js";
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
input[type="number"]::-webkit-outer-spin-button,
|
input[type="number"]::-webkit-outer-spin-button,
|
||||||
@ -86,7 +86,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XCircleIcon } from "@heroicons/vue/20/solid";
|
import { XCircleIcon } from "@heroicons/vue/20/solid";
|
||||||
import type { UserModel } from "~/prisma/client/models";
|
import type { UserModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
const password = ref("");
|
const password = ref("");
|
||||||
@ -4,9 +4,10 @@
|
|||||||
v-for="(_, i) in amount"
|
v-for="(_, i) in amount"
|
||||||
:key="i"
|
:key="i"
|
||||||
:class="[
|
: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',
|
'transition-all cursor-pointer h-2 rounded-full',
|
||||||
]"
|
]"
|
||||||
|
@click="slideTo(i)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -18,8 +19,8 @@ const carousel = inject(injectCarousel)!;
|
|||||||
|
|
||||||
const amount = carousel.maxSlide - carousel.minSlide + 1;
|
const amount = carousel.maxSlide - carousel.minSlide + 1;
|
||||||
|
|
||||||
// function slideTo(index: number) {
|
function slideTo(index: number) {
|
||||||
// const offsetIndex = index + carousel.minSlide;
|
const offsetIndex = index + carousel.minSlide;
|
||||||
// carousel.nav.slideTo(offsetIndex);
|
carousel.nav.slideTo(offsetIndex);
|
||||||
// }
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { GameModel } from "~/prisma/client/models";
|
import type { GameModel } from "~~/prisma/client/models";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -29,6 +29,23 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-8">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-8">
|
||||||
<MultiItemSelector v-model="currentTags" :items="tags" />
|
<MultiItemSelector v-model="currentTags" :items="tags" />
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- image carousel pick -->
|
<!-- image carousel pick -->
|
||||||
@ -444,7 +461,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { GameModel, GameTagModel } from "~/prisma/client/models";
|
import type { GameModel, GameTagModel } from "~~/prisma/client/models";
|
||||||
import { micromark } from "micromark";
|
import { micromark } from "micromark";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
@ -491,11 +508,38 @@ watch(
|
|||||||
{ deep: true },
|
{ 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();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// I don't know why I split these fields off.
|
// I don't know why I split these fields off.
|
||||||
const coreMetadataName = ref(game.value.mName);
|
const coreMetadataName = ref(game.value.mName);
|
||||||
const coreMetadataDescription = ref(game.value.mShortDescription);
|
const coreMetadataDescription = ref(game.value.mShortDescription);
|
||||||
|
|
||||||
const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId));
|
const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId));
|
||||||
const coreMetadataIconFileUpload = ref<FileList | undefined>();
|
const coreMetadataIconFileUpload = ref<FileList | undefined>();
|
||||||
const coreMetadataLoading = ref(false);
|
const coreMetadataLoading = ref(false);
|
||||||
@ -561,7 +605,6 @@ function coreMetadataUpdate_wrapper() {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.then((newGame) => {
|
.then((newGame) => {
|
||||||
console.log(newGame);
|
|
||||||
if (!newGame) return;
|
if (!newGame) return;
|
||||||
Object.assign(game.value, newGame);
|
Object.assign(game.value, newGame);
|
||||||
coreMetadataIconUrl.value = useObject(newGame.mIconObjectId);
|
coreMetadataIconUrl.value = useObject(newGame.mIconObjectId);
|
||||||
285
app/components/GameEditor/Version.vue
Normal file
285
app/components/GameEditor/Version.vue
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<template>
|
||||||
|
<div v-if="game && unimportedVersions" class="p-8">
|
||||||
|
<div>
|
||||||
|
<div class="sm:flex sm:items-center">
|
||||||
|
<div class="sm:flex-auto">
|
||||||
|
<h1 class="text-base font-semibold text-zinc-100">Versions</h1>
|
||||||
|
<p class="mt-2 text-sm text-zinc-400 max-w-lg">
|
||||||
|
Versions are a collection of files that are downloaded to clients.
|
||||||
|
Each version can have multiple configurations, for different
|
||||||
|
platforms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||||
|
<NuxtLink
|
||||||
|
:href="canImport ? `/admin/library/g/${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 rounded-xl border border-zinc-800 bg-zinc-900 shadow-sm">
|
||||||
|
<div>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Version Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||||
|
>
|
||||||
|
Imported
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||||
|
>
|
||||||
|
Platforms
|
||||||
|
</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="version in game.versions"
|
||||||
|
:key="version.versionId"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{{ version.versionName }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||||
|
<RelativeTime :date="version.created" />
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<li
|
||||||
|
v-for="gameVersion in version.gameVersions"
|
||||||
|
:key="gameVersion.versionId"
|
||||||
|
class="px-3 py-2 border border-zinc-800 rounded-lg shadow"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="text-sm flex items-center gap-x-2 text-zinc-200 font-semibold"
|
||||||
|
>
|
||||||
|
<IconsPlatform
|
||||||
|
:platform="
|
||||||
|
platforms[gameVersion.platformId].platformIcon.key
|
||||||
|
"
|
||||||
|
:fallback="
|
||||||
|
platforms[gameVersion.platformId].platformIcon
|
||||||
|
.fallback
|
||||||
|
"
|
||||||
|
class="size-5 text-blue-500"
|
||||||
|
/>
|
||||||
|
<span class="block truncate">{{
|
||||||
|
platforms[gameVersion.platformId].name
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- launch commands -->
|
||||||
|
<div class="space-y-1 mt-4">
|
||||||
|
<div
|
||||||
|
v-if="gameVersion.install"
|
||||||
|
class="flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-display text-xs text-zinc-300 font-semibold uppercase tracking-wide"
|
||||||
|
>Install</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="whitespace-nowrap font-mono text-xs text-zinc-300 bg-zinc-950 px-1 py-0.5 w-fit rounded"
|
||||||
|
>
|
||||||
|
<span class="text-zinc-700">(install dir)/</span
|
||||||
|
>{{ gameVersion.install.command }}
|
||||||
|
{{ gameVersion.install.args }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-sm text-zinc-100"
|
||||||
|
>Launch options</span
|
||||||
|
>
|
||||||
|
<ul class="divide-y divide-zinc-700">
|
||||||
|
<li
|
||||||
|
v-for="launch in gameVersion.launches"
|
||||||
|
:key="launch.command"
|
||||||
|
class="ml-2 py-2 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="font-display text-xs text-zinc-300 font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
{{ launch.name }}
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
class="mt-1 whitespace-nowrap font-mono text-xs text-zinc-300 bg-zinc-950 px-1 py-0.5 w-fit rounded"
|
||||||
|
>
|
||||||
|
<span class="text-zinc-700"
|
||||||
|
>(install dir)/</span
|
||||||
|
>{{ launch.command }} {{ launch.args }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="gameVersion.uninstall"
|
||||||
|
class="flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-display text-xs text-zinc-300 font-semibold uppercase tracking-wide"
|
||||||
|
>Uninstall</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="whitespace-nowrap font-mono text-xs text-zinc-300 bg-zinc-950 px-1 py-0.5 w-fit rounded"
|
||||||
|
>
|
||||||
|
<span class="text-zinc-700">(install dir)/</span
|
||||||
|
>{{ gameVersion.uninstall.command }}
|
||||||
|
{{ gameVersion.uninstall.args }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</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="() => deleteVersion(version.versionId)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
<span class="sr-only">
|
||||||
|
{{ $t("chars.srComma", [version.versionName]) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="game.versions.length === 0">
|
||||||
|
<td colspan="5" class="py-8 text-center text-sm text-zinc-400">
|
||||||
|
No versions
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grow w-full flex items-center justify-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<ExclamationCircleIcon
|
||||||
|
class="h-12 w-12 text-red-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div class="mt-3 text-center sm:mt-5">
|
||||||
|
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||||
|
{{ $t("library.admin.offlineTitle") }}
|
||||||
|
</h1>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm text-zinc-400 max-w-md">
|
||||||
|
{{ $t("library.admin.offline") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SerializeObject, TypedInternalResponse } from "nitropack";
|
||||||
|
import type { H3Error } from "h3";
|
||||||
|
import { ExclamationCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
|
||||||
|
// TODO implement UI for this page
|
||||||
|
|
||||||
|
const props = defineProps<{ unimportedVersions: string[] }>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const hasDeleted = ref(false);
|
||||||
|
|
||||||
|
const canImport = computed(
|
||||||
|
() => hasDeleted.value || props.unimportedVersions.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
type GameFetchType = TypedInternalResponse<
|
||||||
|
"/api/v1/admin/game/:id",
|
||||||
|
unknown,
|
||||||
|
"get"
|
||||||
|
>["game"];
|
||||||
|
const game = defineModel<SerializeObject<GameFetchType>>({ required: true });
|
||||||
|
if (!game.value)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: "Game not provided to editor component",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawPlatforms = await useAdminPlatforms();
|
||||||
|
const platforms = Object.fromEntries(
|
||||||
|
renderPlatforms(rawPlatforms).map((v) => [v.param, v]),
|
||||||
|
);
|
||||||
|
|
||||||
|
async function updateVersionOrder() {
|
||||||
|
try {
|
||||||
|
const newVersions = await $dropFetch("/api/v1/admin/game/version", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: {
|
||||||
|
id: game.value.id,
|
||||||
|
versions: game.value.versions.map((e) => e.versionId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
game.value.versions = newVersions;
|
||||||
|
} catch (e) {
|
||||||
|
createModal(
|
||||||
|
ModalType.Notification,
|
||||||
|
{
|
||||||
|
title: t("errors.version.order.title"),
|
||||||
|
description: t("errors.version.order.desc", {
|
||||||
|
error: (e as H3Error)?.message ?? t("errors.unknown"),
|
||||||
|
}),
|
||||||
|
buttonText: t("common.close"),
|
||||||
|
},
|
||||||
|
(e, c) => c(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteVersion(versionId: string) {
|
||||||
|
await $dropFetch("/api/v1/admin/game/version", {
|
||||||
|
method: "DELETE",
|
||||||
|
body: {
|
||||||
|
id: versionId,
|
||||||
|
},
|
||||||
|
failTitle: "Failed to delete version.",
|
||||||
|
});
|
||||||
|
game.value.versions.splice(
|
||||||
|
game.value.versions.findIndex((e) => e.versionId === versionId),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
hasDeleted.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -77,7 +77,7 @@ const {
|
|||||||
}>
|
}>
|
||||||
| undefined
|
| undefined
|
||||||
| null;
|
| null;
|
||||||
href?: string;
|
href?: string | undefined;
|
||||||
showTitleDescription?: boolean;
|
showTitleDescription?: boolean;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
defaultPlaceholder?: boolean;
|
defaultPlaceholder?: boolean;
|
||||||
@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
import type { GameMetadataSearchResult } from "~~/server/internal/metadata/types";
|
||||||
|
|
||||||
const { game } = defineProps<{
|
const { game } = defineProps<{
|
||||||
game: Omit<GameMetadataSearchResult, "year"> & { sourceName?: string };
|
game: Omit<GameMetadataSearchResult, "year"> & { sourceName?: string };
|
||||||
19
app/components/Icons/GamepadIcon.vue
Normal file
19
app/components/Icons/GamepadIcon.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<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>
|
||||||
@ -9,14 +9,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { HardwarePlatform } from "~/prisma/client/enums";
|
import { HardwarePlatform } from "~~/prisma/client/enums";
|
||||||
import type { Component } from "vue";
|
import type { Component } from "vue";
|
||||||
import LinuxLogo from "./LinuxLogo.vue";
|
import LinuxLogo from "./LinuxLogo.vue";
|
||||||
import WindowsLogo from "./WindowsLogo.vue";
|
import WindowsLogo from "./WindowsLogo.vue";
|
||||||
import MacLogo from "./MacLogo.vue";
|
import MacLogo from "./MacLogo.vue";
|
||||||
import DropLogo from "../DropLogo.vue";
|
import DropLogo from "../DropLogo.vue";
|
||||||
|
|
||||||
const props = defineProps<{ platform: string; fallback?: string }>();
|
const props = defineProps<{ platform: string; fallback?: string | undefined }>();
|
||||||
|
|
||||||
const platformIcons: { [key in HardwarePlatform]: Component } = {
|
const platformIcons: { [key in HardwarePlatform]: Component } = {
|
||||||
[HardwarePlatform.Linux]: LinuxLogo,
|
[HardwarePlatform.Linux]: LinuxLogo,
|
||||||
@ -191,7 +191,7 @@ import {
|
|||||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
import { ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
|
||||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
import type { GameMetadataSearchResult } from "~~/server/internal/metadata/types";
|
||||||
|
|
||||||
const model = ref<GameMetadataSearchResult | undefined>(undefined);
|
const model = ref<GameMetadataSearchResult | undefined>(undefined);
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ import type { Locale } from "vue-i18n";
|
|||||||
|
|
||||||
const { showText = true } = defineProps<{ showText?: boolean }>();
|
const { showText = true } = defineProps<{ showText?: boolean }>();
|
||||||
|
|
||||||
const { locales, locale: currLocale, setLocale } = useI18n();
|
const { locale: currLocale, setLocale, locales } = useI18n();
|
||||||
|
|
||||||
function changeLocale(locale: Locale) {
|
function changeLocale(locale: Locale) {
|
||||||
setLocale(locale);
|
setLocale(locale);
|
||||||
@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TaskLog } from "~/server/internal/tasks";
|
import type { TaskLog } from "~~/server/internal/tasks";
|
||||||
|
|
||||||
defineProps<{ log: typeof TaskLog.infer; short?: boolean }>();
|
defineProps<{ log: typeof TaskLog.infer; short?: boolean }>();
|
||||||
|
|
||||||
@ -162,7 +162,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { GameModel } from "~/prisma/client/models";
|
import type { GameModel } from "~~/prisma/client/models";
|
||||||
import {
|
import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Listbox,
|
Listbox,
|
||||||
@ -171,7 +171,7 @@ import {
|
|||||||
ListboxOption,
|
ListboxOption,
|
||||||
ListboxOptions,
|
ListboxOptions,
|
||||||
} from "@headlessui/vue";
|
} from "@headlessui/vue";
|
||||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
import type { GameMetadataSearchResult } from "~~/server/internal/metadata/types";
|
||||||
import { FetchError } from "ofetch";
|
import { FetchError } from "ofetch";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||||
@ -208,7 +208,7 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const open = defineModel<boolean>({ required: true });
|
const open = defineModel<boolean>({ required: true });
|
||||||
|
|
||||||
const currentGame = ref<(typeof metadataGames.value)[number]>();
|
const currentGame = ref<NonNullable<(typeof metadataGames.value)[number]> | null>(null);
|
||||||
const developed = ref(false);
|
const developed = ref(false);
|
||||||
const published = ref(false);
|
const published = ref(false);
|
||||||
const addGameLoading = ref(false);
|
const addGameLoading = ref(false);
|
||||||
@ -236,7 +236,7 @@ async function addGame() {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
currentGame.value = undefined;
|
currentGame.value = null;
|
||||||
developed.value = false;
|
developed.value = false;
|
||||||
published.value = false;
|
published.value = false;
|
||||||
addGameLoading.value = false;
|
addGameLoading.value = false;
|
||||||
@ -46,7 +46,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { DialogTitle } from "@headlessui/vue";
|
import { DialogTitle } from "@headlessui/vue";
|
||||||
import type { CollectionEntryModel, GameModel } from "~/prisma/client/models";
|
import type { CollectionEntryModel, GameModel } from "~~/prisma/client/models";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -110,7 +110,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CompanyModel } from "~/prisma/client/models";
|
import type { CompanyModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
const open = defineModel<boolean>({ required: true });
|
const open = defineModel<boolean>({ required: true });
|
||||||
|
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { DialogTitle } from "@headlessui/vue";
|
import { DialogTitle } from "@headlessui/vue";
|
||||||
import type { GameTagModel } from "~/prisma/client/models";
|
import type { GameTagModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
created: [tag: GameTagModel];
|
created: [tag: GameTagModel];
|
||||||
@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CollectionModel } from "~/prisma/client/models";
|
import type { CollectionModel } from "~~/prisma/client/models";
|
||||||
import { DialogTitle } from "@headlessui/vue";
|
import { DialogTitle } from "@headlessui/vue";
|
||||||
|
|
||||||
const collection = defineModel<CollectionModel | undefined>();
|
const collection = defineModel<CollectionModel | undefined>();
|
||||||
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DialogTitle } from "@headlessui/vue";
|
import { DialogTitle } from "@headlessui/vue";
|
||||||
import type { UserModel } from "~/prisma/client/models";
|
import type { UserModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
const user = defineModel<UserModel | undefined>();
|
const user = defineModel<UserModel | undefined>();
|
||||||
const deleteLoading = ref(false);
|
const deleteLoading = ref(false);
|
||||||
@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||||
import type { NotificationModel } from "~/prisma/client/models";
|
import type { NotificationModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
const props = defineProps<{ notification: NotificationModel }>();
|
const props = defineProps<{ notification: NotificationModel }>();
|
||||||
|
|
||||||
45
app/components/PieChart/PieChart.vue
Normal file
45
app/components/PieChart/PieChart.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<h2 v-if="title" class="text-lg mb-4 w-full">{{ title }}</h2>
|
||||||
|
<div class="flex flex-col xl:flex-row gap-4">
|
||||||
|
<div class="relative flex grow max-w-[12rem]">
|
||||||
|
<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 justify-center 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: slice.value,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</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>
|
||||||
35
app/components/PieChart/PieSlice.vue
Normal file
35
app/components/PieChart/PieSlice.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<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>
|
||||||
19
app/components/PieChart/types.d.ts
vendored
Normal file
19
app/components/PieChart/types.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
50
app/components/PieChart/utils.ts
Normal file
50
app/components/PieChart/utils.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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);
|
||||||
@ -106,7 +106,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value?: string;
|
value?: string | undefined;
|
||||||
guesses?: Array<{ platform: PlatformRenderable; filename: string }>;
|
guesses?: Array<{ platform: PlatformRenderable; filename: string }>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
31
app/components/ProgressBar.vue
Normal file
31
app/components/ProgressBar.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<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]) }}
|
||||||
|
</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>
|
||||||
43
app/components/RankingList.vue
Normal file
43
app/components/RankingList.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<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>
|
||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
import type { RedistModel, UserPlatformModel } from "~/prisma/client/models";
|
import type { RedistModel, UserPlatformModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
type ModelType = SerializeObject<
|
type ModelType = SerializeObject<
|
||||||
RedistModel & { platform?: UserPlatformModel }
|
RedistModel & { platform?: UserPlatformModel }
|
||||||
193
app/components/SourceTable.vue
Normal file
193
app/components/SourceTable.vue
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<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"
|
||||||
|
class="even:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<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 inline-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.freeSpace,
|
||||||
|
source.fsStats.totalSpace,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:color="
|
||||||
|
getBarColor(
|
||||||
|
getPercentage(
|
||||||
|
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("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";
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPercentage = (value: number, total: number) =>
|
||||||
|
((total - value) * 100) / total;
|
||||||
|
</script>
|
||||||
@ -1,3 +1,12 @@
|
|||||||
|
<i18n>
|
||||||
|
{
|
||||||
|
"en": {
|
||||||
|
"↓": "↓",
|
||||||
|
"↑": "↑"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</i18n>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
@ -176,9 +185,12 @@
|
|||||||
active ? 'bg-zinc-900 outline-hidden' : '',
|
active ? 'bg-zinc-900 outline-hidden' : '',
|
||||||
'w-full text-left block px-4 py-2 text-sm',
|
'w-full text-left block px-4 py-2 text-sm',
|
||||||
]"
|
]"
|
||||||
@click="() => (currentSort = option.param)"
|
@click.prevent="handleSortClick(option, $event)"
|
||||||
>
|
>
|
||||||
{{ option.name }}
|
{{ option.name }}
|
||||||
|
<span v-if="currentSort === option.param">
|
||||||
|
{{ sortOrder === "asc" ? $t("↑") : $t("↓") }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
@ -298,7 +310,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="games?.length ?? 0 > 0"
|
v-if="games?.length ?? 0 > 0"
|
||||||
ref="product-grid"
|
ref="product-grid"
|
||||||
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-6 2xl:grid-cols-7 gap-4"
|
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 -->
|
<!-- Your content -->
|
||||||
<GamePanel
|
<GamePanel
|
||||||
@ -365,7 +377,7 @@ import {
|
|||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
} from "@heroicons/vue/20/solid";
|
} from "@heroicons/vue/20/solid";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
import type { GameModel, GameTagModel } from "~/prisma/client/models";
|
import type { GameModel, GameTagModel } from "~~/prisma/client/models";
|
||||||
import MultiItemSelector from "./MultiItemSelector.vue";
|
import MultiItemSelector from "./MultiItemSelector.vue";
|
||||||
const { showGamePanelTextDecoration } = await $dropFetch(`/api/v1/settings`);
|
const { showGamePanelTextDecoration } = await $dropFetch(`/api/v1/settings`);
|
||||||
|
|
||||||
@ -397,8 +409,13 @@ const sorts: Array<StoreSortOption> = [
|
|||||||
name: "Recently Added",
|
name: "Recently Added",
|
||||||
param: "recent",
|
param: "recent",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Name",
|
||||||
|
param: "name",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const currentSort = ref(sorts[0].param);
|
const currentSort = ref(sorts[0].param);
|
||||||
|
const sortOrder = ref<"asc" | "desc">("desc");
|
||||||
|
|
||||||
const options: Array<StoreFilterOption> = [
|
const options: Array<StoreFilterOption> = [
|
||||||
...(tags.length > 0
|
...(tags.length > 0
|
||||||
@ -474,7 +491,7 @@ async function updateGames(query: string, resetGames: boolean) {
|
|||||||
results: Array<SerializeObject<GameModel>>;
|
results: Array<SerializeObject<GameModel>>;
|
||||||
count: number;
|
count: number;
|
||||||
}>(
|
}>(
|
||||||
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}${query ? "&" + query : ""}`,
|
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}&order=${sortOrder.value}${query ? "&" + query : ""}`,
|
||||||
);
|
);
|
||||||
if (resetGames) {
|
if (resetGames) {
|
||||||
games.value = newValues.results;
|
games.value = newValues.results;
|
||||||
@ -491,6 +508,19 @@ watch(filterQuery, (newUrl) => {
|
|||||||
watch(currentSort, (_) => {
|
watch(currentSort, (_) => {
|
||||||
updateGames(filterQuery.value, true);
|
updateGames(filterQuery.value, true);
|
||||||
});
|
});
|
||||||
|
watch(sortOrder, (_) => {
|
||||||
|
updateGames(filterQuery.value, true);
|
||||||
|
});
|
||||||
|
|
||||||
await 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>
|
</script>
|
||||||
@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckCircleIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
import { CheckCircleIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
||||||
import type { TaskMessage } from "~/server/internal/tasks";
|
import type { TaskMessage } from "~~/server/internal/tasks";
|
||||||
|
|
||||||
defineProps<{ task: TaskMessage | undefined; active?: boolean }>();
|
defineProps<{ task: TaskMessage | undefined; active?: boolean }>();
|
||||||
</script>
|
</script>
|
||||||
52
app/components/TileWithLink.vue
Normal file
52
app/components/TileWithLink.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<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>
|
||||||
@ -46,7 +46,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NotificationModel } from "~/prisma/client/models";
|
import type { NotificationModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
const props = defineProps<{ notifications: Array<NotificationModel> }>();
|
const props = defineProps<{ notifications: Array<NotificationModel> }>();
|
||||||
</script>
|
</script>
|
||||||
@ -81,8 +81,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||||
import { ChevronDownIcon } from "@heroicons/vue/16/solid";
|
import { ChevronDownIcon } from "@heroicons/vue/16/solid";
|
||||||
import { useObject } from "~/composables/objects";
|
|
||||||
import type { NavigationItem } from "~/composables/types";
|
|
||||||
|
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ import type {
|
|||||||
CollectionModel,
|
CollectionModel,
|
||||||
CollectionEntryModel,
|
CollectionEntryModel,
|
||||||
GameModel,
|
GameModel,
|
||||||
} from "~/prisma/client/models";
|
} from "~~/prisma/client/models";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
|
|
||||||
type FullCollection = CollectionModel & {
|
type FullCollection = CollectionModel & {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ArticleModel } from "~/prisma/client/models";
|
import type { ArticleModel } from "~~/prisma/client/models";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
|
|
||||||
export const useNews = () =>
|
export const useNews = () =>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { NotificationModel } from "~/prisma/client/models";
|
import type { NotificationModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
const ws = new WebSocketHandler("/api/v1/notifications/ws");
|
const ws = new WebSocketHandler("/api/v1/notifications/ws");
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { UserPlatform } from "~/prisma/client/client";
|
import type { UserPlatform } from "~~/prisma/client/client";
|
||||||
import { HardwarePlatform } from "~/prisma/client/enums";
|
import { HardwarePlatform } from "~~/prisma/client/enums";
|
||||||
|
|
||||||
export type PlatformRenderable = {
|
export type PlatformRenderable = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { TaskMessage } from "~/server/internal/tasks";
|
import type { TaskMessage } from "~~/server/internal/tasks";
|
||||||
import { WebSocketHandler } from "./ws";
|
import { WebSocketHandler } from "./ws";
|
||||||
|
|
||||||
const websocketHandler = new WebSocketHandler("/api/v1/task");
|
const websocketHandler = new WebSocketHandler("/api/v1/task");
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { UserModel } from "~/prisma/client/models";
|
import type { UserModel } from "~~/prisma/client/models";
|
||||||
|
|
||||||
// undefined = haven't check
|
// undefined = haven't check
|
||||||
// null = check, no user
|
// null = check, no user
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
import type { UserModel } from "~/prisma/client/models";
|
import type { UserModel } from "~~/prisma/client/models";
|
||||||
import type { AuthMec } from "~/prisma/client/enums";
|
import type { AuthMec } from "~~/prisma/client/enums";
|
||||||
|
|
||||||
export const useUsers = () =>
|
export const useUsers = () =>
|
||||||
useState<
|
useState<
|
||||||
@ -166,17 +166,20 @@ import {
|
|||||||
RectangleStackIcon,
|
RectangleStackIcon,
|
||||||
DocumentIcon,
|
DocumentIcon,
|
||||||
} from "@heroicons/vue/24/outline";
|
} from "@heroicons/vue/24/outline";
|
||||||
import type { NavigationItem } from "~/composables/types";
|
|
||||||
import { useCurrentNavigationIndex } from "~/composables/current-page-engine";
|
|
||||||
import { ArrowLeftIcon } from "@heroicons/vue/16/solid";
|
import { ArrowLeftIcon } from "@heroicons/vue/16/solid";
|
||||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||||
|
|
||||||
const i18nHead = useLocaleHead();
|
const i18nHead = useLocaleHead();
|
||||||
|
|
||||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||||
{ label: $t("home"), route: "/admin", prefix: "/admin", icon: HomeIcon },
|
|
||||||
{
|
{
|
||||||
label: $t("userHeader.links.library"),
|
label: $t("header.admin.home"),
|
||||||
|
route: "/admin",
|
||||||
|
prefix: "/admin",
|
||||||
|
icon: HomeIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t("header.admin.library"),
|
||||||
route: "/admin/library",
|
route: "/admin/library",
|
||||||
prefix: "/admin/library",
|
prefix: "/admin/library",
|
||||||
icon: ServerStackIcon,
|
icon: ServerStackIcon,
|
||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
||||||
<UserHeader class="z-50" hydrate-on-idle />
|
<LazyUserHeader class="z-50" hydrate-on-idle />
|
||||||
<div class="grow flex">
|
<div class="grow flex">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
<UserFooter class="z-50" hydrate-on-interaction />
|
<LazyUserFooter class="z-50" hydrate-on-interaction />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex w-full min-h-screen bg-zinc-900">
|
<div v-else class="flex w-full min-h-screen bg-zinc-900">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { CheckIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import type { NotificationModel } from "~/prisma/client/models";
|
import type { NotificationModel } from "~~/prisma/client/models";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
177
app/pages/admin/index.vue
Normal file
177
app/pages/admin/index.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<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">
|
||||||
|
<TileWithLink>
|
||||||
|
<div class="h-full flex">
|
||||||
|
<div class="flex-1 my-auto">
|
||||||
|
<DropLogo />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
|
||||||
|
>
|
||||||
|
<div class="text-2xl flex-1 font-bold">{{ version }}</div>
|
||||||
|
<div class="text-xs flex-1 text-left lg:text-center">
|
||||||
|
{{ t("home.admin.version") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TileWithLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 lg:col-span-1 md:col-span-3">
|
||||||
|
<TileWithLink>
|
||||||
|
<div class="h-full flex">
|
||||||
|
<div class="flex-1 my-auto">
|
||||||
|
<GamepadIcon />
|
||||||
|
</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">{{ gameCount }}</div>
|
||||||
|
<div class="text-xs flex-1 text-left lg:text-center">
|
||||||
|
{{ t("home.admin.games") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TileWithLink>
|
||||||
|
</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">
|
||||||
|
{{ 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">
|
||||||
|
{{ 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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { formatBytes } from "~~/server/internal/utils/files";
|
||||||
|
import GamepadIcon from "~/components/Icons/GamepadIcon.vue";
|
||||||
|
import DropLogo from "~/components/DropLogo.vue";
|
||||||
|
import { ServerStackIcon, UserGroupIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import type { RankItem } from "~/components/RankingList.vue";
|
||||||
|
import type { GameSize } from "~~/server/internal/gamesize";
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: "admin",
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: "Home",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const {
|
||||||
|
version,
|
||||||
|
gameCount,
|
||||||
|
sources,
|
||||||
|
userStats,
|
||||||
|
biggestGamesLatest,
|
||||||
|
biggestGamesCombined,
|
||||||
|
} = await $dropFetch("/api/v1/admin/home");
|
||||||
|
|
||||||
|
const gameToRankItem = (game: GameSize, rank: number): RankItem => ({
|
||||||
|
rank: rank + 1,
|
||||||
|
name: game.gameName,
|
||||||
|
value: formatBytes(game.size),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pieChartData = [
|
||||||
|
{
|
||||||
|
label: t("home.admin.inactiveUsers"),
|
||||||
|
value: userStats.userCount - userStats.activeSessions,
|
||||||
|
},
|
||||||
|
{ label: t("home.admin.activeUsers"), value: userStats.activeSessions },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@ -134,34 +134,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- setup mode -->
|
<!-- setup mode -->
|
||||||
<SwitchGroup as="div" class="max-w-lg flex items-center justify-between">
|
<fieldset class="max-w-lg">
|
||||||
<span class="flex flex-grow flex-col">
|
<legend class="text-sm/6 font-semibold text-white">
|
||||||
<SwitchLabel
|
Select an import mode
|
||||||
as="span"
|
</legend>
|
||||||
class="text-sm font-medium leading-6 text-zinc-100"
|
<div class="mt-2 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
|
||||||
passive
|
<label
|
||||||
>{{ $t("library.admin.import.version.setupMode") }}</SwitchLabel
|
v-for="mode in setupModes"
|
||||||
|
:key="mode.id"
|
||||||
|
:aria-label="mode.title"
|
||||||
|
:aria-description="mode.description"
|
||||||
|
class="cursor-pointer group relative flex rounded-lg border border-white/10 bg-zinc-800/50 p-4 has-checked:bg-blue-500/10 has-checked:outline-2 has-checked:-outline-offset-2 has-checked:outline-blue-500 has-focus-visible:outline-3 has-focus-visible:-outline-offset-1 has-disabled:bg-gray-800 has-disabled:opacity-25"
|
||||||
>
|
>
|
||||||
<SwitchDescription as="span" class="text-sm text-zinc-400">{{
|
<input
|
||||||
$t("library.admin.import.version.setupModeDesc")
|
type="radio"
|
||||||
}}</SwitchDescription>
|
name="mode"
|
||||||
</span>
|
:value="mode.id"
|
||||||
<Switch
|
:checked="versionSettings.onlySetup === mode.value"
|
||||||
v-model="versionSettings.onlySetup"
|
class="absolute inset-0 appearance-none opacity-0 focus:outline-none"
|
||||||
:class="[
|
@click="versionSettings.onlySetup = mode.value"
|
||||||
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',
|
<div class="flex-1">
|
||||||
]"
|
<span class="block text-sm font-medium text-white">{{
|
||||||
>
|
mode.title
|
||||||
<span
|
}}</span>
|
||||||
aria-hidden="true"
|
<span class="mt-1 block text-xs text-zinc-400">{{
|
||||||
:class="[
|
mode.description
|
||||||
versionSettings.onlySetup ? 'translate-x-5' : 'translate-x-0',
|
}}</span>
|
||||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
</div>
|
||||||
]"
|
<CheckCircleIcon
|
||||||
/>
|
class="invisible size-5 text-blue-500 group-has-checked:visible"
|
||||||
</Switch>
|
aria-hidden="true"
|
||||||
</SwitchGroup>
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
<!-- launch commands -->
|
<!-- launch commands -->
|
||||||
<div class="relative max-w-3xl">
|
<div class="relative max-w-3xl">
|
||||||
<label
|
<label
|
||||||
@ -252,7 +259,8 @@
|
|||||||
>Uninstall command</label
|
>Uninstall command</label
|
||||||
>
|
>
|
||||||
<p class="text-zinc-400 text-xs">
|
<p class="text-zinc-400 text-xs">
|
||||||
Executable to be run on uninstalling a game. Useful for installer-only games.
|
Executable to be run on uninstalling a game. Useful for installer-only
|
||||||
|
games.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div
|
<div
|
||||||
@ -301,7 +309,8 @@
|
|||||||
</SwitchDescription>
|
</SwitchDescription>
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
v-model="versionSettings.delta"
|
:model-value="versionSettings.delta || false"
|
||||||
|
@update:model-value="(v) => (versionSettings.delta = v)"
|
||||||
:class="[
|
:class="[
|
||||||
versionSettings.delta ? 'bg-blue-600' : 'bg-zinc-800',
|
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',
|
'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',
|
||||||
@ -460,10 +469,14 @@ import {
|
|||||||
} from "@headlessui/vue";
|
} from "@headlessui/vue";
|
||||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { PlusIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
PlusIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@heroicons/vue/24/outline";
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/solid";
|
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/solid";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
|
import type { ImportGameVersion } from "~~/server/api/v1/admin/import/version/index.post";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "admin",
|
layout: "admin",
|
||||||
@ -474,14 +487,15 @@ const { t } = useI18n();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const gameId = route.params.id.toString();
|
const gameId = route.params.id.toString();
|
||||||
const versions = await $dropFetch(
|
const versions = await $dropFetch(
|
||||||
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
|
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}&mode=game`,
|
||||||
);
|
);
|
||||||
const userPlatforms = await useAdminPlatforms();
|
const userPlatforms = await useAdminPlatforms();
|
||||||
const allPlatforms = renderPlatforms(userPlatforms);
|
const allPlatforms = renderPlatforms(userPlatforms);
|
||||||
const currentlySelectedVersion = ref(-1);
|
const currentlySelectedVersion = ref(-1);
|
||||||
const versionSettings = ref<Partial<typeof ImportVersion.infer>>({
|
|
||||||
id: gameId,
|
const versionSettings = ref<Partial<ImportGameVersion>>({
|
||||||
launches: [],
|
launches: [],
|
||||||
|
onlySetup: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const versionGuesses =
|
const versionGuesses =
|
||||||
@ -489,7 +503,6 @@ const versionGuesses =
|
|||||||
Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>>
|
Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
|
||||||
function updateLaunchCommand(idx: number, value: string) {
|
function updateLaunchCommand(idx: number, value: string) {
|
||||||
versionSettings.value.launches![idx].launchCommand = value;
|
versionSettings.value.launches![idx].launchCommand = value;
|
||||||
autosetPlatform(value);
|
autosetPlatform(value);
|
||||||
@ -539,7 +552,7 @@ async function updateCurrentlySelectedVersion(value: number) {
|
|||||||
const options = await $dropFetch(
|
const options = await $dropFetch(
|
||||||
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
|
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
|
||||||
gameId,
|
gameId,
|
||||||
)}&version=${encodeURIComponent(version)}`,
|
)}&version=${encodeURIComponent(version)}&mode=game`,
|
||||||
);
|
);
|
||||||
versionGuesses.value = options.map((e) => ({
|
versionGuesses.value = options.map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
@ -555,6 +568,7 @@ async function startImport() {
|
|||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
version: versions[currentlySelectedVersion.value],
|
version: versions[currentlySelectedVersion.value],
|
||||||
|
mode: "game",
|
||||||
...versionSettings.value,
|
...versionSettings.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -571,4 +585,26 @@ function startImport_wrapper() {
|
|||||||
importLoading.value = false;
|
importLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setupModes: Array<{
|
||||||
|
id: string;
|
||||||
|
value: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
id: "portable",
|
||||||
|
value: false,
|
||||||
|
title: "Portable",
|
||||||
|
description:
|
||||||
|
"This mode is for games that are designed to be launched directly from the install directory. Drop works best with these.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "setup",
|
||||||
|
value: true,
|
||||||
|
title: "Installer",
|
||||||
|
description:
|
||||||
|
"Also known as 'setup-only', this mode is for installers that modify the system directly, and install to directories like Program Files.",
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
@ -219,7 +219,7 @@ import {
|
|||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { PuzzlePieceIcon, ArchiveBoxIcon } from "@heroicons/vue/24/solid";
|
import { PuzzlePieceIcon, ArchiveBoxIcon } from "@heroicons/vue/24/solid";
|
||||||
import type { FetchError } from "ofetch";
|
import type { FetchError } from "ofetch";
|
||||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
import type { GameMetadataSearchResult } from "~~/server/internal/metadata/types";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "admin",
|
layout: "admin",
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user