Compare commits

..

No commits in common. "develop" and "main" have entirely different histories.

1441 changed files with 16041 additions and 134790 deletions

View file

@ -1,6 +1,43 @@
/sites
/cli
/desktop
/backend # go backend
Dockerfile
.github
.vscode
*.md
node_modules
#### gitignore below
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
.yarn
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
.data
# deploy template
deploy-template/*
!deploy-template/compose.yml
# generated prisma client
/prisma/client
/prisma/validate

View file

@ -2,10 +2,4 @@ DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop"
GIANT_BOMB_API_KEY=""
# Optional, IGDB client information
IGDB_CLIENT_ID=""
IGDB_CLIENT_SECRET=""
EXTERNAL_URL="http://localhost:3000"
NUXT_PORT=4000

55
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,55 @@
name: CI
on:
push:
branches:
- develop
pull_request:
branches:
- develop
permissions:
contents: read
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "yarn"
- name: Install dependencies
run: yarn install --immutable --network-timeout 1000000
- name: Typecheck
run: yarn typecheck
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "yarn"
- name: Install dependencies
run: yarn install --immutable --network-timeout 1000000
- name: Lint
run: yarn lint

View file

@ -1,140 +0,0 @@
name: "Build and release desktop"
on:
workflow_dispatch:
inputs:
tagName:
required: false
type: string
description: "tagName to be associated with this release."
release:
types: [published]
# This can be used to automatically publish nightlies at UTC nighttime
# schedule:
# - cron: "0 2 * * *" # run at 2 AM UTC
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: "macos-14" # for Arm based macs (M1 and above).
args: "--target aarch64-apple-darwin"
- platform: "macos-14" # for Intel based macs.
args: "--target x86_64-apple-darwin"
- platform: "ubuntu-22.04" # for Tauri v1 you could replace this with ubuntu-20.04.
args: ""
- platform: "ubuntu-22.04-arm"
args: "--target aarch64-unknown-linux-gnu"
- platform: "windows-latest"
args: ""
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './desktop/src-tauri -> target'
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-14'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
# Add build.keychain to the user keychain search list so that codesign
# (invoked later by tauri-action WITHOUT an explicit --keychain) can
# resolve the signing identity from it.
security list-keychains -d user -s build.keychain $(security list-keychains -d user | tr -d '"')
echo "Created keychain"
curl https://droposs.org/drop.der --output drop.der
# swiftc libs/appletrust/add-certificate.swift
# ./add-certificate drop.der
# rm add-certificate
# echo "Added certificate to keychain using swift util"
## Script is equivalent to:
sudo security authorizationdb write com.apple.trust-settings.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.der
sudo security authorizationdb remove com.apple.trust-settings.admin
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
echo "Imported certificate"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
- name: Verify Certificate
if: matrix.platform == 'macos-14'
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported. Using identity: $CERT_ID"
- name: install frontend dependencies
run: pnpm install # change this to npm, pnpm or bun depending on which one you use.
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Do NOT set APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD here. Doing so
# makes tauri-action import the cert into its own throwaway keychain and
# look up the identity by Apple-only name prefixes (e.g.
# "Developer ID Application:"), which never matches our "Drop OSS" cert
# and fails with "failed to resolve signing identity". Instead we rely on
# the build.keychain prepared above and only pass the resolved identity.
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
NO_STRIP: true
with:
tagName: ${{ inputs.print_tags || 'v__VERSION__' }} # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: "Auto-release v__VERSION__"
releaseBody: "See the assets to download this version and install. This release was created automatically."
releaseDraft: false
prerelease: true
args: ${{ matrix.args }}
projectPath: './desktop'

View file

@ -1,56 +0,0 @@
name: Droplet CI
on:
push:
branches: [develop]
paths:
- "libraries/droplet/**"
- "libraries/droplet_types/**"
- "libraries/libarchive/**"
- ".github/workflows/droplet-ci.yml"
pull_request:
branches: [develop]
paths:
- "libraries/droplet/**"
- "libraries/droplet_types/**"
- "libraries/libarchive/**"
- ".github/workflows/droplet-ci.yml"
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
ci:
name: Build, Test, Lint
runs-on: ubuntu-latest
defaults:
run:
working-directory: libraries/droplet
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt, clippy
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./libraries/droplet -> target"
- name: Install libarchive
run: |
sudo apt-get update
sudo apt-get install -y libarchive-dev
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run Clippy (lint)
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run tests
run: cargo test --all-features --all --verbose

View file

@ -1,100 +0,0 @@
name: Deploy website to GitHub Pages
on:
# Runs on pushes targeting the default branch
push:
branches: [develop]
paths:
- "sites/promo/**"
- "sites/docs/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- ".github/workflows/pages.yml"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment per the "pages" group, skipping runs queued
# between the in-progress run and the latest queued one. cancel-in-progress defaults
# to false, so in-flight production deployments are allowed to complete.
concurrency: "pages"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
# Only install the promo site (radiant) and docs site (docs-next) and their
# dependencies so the public website deploy stays decoupled from the
# server/desktop build pipelines.
- name: Install dependencies
run: pnpm install --filter radiant... --filter docs-next...
- name: Setup Pages
id: setup_pages
uses: actions/configure-pages@v5
- name: Restore cache
uses: actions/cache@v4
with:
path: |
sites/promo/.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('sites/promo/**.[jt]s', 'sites/promo/**.[jt]sx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-
- name: Build promo site with Next.js
working-directory: sites/promo
run: pnpm run build
env:
PAGES_BASE_PATH: ${{ steps.setup_pages.outputs.base_path }}
- name: Build docs site with Astro
working-directory: sites/docs
run: pnpm run build
# Nest the Starlight docs (built with base: "/docs") inside the promo export
# so both ship from a single GitHub Pages deployment at /docs.
- name: Assemble docs into /docs
run: |
rm -rf sites/promo/out/docs
mkdir -p sites/promo/out/docs
cp -r sites/docs/dist/. sites/promo/out/docs/
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: sites/promo/out
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -1,4 +1,4 @@
name: Build and release server
name: Release Workflow
on:
workflow_dispatch: {}
@ -8,20 +8,10 @@ on:
schedule:
- cron: "0 2 * * *" # run at 2 AM UTC
env:
REGISTRY_IMAGE: ghcr.io/drop-oss/drop
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
@ -29,35 +19,12 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 3 # fix for when this gets triggered by tag
fetch-tags: true
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: |
@ -76,58 +43,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
@ -146,12 +77,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 }}

View file

@ -1,71 +0,0 @@
name: Server CI
on:
push:
branches: [develop]
paths:
- "server/**"
- "libraries/base/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- ".github/workflows/server-ci.yml"
pull_request:
branches: [develop]
paths:
- "server/**"
- "libraries/base/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- ".github/workflows/server-ci.yml"
permissions:
contents: read
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Typecheck
working-directory: server
run: pnpm run typecheck
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Lint
working-directory: server
run: pnpm run lint

39
.gitignore vendored
View file

@ -1,2 +1,37 @@
dist/
node_modules/
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
.yarn
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
.data
# deploy template
deploy-template/*
!deploy-template/compose.yml
# generated prisma client
/prisma/client
/prisma/validate

54
.gitlab-ci.yml Normal file
View file

@ -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

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "drop-base"]
path = drop-base
url = https://github.com/Drop-OSS/drop-base.git

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
drop-base/

View file

@ -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"]
}
}

271
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,271 @@
# CONTRIBUTING GUIDELINES
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)
- [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.

View file

@ -1,160 +1,56 @@
# syntax=docker/dockerfile:1
# Pinned to bookworm so the glibc here matches the torrential build stage
# and the libarchive runtime package is named `libarchive13` (trixie renames it to libarchive13t64).
FROM node:lts-bookworm-slim 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 . .
## 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
# Bookworm-pinned to match the runtime image's glibc (a trixie build would not run on bookworm).
FROM rustlang/rust:nightly-bookworm-slim AS torrential-build
## libarchive-dev + pkg-config let libarchive3-sys link libarchive dynamically (glibc).
## protobuf-compiler is kept for parity (torrential's build.rs uses a vendored protoc).
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config \
libarchive-dev \
protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY . .
## Network resilience on hosts with poor/bursty throughput to crates.io:
## observed on .88 (~11KB/s, transfers occasionally stalling near-zero for
## tens of seconds) -- cargo's libcurl transport aborts a download once it
## sees less than CARGO_HTTP_LOW_SPEED_LIMIT bytes/sec for
## CARGO_HTTP_LOW_SPEED_TIMEOUT seconds (curl default: 10 bytes/s over 30s),
## which a merely-slow-but-alive connection can trip. A from-scratch build
## on such a host burned ~50 minutes downloading crates before one transfer
## exhausted CARGO_NET_RETRY's default 3 attempts and hard-failed the whole
## build (`failed to download from .../libc/0.2.178/download`, exit 101).
## Lower the low-speed floor so a trickling-but-live transfer isn't treated
## as dead, raise the retry budget, and give sparse-index/registry
## operations a longer ceiling before giving up outright.
ENV CARGO_HTTP_LOW_SPEED_LIMIT=1
ENV CARGO_HTTP_LOW_SPEED_TIMEOUT=120
ENV CARGO_NET_RETRY=10
ENV CARGO_HTTP_TIMEOUT=600
ENV CARGO_HTTP_MULTIPLEXING=false
## Resource-pressure cap: bound cargo's parallel job count on a
## memory-contended host. Empty (default) leaves CARGO_BUILD_JOBS unset, so
## cargo falls back to its own CPU-count autodetect -- pass
## --build-arg CARGO_JOBS=2 to constrain it. Rust codegen units are
## memory-hungry per job running in parallel; this is the Rust-side twin of
## SKIP_TYPECHECK below.
ARG CARGO_JOBS=""
## Persistent cache mounts for the registry index/downloads and the
## target/ build dir: any change ANYWHERE in the repo invalidates the
## `COPY . .` above (and therefore this RUN layer) on every build, so
## without these every single build re-downloads and recompiles every
## crate from zero -- the ~50-minute cost that motivated the network
## hardening above, paid again on every future commit. Cache mounts
## live in the buildx cache store, not the image layer, so they survive
## across builds regardless of layer invalidation. sharing=locked
## because this builder can run more than one image build at a time.
## Cache mounts are NOT part of the final layer filesystem, so the
## compiled binary is copied out to a stable (non-cached) path before
## the mount unwinds -- see the run-system stage's COPY --from below,
## which reads /build/torrential-bin, not target/release/torrential
## directly.
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/build/torrential/target,sharing=locked \
if [ -n "$CARGO_JOBS" ]; then export CARGO_BUILD_JOBS="$CARGO_JOBS"; fi; \
cargo build --release --manifest-path ./torrential/Cargo.toml && \
cp ./torrential/target/release/torrential /build/torrential-bin
### 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 apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
# 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 deps and rest of project files
# COPY --from=deps /app/node_modules ./node_modules
COPY . .
COPY --from=deps /app/node_modules ./node_modules
ARG BUILD_DROP_VERSION
ARG BUILD_GIT_REF
## Resource-pressure escape hatch (see nuxt.config.ts's typescript.typeCheck
## comment): --build-arg SKIP_TYPECHECK=true skips vue-tsc's full-project
## typecheck, the single most memory-hungry step of `nuxt build`, for
## building on a host that's already near its memory limit. Off by default.
ARG SKIP_TYPECHECK=false
ENV NUXT_BUILD_SKIP_TYPECHECK=$SKIP_TYPECHECK
# build
RUN pnpm import
RUN pnpm install --shamefully-hoist
RUN pnpm run build
# RUN --mount=type=cache,target=/root/.yarn yarn postinstall && yarn build
## Resource-pressure cap: bound Node's old-space heap for the
## postinstall/build step (Vite/Rolldown/vue-tsc are all Node processes).
## Empty (default) leaves NODE_OPTIONS unset, i.e. Node's own default heap
## sizing -- pass --build-arg NODE_MAX_OLD_SPACE=4096 to constrain it.
ARG NODE_MAX_OLD_SPACE=""
ENV NODE_OPTIONS=""
RUN if [ -n "$NODE_MAX_OLD_SPACE" ]; then export NODE_OPTIONS="--max-old-space-size=$NODE_MAX_OLD_SPACE"; fi; \
pnpm run --filter=drop postinstall && pnpm run --filter=drop 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
# The base stage's `COPY . .` puts the whole repo into the runtime WORKDIR (/app),
# but at runtime only the artifacts copied explicitly below are needed. Drop the
# inherited `torrential` source dir: the service resolves the binary by scanning
# the cwd for `torrential`, and a directory there is spawned as ./torrential and
# fails with EACCES. With it gone, resolution falls through to the `torrential`
# binary installed on PATH (/usr/bin/torrential) below.
RUN rm -rf /app/torrential
# 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
## runtime deps:
## - libarchive13: torrential now links libarchive dynamically (glibc build)
## - p7zip-full: provides the 7z CLI
## - nginx: front-end proxy
## - openssl + ca-certificates: required by Prisma's query engine on Debian
## pnpm itself is provided by corepack (enabled in the base stage)
RUN apt-get update && apt-get install -y --no-install-recommends \
libarchive13 \
p7zip-full \
nginx \
openssl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN pnpm install prisma@7.7.0 --global
RUN apk add --no-cache pnpm
RUN pnpm install prisma@6.11.1
# init prisma to download all required files
RUN pnpm prisma init
COPY --from=build-system /app/server/prisma.config.ts ./
COPY --from=build-system /app/server/.output ./app
COPY --from=build-system /app/server/prisma ./prisma
COPY --from=build-system /app/server/build ./startup
COPY --from=build-system /app/server/build/nginx.conf /nginx.conf
COPY --from=torrential-build /build/torrential-bin /usr/bin/torrential
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
ENV LIBRARY="/library"
ENV DATA="/data"
ENV NGINX_CONFIG="/nginx.conf"
# Nuxt's port
ENV PORT=4000
CMD ["sh", "/app/startup/launch.sh"]

View file

@ -6,7 +6,7 @@
# Drop
[![Website](https://img.shields.io/badge/website-000000?style=for-the-badge&logo=About.me&logoColor=white)](https://droposs.org)
[![Docs](https://img.shields.io/badge/DOCS-black?style=for-the-badge&logo=docusaurus)](https://droposs.org/docs)
[![Docs](https://img.shields.io/badge/DOCS-black?style=for-the-badge&logo=docusaurus)](https://docs.droposs.org/)
[![Static Badge](https://img.shields.io/badge/FORUM-blue?style=for-the-badge)](https://forum.droposs.org)
[![GitHub License](https://img.shields.io/badge/AGPL--3.0-red?style=for-the-badge)](LICENSE)
[![Discord](https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/ACq4qZp4a9)
@ -28,7 +28,7 @@ Drop is an open-source game distribution platform, similar to GameVault or Steam
## Deployment
See our documentation on how to [deploy Drop](https://droposs.org/docs/admin/quickstart) for more information.
See our documentation on how to [deploy Drop](https://docs.droposs.org/docs/guides/quickstart) for more information.
## Contributing

View file

@ -2,4 +2,4 @@
To report a vulnerability, please DO NOT create an issue for it
as this may lead to the vulnerability being exploited before it
can be fixed. Instead, please email [security@droposs.org](mailto:security@droposs.org)
can be fixed. Instead, please email [security@deepcore.dev](mailto:security@deepcore.dev)

View file

@ -29,11 +29,10 @@ 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;
if (!import.meta.client) return;
const realOrigin = window.location.origin.trim();
const chosenOrigin = apiDetails.external.trim();
const ignore = window.localStorage.getItem("ignoreExternalUrl");
@ -52,3 +51,15 @@ if (user.value?.admin) {
});
}
</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
backend/.gitignore vendored
View file

@ -1 +0,0 @@
/bin

View file

@ -1,19 +0,0 @@
package core
import (
"context"
"fmt"
"os"
"github.com/jackc/pgx/v5"
)
func connect() {
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
os.Exit(1)
}
defer conn.Close(context.Background())
}

View file

@ -1,11 +0,0 @@
module drop/core
go 1.26.1
require github.com/jackc/pgx/v5 v5.9.2
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
golang.org/x/text v0.29.0 // indirect
)

View file

@ -1,26 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,5 +0,0 @@
module drop
go 1.26.1
require github.com/gorilla/mux v1.8.1

View file

@ -1,2 +0,0 @@
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=

View file

@ -1,3 +0,0 @@
go 1.26.1
use ./core

View file

@ -1,9 +0,0 @@
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,37 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func handler(res http.ResponseWriter, req *http.Request) {
fmt.Fprintf(res, "G'day there mate")
}
func routingMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url := *r.URL
url.Path = strings.TrimSuffix(r.URL.Path, "/")
r.URL = &url
h.ServeHTTP(w, r)
})
}
func main() {
r := mux.NewRouter().StrictSlash(true)
r.Use(routingMiddleware)
r.HandleFunc("/api/v1", handler)
srv := &http.Server{
Addr: ":3433",
Handler: r,
}
log.Printf("starting drop server on :3433")
srv.ListenAndServe()
}

544
changelog.md Normal file
View file

@ -0,0 +1,544 @@
## Release 0.2.0-beta
### Fixes
- fix recursive dirs util #02d6346
- Fix username length requirement #0a5a649
- remove dynamic imports #0f10626
- fix for missing developers or publishers #25fc957
- split prisma schemas #2859005
- results are returned alphabetically #33d3770
- update prisma schemas #36776cc
- removed global flag #43e32b4
- properly disconnect websockets from task handler #5358f1f
- follow best practices #54c5d55
- future lenience #5c78b20
- fix width of token breaking things #61d88c3
- fixed websocket authentication #62ea9a1
- fix delta manifest generation #6df560c
- admin invitation w/ system user #8463e35
- properly import icons #8945196
- prisma create footprint #952ece8
- game panel now always shows 3 lines exactly #9c2249e
- remove unnecessary import #a361c38
- fix disconnect code #a8f2106
- fix types #b511b40
- add drop-base as git submodule #b75ebd1
- Update README.md with discord link #c6bb21d
- fix expires requirement in the admin endpoint #c7b675f
- fix always being created as admin #c7eb11a
- moved icons and created PlatformClient so we can use the enum on the frontend #cada630
- recurse submodules #db103de
- fix FATAL: "root"... message #dbb315a
- only show versions that are directories #ef8f3ae
### Features
- update prisma & delete games #089c3e0
- manual handshake #12e3125
- fetch game endpoint #1f4d075
- under the hood organisation and consolidation #26a31f6
- 'no images' slide on image carousel #28baabc
- improve feedback when metadata fails #2c19e13
- introduction of 'system user' #2c21a23
- change name, description and icon #2cfe75a
- 'manual' metadata provider #2f52a16
- add disabled state #38fc6b8
- overhauled version importing #39d7ce7
- automatically create library folder if it doesn't exist #39fe9d5
- smoother bar in admin task ui #4488ae2
- add noWrapper option #4f9b949
- add version metadata route #5393db3
- completed admin UI, with minor changes to backend #599da0e
- adjust gradient #5a1f841
- keep track of last connected #69e4c25
- added notification system w/ interwoven refactoring #6e6f09d
- content length header for chunk downloads #76bceb1
- add title to tab #7b0756c
- add button to open in admin panel #7b3b919
- client capability framework + peer API configuration #7d72a86
- customisable image carousel and new layout #937954f
- support more types #9b12d45
- generate a server certificate for mtls APIs #9c4b6f3
- new endpoints, ui and beginnings of main store page #9cbdcbc
- backend #a309651
- more subtle design improvements #a815542
- add aden's carousel pagination design #a86045c
- add header #a8a152e
- client side search #b50e27f
- new ws handler #bc0c47c
- user widget now redirects to actual page #bfafe02
- require lowercase usernames #d7160ab
- more ui improvements #e408ac5
- add modifying game descriptions #e505e58
- mobile nav #e5cf13f
- slightly improved game page #e796b46
- game carousel #ecc819e
- add enum dictionary type #f2e0182
- improved ux #f3ed0f6
- cleanup and raw accessors #f7d767d
- add support for overriding UMU id #fd4a7d1
- add .sh for linux #fe9373a
### Other Changes
- quexeky <git@quexeky.dev>
- fixed manifest generation #03a37f7
- manual ci/cd #03b0b0c
- ability to fetch client certs for p2p #0a715fe
- disable tls in build #0f80fcd
- Updated README.md #17971e0
- Merge pull request #18 from Drop-OSS/develop
- initial work on metadata system #196f87c
- more ui #1bd19ad
- remove log statements #1d5e1bd
- small fixes & SSR disabled #1f575b2
- update information and setup guide #2236622
- metadata engine #22ac7f6
- Update CONTRIBUTING.md #2309407
- slight bug fixes and clean up #24a0d11
- almst complete admin ui and initial store designs #27070b6
- handshakes #2b4382d
- user mobile header #2e44ef3
- more consistent naming for globals #305de9f
- replaced markdown-it with micromark #31e8359
- fixes to store page for mobile clients #328b9ba
- game version re-ordering #329c74d
- verbose yarn install #36568c3
- patch for no version check in manifest generation #395219d
- migrate bcrypt to bcryptjs #3a51c9c
- added download chunk endpoint #3dd6062
- Update README.md #425934d
- build only ci #4273a20
- object storage + full permission system + testing #435551c
- rename admin socket session map #44c6028
- bump droplet and add vue carousel #46551f9
- version importing #46c8f0c
- back to yarn, with nuxt telemetry force disabled #46d35ad
- finished object endpoints #486bce8
- update dependencies and add note about optional dependencies #4fa771a
- use configuration from docs for ci/cd #52315d0
- slight fixes to register logic #583301f
- removed yarn.lock #584bcf1
- Version bump #5f29c28
- immutable application settings framework #5fe2036
- fixed docker daemon location #62a111b
- copy autodevops configuration #6328c24
- Delete .gitlab-ci.yml #69f341b
- admin ui shell #6b5e48d
- bump @drop/droplet version for windows developers #6ba5cdd
- Add LICENSE #6e2dc89
- custom dind #716eac7
- task API #718f5ba
- use gitlab ci variable declaration #7194d35
- move icons into dedicated folder #74fa671
- another stage of client authentication #7523e53
- refactoring #7869043
- moved windows logo into logos dir #789d3ba
- updated text colours across app #7a88f4c
- starting docs infra #7d2a1c6
- more cleaning #7e17626
- slight patch to rename query to be more consistent #7f4db0c
- move to raw docker #803752e
- server side and user client side completed for registration #848a611
- beginnings of download implementation #8674ac7
- more consistent naming for object handler #87230fb
- use autodevops build stage #886beb6
- Updated tailwind config #88c95d6
- change name of store file #8999303
- split prisma schemas #9011cf5
- client initiate #909432a
- more client routes to support Drop app update #91b7e10
- additional polish and QoL features #93bc143
- upload images to games #9b7ee4e
- migrate to pnpm due to ci/cd issues with yarn #9cb2d6d
- run yarn install in CI/CD non interactively #a208fbe
- completed game importing; partial work on version importing #a7c33e7
- remove canvas from dependencies #a8f58eb
- fix registry authentication #ad25d3e
- consolidate type utils #adb4b73
- Updated README.md #b0ef675
- add proper carousel to store page #b2ab827
- move to yarn v2 #b744671
- remove client API deadweight #b9ae26c
- add expires field #be6c30d
- ca groundwork #bfafd2a
- cleanup & polish #c355f6f
- remove bcrypt (debug) #c3914cc
- non rounded bottom #c4391d3
- failed gracefully on invalid chunk index #c4a3e4e
- update deploy template #c4a419f
- migrate to new droplet ca system #c4d8113
- docker based deployment #c5d00b4
- updated CONTRIBUTING.md #cd0d2bf
- update prisma version #ce0a9ab
- README update #ceacd84
- patch metadata handler #cf578bd
- Added SECURITY.md #d3d93b0
- finalised client APIs and authentication method #d4e2dc8
- Update README.md #db916bf
- object storage interface + utility functions #de388a9
- initial commit #e1a789f
- fixed task system #e1c1d7e
- Update file chunk.get.ts #e4339c3
- ui groundwork #e52f072
- Update changelog #eadcaa1
- check for no version in manifest generation #eb3f9f9
- break into single column store on lg devices #ecb381e
- better server side signin redirects #ef13b68
- patch signin #f3672f8
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
## Release 0.2.0-beta
### Fixes
- fix recursive dirs util #02d6346
- Fix username length requirement #0a5a649
- remove dynamic imports #0f10626
- fix for missing developers or publishers #25fc957
- split prisma schemas #2859005
- results are returned alphabetically #33d3770
- update prisma schemas #36776cc
- removed global flag #43e32b4
- properly disconnect websockets from task handler #5358f1f
- follow best practices #54c5d55
- future lenience #5c78b20
- fix width of token breaking things #61d88c3
- fixed websocket authentication #62ea9a1
- fix delta manifest generation #6df560c
- admin invitation w/ system user #8463e35
- properly import icons #8945196
- prisma create footprint #952ece8
- game panel now always shows 3 lines exactly #9c2249e
- remove unnecessary import #a361c38
- fix disconnect code #a8f2106
- fix types #b511b40
- add drop-base as git submodule #b75ebd1
- Update README.md with discord link #c6bb21d
- fix expires requirement in the admin endpoint #c7b675f
- fix always being created as admin #c7eb11a
- moved icons and created PlatformClient so we can use the enum on the frontend #cada630
- recurse submodules #db103de
- fix FATAL: "root"... message #dbb315a
- only show versions that are directories #ef8f3ae
### Features
- update prisma & delete games #089c3e0
- manual handshake #12e3125
- fetch game endpoint #1f4d075
- under the hood organisation and consolidation #26a31f6
- 'no images' slide on image carousel #28baabc
- improve feedback when metadata fails #2c19e13
- introduction of 'system user' #2c21a23
- change name, description and icon #2cfe75a
- 'manual' metadata provider #2f52a16
- add disabled state #38fc6b8
- overhauled version importing #39d7ce7
- automatically create library folder if it doesn't exist #39fe9d5
- smoother bar in admin task ui #4488ae2
- add noWrapper option #4f9b949
- add version metadata route #5393db3
- completed admin UI, with minor changes to backend #599da0e
- adjust gradient #5a1f841
- keep track of last connected #69e4c25
- added notification system w/ interwoven refactoring #6e6f09d
- content length header for chunk downloads #76bceb1
- add title to tab #7b0756c
- add button to open in admin panel #7b3b919
- client capability framework + peer API configuration #7d72a86
- customisable image carousel and new layout #937954f
- support more types #9b12d45
- generate a server certificate for mtls APIs #9c4b6f3
- new endpoints, ui and beginnings of main store page #9cbdcbc
- backend #a309651
- more subtle design improvements #a815542
- add aden's carousel pagination design #a86045c
- add header #a8a152e
- client side search #b50e27f
- new ws handler #bc0c47c
- user widget now redirects to actual page #bfafe02
- require lowercase usernames #d7160ab
- more ui improvements #e408ac5
- add modifying game descriptions #e505e58
- mobile nav #e5cf13f
- slightly improved game page #e796b46
- game carousel #ecc819e
- add enum dictionary type #f2e0182
- improved ux #f3ed0f6
- cleanup and raw accessors #f7d767d
- add support for overriding UMU id #fd4a7d1
- add .sh for linux #fe9373a
### Other Changes
- quexeky <git@quexeky.dev>
- fixed manifest generation #03a37f7
- manual ci/cd #03b0b0c
- ability to fetch client certs for p2p #0a715fe
- disable tls in build #0f80fcd
- Updated README.md #17971e0
- Merge pull request #18 from Drop-OSS/develop
- initial work on metadata system #196f87c
- more ui #1bd19ad
- remove log statements #1d5e1bd
- small fixes & SSR disabled #1f575b2
- update information and setup guide #2236622
- metadata engine #22ac7f6
- Update CONTRIBUTING.md #2309407
- slight bug fixes and clean up #24a0d11
- almst complete admin ui and initial store designs #27070b6
- handshakes #2b4382d
- user mobile header #2e44ef3
- more consistent naming for globals #305de9f
- replaced markdown-it with micromark #31e8359
- fixes to store page for mobile clients #328b9ba
- game version re-ordering #329c74d
- verbose yarn install #36568c3
- patch for no version check in manifest generation #395219d
- migrate bcrypt to bcryptjs #3a51c9c
- added download chunk endpoint #3dd6062
- Update README.md #425934d
- build only ci #4273a20
- object storage + full permission system + testing #435551c
- rename admin socket session map #44c6028
- bump droplet and add vue carousel #46551f9
- version importing #46c8f0c
- back to yarn, with nuxt telemetry force disabled #46d35ad
- finished object endpoints #486bce8
- update dependencies and add note about optional dependencies #4fa771a
- use configuration from docs for ci/cd #52315d0
- slight fixes to register logic #583301f
- removed yarn.lock #584bcf1
- Version bump #5f29c28
- immutable application settings framework #5fe2036
- fixed docker daemon location #62a111b
- copy autodevops configuration #6328c24
- Delete .gitlab-ci.yml #69f341b
- admin ui shell #6b5e48d
- bump @drop/droplet version for windows developers #6ba5cdd
- Add LICENSE #6e2dc89
- custom dind #716eac7
- task API #718f5ba
- use gitlab ci variable declaration #7194d35
- move icons into dedicated folder #74fa671
- another stage of client authentication #7523e53
- refactoring #7869043
- moved windows logo into logos dir #789d3ba
- updated text colours across app #7a88f4c
- starting docs infra #7d2a1c6
- more cleaning #7e17626
- slight patch to rename query to be more consistent #7f4db0c
- move to raw docker #803752e
- server side and user client side completed for registration #848a611
- beginnings of download implementation #8674ac7
- more consistent naming for object handler #87230fb
- use autodevops build stage #886beb6
- Updated tailwind config #88c95d6
- change name of store file #8999303
- split prisma schemas #9011cf5
- client initiate #909432a
- more client routes to support Drop app update #91b7e10
- additional polish and QoL features #93bc143
- upload images to games #9b7ee4e
- migrate to pnpm due to ci/cd issues with yarn #9cb2d6d
- run yarn install in CI/CD non interactively #a208fbe
- completed game importing; partial work on version importing #a7c33e7
- remove canvas from dependencies #a8f58eb
- fix registry authentication #ad25d3e
- consolidate type utils #adb4b73
- Updated README.md #b0ef675
- add proper carousel to store page #b2ab827
- move to yarn v2 #b744671
- remove client API deadweight #b9ae26c
- add expires field #be6c30d
- ca groundwork #bfafd2a
- cleanup & polish #c355f6f
- remove bcrypt (debug) #c3914cc
- non rounded bottom #c4391d3
- failed gracefully on invalid chunk index #c4a3e4e
- update deploy template #c4a419f
- migrate to new droplet ca system #c4d8113
- docker based deployment #c5d00b4
- updated CONTRIBUTING.md #cd0d2bf
- update prisma version #ce0a9ab
- README update #ceacd84
- patch metadata handler #cf578bd
- Added SECURITY.md #d3d93b0
- finalised client APIs and authentication method #d4e2dc8
- Update README.md #db916bf
- object storage interface + utility functions #de388a9
- initial commit #e1a789f
- fixed task system #e1c1d7e
- Update file chunk.get.ts #e4339c3
- ui groundwork #e52f072
- Update changelog #eadcaa1
- check for no version in manifest generation #eb3f9f9
- break into single column store on lg devices #ecb381e
- better server side signin redirects #ef13b68
- patch signin #f3672f8
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
## Release 0.1.0-beta
### Fixes
- remove dynamic imports #0f10626
- fix for missing developers or publishers #25fc957
- split prisma schemas #2859005
- results are returned alphabetically #33d3770
- properly disconnect websockets from task handler #5358f1f
- follow best practices #54c5d55
- future lenience #5c78b20
- fixed websocket authentication #62ea9a1
- fix delta manifest generation #6df560c
- admin invitation w/ system user #8463e35
- properly import icons #8945196
- prisma create footprint #952ece8
- game panel now always shows 3 lines exactly #9c2249e
- remove unnecessary import #a361c38
- fix types #b511b40
- fix expires requirement in the admin endpoint #c7b675f
- moved icons and created PlatformClient so we can use the enum on the frontend #cada630
- only show versions that are directories #ef8f3ae
### Features
- update prisma & delete games #089c3e0
- fetch game endpoint #1f4d075
- under the hood organisation and consolidation #26a31f6
- introduction of 'system user' #2c21a23
- automatically create library folder if it doesn't exist #39fe9d5
- smoother bar in admin task ui #4488ae2
- add version metadata route #5393db3
- completed admin UI, with minor changes to backend #599da0e
- keep track of last connected #69e4c25
- added notification system w/ interwoven refactoring #6e6f09d
- content length header for chunk downloads #76bceb1
- add title to tab #7b0756c
- add button to open in admin panel #7b3b919
- client capability framework + peer API configuration #7d72a86
- generate a server certificate for mtls APIs #9c4b6f3
- new endpoints, ui and beginnings of main store page #9cbdcbc
- more subtle design improvements #a815542
- add header #a8a152e
- client side search #b50e27f
- new ws handler #bc0c47c
- user widget now redirects to actual page #bfafe02
- require lowercase usernames #d7160ab
- more ui improvements #e408ac5
- slightly improved game page #e796b46
- game carousel #ecc819e
- add enum dictionary type #f2e0182
- cleanup and raw accessors #f7d767d
- add support for overriding UMU id #fd4a7d1
### Other Changes
- quexeky <git@quexeky.dev>
- fixed manifest generation #03a37f7
- manual ci/cd #03b0b0c
- ability to fetch client certs for p2p #0a715fe
- disable tls in build #0f80fcd
- Updated README.md #17971e0
- initial work on metadata system #196f87c
- more ui #1bd19ad
- remove log statements #1d5e1bd
- small fixes & SSR disabled #1f575b2
- update information and setup guide #2236622
- metadata engine #22ac7f6
- Update CONTRIBUTING.md #2309407
- slight bug fixes and clean up #24a0d11
- almst complete admin ui and initial store designs #27070b6
- handshakes #2b4382d
- user mobile header #2e44ef3
- more consistent naming for globals #305de9f
- replaced markdown-it with micromark #31e8359
- fixes to store page for mobile clients #328b9ba
- game version re-ordering #329c74d
- verbose yarn install #36568c3
- patch for no version check in manifest generation #395219d
- migrate bcrypt to bcryptjs #3a51c9c
- added download chunk endpoint #3dd6062
- Update README.md #425934d
- build only ci #4273a20
- object storage + full permission system + testing #435551c
- rename admin socket session map #44c6028
- bump droplet and add vue carousel #46551f9
- version importing #46c8f0c
- back to yarn, with nuxt telemetry force disabled #46d35ad
- finished object endpoints #486bce8
- update dependencies and add note about optional dependencies #4fa771a
- use configuration from docs for ci/cd #52315d0
- slight fixes to register logic #583301f
- removed yarn.lock #584bcf1
- Version bump #5f29c28
- immutable application settings framework #5fe2036
- fixed docker daemon location #62a111b
- copy autodevops configuration #6328c24
- Delete .gitlab-ci.yml #69f341b
- admin ui shell #6b5e48d
- bump @drop/droplet version for windows developers #6ba5cdd
- Add LICENSE #6e2dc89
- task API #718f5ba
- use gitlab ci variable declaration #7194d35
- move icons into dedicated folder #74fa671
- another stage of client authentication #7523e53
- refactoring #7869043
- moved windows logo into logos dir #789d3ba
- updated text colours across app #7a88f4c
- starting docs infra #7d2a1c6
- more cleaning #7e17626
- slight patch to rename query to be more consistent #7f4db0c
- move to raw docker #803752e
- server side and user client side completed for registration #848a611
- beginnings of download implementation #8674ac7
- more consistent naming for object handler #87230fb
- use autodevops build stage #886beb6
- Updated tailwind config #88c95d6
- change name of store file #8999303
- split prisma schemas #9011cf5
- client initiate #909432a
- more client routes to support Drop app update #91b7e10
- additional polish and QoL features #93bc143
- upload images to games #9b7ee4e
- migrate to pnpm due to ci/cd issues with yarn #9cb2d6d
- run yarn install in CI/CD non interactively #a208fbe
- completed game importing; partial work on version importing #a7c33e7
- remove canvas from dependencies #a8f58eb
- fix registry authentication #ad25d3e
- consolidate type utils #adb4b73
- Updated README.md #b0ef675
- add proper carousel to store page #b2ab827
- move to yarn v2 #b744671
- remove client API deadweight #b9ae26c
- add expires field #be6c30d
- ca groundwork #bfafd2a
- cleanup & polish #c355f6f
- remove bcrypt (debug) #c3914cc
- non rounded bottom #c4391d3
- failed gracefully on invalid chunk index #c4a3e4e
- update deploy template #c4a419f
- migrate to new droplet ca system #c4d8113
- docker based deployment #c5d00b4
- updated CONTRIBUTING.md #cd0d2bf
- update prisma version #ce0a9ab
- README update #ceacd84
- patch metadata handler #cf578bd
- Added SECURITY.md #d3d93b0
- finalised client APIs and authentication method #d4e2dc8
- Update README.md #db916bf
- object storage interface + utility functions #de388a9
- initial commit #e1a789f
- fixed task system #e1c1d7e
- Update file chunk.get.ts #e4339c3
- ui groundwork #e52f072
- check for no version in manifest generation #eb3f9f9
- break into single column store on lg devices #ecb381e
- better server side signin redirects #ef13b68
- patch signin #f3672f8
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)

View file

@ -1 +0,0 @@
use flake

4
cli/.gitignore vendored
View file

@ -1,4 +0,0 @@
/target
logs/
.vscode
.direnv

3396
cli/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,27 +0,0 @@
[package]
name = "downpour"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
async-trait = "0.1.89"
chrono = "0.4.43"
clap = { version = "4.5.54", features = ["derive"] }
console = "0.16.2"
dialoguer = "0.12.0"
dirs = "6.0.0"
droplet-rs = { path = "../libraries/droplet" }
fern = { version = "0.7.1", features = ["colored"] }
futures = "0.3.31"
indicatif = "0.18.3"
log = "0.4.29"
opendal = { version = "0.55.0", features = ["services-s3"] }
rand = "0.9.3"
reqwest = { version = "0.13.1", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
tokio = { version = "1.48.0", features = ["fs", "macros"] }
tokio-util = { version = "0.7.18", features = ["compat"] }
url = "2.5.8"
webbrowser = "1.0.6"

View file

@ -1,3 +0,0 @@
# CLI (`downpour`)
The cli way to access Drop. Used for admin tasks that require local access, like uploading game content.

96
cli/flake.lock generated
View file

@ -1,96 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1768564909,
"narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1768704795,
"narHash": "sha256-Y33TAp2BHEcuspYvcmBXXD0qdvjftv73PwyKTDOjoSY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "4b7472a78857ac789fb26616040f55cfcbd36c6e",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,52 +0,0 @@
{
description = "Drop-OSS app development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs =
{
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
libraries = with pkgs; [
glib
glibc
openssl
];
in
{
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
pkg-config
git
rust-bin.nightly.latest.default
rust-analyzer
cargo-expand
];
buildInputs = libraries;
shellHook = ''
export LD_LIBRARY_PATH="${
pkgs.lib.makeLibraryPath libraries
}:$LD_LIBRARY_PATH"
echo "Downpour development environment loaded"
'';
};
}
);
}

View file

@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"

View file

@ -1,10 +0,0 @@
# Downpour CLI spec
`downpour [command] --opts`
## Commands:
- new <path/s3 name> <public endpoint> - creates/initalizes a depot at the endpoint. Creates manifest.json and speedtest
- connect <s3 endpoint> <key> <secret> [name] - connects to an s3 endpoint and saves the endpoint to some sort of credentials file. Name is either as provided or the hostname of the endpoint
- upload <game id> <localpath> <path/s3 name> - uploads game as described before. Should fail if depot isn't initialized with new from above
- copy <game id> <version id> <src path/s3 name> <dest path/s3 name> - copies between two depots
- mark [exists/absent] <game id> <version id> <path/s3 name> - modifies depot's manifest.json to show content exists or is absent without copying (for third party copies)
- rename <public endpoint> <new public endpoint> - renames an endpoint [NEEDS API ROUTES - can't do yet]
- delete <public endpoint> - delete an endpoint [NEEDS API ROUTES - can't do yet]

View file

@ -1,69 +0,0 @@
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::{commands::connect::config_option::ConfigOptionCli, interactive_variable};
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
/// Specify data file path
#[arg(short, long)]
pub data: Option<String>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Configures downpour endpoints
Connect {
#[arg(short, long)]
name: Option<String>,
#[command(subcommand)]
option: ConfigOptionCli,
},
/// Uploads new game version to depot
Upload {
#[clap(flatten)]
info: UploadInfoCli,
#[arg(short, long)]
/// Alias of a given connection
name: Option<String>,
},
}
#[derive(Args)]
pub struct UploadInfo {
pub path: String,
pub game_id: String,
pub version_id: String,
}
#[derive(Args)]
pub struct UploadInfoCli {
/// Relative path to new version files
#[arg(short, long, default_value_t = String::from("."))]
pub path: String,
/// ID of game to attach to
#[arg(short, long)]
pub game_id: Option<String>,
/// Version ID to attach to
#[arg(short, long)]
pub version_id: Option<String>,
}
impl UploadInfoCli {
pub fn interactive_configure(self) -> UploadInfo {
let path = self.path;
interactive_variable!(self, game_id, "Game ID");
interactive_variable!(self, version_id, "Version ID");
UploadInfo {
path,
game_id,
version_id,
}
}
}
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum UploadStyle {
S3,
}

View file

@ -1,152 +0,0 @@
use crate::{
commands::connect::{
config_option::{ConfigOption, ConfigOptionCli},
configurable::Configure,
speedtest::{SPEEDTEST_PATH, Speedtest},
},
manifest::DepotManifest,
};
use dialoguer::{Confirm, theme::ColorfulTheme};
use futures::AsyncWriteExt;
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, info};
use opendal::Operator;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, ops::Not};
use tokio_util::compat::FuturesAsyncWriteCompatExt;
const CONFIG_DIR: &str = "downpour/config.json";
#[derive(Serialize, Deserialize)]
pub struct Config {
configurations: HashMap<String, ConfigOption>,
active: Option<String>,
}
impl Config {
pub fn new() -> Self {
Self {
configurations: HashMap::new(),
active: None,
}
}
pub fn exists(&self, name: &String) -> bool {
self.configurations.contains_key(name)
}
pub fn save(&self) -> anyhow::Result<()> {
let json = serde_json::to_string(self)?;
let save_path = dirs::config_dir()
.expect("Apparently your home directory doesn't exist") // Should probably formalise that error
.join(CONFIG_DIR);
fs::create_dir_all(save_path.parent().unwrap())?;
fs::write(save_path, json)?;
Ok(())
}
pub fn read() -> Self {
let save_path = dirs::config_dir()
.expect("Apparently your home directory doesn't exist") // Should probably formalise that error
.join(CONFIG_DIR);
if fs::exists(&save_path)
.unwrap_or_else(|_| panic!("Could not read save path {:#?}", &save_path))
{
serde_json::from_str(&fs::read_to_string(save_path).unwrap()).unwrap()
} else {
Config::new()
}
}
pub fn add_item(&mut self, name: String, object: ConfigOption) {
if matches!(object, ConfigOption::S3(..)) {
self.active = Some(name.clone())
}
self.configurations.insert(name, object);
self.save().expect("Failed to save config");
}
pub fn get_active(&self) -> Option<&ConfigOption> {
if let Some(active) = &self.active {
self.configurations.get(active)
} else {
None
}
}
pub fn get<T: AsRef<str>>(&self, name: T) -> Option<&ConfigOption> {
self.configurations.get(name.as_ref())
}
}
pub async fn manage_configuration(
config: &mut Config,
name: Option<String>,
option: ConfigOptionCli,
) -> anyhow::Result<()> {
let mut name = name;
if let Some(name) = &name
&& config.exists(name)
{
let confirm = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(format!(
"An entry already exists with the name \"{}\". Would you like to overwrite it?",
name
))
.interact()?;
if !confirm {
return Err(anyhow::anyhow!("User cancelled action"));
}
}
let config_option = match option {
ConfigOptionCli::S3(s3_config_cli) => s3_config_cli.clone().configure(&mut name).await?,
};
let name = name.expect("Default name was not provided by ConfigOption. This is a bug");
config.add_item(name, config_option.clone());
let operator = config_option.build()?;
generate_manifest(&operator).await?;
info!("Finished uploading manifest");
generate_speedtest(&operator).await?;
info!("Finished uploading speedtest");
Ok(())
}
async fn generate_speedtest(operator: &Operator) -> anyhow::Result<()> {
// Workaround to operator.exists("...") also logging a 404 warning
let lister = operator.list_with(SPEEDTEST_PATH).limit(1).await?;
if lister.is_empty().not() {
info!("Speedtest already exists on Depot. Skipping speedtest upload...");
return Ok(());
}
let mut writer = operator
.writer(SPEEDTEST_PATH)
.await?
.into_futures_async_write()
.compat_write();
let progress_bar = ProgressBar::new(10_000).with_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%")
.unwrap(),
);
let mut reader = Speedtest::new(|progress| {
let progress_int = (progress * 100f32).round() as u64;
progress_bar.set_position(progress_int);
});
let written = tokio::io::copy(&mut reader, &mut writer).await?;
progress_bar.finish();
debug!("Wrote {} bytes to {:?}", written, operator.info());
writer.into_inner().close().await?;
debug!("Closed writer");
Ok(())
}
async fn generate_manifest(operator: &Operator) -> anyhow::Result<()> {
let lister = operator.list_with("manifest.json").limit(1).await?;
if lister.is_empty().not() {
info!("Manifest already exists on Depot. Skipping manifest upload...");
return Ok(());
}
let data = DepotManifest::new();
operator
.write("manifest.json", serde_json::to_string(&data)?)
.await?;
Ok(())
}

View file

@ -1,27 +0,0 @@
use clap::Subcommand;
use opendal::{Operator, layers::LoggingLayer};
use serde::{Deserialize, Serialize};
use crate::{
commands::connect::s3::{S3Config, S3ConfigCli},
operator_builder::OperatorBuilder,
};
#[derive(Subcommand, Clone)]
pub enum ConfigOptionCli {
// Connect to any S3-compatible endpoint
S3(S3ConfigCli),
}
#[derive(Serialize, Deserialize, Clone)]
pub enum ConfigOption {
S3(S3Config),
}
impl ConfigOption {
pub fn build(&self) -> anyhow::Result<Operator> {
Ok(match self {
ConfigOption::S3(s3_config) => s3_config.build()?,
}
.layer(LoggingLayer::default()))
}
}

View file

@ -1,5 +0,0 @@
use crate::commands::connect::config_option::ConfigOption;
pub trait Configure {
async fn configure(self, name: &mut Option<String>) -> anyhow::Result<ConfigOption>;
}

View file

@ -1,47 +0,0 @@
use std::str::FromStr;
use dialoguer::{Input, theme::ColorfulTheme};
#[macro_export]
macro_rules! interactive_variable {
($value:ident, $var:ident, $prompt:expr) => {
let $var = if let Some($var) = $value.$var {
$var
} else {
$crate::commands::connect::interactive::query_variable($prompt).unwrap()
};
};
}
#[macro_export]
macro_rules! interactive_optional_variable {
($value:ident, $var:ident, $prompt:expr) => {
let $var = if let Some($var) = $value.$var {
Some($var)
} else {
$crate::commands::connect::interactive::query_optional_variable($prompt).unwrap()
};
};
}
pub fn query_variable<T: Clone + FromStr + ToString>(prompt: impl ToString) -> dialoguer::Result<T>
where
<T as FromStr>::Err: ToString,
{
Input::with_theme(&ColorfulTheme::default())
.with_prompt(prompt.to_string())
.interact_text()
}
pub fn query_optional_variable<T: Clone + FromStr + ToString>(
prompt: impl ToString,
) -> dialoguer::Result<Option<T>>
where
<T as FromStr>::Err: ToString,
{
let input: T = Input::with_theme(&ColorfulTheme::default())
.with_prompt(prompt.to_string())
.allow_empty(true)
.interact_text()?;
if input.to_string().is_empty() {
return Ok(None);
}
Ok(Some(input))
}

View file

@ -1,7 +0,0 @@
pub mod config;
pub mod configurable;
pub mod s3;
#[macro_use]
pub mod interactive;
pub mod config_option;
pub mod speedtest;

View file

@ -1,67 +0,0 @@
use clap::Args;
use opendal::Operator;
use serde::{Deserialize, Serialize};
use crate::{
commands::connect::{config_option::ConfigOption, configurable::Configure},
interactive_variable,
operator_builder::OperatorBuilder,
};
#[derive(Args, Clone)]
pub struct S3ConfigCli {
key_id: Option<String>,
secret_key: Option<String>,
endpoint: Option<String>,
region: Option<String>,
bucket_name: Option<String>,
root: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct S3Config {
key_id: String,
secret_key: String,
endpoint: String,
region: String,
bucket_name: String,
root: Option<String>,
}
impl Configure for S3ConfigCli {
async fn configure(self, name: &mut Option<String>) -> anyhow::Result<ConfigOption> {
interactive_variable!(self, key_id, "S3 Key ID");
interactive_variable!(self, secret_key, "S3 Secret Key");
interactive_variable!(self, region, "S3 Region");
interactive_variable!(self, bucket_name, "S3 Bucket Name");
interactive_variable!(self, endpoint, "S3 Endpoint");
if let None = name {
*name = Some(endpoint.clone());
}
Ok(ConfigOption::S3(S3Config {
secret_key,
key_id,
region,
bucket_name,
endpoint,
root: self.root,
}))
}
}
impl OperatorBuilder for S3Config {
fn build(&self) -> anyhow::Result<Operator> {
let builder = opendal::services::S3::default()
.access_key_id(&self.key_id)
.secret_access_key(&self.secret_key)
.region(&self.region)
.endpoint(&self.endpoint)
.root(self.root.as_deref().unwrap_or("/"))
.bucket(&self.bucket_name)
.disable_config_load();
let op: Operator = Operator::new(builder)?.finish();
Ok(op)
}
}

View file

@ -1,41 +0,0 @@
use rand::{RngCore, SeedableRng, rng, rngs::StdRng};
use tokio::io::AsyncRead;
#[derive(Clone, Debug)]
pub struct Speedtest<F: Fn(f32)> {
core: rand::rngs::StdRng,
to_write: usize,
callback: Box<F>,
}
pub const SPEEDTEST_BYTES: usize = 64 * 1024 * 1024;
pub const SPEEDTEST_PATH: &str = "speedtest";
impl<F: Fn(f32)> AsyncRead for Speedtest<F> {
fn poll_read(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let mut s = self;
let to_write = buf.remaining().min(s.to_write);
let filled = {
let fill_slice = buf.initialize_unfilled_to(to_write);
s.core.fill_bytes(fill_slice);
fill_slice.len()
};
s.to_write = s.to_write.saturating_sub(filled);
(s.callback)((1f32 - (s.to_write as f32 / SPEEDTEST_BYTES as f32)) * 100f32);
buf.advance(filled);
std::task::Poll::Ready(Ok(()))
}
}
impl<F: Fn(f32)> Speedtest<F> {
pub fn new(callback: F) -> Self {
Self {
core: StdRng::from_rng(&mut rng()),
to_write: SPEEDTEST_BYTES,
callback: Box::new(callback),
}
}
}

View file

@ -1,2 +0,0 @@
pub mod connect;
pub mod upload;

View file

@ -1,79 +0,0 @@
use std::path::Path;
use crate::{
cli::UploadInfo,
commands::connect::{config::Config, config_option::ConfigOption},
manifest::{ClosureFactory, CompressionOption, DepotManifest, generate_v2_manifest},
operator_builder::OperatorBuilder,
};
use futures::AsyncWriteExt;
use log::info;
use opendal::{FuturesAsyncWriter, Operator};
use tokio_util::compat::{Compat, FuturesAsyncWriteCompatExt};
pub async fn upload(
upload_info: &UploadInfo,
config: Config,
name: &Option<String>,
) -> anyhow::Result<()> {
let game_id = upload_info.game_id.clone();
let path = upload_info.path.clone();
let version_id = upload_info.version_id.clone();
let operator = get_operator(config, name)?;
let mut existing_depot_manifest = get_depot_manifest(&operator).await?;
info!("Uploading chunks");
let v2_manifest = generate_v2_manifest(
Path::new(&path),
ClosureFactory::new(
async move |id: String| {
info!("Uploading chunk id {id}");
let writer = operator
.writer(&format!("{game_id}/{version_id}/{id}"))
.await
.unwrap()
.into_futures_async_write()
.compat_write();
writer
},
|writer: Compat<FuturesAsyncWriter>| async {
writer.into_inner().close().await.unwrap();
},
),
)
.await?;
info!("Finished uploading chunks");
existing_depot_manifest.append(
upload_info.game_id.to_string(),
upload_info.version_id.to_string(),
CompressionOption::None,
);
Ok(())
}
async fn get_depot_manifest(operator: &Operator) -> Result<DepotManifest, anyhow::Error> {
let existing_depot_manifest = operator.read("manifest.json").await?.to_bytes();
let existing_depot_manifest: DepotManifest =
serde_json::from_slice(existing_depot_manifest.as_ref())?;
Ok(existing_depot_manifest)
}
fn get_operator(config: Config, name: &Option<String>) -> anyhow::Result<Operator> {
let operator = match if let Some(name) = name {
config
.get(name)
.ok_or(anyhow::anyhow!("Name does not exist"))?
} else {
config.get_active().ok_or(anyhow::anyhow!(
"No active connection set. Please specify with --name"
))?
} {
ConfigOption::S3(s3_config) => s3_config.build()?,
};
Ok(operator)
}

View file

@ -1 +0,0 @@
pub mod interface;

View file

@ -1,53 +0,0 @@
use fern::colors::{Color, ColoredLevelConfig};
use log::LevelFilter;
use std::env;
use std::fs;
use std::io;
pub fn configure_logging() -> anyhow::Result<()> {
let log_level = env::var("RUST_LOG")
.unwrap_or_else(|_| "info".to_string())
.parse::<LevelFilter>()?;
let log_dir = env::var("LOG_FILE_DIR").unwrap_or_else(|_| "logs".to_string());
fs::create_dir_all(&log_dir)?;
let colors = ColoredLevelConfig::new()
.error(Color::Red)
.warn(Color::Yellow)
.info(Color::Blue)
.debug(Color::Green)
.trace(Color::Magenta);
fern::Dispatch::new()
.chain(
fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"[{}] {}: {}",
chrono::Local::now().format("%H:%M:%S%.3f"),
colors.color(record.level()),
message
))
})
.chain(io::stdout()),
)
.chain(
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{}] {} {} - {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(),
record.target(),
message
))
})
.chain(fern::log_file(format!("{}/app.log", log_dir))?),
)
.level(log_level)
.apply()?;
Ok(())
}

View file

@ -1,34 +0,0 @@
#![feature(async_fn_traits)]
use crate::commands::connect::config::manage_configuration;
use crate::{
cli::{Cli, Commands},
commands::connect::config::Config,
commands::upload,
};
use clap::Parser;
mod cli;
mod commands;
mod logging;
mod manifest;
mod operator_builder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
crate::logging::configure_logging()?;
let cli = Cli::parse();
let mut config = Config::read();
match cli.command {
Commands::Connect { name, option } => {
manage_configuration(&mut config, name, option).await?
}
Commands::Upload { info, name } => {
let info = info.interactive_configure();
upload::interface::upload(&info, config, &name).await?;
}
};
Ok(())
}

View file

@ -1,114 +0,0 @@
use std::{collections::HashMap, path::Path};
use async_trait::async_trait;
use droplet_rs::manifest::{Manifest, ManifestWriterFactory, generate_manifest_rusty};
use indicatif::{ProgressBar, ProgressStyle};
use log::info;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWrite;
#[derive(Serialize, Deserialize)]
pub struct DepotManifest {
content: HashMap<String, DepotManifestGameData>,
}
#[derive(Serialize, Deserialize)]
struct DepotManifestGameData {
version_id: String,
compression: CompressionOption,
}
#[derive(Serialize, Deserialize)]
pub enum CompressionOption {
None,
Gzip,
Zstd,
}
impl DepotManifest {
pub fn new() -> Self {
Self {
content: HashMap::new(),
}
}
pub fn append(&mut self, game_id: String, version_id: String, compression: CompressionOption) {
self.content.insert(
game_id,
DepotManifestGameData {
version_id,
compression,
},
);
}
}
pub struct ClosureFactory<Writer, Factory, Closer>
where
Writer: AsyncWrite + Unpin,
Factory: AsyncFn(String) -> Writer,
Closer: AsyncFn(Writer),
{
writer: Factory,
closer: Closer,
}
#[async_trait]
impl<
W: AsyncWrite + Unpin + Send + Sync,
F: AsyncFn(String) -> W + Send + Sync + 'static,
C: AsyncFn(W) + Send + Sync,
> ManifestWriterFactory for ClosureFactory<W, F, C>
where
for<'a> F::CallRefFuture<'a>: Send,
for<'b> C::CallRefFuture<'b>: Send,
{
type Writer = W;
async fn create(&self, id: String) -> anyhow::Result<Self::Writer> {
let func = &self.writer;
let output = func(id).await;
Ok(output)
}
async fn close(&self, writer: Self::Writer) -> anyhow::Result<()> {
let func = &self.closer;
func(writer).await;
Ok(())
}
}
impl<
W: AsyncWrite + Unpin + Send + Sync,
F: AsyncFn(String) -> W + Send + Sync + 'static,
C: AsyncFn(W) + Sync,
> ClosureFactory<W, F, C>
where
for<'a> F::CallRefFuture<'a>: Send,
for<'b> C::CallRefFuture<'b>: Send,
{
pub fn new(f: F, c: C) -> Self {
Self {
writer: f,
closer: c,
}
}
}
pub async fn generate_v2_manifest<Factory>(dir: &Path, factory: Factory) -> anyhow::Result<Manifest>
where
Factory: ManifestWriterFactory,
{
let progress_bar = ProgressBar::new(10_000).with_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%")
.unwrap(),
);
generate_manifest_rusty(
dir,
|progress| {
let progress_int = (progress * 100f32).round() as u64;
progress_bar.set_position(progress_int);
},
|log| progress_bar.suspend(|| info!("{}", log)),
Some(&factory),
None,
)
.await
}

View file

@ -1,5 +0,0 @@
use opendal::Operator;
pub trait OperatorBuilder {
fn build(&self) -> anyhow::Result<Operator>;
}

View file

@ -53,17 +53,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 +67,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",
@ -87,13 +74,19 @@ const navigation: Ref<
icon: BellIcon,
count: notifications.value.length,
},
{
label: t("account.token.title"),
route: "/account/tokens",
prefix: "/account/tokens",
icon: CodeBracketIcon,
},
{
label: t("account.settings"),
route: "/account/settings",
prefix: "/account/settings",
icon: WrenchScrewdriverIcon,
},
]);
];
const currentPageIndex = useCurrentNavigationIndex(navigation.value);
const currentPageIndex = useCurrentNavigationIndex(navigation);
</script>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 aria-hidden="true" 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>

View file

@ -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>

View file

@ -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>();

View file

@ -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,114 +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>
<div class="flex flex-col">
<label class="text-sm/6 font-medium text-zinc-100">
{{ $t("library.admin.game.ageRatings") }}
</label>
<div class="mt-2 space-y-2">
<div
v-for="(ar, idx) in ageRatings"
:key="ar.organization"
class="flex items-center gap-2"
>
<span
class="inline-flex items-center rounded-full bg-zinc-800 px-2.5 py-0.5 text-sm font-medium text-zinc-100"
>
{{
$t("library.admin.game.ageRatingLabel", {
organization: ar.organization,
rating: ar.rating,
})
}}
</span>
<button
type="button"
class="text-red-400 hover:text-red-300 text-sm"
@click="() => removeAgeRating(idx)"
>
{{ $t("library.admin.game.removeAgeRating") }}
</button>
</div>
<p
v-if="ageRatings.length === 0 && !showAddAgeRating"
class="text-sm text-zinc-400"
>
{{ $t("library.admin.game.ageRatingsEmpty") }}
</p>
<div v-if="showAddAgeRating" class="flex items-center gap-2">
<select
v-model="newAgeRatingOrg"
class="rounded-md bg-zinc-800 px-2 py-1 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 focus:outline-blue-600"
>
<option
v-for="org in availableOrganizations"
:key="org"
:value="org"
>
{{ org }}
</option>
</select>
<select
v-model="newAgeRatingValue"
:disabled="!newAgeRatingOrg"
class="rounded-md bg-zinc-800 px-2 py-1 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 focus:outline-blue-600"
>
<option
v-for="r in newAgeRatingOrgValues"
:key="r"
:value="r"
>
{{ r }}
</option>
</select>
<button
type="button"
class="rounded-md bg-blue-600 px-2 py-1 text-sm font-semibold text-white hover:bg-blue-500"
:disabled="!newAgeRatingOrg || !newAgeRatingValue"
@click="addAgeRating"
>
{{ $t("add") }}
</button>
<button
type="button"
class="text-zinc-400 hover:text-zinc-300 text-sm"
@click="showAddAgeRating = false"
>
{{ $t("cancel") }}
</button>
</div>
<button
v-if="!showAddAgeRating && availableOrganizations.length > 0"
type="button"
class="text-sm text-blue-400 hover:text-blue-300"
@click="showAddAgeRating = true"
>
{{ $t("library.admin.game.addAgeRating") }}
</button>
</div>
</div>
<MultiItemSelector v-model="currentTags" :items="tags" />
</div>
<!-- image carousel pick -->
@ -553,9 +444,7 @@
</template>
<script setup lang="ts">
import type { GameModel } from "~/prisma/client/models";
import { AgeRatingOrganization } from "~/prisma/client/enums";
import { getAvailableRatings } from "~/utils/ageRatings";
import type { GameModel, GameTagModel } from "~/prisma/client/models";
import { micromark } from "micromark";
import {
CheckIcon,
@ -565,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);
@ -573,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,
@ -585,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(
@ -598,102 +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 allOrganizations = Object.values(AgeRatingOrganization);
const ageRatings = ref(
(game.value.ageRatings ?? []).map((ar) => ({
organization: ar.organization,
rating: ar.rating,
})),
);
const showAddAgeRating = ref(false);
const newAgeRatingOrg = ref("");
const newAgeRatingValue = ref("");
const availableOrganizations = computed(() =>
allOrganizations.filter(
(org) => !ageRatings.value.some((ar) => ar.organization === org),
),
);
const newAgeRatingOrgValues = computed(() =>
newAgeRatingOrg.value
? getAvailableRatings(newAgeRatingOrg.value as AgeRatingOrganization)
: [],
);
watch(newAgeRatingOrg, () => {
newAgeRatingValue.value = "";
});
async function saveAgeRatings() {
await $dropFetch(`/api/v1/admin/game/:id/age-ratings`, {
method: "PATCH",
params: { id: game.value.id },
body: { ageRatings: ageRatings.value },
failTitle: "Failed to update age ratings",
});
}
async function addAgeRating() {
if (!newAgeRatingOrg.value || !newAgeRatingValue.value) return;
ageRatings.value.push({
organization: newAgeRatingOrg.value as AgeRatingOrganization,
rating: newAgeRatingValue.value,
});
newAgeRatingOrg.value = "";
newAgeRatingValue.value = "";
showAddAgeRating.value = false;
await saveAgeRatings();
}
async function removeAgeRating(index: number) {
ageRatings.value.splice(index, 1);
await saveAgeRatings();
}
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);
@ -759,6 +561,7 @@ function coreMetadataUpdate_wrapper() {
);
})
.then((newGame) => {
console.log(newGame);
if (!newGame) return;
Object.assign(game.value, newGame);
coreMetadataIconUrl.value = useObject(newGame.mIconObjectId);
@ -966,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>

View file

@ -0,0 +1,196 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<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.versionPriority") }}
<!-- import games button -->
<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>
</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="mt-2 space-y-4"
@update="() => updateVersionOrder()"
>
<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"
/>
<button @click="() => deleteVersion(item.versionName)">
<TrashIcon class="w-5 h-5 text-red-600" />
</button>
</div>
</div>
</template>
</draggable>
<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>
</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 { 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 } 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 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",
});
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.versionName),
},
});
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)?.statusMessage ?? t("errors.unknown"),
}),
buttonText: t("common.close"),
},
(e, c) => c(),
);
}
}
async function deleteVersion(versionName: string) {
try {
await $dropFetch("/api/v1/admin/game/version", {
method: "DELETE",
body: {
id: game.value.id,
versionName: versionName,
},
});
game.value.versions.splice(
game.value.versions.findIndex((e) => e.versionName === versionName),
1,
);
hasDeleted.value = true;
} catch (e) {
createModal(
ModalType.Notification,
{
title: t("errors.version.delete.title"),
description: t("errors.version.delete.desc", {
error: (e as H3Error)?.statusMessage ?? t("errors.unknown"),
}),
buttonText: t("common.close"),
},
(e, c) => c(),
);
}
}
</script>

View file

@ -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>

View file

@ -1,9 +1,9 @@
<template>
<div>
<SelectorLanguageListbox />
<LanguageSelectorListbox />
<NuxtLink
class="mt-1 transition text-blue-500 hover:text-blue-600 text-sm"
to="https://devops.quasarke.net/drop-oss/drop"
to="https://translate.droposs.org/engage/drop/"
target="_blank"
>
<i18n-t

View file

@ -1,6 +1,6 @@
<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 v-if="!short" class="text-zinc-500">{{ log.timestamp }}</span>
<span
:class="[
colours[log.level] || 'text-green-400',
@ -8,8 +8,9 @@
]"
>{{ 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>
<pre :class="[short ? 'line-clamp-1' : '', 'mt-[1px]']">{{
log.message
}}</pre>
</span>
</template>

View file

@ -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>

View file

@ -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;
}

View file

@ -119,10 +119,10 @@
<div class="flex h-6 shrink-0 items-center">
<div class="group grid size-4 grid-cols-1">
<input
:id="`acl-${acl}`"
id="acl"
v-model="currentACLs[acl]"
aria-describedby="acl-description"
:name="`acl-${acl}`"
name="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"
/>
@ -150,7 +150,7 @@
</div>
<div class="text-sm/6">
<label
:for="`acl-${acl}`"
for="acl"
class="font-display font-medium text-white"
>{{ acl }}</label
>

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -0,0 +1,115 @@
<template>
<div>
<div class="inline-flex gap-1 items-center flex-wrap">
<span
v-for="item in enabledItems"
:key="item.param"
class="inline-flex items-center gap-x-0.5 rounded-md bg-blue-600/10 px-2 py-1 text-xs font-medium text-blue-500 ring-1 ring-blue-800 ring-inset"
>
{{ item.name }}
<button
type="button"
class="group relative -mr-1 size-3.5 rounded-xs hover:bg-blue-600/20"
@click="() => remove(item.param)"
>
<span class="sr-only">{{ $t("common.remove") }}</span>
<svg
viewBox="0 0 14 14"
class="size-3.5 stroke-blue-500 group-hover:stroke-blue-400"
>
<path d="M4 4l6 6m0-6l-6 6" />
</svg>
<span class="absolute -inset-1" />
</button>
</span>
<span
v-if="enabledItems.length == 0"
class="font-display uppercase text-xs font-bold text-zinc-700"
>
{{ $t("common.noSelected") }}
</span>
</div>
<Combobox as="div" @update:model-value="add">
<div class="relative mt-2">
<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="Start typing..."
@change="search = $event.target.value"
@blur="search = ''"
/>
<ComboboxButton
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>
<ComboboxOptions
v-if="filteredItems.length > 0 || search.length > 0"
class="absolute 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-hidden sm:text-sm"
>
<ComboboxOption
v-for="item in filteredItems"
:key="item.param"
v-slot="{ active }"
:value="item.param"
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">
{{ item.name }}
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { ChevronDownIcon } from "@heroicons/vue/20/solid";
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
} from "@headlessui/vue";
const props = defineProps<{
items: Array<StoreSortOption>;
}>();
const model = defineModel<{ [key: string]: boolean }>();
const search = ref("");
const filteredItems = computed(() =>
props.items.filter(
(item) =>
!model.value?.[item.param] &&
item.name.toLowerCase().includes(search.value.toLowerCase()),
),
);
const enabledItems = computed(() =>
props.items.filter((e) => model.value?.[e.param]),
);
function add(item: string) {
search.value = "";
model.value ??= {};
model.value[item] = true;
}
function remove(item: string) {
model.value ??= {};
model.value[item] = false;
}
</script>

View file

@ -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`, {

View file

@ -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>

View file

@ -19,7 +19,7 @@
</p>
<NuxtLink
class="mt-4 rounded-md inline-flex items-center text-sm font-semibold text-blue-500 hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
href="https://drop-docs.quasarke.net/docs/admin/authentication/simple"
href="https://docs.droposs.org/docs/authentication/simple"
target="_blank"
>
<i18n-t
@ -74,7 +74,7 @@
</p>
<NuxtLink
class="mt-4 rounded-md inline-flex items-center text-sm font-semibold text-blue-500 hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
href="https://drop-docs.quasarke.net/docs/admin/authentication/oidc"
href="https://docs.droposs.org/docs/authentication/oidc"
target="_blank"
>
<i18n-t

Some files were not shown because too many files have changed in this diff Show more