Compare commits

...

8 Commits

Author SHA1 Message Date
DecDuck 7c629a2f26 Add #269 2026-06-21 19:32:54 +10:00
DecDuck 5bab286061 Implement #268 2026-06-21 18:43:08 +10:00
DecDuck 505c324c26 Fix #414 2026-06-21 17:01:37 +10:00
DecDuck cbecd1161d Publish docs, update links (#431)
* Publish docs, update links

* Fix sitemap gen

* Migrate to Astro v6

* Fix server lint
2026-06-21 16:39:34 +10:00
DecDuck 9185089c99 Fix v0.4.0 process handler, add override menu (#430)
* Fix Windows and Linux launch

* Add process handler selector, pin Prisma

* Regenerate lcofkiel

* Fix torrential inclusion in image

* Fix layouting

* Implement tree kill for Windows

* Fix server lint
2026-06-21 15:24:33 +10:00
DecDuck 0290718ee0 Fix droposs.org build, finish website (#429)
* Fix compile issues

* Finish up website
2026-06-21 11:31:21 +10:00
DecDuck 2e86422004 Add lints, new website publish (#428)
* Add lints and new website

* Fix droplet CI

* Fix droplet ci again

* Fix clippy lints
2026-06-21 11:16:39 +10:00
DecDuck 796abf478f Fix GitHub Actions build (#427)
* Fix server build

* Remove server drop-base submod

* Update lockfile

* Use debian images for build

* Fix pino errors, lint

* Fix macOS keychain lookup
2026-06-21 10:37:54 +10:00
77 changed files with 2883 additions and 2880 deletions
+10 -2
View File
@@ -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:
+56
View File
@@ -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
+100
View File
@@ -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
-2
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -6,7 +6,7 @@
# Drop
[![Website](https://img.shields.io/badge/website-000000?style=for-the-badge&logo=About.me&logoColor=white)](https://droposs.org)
[![Docs](https://img.shields.io/badge/DOCS-black?style=for-the-badge&logo=docusaurus)](https://docs.droposs.org/)
[![Docs](https://img.shields.io/badge/DOCS-black?style=for-the-badge&logo=docusaurus)](https://droposs.org/docs)
[![Static Badge](https://img.shields.io/badge/FORUM-blue?style=for-the-badge)](https://forum.droposs.org)
[![GitHub License](https://img.shields.io/badge/AGPL--3.0-red?style=for-the-badge)](LICENSE)
[![Discord](https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/ACq4qZp4a9)
@@ -28,7 +28,7 @@ Drop is an open-source game distribution platform, similar to GameVault or Steam
## Deployment
See our documentation on how to [deploy Drop](https://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
View File
@@ -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.
-20
View File
@@ -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.
-23
View File
@@ -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
View File
@@ -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>
+3 -2
View File
@@ -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>
+1 -1
View File
@@ -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"
+794 -1525
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -53,6 +53,7 @@ export type GameVersion = {
userConfiguration: {
launchTemplate: string;
overrideProtonPath: string;
overrideHandler: string | undefined;
enableUpdates: boolean
};
setups: Array<{ platform: string }>;
+1 -1
View File
@@ -7,7 +7,7 @@
"tauri": "tauri"
},
"dependencies": {
"pino": "^9.7.0",
"pino": "^9.14.0",
"pino-pretty": "^13.1.1",
"tauri": "^0.15.0"
},
+1
View File
@@ -1392,6 +1392,7 @@ dependencies = [
"http-serde 2.1.1",
"humansize",
"known-folders",
"libloading",
"log",
"log4rs",
"md5 0.7.0",
+3
View File
@@ -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"
+3
View File
@@ -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;
}
+50 -10
View File
@@ -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
+6 -1
View File
@@ -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
View File
@@ -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
-2
View File
@@ -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 -1
View File
@@ -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 -3
View File
@@ -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());
+6 -4
View File
@@ -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)?))));
-1
View File
@@ -24,4 +24,3 @@ pub struct Manifest {
pub size: u64,
pub key: [u8; 16],
}
+947 -679
View File
File diff suppressed because it is too large Load Diff
-158
View File
@@ -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
View File
@@ -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
View File
@@ -5,4 +5,4 @@ plugins:
opt: target=ts
inputs:
- directory: ../
- directory: ../
+2 -2
View File
@@ -19,7 +19,7 @@
</p>
<NuxtLink
class="mt-4 rounded-md inline-flex items-center text-sm font-semibold text-blue-500 hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
href="https://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
+3 -2
View File
@@ -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,
},
};
+2 -2
View File
@@ -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: [
+6 -1
View File
@@ -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
+4 -4
View File
@@ -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 } },
},
}
},
]);
+3
View File
@@ -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
View File
@@ -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);
+1 -2
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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
+39 -23
View File
@@ -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 = {
+13 -4
View File
@@ -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;
}
+5 -2
View File
@@ -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,
);
}
}
+1 -1
View File
@@ -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}`,
+7 -6
View File
@@ -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",
});
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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.
![Screenshot showing how to override the proton version](./proton-options-override.png)
![Screenshot showing how to override the proton version](./proton-options-override.png)
## 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
View File
@@ -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
+2 -2
View File
@@ -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">
+81 -7
View File
@@ -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&apos;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 &amp; open-source.
</dt>{' '}
<dd className="inline">ASDASDASDAS</dd>
<dd className="inline">
Run Drop entirely on your own hardware. Your library, your data,
your rules &mdash; 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}>
+2 -2
View File
@@ -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 &rarr;
</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/">
+4 -4
View File
@@ -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>
+1 -1
View File
@@ -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>
)
}
+1
View File
@@ -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">