mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-06-22 04:11:32 +10:00
Compare commits
6 Commits
ba3faba9ec
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 38c11567ef | |||
| cbecd1161d | |||
| 9185089c99 | |||
| 0290718ee0 | |||
| 2e86422004 | |||
| 796abf478f |
@@ -83,6 +83,10 @@ jobs:
|
||||
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"
|
||||
|
||||
@@ -118,8 +122,12 @@ jobs:
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
# 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:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
@@ -1,12 +1,24 @@
|
||||
name: CI
|
||||
name: Server CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "server/**"
|
||||
- "libraries/base/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- ".github/workflows/server-ci.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "server/**"
|
||||
- "libraries/base/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- ".github/workflows/server-ci.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -18,8 +30,6 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -34,6 +44,7 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: server
|
||||
run: pnpm run typecheck
|
||||
|
||||
lint:
|
||||
@@ -42,8 +53,6 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -58,4 +67,5 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Lint
|
||||
working-directory: server
|
||||
run: pnpm run lint
|
||||
@@ -89,8 +89,6 @@ jobs:
|
||||
build-args: |
|
||||
BUILD_DROP_VERSION=${{ steps.get_final_ver.outputs.final_ver }}
|
||||
BUILD_GIT_REF=${{ github.sha }}
|
||||
context: ./server
|
||||
file: ./server/Dockerfile
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
|
||||
+36
-7
@@ -1,6 +1,8 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:lts-alpine AS base
|
||||
# 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
|
||||
@@ -20,11 +22,17 @@ FROM base AS deps
|
||||
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
### BUILD TORRENTIAL
|
||||
FROM rustlang/rust:nightly-alpine AS torrential-build
|
||||
RUN apk add musl-dev pkgconfig libarchive-dev libarchive
|
||||
# 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 . .
|
||||
RUN apk add protoc
|
||||
RUN cargo build --release --manifest-path ./torrential/Cargo.toml
|
||||
|
||||
### BUILD APP
|
||||
@@ -34,7 +42,8 @@ ENV NODE_ENV=production
|
||||
ENV NUXT_TELEMETRY_DISABLED=1
|
||||
|
||||
## add git so drop can determine its git ref at build
|
||||
RUN apk add --no-cache git
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
## copy deps and rest of project files
|
||||
COPY . .
|
||||
@@ -54,9 +63,29 @@ FROM base AS run-system
|
||||
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
|
||||
RUN apk add --no-cache pnpm 7zip nginx
|
||||
RUN pnpm install prisma@7.3.0 --global
|
||||
## 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
|
||||
# init prisma to download all required files
|
||||
RUN pnpm prisma init
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# Drop
|
||||
|
||||
[](https://droposs.org)
|
||||
[](https://docs.droposs.org/)
|
||||
[](https://droposs.org/docs)
|
||||
[](https://forum.droposs.org)
|
||||
[](LICENSE)
|
||||
[](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://docs.droposs.org/docs/guides/quickstart) for more information.
|
||||
See our documentation on how to [deploy Drop](https://droposs.org/docs/admin/quickstart) for more information.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Arch Linux, Windows]
|
||||
- App Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
Vendored
-23
@@ -1,23 +0,0 @@
|
||||
on: push
|
||||
name: Clippy check
|
||||
jobs:
|
||||
clippy_check:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
checks: write
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: install dependencies (ubuntu only)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --manifest-path ./src-tauri/Cargo.toml
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
name: "publish"
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
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:
|
||||
submodules: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
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: './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
|
||||
|
||||
|
||||
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 }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
NO_STRIP: true
|
||||
with:
|
||||
tagName: 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 }}
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<Listbox
|
||||
as="div"
|
||||
v-model="model.overrideHandler"
|
||||
class="mt-6"
|
||||
v-if="handlers.length > 1"
|
||||
>
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-white"
|
||||
>Launch method</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="grid w-full cursor-default grid-cols-1 rounded-md bg-white/5 py-1.5 pr-2 pl-3 text-left text-white outline-1 -outline-offset-1 outline-white/10 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-500 sm:text-sm/6"
|
||||
>
|
||||
<span
|
||||
v-if="currentHandler"
|
||||
class="col-start-1 row-start-1 truncate pr-6"
|
||||
>{{ currentHandler.name }}</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="col-start-1 row-start-1 truncate pr-6 italic text-zinc-400"
|
||||
>Automatic</span
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="col-start-1 row-start-1 size-5 self-center justify-self-end text-zinc-400 sm:size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class=""
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-800 py-1 text-base outline-1 -outline-offset-1 outline-white/10 sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
:value="undefined"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-500 text-white outline-hidden' : 'text-white',
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate italic',
|
||||
]"
|
||||
>Automatic</span
|
||||
>
|
||||
<span class="block truncate text-xs text-zinc-400"
|
||||
>Pick the best method for this game.</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-400',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="handler in handlers"
|
||||
:key="handler.id"
|
||||
:value="handler.id"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-500 text-white outline-hidden' : 'text-white',
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ handler.name }}</span
|
||||
>
|
||||
<span class="block truncate text-xs text-zinc-400">{{
|
||||
handler.description
|
||||
}}</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-400',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
Override how this game is launched.
|
||||
</p>
|
||||
</Listbox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
||||
import { CheckIcon } from "@heroicons/vue/20/solid";
|
||||
import type { GameVersion } from "~/types";
|
||||
|
||||
type ProcessHandlerOption = { id: string; name: string; description: string };
|
||||
|
||||
const model = defineModel<GameVersion["userConfiguration"]>({ required: true });
|
||||
const props = defineProps<{ gameId: string }>();
|
||||
|
||||
const handlers = await invoke<ProcessHandlerOption[]>("get_process_handlers", {
|
||||
id: props.gameId,
|
||||
});
|
||||
|
||||
const currentHandler = computed(() =>
|
||||
handlers.find((v) => v.id == model.value.overrideHandler),
|
||||
);
|
||||
</script>
|
||||
@@ -23,16 +23,19 @@
|
||||
</p>
|
||||
|
||||
<ProtonSelector v-model="model" v-if="$props.protonEnabled" />
|
||||
<HandlerSelector v-model="model" :game-id="$props.gameId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GameVersion } from "~/types";
|
||||
import ProtonSelector from "./ProtonSelector.vue";
|
||||
import HandlerSelector from "./HandlerSelector.vue";
|
||||
|
||||
const model = defineModel<GameVersion["userConfiguration"]>({ required: true });
|
||||
|
||||
const props = defineProps<{
|
||||
protonEnabled: boolean;
|
||||
gameId: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ModalTemplate size-class="max-w-4xl" v-model="open">
|
||||
<template #default>
|
||||
<div class="flex flex-row gap-x-4 h-96">
|
||||
<div class="flex flex-row gap-x-4 min-h-96">
|
||||
<nav class="flex flex-1 flex-col" aria-label="Sidebar">
|
||||
<ul role="list" class="-mx-2 space-y-1">
|
||||
<li v-for="(tab, tabIdx) in tabs" :key="tab.name">
|
||||
@@ -29,11 +29,12 @@
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="border-l-2 border-zinc-800 w-full grow pl-4 overflow-y-scroll">
|
||||
<div class="border-l-2 border-zinc-800 w-full grow pl-4">
|
||||
<component
|
||||
v-model="configuration"
|
||||
:is="tabs[currentTabIndex]?.page"
|
||||
:proton-enabled="protonEnabled"
|
||||
:game-id="props.gameId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"koa": "^2.16.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"micromark": "^4.0.1",
|
||||
"nuxt": "^3.21.6",
|
||||
"nuxt": "^4.4.8",
|
||||
"scss": "^0.2.4",
|
||||
"vue-router": "latest",
|
||||
"vuedraggable": "^4.1.0"
|
||||
|
||||
Generated
+794
-1525
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ export type GameVersion = {
|
||||
userConfiguration: {
|
||||
launchTemplate: string;
|
||||
overrideProtonPath: string;
|
||||
overrideHandler: string | undefined;
|
||||
enableUpdates: boolean
|
||||
};
|
||||
setups: Array<{ platform: string }>;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "^9.7.0",
|
||||
"pino": "^9.14.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"tauri": "^0.15.0"
|
||||
},
|
||||
|
||||
Generated
+1
@@ -1392,6 +1392,7 @@ dependencies = [
|
||||
"http-serde 2.1.1",
|
||||
"humansize",
|
||||
"known-folders",
|
||||
"libloading",
|
||||
"log",
|
||||
"log4rs",
|
||||
"md5 0.7.0",
|
||||
|
||||
@@ -136,6 +136,9 @@ tauri-build = { version = "*", features = [] }
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
libloading = "0.7"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
|
||||
@@ -79,6 +79,7 @@ pub mod data {
|
||||
UserConfiguration {
|
||||
launch_template: "{}".to_owned(),
|
||||
override_proton_path: None,
|
||||
override_handler: None,
|
||||
enable_updates: false,
|
||||
}
|
||||
}
|
||||
@@ -88,6 +89,8 @@ pub mod data {
|
||||
pub struct UserConfiguration {
|
||||
pub launch_template: String,
|
||||
pub override_proton_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub override_handler: Option<String>,
|
||||
pub enable_updates: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::{fs::create_dir_all, path::PathBuf, process::Command};
|
||||
use std::{
|
||||
fs::create_dir_all,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use client::compat::{COMPAT_INFO, UMU_LAUNCHER_EXECUTABLE};
|
||||
use database::{
|
||||
Database, DownloadableMetadata, GameVersion, db::DATA_ROOT_DIR, platform::Platform,
|
||||
};
|
||||
|
||||
use crate::{error::ProcessError, process_manager::ProcessHandler};
|
||||
use crate::{error::ProcessError, parser::ParsedCommand, process_manager::ProcessHandler};
|
||||
|
||||
pub struct MacLauncher;
|
||||
impl ProcessHandler for MacLauncher {
|
||||
@@ -25,11 +29,89 @@ impl ProcessHandler for MacLauncher {
|
||||
}
|
||||
|
||||
fn modify_command(&self, _command: &mut Command) {}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"macos"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Direct"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Launches the game directly on macOS."
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
|
||||
fn apply_no_window(command: &mut Command) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
}
|
||||
|
||||
enum WindowsLaunchStrategy {
|
||||
Direct,
|
||||
Cmd,
|
||||
Powershell,
|
||||
}
|
||||
|
||||
// Wrap a launch command for Windows; with no strategy, detect it from the file extension.
|
||||
fn windows_launch_command(
|
||||
launch_command: String,
|
||||
current_dir: &str,
|
||||
strategy: Option<WindowsLaunchStrategy>,
|
||||
) -> Result<String, ProcessError> {
|
||||
let mut parsed = ParsedCommand::parse(launch_command)?;
|
||||
|
||||
let strategy = strategy.unwrap_or_else(|| {
|
||||
let extension = Path::new(&parsed.command)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(str::to_ascii_lowercase);
|
||||
match extension.as_deref() {
|
||||
Some("ps1") => WindowsLaunchStrategy::Powershell,
|
||||
Some("exe") | Some("com") => WindowsLaunchStrategy::Direct,
|
||||
_ => WindowsLaunchStrategy::Cmd,
|
||||
}
|
||||
});
|
||||
|
||||
match strategy {
|
||||
// PowerShell scripts
|
||||
WindowsLaunchStrategy::Powershell => {
|
||||
parsed.make_absolute(PathBuf::from(current_dir));
|
||||
let script = std::mem::replace(&mut parsed.command, "powershell".to_owned());
|
||||
let mut args = vec![
|
||||
"-NoProfile".to_owned(),
|
||||
"-ExecutionPolicy".to_owned(),
|
||||
"Bypass".to_owned(),
|
||||
"-File".to_owned(),
|
||||
script,
|
||||
];
|
||||
args.append(&mut parsed.args);
|
||||
parsed.args = args;
|
||||
}
|
||||
// Direct executables
|
||||
WindowsLaunchStrategy::Direct => {
|
||||
parsed.make_absolute(PathBuf::from(current_dir));
|
||||
}
|
||||
// cmd.exe, for batch files, builtins, PATHEXT resolution, %VAR% expansion, etc.
|
||||
WindowsLaunchStrategy::Cmd => {
|
||||
let command = std::mem::replace(&mut parsed.command, "cmd".to_owned());
|
||||
let mut args = vec!["/C".to_owned(), command];
|
||||
args.append(&mut parsed.args);
|
||||
parsed.args = args;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parsed.reconstruct())
|
||||
}
|
||||
|
||||
pub struct WindowsLauncher;
|
||||
impl ProcessHandler for WindowsLauncher {
|
||||
fn create_launch_process(
|
||||
@@ -37,22 +119,169 @@ impl ProcessHandler for WindowsLauncher {
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
_game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
Ok(format!("pwsh \"cmd /C \"{}\"\"", launch_command))
|
||||
windows_launch_command(launch_command, current_dir, None)
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn modify_command(&self, command: &mut Command) {
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(target_os = "windows")]
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
apply_no_window(command);
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"windows-auto"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Automatic"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Detects the file type and launches it directly, or through cmd or PowerShell."
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowsDirectLauncher;
|
||||
impl ProcessHandler for WindowsDirectLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
_game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
windows_launch_command(launch_command, current_dir, Some(WindowsLaunchStrategy::Direct))
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn modify_command(&self, command: &mut Command) {
|
||||
apply_no_window(command);
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"windows-direct"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Direct executable"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Runs the executable directly, without a shell."
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowsCmdLauncher;
|
||||
impl ProcessHandler for WindowsCmdLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
_game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
windows_launch_command(launch_command, current_dir, Some(WindowsLaunchStrategy::Cmd))
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn modify_command(&self, command: &mut Command) {
|
||||
apply_no_window(command);
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"windows-cmd"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Command Prompt (cmd)"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Launches through cmd.exe. Supports batch files, builtins and %VAR% expansion."
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowsPowershellLauncher;
|
||||
impl ProcessHandler for WindowsPowershellLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
_game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
windows_launch_command(
|
||||
launch_command,
|
||||
current_dir,
|
||||
Some(WindowsLaunchStrategy::Powershell),
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn modify_command(&self, command: &mut Command) {
|
||||
apply_no_window(command);
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"windows-powershell"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PowerShell"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Runs the command as a PowerShell script (-File)."
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LinuxNativeLauncher;
|
||||
impl ProcessHandler for LinuxNativeLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
_game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
// Run native Linux games directly, no umu-run wrapper
|
||||
Ok(launch_command)
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn modify_command(&self, _command: &mut Command) {}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"linux-native"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Native (direct)"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Runs the native Linux game directly on the host."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +330,18 @@ impl ProcessHandler for UMUNativeLauncher {
|
||||
}
|
||||
|
||||
fn modify_command(&self, _command: &mut Command) {}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"linux-umu"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Steam Linux Runtime (umu-run)"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Runs the native Linux game inside umu-run's Steam Linux Runtime."
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UMUCompatLauncher;
|
||||
@@ -168,6 +409,18 @@ impl ProcessHandler for UMUCompatLauncher {
|
||||
}
|
||||
|
||||
fn modify_command(&self, _command: &mut Command) {}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"proton-umu"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Proton (umu-run)"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Runs the Windows game through Proton using umu-run."
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AsahiMuvmLauncher;
|
||||
@@ -228,4 +481,16 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
||||
}
|
||||
|
||||
fn modify_command(&self, _command: &mut Command) {}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"proton-muvm"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Proton + muvm (Asahi)"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Runs through Proton inside a muvm microVM, for Apple Silicon / Asahi Linux."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ use crate::{
|
||||
format::DropFormatArgs,
|
||||
parser::{LaunchParameters, ParsedCommand},
|
||||
process_handlers::{
|
||||
AsahiMuvmLauncher, MacLauncher, UMUCompatLauncher, UMUNativeLauncher, WindowsLauncher,
|
||||
AsahiMuvmLauncher, LinuxNativeLauncher, MacLauncher, UMUCompatLauncher, UMUNativeLauncher,
|
||||
WindowsCmdLauncher, WindowsDirectLauncher, WindowsLauncher, WindowsPowershellLauncher,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -54,6 +55,13 @@ pub struct LaunchOption {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProcessHandlerOption {
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl ProcessManager<'_> {
|
||||
pub fn new(app_handle: AppHandle) -> Self {
|
||||
let log_output_dir = DATA_ROOT_DIR.join("logs");
|
||||
@@ -76,6 +84,22 @@ impl ProcessManager<'_> {
|
||||
(Platform::Windows, Platform::Windows),
|
||||
&WindowsLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Windows, Platform::Windows),
|
||||
&WindowsDirectLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Windows, Platform::Windows),
|
||||
&WindowsCmdLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Windows, Platform::Windows),
|
||||
&WindowsPowershellLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Linux),
|
||||
&LinuxNativeLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Linux),
|
||||
&UMUNativeLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
@@ -101,7 +125,7 @@ impl ProcessManager<'_> {
|
||||
match self.processes.get_mut(&game_id) {
|
||||
Some(process) => {
|
||||
process.manually_killed = true;
|
||||
process.handle.kill()?;
|
||||
kill_process_tree(&process.handle)?;
|
||||
let exit_status = process.handle.wait()?;
|
||||
info!("exit status: {:?}", exit_status);
|
||||
Ok(())
|
||||
@@ -188,7 +212,21 @@ impl ProcessManager<'_> {
|
||||
&self,
|
||||
db_lock: &Database,
|
||||
target_platform: &Platform,
|
||||
override_id: Option<&str>,
|
||||
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
|
||||
// An explicit override wins, as long as it's valid for the current platform.
|
||||
if let Some(override_id) = override_id
|
||||
&& let Some(handler) = self.game_launchers.iter().find(|e| {
|
||||
let (e_current, e_target) = e.0;
|
||||
e_current == self.current_platform
|
||||
&& e_target == *target_platform
|
||||
&& e.1.id() == override_id
|
||||
&& e.1.valid_for_platform(db_lock, target_platform)
|
||||
})
|
||||
{
|
||||
return Ok(handler.1);
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.game_launchers
|
||||
.iter()
|
||||
@@ -204,10 +242,44 @@ impl ProcessManager<'_> {
|
||||
|
||||
pub fn valid_platform(&self, platform: &Platform) -> bool {
|
||||
let db_lock = borrow_db_checked();
|
||||
let process_handler = self.fetch_process_handler(&db_lock, platform);
|
||||
let process_handler = self.fetch_process_handler(&db_lock, platform, None);
|
||||
process_handler.is_ok()
|
||||
}
|
||||
|
||||
pub fn get_process_handlers(
|
||||
&self,
|
||||
game_id: String,
|
||||
) -> Result<Vec<ProcessHandlerOption>, ProcessError> {
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
let meta = db_lock
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&game_id)
|
||||
.cloned()
|
||||
.ok_or(ProcessError::NotInstalled)?;
|
||||
|
||||
let target_platform = meta.target_platform;
|
||||
|
||||
let handlers = self
|
||||
.game_launchers
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
let (e_current, e_target) = e.0;
|
||||
e_current == self.current_platform
|
||||
&& e_target == target_platform
|
||||
&& e.1.valid_for_platform(&db_lock, &target_platform)
|
||||
})
|
||||
.map(|e| ProcessHandlerOption {
|
||||
id: e.1.id().to_string(),
|
||||
name: e.1.name().to_string(),
|
||||
description: e.1.description().to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(handlers)
|
||||
}
|
||||
|
||||
pub fn get_launch_options(game_id: String) -> Result<Vec<LaunchOption>, ProcessError> {
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
@@ -310,7 +382,12 @@ impl ProcessManager<'_> {
|
||||
|
||||
let target_platform = meta.target_platform;
|
||||
|
||||
let process_handler = self.fetch_process_handler(&db_lock, &target_platform)?;
|
||||
let process_handler = self.fetch_process_handler(
|
||||
&db_lock,
|
||||
&target_platform,
|
||||
game_version.user_configuration.override_handler.as_deref(),
|
||||
)?;
|
||||
debug!("using process handler {:?}", process_handler.id());
|
||||
|
||||
let (target_command, emulator) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
@@ -516,6 +593,30 @@ impl ProcessManager<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn kill_process_tree(handle: &SharedChild) -> io::Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// handle.kill() only terminates the launched process (often a cmd or
|
||||
// powershell wrapper), orphaning the actual game. taskkill /T kills the
|
||||
// whole process tree.
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let pid = handle.id().to_string();
|
||||
let killed = Command::new("taskkill")
|
||||
.args(["/F", "/T", "/PID", pid.as_str()])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(false);
|
||||
if killed {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
handle.kill()
|
||||
}
|
||||
|
||||
pub trait ProcessHandler: Send + 'static {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
@@ -529,4 +630,8 @@ pub trait ProcessHandler: Send + 'static {
|
||||
fn valid_for_platform(&self, db: &Database, target: &Platform) -> bool;
|
||||
|
||||
fn modify_command(&self, command: &mut Command);
|
||||
|
||||
fn id(&self) -> &'static str;
|
||||
fn name(&self) -> &'static str;
|
||||
fn description(&self) -> &'static str;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,17 @@
|
||||
#![deny(clippy::all)]
|
||||
|
||||
use std::{
|
||||
env, fs::File, io::Write, panic::PanicHookInfo, path::Path, str::FromStr,
|
||||
sync::nonpoison::Mutex, time::SystemTime,
|
||||
env,
|
||||
fs::File,
|
||||
io::Write,
|
||||
panic::PanicHookInfo,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
nonpoison::Mutex,
|
||||
},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use ::client::{
|
||||
@@ -260,6 +269,7 @@ pub fn run() {
|
||||
get_autostart_enabled,
|
||||
open_process_logs,
|
||||
get_launch_options,
|
||||
get_process_handlers,
|
||||
#[cfg(target_os = "linux")]
|
||||
::process::compat::fetch_proton_paths,
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -359,8 +369,17 @@ pub fn run() {
|
||||
)
|
||||
.expect("Failed to generate menu");
|
||||
|
||||
if env::var("NO_TRAY_ICON").is_ok_and(|value| value.to_lowercase() == "true") {
|
||||
TRAY_DISABLED.store(true, Ordering::Relaxed);
|
||||
} else if !tray_icon_supported() {
|
||||
warn!(
|
||||
"appindicator library not available at runtime, disabling system tray icon"
|
||||
);
|
||||
TRAY_DISABLED.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
run_on_tray(|| {
|
||||
TrayIconBuilder::new()
|
||||
let tray = TrayIconBuilder::new()
|
||||
.icon(
|
||||
app.default_window_icon()
|
||||
.expect("Failed to get default window icon")
|
||||
@@ -383,8 +402,12 @@ pub fn run() {
|
||||
warn!("menu event not handled: {:?}", event.id);
|
||||
}
|
||||
})
|
||||
.build(app)
|
||||
.expect("error while setting up tray menu");
|
||||
.build(app);
|
||||
|
||||
if let Err(e) = tray {
|
||||
warn!("failed to set up system tray icon, disabling tray: {e}");
|
||||
TRAY_DISABLED.store(true, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
@@ -445,13 +468,30 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
static TRAY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn tray_icon_supported() -> bool {
|
||||
[
|
||||
"libayatana-appindicator3.so.1",
|
||||
"libappindicator3.so.1",
|
||||
"libayatana-appindicator3.so",
|
||||
"libappindicator3.so",
|
||||
]
|
||||
.iter()
|
||||
.any(|name| unsafe { libloading::Library::new(name) }.is_ok())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn tray_icon_supported() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run_on_tray<T: FnOnce()>(f: T) {
|
||||
if match std::env::var("NO_TRAY_ICON") {
|
||||
Ok(s) => s.to_lowercase() != "true",
|
||||
Err(_) => true,
|
||||
} {
|
||||
(f)();
|
||||
if TRAY_DISABLED.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
(f)();
|
||||
}
|
||||
|
||||
// TODO: Refactor
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use process::{
|
||||
PROCESS_MANAGER,
|
||||
error::ProcessError,
|
||||
process_manager::{LaunchOption, ProcessManager},
|
||||
process_manager::{LaunchOption, ProcessHandlerOption, ProcessManager},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tauri::AppHandle;
|
||||
@@ -16,6 +16,11 @@ pub fn get_launch_options(id: String) -> Result<Vec<LaunchOption>, ProcessError>
|
||||
Ok(launch_options)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_process_handlers(id: String) -> Result<Vec<ProcessHandlerOption>, ProcessError> {
|
||||
PROCESS_MANAGER.lock().get_process_handlers(id)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "result", content = "data")]
|
||||
pub enum LaunchResult {
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: Build, Test, Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
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: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install libarchive
|
||||
run: |
|
||||
sudo apt-get install libarchive-dev -y
|
||||
|
||||
|
||||
- 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
|
||||
@@ -1,7 +1,5 @@
|
||||
#![deny(clippy::all)]
|
||||
#![feature(impl_trait_in_bindings)]
|
||||
#![feature(nonpoison_mutex)]
|
||||
#![feature(sync_nonpoison)]
|
||||
pub mod file_utils;
|
||||
pub mod manifest;
|
||||
pub mod ssl;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use droplet_rs::manifest::{ManifestWriterFactory, generate_manifest_rusty};
|
||||
use droplet_rs::manifest::{generate_manifest_rusty, ManifestWriterFactory};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
struct SinkFactory {}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::{collections::HashMap, ops::Not, path::Path};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
pub use droplet_types::{ChunkData, FileEntry, Manifest};
|
||||
use futures::StreamExt;
|
||||
use hex::ToHex as _;
|
||||
use humansize::{format_size, BINARY};
|
||||
@@ -9,8 +10,6 @@ use sha2::{Digest as _, Sha256};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::{AsyncReadExt as _, AsyncWrite};
|
||||
use tokio::sync::Semaphore;
|
||||
pub use droplet_types::{ChunkData, FileEntry, Manifest};
|
||||
|
||||
|
||||
pub const CHUNK_SIZE: u64 = 1024 * 1024 * 64;
|
||||
pub const MAX_FILE_COUNT: usize = 512;
|
||||
@@ -44,7 +43,7 @@ where
|
||||
"Could not create backend for path. Is this structure supported?"
|
||||
))?()?;
|
||||
let mut files = backend.list_files().await?;
|
||||
files.sort_by(|a, b| b.size.cmp(&a.size));
|
||||
files.sort_by_key(|b| std::cmp::Reverse(b.size));
|
||||
|
||||
log_sfn("organising files into chunks...".to_string());
|
||||
|
||||
|
||||
@@ -34,9 +34,11 @@ const SUPPORTED_FILE_EXTENSIONS: [&str; 11] = [
|
||||
];
|
||||
|
||||
pub mod types;
|
||||
pub fn create_backend_constructor<'a, P>(
|
||||
path: P,
|
||||
) -> Option<Box<dyn FnOnce() -> Result<Box<dyn VersionBackend + Send + Sync + 'a>>>>
|
||||
|
||||
type BackendConstructor<'a> =
|
||||
Box<dyn FnOnce() -> Result<Box<dyn VersionBackend + Send + Sync + 'a>>>;
|
||||
|
||||
pub fn create_backend_constructor<'a, P>(path: P) -> Option<BackendConstructor<'a>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -53,7 +55,7 @@ where
|
||||
}));
|
||||
};
|
||||
|
||||
let file_extension = path.extension().map(|v| v.to_str()).flatten()?;
|
||||
let file_extension = path.extension().and_then(|v| v.to_str())?;
|
||||
if SUPPORTED_FILE_EXTENSIONS.contains(&file_extension) {
|
||||
let buf = path.to_path_buf();
|
||||
return Some(Box::new(move || Ok(Box::new(ZipVersionBackend::new(buf)?))));
|
||||
|
||||
@@ -24,4 +24,3 @@ pub struct Manifest {
|
||||
pub size: u64,
|
||||
pub key: [u8; 16],
|
||||
}
|
||||
|
||||
|
||||
Generated
+947
-679
File diff suppressed because it is too large
Load Diff
Vendored
-158
@@ -1,158 +0,0 @@
|
||||
name: Release Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
release:
|
||||
types: [published]
|
||||
# This can be used to automatically publish nightlies at UTC nighttime
|
||||
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 }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- 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: |
|
||||
BASE_VER=v$(jq -r '.version' package.json)
|
||||
TODAY=$(date +'%Y.%m.%d')
|
||||
|
||||
echo "Today will be: $TODAY"
|
||||
echo "today=$TODAY" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
FINAL_VER="$BASE_VER"
|
||||
else
|
||||
FINAL_VER="${BASE_VER}-nightly.$TODAY"
|
||||
fi
|
||||
|
||||
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
|
||||
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 }}
|
||||
|
||||
- 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
|
||||
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
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/drop-OSS/drop
|
||||
tags: |
|
||||
type=schedule,pattern=nightly
|
||||
type=schedule,pattern=nightly.${{ steps.get_final_ver.outputs.today }}
|
||||
type=semver,pattern=v{{version}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
# 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: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Server
|
||||
|
||||
The hosted, accessible portion of Drop. Exposes a web UI and API for applications to use.
|
||||
The hosted, accessible portion of Drop. Exposes a web UI and API for applications to use.
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@ plugins:
|
||||
opt: target=ts
|
||||
|
||||
inputs:
|
||||
- directory: ../
|
||||
- directory: ../
|
||||
|
||||
@@ -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://docs.droposs.org/docs/authentication/simple"
|
||||
href="https://droposs.org/docs/admin/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://docs.droposs.org/docs/authentication/oidc"
|
||||
href="https://droposs.org/docs/admin/authentication/oidc"
|
||||
target="_blank"
|
||||
>
|
||||
<i18n-t
|
||||
|
||||
@@ -174,13 +174,14 @@ const optionsMetadata: {
|
||||
Filesystem: {
|
||||
title: t("library.admin.sources.fsTitle"),
|
||||
description: t("library.admin.sources.fsDesc"),
|
||||
docsLink: "https://docs.droposs.org/docs/library#drop-style",
|
||||
docsLink: "https://droposs.org/docs/reference/library-sources#drop-style",
|
||||
icon: DropLogo,
|
||||
},
|
||||
FlatFilesystem: {
|
||||
title: t("library.admin.sources.fsFlatTitle"),
|
||||
description: t("library.admin.sources.fsFlatDesc"),
|
||||
docsLink: "https://docs.droposs.org/docs/library#flat-style-or-compat",
|
||||
docsLink:
|
||||
"https://droposs.org/docs/reference/library-sources#compatibility-flat-style",
|
||||
icon: BackwardIcon,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -134,11 +134,11 @@ const navigation = computed(() => ({
|
||||
// { name: t("footer.api"), href: "https://api.droposs.org/" },
|
||||
{
|
||||
name: t("footer.docs.server"),
|
||||
href: "https://docs.droposs.org/docs/guides/quickstart",
|
||||
href: "https://droposs.org/docs/admin/quickstart",
|
||||
},
|
||||
{
|
||||
name: t("footer.docs.client"),
|
||||
href: "https://docs.droposs.org/docs/guides/client",
|
||||
href: "https://droposs.org/docs/user",
|
||||
},
|
||||
],
|
||||
about: [
|
||||
|
||||
@@ -9,7 +9,12 @@ export const updateUser = async () => {
|
||||
const user = useUser();
|
||||
if (user.value === null) return;
|
||||
|
||||
user.value = await $dropFetch<UserModel | null>("/api/v1/user");
|
||||
user.value = await $dropFetch<UserModel | null>("/api/v1/user", {
|
||||
// Forward headers manually when called outside a component
|
||||
headers: import.meta.server
|
||||
? useRequestHeaders(["cookie", "authorization"])
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export async function completeSignin() {
|
||||
|
||||
Submodule server/drop-base deleted from dad3487be6
@@ -11,9 +11,9 @@ export default withNuxt([
|
||||
eslintConfigPrettier,
|
||||
|
||||
// vue-i18n plugin
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error
|
||||
...vueI18n.configs.recommended,
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error
|
||||
{
|
||||
rules: {
|
||||
// Optional.
|
||||
@@ -37,10 +37,10 @@ export default withNuxt([
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error
|
||||
{
|
||||
plugins: {
|
||||
drop: { rules: { "no-prisma-delete": noPrismaDelete } },
|
||||
},
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -547,6 +547,9 @@
|
||||
"sources": {
|
||||
"create": "Create source",
|
||||
"createDesc": "Drop will use this source to access your game library, and make them available.",
|
||||
"deleteButton": "Delete source",
|
||||
"deleteDesc": "Deleting \"{0}\" will cascade delete the library, all of its games, all of their versions, and all of their metadata. This action cannot be undone.",
|
||||
"deleteTitle": "Delete library source?",
|
||||
"desc": "Configure your library sources, where Drop will look for new games and versions to import.",
|
||||
"documentationLink": "Documentation {arrow}",
|
||||
"edit": "Edit source",
|
||||
|
||||
+51
-14
@@ -126,16 +126,50 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="sticky top-0 z-40 flex items-center gap-x-6 bg-zinc-900 px-4 py-4 shadow-sm sm:px-6 lg:hidden"
|
||||
class="sticky top-0 z-40 lg:pl-20 border-b border-zinc-800 bg-zinc-950 shadow-sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<span class="sr-only">{{ $t("header.openSidebar") }}</span>
|
||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
<div class="flex items-center gap-x-4 px-4 py-2 sm:px-6 lg:px-8">
|
||||
<button
|
||||
type="button"
|
||||
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<span class="sr-only">{{ $t("header.openSidebar") }}</span>
|
||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<ol class="inline-flex items-center gap-3">
|
||||
<li>
|
||||
<Menu as="div" class="relative inline-block">
|
||||
<MenuButton>
|
||||
<UserHeaderWidget :notifications="unreadNotifications.length">
|
||||
<BellIcon class="h-5" />
|
||||
</UserHeaderWidget>
|
||||
</MenuButton>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute right-0 top-10 z-50 w-96 focus:outline-none shadow-md"
|
||||
>
|
||||
<UserHeaderNotificationWidgetPanel
|
||||
:notifications="unreadNotifications"
|
||||
/>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</li>
|
||||
<UserHeaderUserWidget />
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="lg:pl-20 min-h-screen bg-zinc-900 flex flex-col">
|
||||
@@ -156,6 +190,9 @@ import {
|
||||
DialogPanel,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
} from "@headlessui/vue";
|
||||
import {
|
||||
Bars3Icon,
|
||||
@@ -168,7 +205,7 @@ import {
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import type { NavigationItem } from "~/composables/types";
|
||||
import { useCurrentNavigationIndex } from "~/composables/current-page-engine";
|
||||
import { ArrowLeftIcon } from "@heroicons/vue/16/solid";
|
||||
import { ArrowLeftIcon, BellIcon } from "@heroicons/vue/16/solid";
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
import type { Settings } from "~/server/internal/utils/types";
|
||||
|
||||
@@ -219,10 +256,10 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
},
|
||||
];
|
||||
|
||||
// const notifications = useNotifications();
|
||||
// const unreadNotifications = computed(() =>
|
||||
// notifications.value.filter((e) => !e.read)
|
||||
// );
|
||||
const notifications = useNotifications();
|
||||
const unreadNotifications = computed(() =>
|
||||
notifications.value.filter((e) => !e.read),
|
||||
);
|
||||
|
||||
const currentNavigationIndex = useCurrentNavigationIndex(navigation);
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ const whitelistedPrefixes = ["/auth", "/api", "/setup"];
|
||||
const requireAdmin = ["/admin"];
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
if (import.meta.server) return;
|
||||
const error = useError();
|
||||
if (error.value !== undefined) return;
|
||||
if (whitelistedPrefixes.findIndex((e) => to.fullPath.startsWith(e)) != -1)
|
||||
return;
|
||||
|
||||
const user = useUser();
|
||||
if (user === undefined) {
|
||||
if (user.value === undefined) {
|
||||
await updateUser();
|
||||
}
|
||||
if (!user.value) {
|
||||
|
||||
+5
-5
@@ -29,8 +29,8 @@
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/kit": "^3.20.1",
|
||||
"@nuxtjs/i18n": "^9.5.5",
|
||||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@prisma/adapter-pg": "7.7.0",
|
||||
"@prisma/client": "7.7.0",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
@@ -56,9 +56,9 @@
|
||||
"nuxt-security": "2.2.0",
|
||||
"otp-io": "^1.2.7",
|
||||
"parse-cosekey": "^1.0.2",
|
||||
"pino": "9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"prisma": "7.3.0",
|
||||
"pino": "^9.14.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"prisma": "7.7.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.7.1",
|
||||
"shescape": "^2.1.10",
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
|
||||
<NuxtLink
|
||||
class="transition text-xs text-zinc-600 hover:underline hover:text-zinc-400"
|
||||
href="https://docs.droposs.org/docs/library"
|
||||
href="https://droposs.org/docs/reference/library-sources"
|
||||
target="_blank"
|
||||
>
|
||||
<i18n-t
|
||||
|
||||
@@ -279,13 +279,14 @@ const optionsMetadata: {
|
||||
Filesystem: {
|
||||
title: t("library.admin.sources.fsTitle"),
|
||||
description: t("library.admin.sources.fsDesc"),
|
||||
docsLink: "https://docs.droposs.org/docs/library#drop-style",
|
||||
docsLink: "https://droposs.org/docs/reference/library-sources#drop-style",
|
||||
icon: DropLogo,
|
||||
},
|
||||
FlatFilesystem: {
|
||||
title: t("library.admin.sources.fsFlatTitle"),
|
||||
description: t("library.admin.sources.fsFlatDesc"),
|
||||
docsLink: "https://docs.droposs.org/docs/library#flat-style-or-compat",
|
||||
docsLink:
|
||||
"https://droposs.org/docs/reference/library-sources#compatibility-flat-style",
|
||||
icon: BackwardIcon,
|
||||
},
|
||||
};
|
||||
@@ -346,30 +347,45 @@ function edit(index: number) {
|
||||
actionSourceOpen.value = true;
|
||||
}
|
||||
|
||||
async function deleteSource(index: number) {
|
||||
function deleteSource(index: number) {
|
||||
const source = sources.value[index];
|
||||
if (!source) return;
|
||||
|
||||
try {
|
||||
await $dropFetch("/api/v1/admin/library/sources", {
|
||||
method: "DELETE",
|
||||
body: { id: source.id },
|
||||
headers,
|
||||
});
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: t("errors.library.source.delete.title"),
|
||||
description: t("errors.library.source.delete.desc", [
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
e?.statusMessage ?? t("errors.unknown"),
|
||||
]),
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
}
|
||||
createModal(
|
||||
ModalType.Confirmation,
|
||||
{
|
||||
title: t("library.admin.sources.deleteTitle"),
|
||||
description: t("library.admin.sources.deleteDesc", [source.name]),
|
||||
buttonText: t("library.admin.sources.deleteButton"),
|
||||
},
|
||||
async (event, close) => {
|
||||
if (event !== "confirm") return close();
|
||||
|
||||
sources.value.splice(index, 1);
|
||||
try {
|
||||
await $dropFetch("/api/v1/admin/library/sources", {
|
||||
method: "DELETE",
|
||||
body: { id: source.id },
|
||||
headers,
|
||||
});
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: t("errors.library.source.delete.title"),
|
||||
description: t("errors.library.source.delete.desc", [
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
e?.statusMessage ?? t("errors.unknown"),
|
||||
]),
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
return close();
|
||||
}
|
||||
|
||||
const currentIndex = sources.value.findIndex((s) => s.id === source.id);
|
||||
if (currentIndex !== -1) sources.value.splice(currentIndex, 1);
|
||||
close();
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,9 +14,9 @@ export const FilesystemProviderConfig = type({
|
||||
baseDir: "string",
|
||||
});
|
||||
|
||||
export class FilesystemProvider
|
||||
implements LibraryProvider<typeof FilesystemProviderConfig.infer>
|
||||
{
|
||||
export class FilesystemProvider implements LibraryProvider<
|
||||
typeof FilesystemProviderConfig.infer
|
||||
> {
|
||||
private config: typeof FilesystemProviderConfig.infer;
|
||||
private myId: string;
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ export const FlatFilesystemProviderConfig = type({
|
||||
baseDir: "string",
|
||||
});
|
||||
|
||||
export class FlatFilesystemProvider
|
||||
implements LibraryProvider<typeof FlatFilesystemProviderConfig.infer>
|
||||
{
|
||||
export class FlatFilesystemProvider implements LibraryProvider<
|
||||
typeof FlatFilesystemProviderConfig.infer
|
||||
> {
|
||||
private config: typeof FlatFilesystemProviderConfig.infer;
|
||||
private myId: string;
|
||||
|
||||
|
||||
@@ -188,7 +188,10 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
return url.pathname.replace("/games/", "").replace(/\/$/, "");
|
||||
}
|
||||
default: {
|
||||
logger.warn("Pcgamingwiki, unknown host", url.hostname);
|
||||
logger.warn(
|
||||
{ hostname: url.hostname },
|
||||
"Pcgamingwiki, unknown host",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -222,8 +225,8 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
});
|
||||
if (ratingObj instanceof type.errors) {
|
||||
logger.info(
|
||||
{ summary: ratingObj.summary },
|
||||
"pcgamingwiki: failed to properly get review rating",
|
||||
ratingObj.summary,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
@@ -327,7 +330,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
* @returns
|
||||
*/
|
||||
private parseTS(isoStr: string): DateTime {
|
||||
return DateTime.fromISO(isoStr.split(";")[0]);
|
||||
return DateTime.fromISO(isoStr.split(";")[0]!);
|
||||
}
|
||||
|
||||
private parseWebsitesGetFirst(websiteStr?: string | null): string {
|
||||
@@ -429,7 +432,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
);
|
||||
|
||||
const released = game.Released
|
||||
? DateTime.fromISO(game.Released.split(";")[0]).toJSDate()
|
||||
? DateTime.fromISO(game.Released.split(";")[0]!).toJSDate()
|
||||
: new Date();
|
||||
|
||||
const metadata: GameMetadata = {
|
||||
|
||||
@@ -306,7 +306,8 @@ export class SteamProvider implements MetadataProvider {
|
||||
"https://store.steampowered.com/publisher/",
|
||||
),
|
||||
)
|
||||
.map((v) => v.attribs.href);
|
||||
.map((v) => v.attribs.href)
|
||||
.filter((v) => v !== undefined);
|
||||
|
||||
const companies: {
|
||||
[key: string]: {
|
||||
@@ -320,6 +321,8 @@ export class SteamProvider implements MetadataProvider {
|
||||
.substring("https://store.steampowered.com/".length, v.indexOf("?"))
|
||||
.split("/");
|
||||
|
||||
if (!type || !name) return;
|
||||
|
||||
companies[name] ??= { pub: false, dev: false };
|
||||
switch (type) {
|
||||
case "publisher":
|
||||
@@ -546,7 +549,9 @@ export class SteamProvider implements MetadataProvider {
|
||||
let titleMatch = ogTitleRegex.exec(html);
|
||||
titleMatch ??= titleTagRegex.exec(html);
|
||||
|
||||
return titleMatch ? this._decodeHtmlEntities(titleMatch[1]) : undefined;
|
||||
return titleMatch && titleMatch[1]
|
||||
? this._decodeHtmlEntities(titleMatch[1])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _extractDescription(html: string): string | undefined {
|
||||
@@ -558,7 +563,9 @@ export class SteamProvider implements MetadataProvider {
|
||||
let descMatch = ogDescRegex.exec(html);
|
||||
descMatch ??= nameDescRegex.exec(html);
|
||||
|
||||
return descMatch ? this._decodeHtmlEntities(descMatch[1]) : undefined;
|
||||
return descMatch && descMatch[1]
|
||||
? this._decodeHtmlEntities(descMatch[1])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _extractImage(html: string): string | undefined {
|
||||
@@ -583,6 +590,7 @@ export class SteamProvider implements MetadataProvider {
|
||||
curatorUrlMatch ??= linkfilterRegex.exec(html);
|
||||
|
||||
if (!curatorUrlMatch) return undefined;
|
||||
if (!curatorUrlMatch[1]) return undefined;
|
||||
|
||||
try {
|
||||
return decodeURIComponent(curatorUrlMatch[1]);
|
||||
@@ -601,11 +609,12 @@ export class SteamProvider implements MetadataProvider {
|
||||
bannerMatch ??= backgroundImageRegex.exec(html);
|
||||
|
||||
if (!bannerMatch) return undefined;
|
||||
if (!bannerMatch[1]) return undefined;
|
||||
|
||||
let bannerUrl = bannerMatch[1].replace(/['"]/g, "");
|
||||
// Clean up the URL
|
||||
if (bannerUrl.includes("?")) {
|
||||
bannerUrl = bannerUrl.split("?")[0];
|
||||
bannerUrl = bannerUrl.split("?")[0]!;
|
||||
}
|
||||
return bannerUrl;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,10 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||
const metadata = objectMetadata(metadataRaw);
|
||||
if (metadata instanceof type.errors) {
|
||||
logger.error("FsObjectBackend#fetchMetadata", metadata.summary);
|
||||
logger.error(
|
||||
{ summary: metadata.summary },
|
||||
"FsObjectBackend#fetchMetadata",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
await this.metadataCache.set(id, metadata);
|
||||
@@ -198,8 +201,8 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
);
|
||||
} catch (error) {
|
||||
cleanupLogger.error(
|
||||
{ error },
|
||||
`[FsObjectBackend#cleanupMetadata]: Failed to remove ${file}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ class TaskHandler {
|
||||
parentTask?.progress ??
|
||||
((progress: number) => {
|
||||
if (progress < 0 || progress > 100) {
|
||||
logger.error("Progress must be between 0 and 100", { progress });
|
||||
logger.error({ progress }, "Progress must be between 0 and 100");
|
||||
return;
|
||||
}
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
|
||||
@@ -49,10 +49,13 @@ export default defineDropTask({
|
||||
|
||||
// if response failed somehow
|
||||
if (!response.ok) {
|
||||
logger.info("Failed to check for update ", {
|
||||
status: response.status,
|
||||
body: response.body,
|
||||
});
|
||||
logger.info(
|
||||
{
|
||||
status: response.status,
|
||||
body: response.body,
|
||||
},
|
||||
"Failed to check for update ",
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to check for update: ${response.status} ${response.body}`,
|
||||
|
||||
@@ -41,7 +41,7 @@ export default defineConfig({
|
||||
{ slug: "user" },
|
||||
{
|
||||
label: "Install",
|
||||
autogenerate: { directory: "user/install" },
|
||||
items: [{ autogenerate: { directory: "user/install" } }],
|
||||
},
|
||||
{
|
||||
label: "Usage",
|
||||
@@ -65,25 +65,26 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
label: "Going further",
|
||||
autogenerate: { directory: "admin/going-further" },
|
||||
items: [{ autogenerate: { directory: "admin/going-further" } }],
|
||||
},
|
||||
{
|
||||
label: "Metadata",
|
||||
autogenerate: { directory: "admin/metadata" },
|
||||
items: [{ autogenerate: { directory: "admin/metadata" } }],
|
||||
},
|
||||
{
|
||||
label: "Authentication",
|
||||
autogenerate: { directory: "admin/authentication" },
|
||||
items: [{ autogenerate: { directory: "admin/authentication" } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Reference",
|
||||
autogenerate: { directory: "reference" },
|
||||
items: [{ autogenerate: { directory: "reference" } }],
|
||||
},
|
||||
],
|
||||
customCss: ["./src/styles/drop.css"],
|
||||
}),
|
||||
],
|
||||
site: "https://docs-next.droposs.org/",
|
||||
site: "https://droposs.org",
|
||||
base: "/docs",
|
||||
});
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.4",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2",
|
||||
"starlight-image-zoom": "^0.13.2",
|
||||
"starlight-links-validator": "^0.19.2",
|
||||
"@astrojs/starlight": "^0.40.0",
|
||||
"astro": "^6.4.8",
|
||||
"sharp": "^0.35.2",
|
||||
"starlight-image-zoom": "^0.14.2",
|
||||
"starlight-links-validator": "^0.24.1",
|
||||
"starlight-theme-rapide": "^0.5.2"
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ For convenience's sake, we can also specify file extensions for Drop's auto-dete
|
||||
|
||||
Add a launch executable for every platform you want to support. The options are fairly self-explanatory, but make sure to use the `{rom}` placeholder, and optionally add the file extensions.
|
||||
|
||||
Read the [Command Parsing](/reference/command-parsing/) article to understand how it's parsed and substituted.
|
||||
Read the [Command Parsing](/docs/reference/command-parsing/) article to understand how it's parsed and substituted.
|
||||
|
||||
3. ## Import your game
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ If you're using a library source that supports versioning, you can add and impor
|
||||
|
||||
2. ### Follow the import guide again
|
||||
|
||||
Follow the [import guide again](/admin/guides/import-version/), but this time for your new version folder.
|
||||
Follow the [import guide again](/docs/admin/guides/import-version/), but this time for your new version folder.
|
||||
|
||||
</Steps>
|
||||
|
||||
@@ -39,7 +39,7 @@ You can stack many "update mode" versions on top of each other, and they will pi
|
||||
|
||||
2. ### Follow the import guide again
|
||||
|
||||
Follow the [import guide again](/admin/guides/import-version/), but this time for your new version folder.
|
||||
Follow the [import guide again](/docs/admin/guides/import-version/), but this time for your new version folder.
|
||||
|
||||
3. ### Before import, enable update mode
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Setting up OIDC
|
||||
---
|
||||
|
||||
:::note
|
||||
You can find reference information in the [OIDC authentication docs](/admin/authentication/oidc/).
|
||||
You can find reference information in the [OIDC authentication docs](/docs/admin/authentication/oidc/).
|
||||
:::
|
||||
|
||||
## Authentik
|
||||
|
||||
@@ -10,7 +10,7 @@ To import games and start using Drop, you must first create a library to import
|
||||
|
||||
1. **Decide on a library layout.**
|
||||
|
||||
Drop supports different layouts for your files on disk, you can read more about them in the [Library Sources](/reference/library-sources) reference section.
|
||||
Drop supports different layouts for your files on disk, you can read more about them in the [Library Sources](/docs/reference/library-sources) reference section.
|
||||
|
||||
2. **Mount your library in the Docker container.**
|
||||
|
||||
@@ -28,7 +28,7 @@ To import games and start using Drop, you must first create a library to import
|
||||
- `/mnt/media/my-drop-library` is the path to your library.
|
||||
- `/library` is a **unique** path inside the container. **Use something else if another volume mounts to `/library`**.
|
||||
|
||||
If you followed the [Quickstart](/admin/quickstart/) guide, you'll have already set up a library at `./library` pointing to `/library` within the container. You may want to instead edit that line in the `volumes` section to point to where your library is located.
|
||||
If you followed the [Quickstart](/docs/admin/quickstart/) guide, you'll have already set up a library at `./library` pointing to `/library` within the container. You may want to instead edit that line in the `volumes` section to point to where your library is located.
|
||||
|
||||
3. **Open library source interface in Admin Dashboard.**
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Drop automatically parses and formats the URL, so there are no requirements on t
|
||||
|
||||
## LAN
|
||||
|
||||
The `compose.yaml` provided in the [Quickstart guide](/admin/quickstart/) already exposes the Drop instance on port 3000. If you're on the same LAN as your Drop instance, you can find it's IP and then use:
|
||||
The `compose.yaml` provided in the [Quickstart guide](/docs/admin/quickstart/) already exposes the Drop instance on port 3000. If you're on the same LAN as your Drop instance, you can find it's IP and then use:
|
||||
|
||||
```
|
||||
http://[instance IP]:3000
|
||||
|
||||
@@ -60,7 +60,7 @@ Once you've got a library set up, and have imported a game, you can import a ver
|
||||
A installer version uses "setup mode". Enable the option, and then add the installer executable in setup commands.
|
||||
|
||||
:::note
|
||||
Setup and launch commands are parsed in a cross-platform, POSIX style. It's not relevant for simple setups, but useful to know. Read more about it in [Command Parsing](/reference/command-parsing/).
|
||||
Setup and launch commands are parsed in a cross-platform, POSIX style. It's not relevant for simple setups, but useful to know. Read more about it in [Command Parsing](/docs/reference/command-parsing/).
|
||||
:::
|
||||
|
||||
6. ### **Wait for import.**
|
||||
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
|
||||
**The main things in this `compose.yaml` is the volumes attached to the `drop` service:**
|
||||
|
||||
1. `./library` is where you will put your games to be imported into Drop. See '[Creating a library](/admin/guides/creating-library/)' once you're set up.
|
||||
1. `./library` is where you will put your games to be imported into Drop. See '[Creating a library](/docs/admin/guides/creating-library/)' once you're set up.
|
||||
2. `./data` is where Drop will store anything that's using the default file-system backed storage system. Typically, these are objects.
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -7,7 +7,7 @@ hero:
|
||||
file: ../../assets/drop.svg
|
||||
actions:
|
||||
- text: Quickstart
|
||||
link: /admin/quickstart
|
||||
link: /docs/admin/quickstart
|
||||
icon: right-arrow
|
||||
- text: Download client
|
||||
link: https://droposs.org/download
|
||||
|
||||
@@ -89,7 +89,7 @@ Drop's [dockerfile](https://github.com/Drop-OSS/drop/blob/develop/Dockerfile) pr
|
||||
:::
|
||||
|
||||
```bash
|
||||
npm install prisma@7.3.0 dotenv # dotenv is required
|
||||
npm install prisma@7.7.0 dotenv # dotenv is required
|
||||
```
|
||||
|
||||
Then, with your database running:
|
||||
|
||||
@@ -21,12 +21,40 @@ Then, what happens with this, depends on the type of game we're launching:
|
||||
|
||||
## Normal (no emulator)
|
||||
|
||||
Drop reconstructs the original shell string, and passes it into platform-specific command wrappers. For Windows, this means nothing. For Linux, it gets wrapped in `umu-run`.
|
||||
Drop reconstructs the original shell string, and passes it into a platform-specific command wrapper, called a **launch method**. Drop picks a sensible launch method automatically, but you can override it per-game for troubleshooting — see [Launch methods](#launch-methods) below.
|
||||
|
||||
By default, on Windows the command is launched based on its file type: `.exe` files run directly, `.bat` and `.cmd` files run through `cmd`, `.ps1` files run through PowerShell, and anything else is handed to `cmd` so builtins, `PATHEXT` resolution and `%VAR%` expansion all work. On Linux, native games run directly on the host, while games targeting Windows are wrapped in `umu-run` (with Proton).
|
||||
|
||||
It is then parsed again, and then passed into process creation, mapping the environment variable, command, and arguments into their respective platform-dependent places.
|
||||
|
||||
Drop logs out it's final parsed command, if you want to look at it in the client logs.
|
||||
|
||||
## Launch methods
|
||||
|
||||
The wrapper Drop uses to start a game is called a **launch method** (a *process handler* internally). Drop automatically selects the best available method for each game, but if a game won't launch you can override it under **Game Options → Launch → Launch method**.
|
||||
|
||||
Only methods supported by your current platform (and the game's target platform) are listed, each with a short description in the client.
|
||||
|
||||
### Windows
|
||||
|
||||
| Method | Description |
|
||||
| ------ | ----------- |
|
||||
| **Automatic** *(default)* | Detects the file type and launches it directly, or through `cmd` or PowerShell. |
|
||||
| **Direct executable** | Runs the executable directly, without a shell. |
|
||||
| **Command Prompt (cmd)** | Launches through `cmd.exe`. Supports batch files, builtins and `%VAR%` expansion. |
|
||||
| **PowerShell** | Runs the command as a PowerShell script (`-File`). |
|
||||
|
||||
### Linux
|
||||
|
||||
| Method | Description |
|
||||
| ------ | ----------- |
|
||||
| **Native (direct)** *(default for Linux games)* | Runs the native Linux game directly on the host. |
|
||||
| **Steam Linux Runtime (umu-run)** | Runs the native Linux game inside `umu-run`'s Steam Linux Runtime. Requires [UMU launcher](/docs/user/usage/proton/). |
|
||||
| **Proton (umu-run)** *(default for Windows games)* | Runs a Windows game through Proton, using `umu-run`. Requires [Proton](/docs/user/usage/proton/). |
|
||||
| **Proton + muvm (Asahi)** | Runs a Windows game through Proton inside a muvm microVM, for Apple Silicon / Asahi Linux. |
|
||||
|
||||
On macOS, games are always launched directly.
|
||||
|
||||
## Emulators
|
||||
For emulators, we have the "emulator version" (version containing the emulator), and the "emulated version" (version containing the ROM).
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ In the UI, you'll be prompted to "import" each folder separately:
|
||||
|
||||
So your game has gotten an update and you've got new files. All you need to do is create a new version folder inside the game folder, and move all the files you have into that folder. Then, import it within the Drop admin UI.
|
||||
|
||||
If you have files that you're supposed to **paste over the previous version**, Drop supports that! Read [Update mode](/reference/update-mode/) to find out more.
|
||||
If you have files that you're supposed to **paste over the previous version**, Drop supports that! Read [Update mode](/docs/reference/update-mode/) to find out more.
|
||||
|
||||
# Compatibility (flat-style)
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@ title: Getting Started
|
||||
|
||||
Drop clients are available for download from [our website](https://droposs.org/download), or follow one of our installation guides on the sidebar. Download the correct version for your platform, and open it up.
|
||||
|
||||
The client will walk you through the setup and sign-in process to get started. You'll need a Drop instance you can connect to, and an account on the server. If you don't have one, you can follow the [Quickstart](/admin/quickstart/) guide to set up your own.
|
||||
The client will walk you through the setup and sign-in process to get started. You'll need a Drop instance you can connect to, and an account on the server. If you don't have one, you can follow the [Quickstart](/docs/admin/quickstart/) guide to set up your own.
|
||||
|
||||
@@ -55,4 +55,8 @@ To launch any Windows game, you **must** first set a default Proton version.
|
||||
|
||||
Drop uses a global default Proton version to launch games by default. You can override this in a game's options.
|
||||
|
||||

|
||||

|
||||
|
||||
## Choosing a launch method
|
||||
|
||||
Proton isn't the only thing you can change per-game. If a game won't start, you can also try a different **launch method** from the same **Game Options → Launch** menu — for example, forcing a Windows game through Proton, or running a native Linux game inside the Steam Linux Runtime. See [Launch methods](/docs/reference/command-parsing/#launch-methods) for the full list.
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
name: Deploy Next.js site to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the main branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
# 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, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Setup Pages
|
||||
id: setup_pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.next/cache
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[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 with Next.js
|
||||
run: pnpm run build
|
||||
env:
|
||||
PAGES_BASE_PATH: ${{ steps.setup_pages.outputs.base_path }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./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
|
||||
@@ -46,7 +46,7 @@ function Header() {
|
||||
<div className="flex flex-col gap-y-2 border-b border-dotted border-zinc-800 pb-4">
|
||||
<dt className="text-sm/6 text-zinc-400">Lines of code</dt>
|
||||
<dd className="order-first text-6xl font-medium tracking-tight">
|
||||
<AnimatedNumber start={10} end={40} />k
|
||||
<AnimatedNumber start={0} end={75} />k
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-2 border-b border-dotted border-zinc-800 pb-4">
|
||||
@@ -61,7 +61,7 @@ function Header() {
|
||||
<div className="flex flex-col gap-y-2 max-sm:border-b max-sm:border-dotted max-sm:border-gray-200 max-sm:pb-4">
|
||||
<dt className="text-sm/6 text-zinc-400">Docker pulls</dt>
|
||||
<dd className="order-first text-6xl font-medium tracking-tight">
|
||||
<AnimatedNumber start={0} end={48.8} decimals={1} />k
|
||||
<AnimatedNumber start={0} end={210} decimals={1} />k
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
|
||||
@@ -6,7 +6,14 @@ import { Gradient } from '@/components/gradient'
|
||||
import { LogoCluster } from '@/components/logo-cluster'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
import { Heading, Subheading } from '@/components/text'
|
||||
import { ArrowDownCircleIcon } from '@heroicons/react/24/solid'
|
||||
import {
|
||||
BuildingStorefrontIcon,
|
||||
CloudArrowDownIcon,
|
||||
ComputerDesktopIcon,
|
||||
PencilSquareIcon,
|
||||
ServerStackIcon,
|
||||
ShieldCheckIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -29,7 +36,7 @@ function Hero() {
|
||||
Steam and Epic.
|
||||
</p>
|
||||
<div className="mt-12 flex flex-col gap-x-6 gap-y-4 sm:flex-row">
|
||||
<Button href="https://docs.droposs.org/docs/guides/quickstart">
|
||||
<Button href="/docs/admin/quickstart">
|
||||
Get started
|
||||
</Button>
|
||||
<Button variant="outline" href="/about">
|
||||
@@ -54,7 +61,7 @@ function FeatureSection() {
|
||||
|
||||
<p className="mt-6 text-lg/8 text-zinc-400">
|
||||
Drop is built from the ground up to be flexible, fast, and
|
||||
beautiful. It's designed to scale with your library, and handle
|
||||
beautiful. It's designed to scale with your library, and handle
|
||||
thousands of games.
|
||||
</p>
|
||||
</div>
|
||||
@@ -77,13 +84,80 @@ function FeatureSection() {
|
||||
<dl className="mx-auto grid max-w-2xl grid-cols-1 gap-x-6 gap-y-10 text-base/7 text-zinc-400 sm:grid-cols-2 lg:mx-0 lg:max-w-none lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
|
||||
<div className="relative pl-9">
|
||||
<dt className="inline font-semibold text-zinc-100">
|
||||
<ArrowDownCircleIcon
|
||||
<ServerStackIcon
|
||||
aria-hidden="true"
|
||||
className="absolute top-1 left-1 size-5 text-blue-600"
|
||||
/>
|
||||
ADASDASD
|
||||
Self-hosted & open-source.
|
||||
</dt>{' '}
|
||||
<dd className="inline">ASDASDASDAS</dd>
|
||||
<dd className="inline">
|
||||
Run Drop entirely on your own hardware. Your library, your data,
|
||||
your rules — all under the AGPLv3.
|
||||
</dd>
|
||||
</div>
|
||||
<div className="relative pl-9">
|
||||
<dt className="inline font-semibold text-zinc-100">
|
||||
<PencilSquareIcon
|
||||
aria-hidden="true"
|
||||
className="absolute top-1 left-1 size-5 text-blue-600"
|
||||
/>
|
||||
Rich metadata editing.
|
||||
</dt>{' '}
|
||||
<dd className="inline">
|
||||
Customise names, descriptions, and icons with full Markdown and
|
||||
image support.
|
||||
</dd>
|
||||
</div>
|
||||
<div className="relative pl-9">
|
||||
<dt className="inline font-semibold text-zinc-100">
|
||||
<CloudArrowDownIcon
|
||||
aria-hidden="true"
|
||||
className="absolute top-1 left-1 size-5 text-blue-600"
|
||||
/>
|
||||
Automatic imports.
|
||||
</dt>{' '}
|
||||
<dd className="inline">
|
||||
Pull cover art and game details straight from IGDB, GiantBomb, and
|
||||
PCGamingWiki.
|
||||
</dd>
|
||||
</div>
|
||||
<div className="relative pl-9">
|
||||
<dt className="inline font-semibold text-zinc-100">
|
||||
<BuildingStorefrontIcon
|
||||
aria-hidden="true"
|
||||
className="absolute top-1 left-1 size-5 text-blue-600"
|
||||
/>
|
||||
A built-in store.
|
||||
</dt>{' '}
|
||||
<dd className="inline">
|
||||
Let users browse, filter, and collect games through a fully
|
||||
featured store.
|
||||
</dd>
|
||||
</div>
|
||||
<div className="relative pl-9">
|
||||
<dt className="inline font-semibold text-zinc-100">
|
||||
<ShieldCheckIcon
|
||||
aria-hidden="true"
|
||||
className="absolute top-1 left-1 size-5 text-blue-600"
|
||||
/>
|
||||
Flexible authentication.
|
||||
</dt>{' '}
|
||||
<dd className="inline">
|
||||
Use simple accounts or hook into your existing SSO.
|
||||
</dd>
|
||||
</div>
|
||||
<div className="relative pl-9">
|
||||
<dt className="inline font-semibold text-zinc-100">
|
||||
<ComputerDesktopIcon
|
||||
aria-hidden="true"
|
||||
className="absolute top-1 left-1 size-5 text-blue-600"
|
||||
/>
|
||||
Native desktop client.
|
||||
</dt>{' '}
|
||||
<dd className="inline">
|
||||
Download, install, and play your whole library through a
|
||||
cross-platform desktop client.
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
@@ -127,7 +201,7 @@ function BentoSection() {
|
||||
<BentoCard
|
||||
eyebrow="Authentication"
|
||||
title="Flexible authentication"
|
||||
description="Drop supports both simple and SSO authentication, with more features like SCIM on the way."
|
||||
description="Drop supports both simple and SSO authentication, so users can sign in however suits them."
|
||||
graphic={
|
||||
<div className="flex h-full w-full items-center justify-center p-4">
|
||||
<div className="bg-position-center h-full w-full grow rounded-lg bg-[url(/screenshots/authentication.png)] bg-cover bg-no-repeat" />
|
||||
|
||||
@@ -35,6 +35,12 @@ export function BentoCard({
|
||||
>
|
||||
<div className="relative h-80 shrink-0">
|
||||
{graphic}
|
||||
{fade.includes('top') && (
|
||||
<div className="absolute inset-0 bg-linear-to-b from-zinc-900 to-50%" />
|
||||
)}
|
||||
{fade.includes('bottom') && (
|
||||
<div className="absolute inset-0 bg-linear-to-t from-zinc-900 to-50%" />
|
||||
)}
|
||||
</div>
|
||||
<div className="relative p-10">
|
||||
<Subheading as="h3" dark={dark}>
|
||||
|
||||
@@ -23,7 +23,7 @@ function CallToAction() {
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
href="https://docs.droposs.org/docs/guides/quickstart"
|
||||
href="/docs/admin/quickstart"
|
||||
>
|
||||
Quickstart →
|
||||
</Button>
|
||||
@@ -65,7 +65,7 @@ function Sitemap() {
|
||||
<div>
|
||||
<SitemapHeading>Documentation</SitemapHeading>
|
||||
<SitemapLinks>
|
||||
<SitemapLink href="https://docs.droposs.org/">
|
||||
<SitemapLink href="/docs">
|
||||
Self-hosters
|
||||
</SitemapLink>
|
||||
<SitemapLink href="https://developer.droposs.org/">
|
||||
|
||||
@@ -80,11 +80,11 @@ export function Gallery() {
|
||||
)
|
||||
.map((file) => (
|
||||
<div key={file.url} className="relative w-full">
|
||||
<div className="group overflow-hidden rounded-lg bg-gray-100 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-blue-600">
|
||||
<div className="group relative block w-full overflow-hidden rounded-lg bg-gray-100 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-blue-600">
|
||||
<img
|
||||
alt=""
|
||||
src={file.url}
|
||||
className="pointer-events-none aspect-10/7 aspect-auto rounded-lg object-cover outline -outline-offset-1 outline-black/5 group-hover:opacity-75"
|
||||
className="pointer-events-none block w-full rounded-lg object-cover outline -outline-offset-1 outline-black/5 group-hover:opacity-75"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -96,10 +96,10 @@ export function Gallery() {
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="pointer-events-none mt-2 block truncate text-sm font-medium text-gray-900">
|
||||
<p className="pointer-events-none mt-2 block truncate text-sm font-medium text-zinc-100">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="pointer-events-none block text-xs font-medium text-gray-500">
|
||||
<p className="pointer-events-none block text-xs font-medium text-zinc-400">
|
||||
{file.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@ function Circles() {
|
||||
<Circle size={400} opacity="5%" delay={0.3} />
|
||||
<Circle size={272} opacity="5%" delay={0.15} />
|
||||
<Circle size={144} opacity="10%" delay={0} />
|
||||
<div className="absolute inset-0 bg-linear-to-t from-white to-35%" />
|
||||
<div className="absolute inset-0 bg-linear-to-t from-zinc-900 to-35%" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ function SponsorCard({
|
||||
className="relative flex w-64 rounded-3xl sm:w-72 bg-black"
|
||||
>
|
||||
<figure className="relative p-10">
|
||||
<img alt={name} src={img} className="mb-4 size-12 rounded-full" />
|
||||
<figcaption className="pb-3 border-b border-white/20">
|
||||
<p className="text-sm/6 font-medium text-white">{name}</p>
|
||||
<p className="text-sm/6 font-medium">
|
||||
|
||||
Reference in New Issue
Block a user