mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-06-22 12:21:34 +10:00
Compare commits
460 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fb0a185f6 | |||
| 70db79b50f | |||
| ac7ef6303b | |||
| efbc86e73e | |||
| a0bc4bbc4c | |||
| 90277653cb | |||
| ac355918ed | |||
| d6830c3428 | |||
| cbf480bef9 | |||
| afaaaf2eb5 | |||
| 14f0833d17 | |||
| 7f7d8c8f45 | |||
| 52a7de0a8b | |||
| dbded55113 | |||
| aa3105aecd | |||
| 0c48d42c2d | |||
| 836ba33fe4 | |||
| 1f41e52a86 | |||
| 17e4734cfb | |||
| b681476373 | |||
| 3a9eb82fdf | |||
| 7e5e7b032b | |||
| df291c3e9a | |||
| bf691a7f5c | |||
| 597a2264e8 | |||
| 1a2d3c8207 | |||
| 471e85d7c6 | |||
| f9f437dd85 | |||
| 143846c48a | |||
| 2fb909f73d | |||
| 0f773a9779 | |||
| 92a98a5984 | |||
| 464af37afb | |||
| f8dc3fef55 | |||
| 6d35e2697d | |||
| 0d02be2392 | |||
| 48f796ae4b | |||
| 125fe9e6e2 | |||
| 29f3094ad4 | |||
| 733aee3977 | |||
| e3ed60feae | |||
| bfa2c0a641 | |||
| 952df560ec | |||
| 1db2229ad3 | |||
| 731499be81 | |||
| 5aa0899bcf | |||
| 30b065dde3 | |||
| 1f510bee57 | |||
| 07b34c874d | |||
| 19ff73cc30 | |||
| e8633ceca2 | |||
| 770294d559 | |||
| c0c55d35f4 | |||
| a47debda91 | |||
| c449b45009 | |||
| f1f19c8263 | |||
| 31ad8505b7 | |||
| feedcfc5c4 | |||
| a5facbd648 | |||
| 3b1d04251c | |||
| fd4ec3fd1c | |||
| 0a270b267c | |||
| ec6d38d7af | |||
| a9d8ddc0f5 | |||
| dada379354 | |||
| 86c7aa33ce | |||
| 26abb75e94 | |||
| 8eec8b19dd | |||
| 582acfb385 | |||
| 456902c784 | |||
| 87215c4a1e | |||
| d361e01eef | |||
| 8e109dd562 | |||
| 8f429e1e56 | |||
| e362f732e7 | |||
| 86c2d00726 | |||
| d4b89b5dc5 | |||
| ff1255f948 | |||
| 96742cc918 | |||
| c2bb835b0f | |||
| f384492ed2 | |||
| 22a7cfa544 | |||
| 228d109692 | |||
| dc89ff95d8 | |||
| 04c56fd985 | |||
| ca03be7f43 | |||
| 35a2d98790 | |||
| c4d8b24295 | |||
| 42349ad4e1 | |||
| e7566a6316 | |||
| fdffd9a32a | |||
| 4c3413ae63 | |||
| 30e3e7289a | |||
| 12ba416ca5 | |||
| e4aeaee6e7 | |||
| 9d943bc5dc | |||
| 66d1413eb5 | |||
| e572b61af9 | |||
| f9b774ddb5 | |||
| 106b3f66a4 | |||
| 657fd50702 | |||
| 7400fae11b | |||
| 043ef6dcd2 | |||
| 6ea50bffc8 | |||
| 9584d69e97 | |||
| 5ceff44993 | |||
| 372a9bdd97 | |||
| fe82c78571 | |||
| fd11d41dc5 | |||
| 9242a810b0 | |||
| 0b9d0a4ad9 | |||
| 17d3e0ef54 | |||
| 4fd2b159a6 | |||
| d6d457f999 | |||
| 54b3bc3a7e | |||
| 2cbee3d495 | |||
| 7263ec53ac | |||
| 0edfdbdfce | |||
| 114d235a6a | |||
| a47615a274 | |||
| 1987c578bc | |||
| b2327b21fe | |||
| b22681c555 | |||
| 931913b836 | |||
| 2be0e2f88c | |||
| 7df175b747 | |||
| b6d05a6d09 | |||
| 8d88728c99 | |||
| 7141735664 | |||
| 82baeb909a | |||
| 2a85322f64 | |||
| 088cb68604 | |||
| 81be7ccf58 | |||
| a9d1a442f6 | |||
| 97043d6366 | |||
| 756bf8f93f | |||
| 9dc35c80c5 | |||
| 0f35d4a445 | |||
| 57f50b0306 | |||
| 065951d91f | |||
| 36e6c92938 | |||
| e7109e58bb | |||
| 17372a9c06 | |||
| d7297707d7 | |||
| c809c8fcbf | |||
| 68f5f88347 | |||
| 6f8e28d711 | |||
| 47dc364d4e | |||
| 3b4f940983 | |||
| 1048653eef | |||
| f1c932b7d7 | |||
| 7c420ba7d7 | |||
| 7974361e5b | |||
| 01171d788c | |||
| eec709a6e9 | |||
| 5384759261 | |||
| e3022bc52b | |||
| c7af02c15e | |||
| 96a1199fff | |||
| 2cfc2cee7c | |||
| f5e52321b8 | |||
| 58d558159d | |||
| e4e1c66bca | |||
| 1996b97e99 | |||
| cb4937b590 | |||
| 57042892c4 | |||
| 1f309606c9 | |||
| f9e6702d40 | |||
| 690aa042df | |||
| 87f01a9984 | |||
| c1272dc7a7 | |||
| 257cdacad4 | |||
| 2c9fdebf25 | |||
| 2027c69c0e | |||
| e08a13f2c3 | |||
| 6ed7e76b17 | |||
| 573bf87cbb | |||
| e8afa274a7 | |||
| d4d1eaf2e2 | |||
| 6918e78cf9 | |||
| cd93ba2197 | |||
| c052511ff3 | |||
| 19d1a9dd0e | |||
| 66400f4875 | |||
| 88a5dc2a58 | |||
| cf0af15854 | |||
| 61764e81b8 | |||
| 98c8258127 | |||
| 3527f678e5 | |||
| fce084f95a | |||
| 1ad1ebb3fd | |||
| 1de9ebdfa5 | |||
| bd1cb67cd0 | |||
| 3225f536ce | |||
| 58f91f4ac4 | |||
| 8fc37936dc | |||
| 0ca9a3b2f7 | |||
| f8ae5b70c0 | |||
| 7a3b30b012 | |||
| 4e8cffd778 | |||
| bf7eb5b986 | |||
| 77d06df7d3 | |||
| 2755aa472b | |||
| 2b7bc6965d | |||
| 08164cae68 | |||
| 2ca96c34f7 | |||
| 4b4e067fac | |||
| d2b485456a | |||
| 793b57a163 | |||
| d9218dea59 | |||
| 789361ea73 | |||
| ffc1537d7f | |||
| 9d07070ef6 | |||
| 0f0874c10a | |||
| 137241fdbb | |||
| 9515a21dc6 | |||
| c3ee948682 | |||
| 9608cc9742 | |||
| 88aaa2a71b | |||
| 133503582a | |||
| 1eede0f035 | |||
| b6f52f660a | |||
| a1f65b7e59 | |||
| 1ce707788d | |||
| 31aaec74af | |||
| 97792f0707 | |||
| b6189d12e7 | |||
| 0877638fc4 | |||
| 090d2e6586 | |||
| a64a2479ba | |||
| d8d5b938f0 | |||
| 3afd36a821 | |||
| ce8887528f | |||
| d4dd259b5f | |||
| 256fbd6afa | |||
| 9344d94e4c | |||
| 1286248207 | |||
| 2ef8f2f93c | |||
| 86053815f0 | |||
| 88453f1ec4 | |||
| 623ab7d786 | |||
| 1ed15902a3 | |||
| 3a55075532 | |||
| 6c7866ad14 | |||
| f78b29b7fd | |||
| d8e964e06b | |||
| 5d8f9d3813 | |||
| 28bf070ce2 | |||
| 866c4d354e | |||
| e7837af0e7 | |||
| 97b9b6dc11 | |||
| 09fd01d9b5 | |||
| 8fbe02a1b7 | |||
| dce116b66f | |||
| d167780562 | |||
| 6e057afb6d | |||
| 1967de72c8 | |||
| bfcc7519bf | |||
| 1a2aca9999 | |||
| 282e5bc2a6 | |||
| f369462e7f | |||
| 6317ad2657 | |||
| 42ebbf2922 | |||
| 7c1dec9401 | |||
| ecd26a42a8 | |||
| cf0aa948fe | |||
| 934c176974 | |||
| eea8f82bf9 | |||
| 892f64fe3a | |||
| 6bc3173d3a | |||
| 93a58c0d04 | |||
| 3298b5f3ee | |||
| 6d03266ade | |||
| 1b3cf498f4 | |||
| 0bfe9803ac | |||
| 617278281e | |||
| 994db6c26a | |||
| 36776cc61e | |||
| a309651fe4 | |||
| 716eac79bf | |||
| f3ed0f6430 | |||
| c7eb11a836 | |||
| 39d7ce7d1b | |||
| 02d6346b01 | |||
| a8f21068fc | |||
| 2cfe75a551 | |||
| 5a1f8411de | |||
| a86045c307 | |||
| db103de24d | |||
| e505e58192 | |||
| b75ebd13b7 | |||
| 937954fa02 | |||
| 43e32b44a2 | |||
| 61d88c3091 | |||
| 12e312593e | |||
| 4f9b94921a | |||
| 0a5a649cfe | |||
| 2f52a16d52 | |||
| 38fc6b81df | |||
| 28baabc122 | |||
| 9b12d4576c | |||
| e5cf13fd93 | |||
| dbb315a8d7 | |||
| 2c19e13c09 | |||
| fe9373af78 | |||
| eadcaa1b05 | |||
| 5f29c28e04 | |||
| db916bf970 | |||
| 2309407ca6 | |||
| 5c78b20504 | |||
| cada630e81 | |||
| a361c38e82 | |||
| 0f10626b1b | |||
| 8945196633 | |||
| 31e8359ec0 | |||
| 089c3e03f6 | |||
| fd4a7d1981 | |||
| b50e27f4b0 | |||
| 54c5d55da7 | |||
| 25fc957092 | |||
| 5393db3236 | |||
| 6df560ca37 | |||
| 789d3ba2f2 | |||
| c4391d3f4d | |||
| 1f4d07568f | |||
| e408ac5df8 | |||
| 305de9f45a | |||
| ecc819eebc | |||
| 9cbdcbcdf6 | |||
| 9c2249ed08 | |||
| 7b3b919581 | |||
| ef8f3ae6fd | |||
| 7a88f4c52d | |||
| 6e6f09dba0 | |||
| 62ea9a116b | |||
| f7d767d73e | |||
| 26a31f6d56 | |||
| 5358f1f52c | |||
| bc0c47c487 | |||
| 8463e35a10 | |||
| 2859005ad4 | |||
| 44c60280ef | |||
| 9011cf5c83 | |||
| 2c21a235b2 | |||
| 87230fb0e7 | |||
| 4488ae269b | |||
| bfafe02d48 | |||
| d7160abc47 | |||
| 76bceb121f | |||
| 952ece8c83 | |||
| 33d37700e1 | |||
| a815542604 | |||
| e796b465d1 | |||
| b511b40d7c | |||
| 599da0e348 | |||
| c7b675f841 | |||
| be6c30dfee | |||
| 7d72a86876 | |||
| adb4b7381e | |||
| f2e018277b | |||
| 9c4b6f35bb | |||
| b9ae26cb27 | |||
| ce0a9ab538 | |||
| 8999303f0a | |||
| 69e4c2592b | |||
| 1d5e1bda85 | |||
| 39fe9d55fd | |||
| a8a152e578 | |||
| 74fa671b69 | |||
| 7b0756c6bd | |||
| 17971e0a5a | |||
| d3d93b03de | |||
| 22366221b2 | |||
| b0ef675e7e | |||
| cd0d2bfdea | |||
| 4273a20180 | |||
| c4a419f2e4 | |||
| 3a51c9cc9c | |||
| c3914cc1ed | |||
| 69f341b329 | |||
| b03d790247 | |||
| df41dd1c6f | |||
| ad25d3e462 | |||
| 8097dd6721 | |||
| 803752e745 | |||
| 6328c24537 | |||
| f40f8c87f4 | |||
| 886beb62ea | |||
| 0f80fcd830 | |||
| 52315d09da | |||
| 62a111b0fc | |||
| 7194d35cf5 | |||
| 03b0b0c38b | |||
| b744671e57 | |||
| a8f58eba7c | |||
| 46d35adfdb | |||
| eb66c5c07a | |||
| 13bfad4966 | |||
| e47944a3b0 | |||
| 9cb2d6d02f | |||
| 36568c3845 | |||
| a208fbedbd | |||
| 584bcf1cdf | |||
| c5d00b4766 | |||
| 5fe2036f0b | |||
| 583301ff40 | |||
| 88c95d6bf7 | |||
| 848a611751 | |||
| 2e44ef3501 | |||
| ecb381e1ca | |||
| b2ab827a55 | |||
| 46551f9330 | |||
| e4339c34ec | |||
| 7d2a1c6952 | |||
| 24a0d118f2 | |||
| ef13b68592 | |||
| c4a3e4e9a7 | |||
| 7f4db0c1dc | |||
| 3dd6062af4 | |||
| 6e2dc89670 | |||
| 93bc143dac | |||
| 03a37f72aa | |||
| 7e176262cc | |||
| e1c1d7ea39 | |||
| c355f6fdbb | |||
| 0a715fef08 | |||
| 395219d0cb | |||
| eb3f9f91ca | |||
| cf578bd005 | |||
| 1f575b2bc0 | |||
| 91b7e1071c | |||
| 329c74d3ce | |||
| 8674ac7211 | |||
| 328b9ba46c | |||
| 9b7ee4e746 | |||
| 27070b6a4c | |||
| 46c8f0c48a | |||
| a7c33e7d43 | |||
| 718f5ba514 | |||
| f3672f81dd | |||
| 6b5e48d6fe | |||
| 486bce8bc7 | |||
| 435551c207 | |||
| de388a937a | |||
| 4fa771a0b5 | |||
| 6ba5cdddc5 | |||
| d4e2dc8cb6 | |||
| 425934d3ef | |||
| c4d81135a2 | |||
| 2b4382d013 | |||
| 7523e536b5 | |||
| 909432a6ce | |||
| ceacd8469d | |||
| 7869043c28 | |||
| bfafd2a044 | |||
| 1bd19ad917 | |||
| e52f072091 | |||
| 22ac7f6b15 | |||
| 196f87c219 | |||
| e1a789fa36 |
+25
-5
@@ -1,6 +1,26 @@
|
||||
/sites
|
||||
/cli
|
||||
/desktop
|
||||
/backend # go backend
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
node_modules
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
.data
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop"
|
||||
|
||||
CLIENT_CERTIFICATES="./.data/ca"
|
||||
|
||||
FS_BACKEND_PATH="./.data/objects"
|
||||
|
||||
GIANT_BOMB_API_KEY=""
|
||||
|
||||
EXTERNAL_URL="http://localhost:3000"
|
||||
|
||||
NUXT_PORT=4000
|
||||
@@ -0,0 +1,46 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --network-timeout 1000000
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --network-timeout 1000000
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
@@ -1,140 +0,0 @@
|
||||
name: "Build and release desktop"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tagName:
|
||||
required: false
|
||||
type: string
|
||||
description: "tagName to be associated with this release."
|
||||
release:
|
||||
types: [published]
|
||||
# This can be used to automatically publish nightlies at UTC nighttime
|
||||
# schedule:
|
||||
# - cron: "0 2 * * *" # run at 2 AM UTC
|
||||
|
||||
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: "macos-14" # for Arm based macs (M1 and above).
|
||||
args: "--target aarch64-apple-darwin"
|
||||
- platform: "macos-14" # for Intel based macs.
|
||||
args: "--target x86_64-apple-darwin"
|
||||
- platform: "ubuntu-22.04" # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||
args: ""
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
args: "--target aarch64-unknown-linux-gnu"
|
||||
- platform: "windows-latest"
|
||||
args: ""
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: pnpm
|
||||
|
||||
|
||||
- name: install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||
targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './desktop/src-tauri -> target'
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-14'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security set-keychain-settings -t 3600 -u build.keychain
|
||||
|
||||
# Add build.keychain to the user keychain search list so that codesign
|
||||
# (invoked later by tauri-action WITHOUT an explicit --keychain) can
|
||||
# resolve the signing identity from it.
|
||||
security list-keychains -d user -s build.keychain $(security list-keychains -d user | tr -d '"')
|
||||
|
||||
echo "Created keychain"
|
||||
|
||||
curl https://droposs.org/drop.der --output drop.der
|
||||
|
||||
# swiftc libs/appletrust/add-certificate.swift
|
||||
# ./add-certificate drop.der
|
||||
# rm add-certificate
|
||||
|
||||
# echo "Added certificate to keychain using swift util"
|
||||
|
||||
## Script is equivalent to:
|
||||
sudo security authorizationdb write com.apple.trust-settings.admin allow
|
||||
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.der
|
||||
sudo security authorizationdb remove com.apple.trust-settings.admin
|
||||
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
echo "Imported certificate"
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security find-identity -v -p codesigning build.keychain
|
||||
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-14'
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported. Using identity: $CERT_ID"
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: pnpm install # change this to npm, pnpm or bun depending on which one you use.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Do NOT set APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD here. Doing so
|
||||
# makes tauri-action import the cert into its own throwaway keychain and
|
||||
# look up the identity by Apple-only name prefixes (e.g.
|
||||
# "Developer ID Application:"), which never matches our "Drop OSS" cert
|
||||
# and fails with "failed to resolve signing identity". Instead we rely on
|
||||
# the build.keychain prepared above and only pass the resolved identity.
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
NO_STRIP: true
|
||||
with:
|
||||
tagName: ${{ inputs.print_tags || 'v__VERSION__' }} # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseName: "Auto-release v__VERSION__"
|
||||
releaseBody: "See the assets to download this version and install. This release was created automatically."
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
args: ${{ matrix.args }}
|
||||
projectPath: './desktop'
|
||||
@@ -1,56 +0,0 @@
|
||||
name: Droplet CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "libraries/droplet/**"
|
||||
- "libraries/droplet_types/**"
|
||||
- "libraries/libarchive/**"
|
||||
- ".github/workflows/droplet-ci.yml"
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "libraries/droplet/**"
|
||||
- "libraries/droplet_types/**"
|
||||
- "libraries/libarchive/**"
|
||||
- ".github/workflows/droplet-ci.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: Build, Test, Lint
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: libraries/droplet
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./libraries/droplet -> target"
|
||||
|
||||
- name: Install libarchive
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-dev
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Run Clippy (lint)
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --all-features --all --verbose
|
||||
@@ -1,100 +0,0 @@
|
||||
name: Deploy website to GitHub Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "sites/promo/**"
|
||||
- "sites/docs/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- ".github/workflows/pages.yml"
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment per the "pages" group, skipping runs queued
|
||||
# between the in-progress run and the latest queued one. cancel-in-progress defaults
|
||||
# to false, so in-flight production deployments are allowed to complete.
|
||||
concurrency: "pages"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
# Only install the promo site (radiant) and docs site (docs-next) and their
|
||||
# dependencies so the public website deploy stays decoupled from the
|
||||
# server/desktop build pipelines.
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter radiant... --filter docs-next...
|
||||
|
||||
- name: Setup Pages
|
||||
id: setup_pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
sites/promo/.next/cache
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('sites/promo/**.[jt]s', 'sites/promo/**.[jt]sx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Build promo site with Next.js
|
||||
working-directory: sites/promo
|
||||
run: pnpm run build
|
||||
env:
|
||||
PAGES_BASE_PATH: ${{ steps.setup_pages.outputs.base_path }}
|
||||
|
||||
- name: Build docs site with Astro
|
||||
working-directory: sites/docs
|
||||
run: pnpm run build
|
||||
|
||||
# Nest the Starlight docs (built with base: "/docs") inside the promo export
|
||||
# so both ship from a single GitHub Pages deployment at /docs.
|
||||
- name: Assemble docs into /docs
|
||||
run: |
|
||||
rm -rf sites/promo/out/docs
|
||||
mkdir -p sites/promo/out/docs
|
||||
cp -r sites/docs/dist/. sites/promo/out/docs/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: sites/promo/out
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
|
||||
jobs:
|
||||
web:
|
||||
name: Push website Docker image to registry
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
buildkitd-flags: --debug
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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=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: Build and push image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,71 +0,0 @@
|
||||
name: Server CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "server/**"
|
||||
- "libraries/base/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- ".github/workflows/server-ci.yml"
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "server/**"
|
||||
- "libraries/base/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- ".github/workflows/server-ci.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: server
|
||||
run: pnpm run typecheck
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Lint
|
||||
working-directory: server
|
||||
run: pnpm run lint
|
||||
@@ -1,157 +0,0 @@
|
||||
name: Build and release server
|
||||
|
||||
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:
|
||||
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 }}
|
||||
+36
-2
@@ -1,2 +1,36 @@
|
||||
dist/
|
||||
node_modules/
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
.yarn
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
.data
|
||||
|
||||
|
||||
# deploy template
|
||||
deploy-template/*
|
||||
|
||||
!deploy-template/compose.yml
|
||||
|
||||
# generated prisma client
|
||||
/prisma/client
|
||||
@@ -0,0 +1,54 @@
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
services:
|
||||
- docker:24.0.5-dind
|
||||
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:latest
|
||||
variables:
|
||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA
|
||||
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest
|
||||
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
|
||||
script:
|
||||
- docker build -t $IMAGE_NAME .
|
||||
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
|
||||
- docker push $IMAGE_NAME
|
||||
- docker push $LATEST_IMAGE_NAME
|
||||
- |
|
||||
if [ $CI_COMMIT_TAG ]; then
|
||||
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
|
||||
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
docker push $PUBLISH_IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
fi
|
||||
|
||||
build-arm64:
|
||||
stage: build
|
||||
image: arm64v8/docker:latest
|
||||
tags:
|
||||
- aarch64
|
||||
variables:
|
||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA-arm64
|
||||
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest-arm64
|
||||
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-arm64
|
||||
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest-arm64
|
||||
script:
|
||||
- docker build -t $IMAGE_NAME . --platform=linux/arm64
|
||||
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
|
||||
- docker push $IMAGE_NAME
|
||||
- docker push $LATEST_IMAGE_NAME
|
||||
- |
|
||||
if [ $CI_COMMIT_TAG ]; then
|
||||
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
|
||||
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
docker push $PUBLISH_IMAGE_NAME
|
||||
docker push $PUBLISH_LATEST_IMAGE_NAME
|
||||
fi
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "drop-base"]
|
||||
path = drop-base
|
||||
url = https://github.com/Drop-OSS/drop-base.git
|
||||
@@ -0,0 +1 @@
|
||||
drop-base/
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"spellchecker.ignoreWordsList": ["mTLS", "Wireguard"],
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 5432,
|
||||
"driver": "PostgreSQL",
|
||||
"name": "drop",
|
||||
"database": "drop",
|
||||
"username": "drop",
|
||||
"password": "drop"
|
||||
}
|
||||
],
|
||||
// allow autocomplete for ArkType expressions like "string | num"
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
// prioritize ArkType's "type" for autoimports
|
||||
"typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"]
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
# CONTRIBUTING GUIDELINES
|
||||
|
||||
Drop is a community-driven project. Contribution is welcome, encouraged, and appreciated.
|
||||
It is also essential for the development of the project.
|
||||
|
||||
First, please take a moment to review our [code of conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
These guidelines are an attempt at better addressing pending
|
||||
issues and pull requests. Please read them closely.
|
||||
|
||||
Foremost, be so kind as to [search](#use-the-search-luke). This ensures any contribution
|
||||
you would make is not already covered.
|
||||
|
||||
<!-- TOC updateonsave:true depthfrom:2 -->
|
||||
|
||||
- [Reporting Issues](#reporting-issues)
|
||||
- [You have a problem](#you-have-a-problem)
|
||||
- [You have a suggestion](#you-have-a-suggestion)
|
||||
- [Submitting Pull Requests](#submitting-pull-requests)
|
||||
- [Getting started](#getting-started)
|
||||
- [You have a solution](#you-have-a-solution)
|
||||
- [You have an addition](#you-have-an-addition)
|
||||
- [Use the Search, Luke](#use-the-search-luke)
|
||||
- [Commit Guidelines](#commit-guidelines)
|
||||
- [Format](#format)
|
||||
- [Style](#style)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
### You have a problem
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any open issue already covering
|
||||
your problem.
|
||||
|
||||
If you find one, comment on it, so we know more people are experiencing it.
|
||||
|
||||
<!--
|
||||
TODO: Add Troubleshooting
|
||||
If not, look at the [Troubleshooting](https://github.com/Drop-OSS/docs/Troubleshooting)
|
||||
page for instructions on how to gather data to better debug your problem.
|
||||
-->
|
||||
|
||||
If you cannot find an existing issue, you can go ahead and create an issue with as much
|
||||
detail as you can provide.
|
||||
It should include the data gathered as indicated above, along with the following:
|
||||
|
||||
1. How to reproduce the problem
|
||||
2. What the correct behavior should be
|
||||
3. What the actual behavior is
|
||||
|
||||
Please copy to anyone relevant (e.g. plugin maintainers) by mentioning their GitHub handle
|
||||
(starting with `@`) in your message.
|
||||
|
||||
We will do our very best to help you.
|
||||
|
||||
### You have a suggestion
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any open issue already covering
|
||||
your suggestion.
|
||||
|
||||
If you find one, comment on it, so we know more people are supporting it.
|
||||
|
||||
If not, you can go ahead and create an issue. Please copy to anyone relevant (e.g. plugin
|
||||
maintainers) by mentioning their GitHub handle (starting with `@`) in your message.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
### Getting started
|
||||
|
||||
You should be familiar with the basics of
|
||||
[contributing on GitHub](https://help.github.com/articles/using-pull-requests)
|
||||
|
||||
<!--and have a fork
|
||||
[properly set up](https://github.com/drop/docs/Contribution-Technical-Practices).
|
||||
-->
|
||||
|
||||
You MUST always create PRs with _a dedicated branch_ based on the latest upstream tree.
|
||||
|
||||
If you create your own PR, please make sure you do it right. Also be so kind as to reference
|
||||
any issue that would be solved in the PR description body,
|
||||
[for instance](https://help.github.com/articles/closing-issues-via-commit-messages/)
|
||||
_"Fixes #XXXX"_ for issue number XXXX.
|
||||
|
||||
### You have a solution
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any open issue already covering
|
||||
your [problem](#you-have-a-problem), and any pending/merged/rejected PR covering your solution.
|
||||
|
||||
If the solution is already reported, try it out and +1 the pull request if the
|
||||
solution works ok. On the other hand, if you think your solution is better, post
|
||||
it with reference to the other one so we can have both solutions to compare.
|
||||
|
||||
If not, then go ahead and submit a PR. Please copy to anyone relevant (e.g. plugin
|
||||
maintainers) by mentioning their GitHub handle (starting with `@`) in your message.
|
||||
|
||||
### You have an addition
|
||||
|
||||
We are absolutely accepting more contributions or features to drop, but please, make sure
|
||||
that it is reasonable. Contributions that only cover a very small niche are likely to not
|
||||
be added.
|
||||
|
||||
Please be so kind as to [search](#use-the-search-luke) for any pending, merged or rejected Pull Requests
|
||||
covering or related to what you want to add.
|
||||
|
||||
If you find one, try it out and work with the author on a common solution.
|
||||
|
||||
If not, then go ahead and submit a PR. Please copy to anyone relevant (e.g. plugin
|
||||
maintainers) by mentioning their GitHub handle (starting with `@`) in your message.
|
||||
|
||||
For any extensive change, such as API changes, you will have to find testers to +1 your PR.
|
||||
|
||||
---
|
||||
|
||||
## Use the Search, Luke
|
||||
|
||||
_May the Force (of past experiences) be with you_
|
||||
|
||||
GitHub offers [many search features](https://help.github.com/articles/searching-github/)
|
||||
to help you check whether a similar contribution to yours already exists. Please search
|
||||
before making any contribution, it avoids duplicates and eases maintenance. Trust me,
|
||||
that works 90% of the time.
|
||||
|
||||
You can also take a look at the [FAQ](https://github.com/Drop-OSS/docs/wiki/FAQ)
|
||||
to be sure your contribution has not already come up.
|
||||
|
||||
If all fails, your thing has probably not been reported yet, so you can go ahead
|
||||
and [create an issue](#reporting-issues) or [submit a PR](#submitting-pull-requests).
|
||||
|
||||
---
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
Drop uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||
specification. The automatic changelog tool uses these to automatically generate
|
||||
a changelog based on the commit messages. Here's a guide to writing a commit message
|
||||
to allow this:
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
type(scope)!: subject
|
||||
```
|
||||
|
||||
- `type`: the type of the commit is one of the following:
|
||||
|
||||
- `feat`: new features.
|
||||
- `fix`: bug fixes.
|
||||
- `docs`: documentation changes.
|
||||
- `refactor`: refactor of a particular code section without introducing
|
||||
new features or bug fixes.
|
||||
- `style`: code style improvements.
|
||||
- `perf`: performance improvements.
|
||||
- `test`: changes to the test suite.
|
||||
- `ci`: changes to the CI system.
|
||||
- `build`: changes to the build system.
|
||||
- `chore`: for other changes that don't match previous types. This doesn't appear
|
||||
in the changelog.
|
||||
|
||||
- `scope`: section of the codebase that the commit makes changes to. If it makes changes to
|
||||
many sections, or if no section in particular is modified, leave blank without the parentheses.
|
||||
Examples:
|
||||
|
||||
- Commit that changes the `git` plugin:
|
||||
|
||||
```
|
||||
feat(git): add alias for `git commit`
|
||||
```
|
||||
|
||||
- Commit that changes many plugins:
|
||||
|
||||
```
|
||||
style: fix inline declaration of arrays
|
||||
```
|
||||
|
||||
For changes to plugins or themes, the scope should be the plugin or theme name:
|
||||
|
||||
- ✅ `fix(agnoster): commit subject`
|
||||
- ❌ `fix(theme/agnoster): commit subject`
|
||||
|
||||
- `!`: this goes after the `scope` (or the `type` if scope is empty), to indicate that the commit
|
||||
introduces breaking changes.
|
||||
|
||||
Optionally, you can specify a message that the changelog tool will display to the user to indicate
|
||||
what's changed and what they can do to deal with it. You can use multiple lines to type this message;
|
||||
the changelog parser will keep reading until the end of the commit message or until it finds an empty
|
||||
line.
|
||||
|
||||
Example (made up):
|
||||
|
||||
```
|
||||
style(agnoster)!: change dirty git repo glyph
|
||||
|
||||
BREAKING CHANGE: the glyph to indicate when a git repository is dirty has
|
||||
changed from a Powerline character to a standard UTF-8 emoji. You can
|
||||
change it back by setting `ZSH_THEME_DIRTY_GLYPH`.
|
||||
|
||||
Fixes #420
|
||||
|
||||
Co-authored-by: Username <email>
|
||||
```
|
||||
|
||||
- `subject`: a brief description of the changes. This will be displayed in the changelog. If you need
|
||||
to specify other details, you can use the commit body, but it won't be visible.
|
||||
|
||||
Formatting tricks: the commit subject may contain:
|
||||
|
||||
- Links to related issues or PRs by writing `#issue`. This will be highlighted by the changelog tool:
|
||||
|
||||
```
|
||||
feat(archlinux): add support for aura AUR helper (#9467)
|
||||
```
|
||||
|
||||
- Formatted inline code by using backticks: the text between backticks will also be highlighted by
|
||||
the changelog tool:
|
||||
```
|
||||
feat(shell-proxy): enable unexported `DEFAULT_PROXY` setting (#9774)
|
||||
```
|
||||
|
||||
### Style
|
||||
|
||||
Try to keep the first commit line short. It's harder to do using this commit style but try to be
|
||||
concise, and if you need more space, you can use the commit body. Try to make sure that the commit
|
||||
subject is clear and precise enough that users will know what changed by just looking at the changelog.
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
## Volunteer
|
||||
|
||||
Very nice!! :)
|
||||
|
||||
Please have a look at the [Volunteer](https://github.com/ohmyzsh/ohmyzsh/wiki/Volunteers)
|
||||
page for instructions on where to start and more.
|
||||
-->
|
||||
|
||||
## Reference
|
||||
|
||||
This contributing guide is adapted from the
|
||||
[oh-my-zsh contribution guide](https://github.com/ohmyzsh/ohmyzsh/blob/master/CONTRIBUTING.md).
|
||||
If there are any issues with this, please email admin@deepcore.dev.
|
||||
+20
-95
@@ -1,105 +1,30 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# pull pre-configured and updated build environment
|
||||
FROM debian:testing-20250317-slim AS build-system
|
||||
|
||||
# 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
|
||||
# setup workdir - has to be the same filepath as app because fuckin' Prisma
|
||||
WORKDIR /app
|
||||
|
||||
## so corepack knows pnpm's version
|
||||
# install dependencies and build
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install node-corepack -y
|
||||
RUN corepack enable
|
||||
COPY . .
|
||||
## prevent prompt to download
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
## setup for offline
|
||||
RUN corepack pack
|
||||
## don't call out to network anymore
|
||||
ENV COREPACK_ENABLE_NETWORK=0
|
||||
|
||||
### INSTALL DEPS ONCE
|
||||
FROM base AS deps
|
||||
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
### BUILD TORRENTIAL
|
||||
# Bookworm-pinned to match the runtime image's glibc (a trixie build would not run on bookworm).
|
||||
FROM rustlang/rust:nightly-bookworm-slim AS torrential-build
|
||||
## libarchive-dev + pkg-config let libarchive3-sys link libarchive dynamically (glibc).
|
||||
## protobuf-compiler is kept for parity (torrential's build.rs uses a vendored protoc).
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
pkg-config \
|
||||
libarchive-dev \
|
||||
protobuf-compiler \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN cargo build --release --manifest-path ./torrential/Cargo.toml
|
||||
|
||||
### BUILD APP
|
||||
FROM base AS build-system
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NUXT_TELEMETRY_DISABLED=1
|
||||
|
||||
## add git so drop can determine its git ref at build
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
## copy deps and rest of project files
|
||||
COPY . .
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
|
||||
ARG BUILD_DROP_VERSION
|
||||
ARG BUILD_GIT_REF
|
||||
|
||||
## build
|
||||
RUN pnpm run --filter=drop postinstall && pnpm run --filter=drop build
|
||||
|
||||
RUN NUXT_TELEMETRY_DISABLED=1 yarn install --network-timeout 1000000
|
||||
RUN NUXT_TELEMETRY_DISABLED=1 yarn prisma generate
|
||||
RUN NUXT_TELEMETRY_DISABLED=1 yarn build
|
||||
|
||||
# create run environment for Drop
|
||||
FROM base AS run-system
|
||||
FROM node:lts-slim AS run-system
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NUXT_TELEMETRY_DISABLED=1
|
||||
WORKDIR /app
|
||||
|
||||
# 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
|
||||
COPY --from=build-system /app/.output ./app
|
||||
COPY --from=build-system /app/prisma ./prisma
|
||||
COPY --from=build-system /app/package.json ./
|
||||
COPY --from=build-system /app/build ./startup
|
||||
|
||||
# RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn add --network-timeout 1000000 --no-lockfile --ignore-scripts prisma@6.11.1
|
||||
## runtime deps:
|
||||
## - libarchive13: torrential now links libarchive dynamically (glibc build)
|
||||
## - p7zip-full: provides the 7z CLI
|
||||
## - nginx: front-end proxy
|
||||
## - openssl + ca-certificates: required by Prisma's query engine on Debian
|
||||
## pnpm itself is provided by corepack (enabled in the base stage)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libarchive13 \
|
||||
p7zip-full \
|
||||
nginx \
|
||||
openssl \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN pnpm install prisma@7.7.0 --global
|
||||
# init prisma to download all required files
|
||||
RUN pnpm prisma init
|
||||
# OpenSSL as a dependency for Drop (TODO: seperate build environment)
|
||||
RUN apt-get update -y && apt-get install -y openssl
|
||||
RUN yarn global add prisma@6.7.0
|
||||
|
||||
COPY --from=build-system /app/server/prisma.config.ts ./
|
||||
COPY --from=build-system /app/server/.output ./app
|
||||
COPY --from=build-system /app/server/prisma ./prisma
|
||||
COPY --from=build-system /app/server/build ./startup
|
||||
COPY --from=build-system /app/server/build/nginx.conf /nginx.conf
|
||||
COPY --from=torrential-build /build/torrential/target/release/torrential /usr/bin/
|
||||
|
||||
ENV LIBRARY="/library"
|
||||
ENV DATA="/data"
|
||||
ENV NGINX_CONFIG="/nginx.conf"
|
||||
# Nuxt's port
|
||||
ENV PORT=4000
|
||||
|
||||
CMD ["sh", "/app/startup/launch.sh"]
|
||||
CMD ["/app/startup/launch.sh"]
|
||||
@@ -6,32 +6,72 @@
|
||||
# Drop
|
||||
|
||||
[](https://droposs.org)
|
||||
[](https://droposs.org/docs)
|
||||
[](https://forum.droposs.org)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/ACq4qZp4a9)
|
||||
[](https://opencollective.com/drop-oss)
|
||||
[
|
||||
](https://translate.droposs.org/engage/drop/)
|
||||
|
||||
Drop is an open-source game distribution platform, similar to GameVault or Steam. It's designed to distribute and share DRM-free games quickly, all while being incredibly flexible, beautiful, and fast.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://droposs.org/_ipx/f_webp&q_80/images/carousel/store.png" alt="Drop Screenshot" width="900rem"/>
|
||||
</div>
|
||||
Drop is an open-source game distribution platform, like GameVault or Steam. It's designed to distribute and shared DRM-free game quickly, all while being incredibly flexible, beautiful and fast.
|
||||
|
||||
## Philosophy
|
||||
|
||||
1. Drop is flexible. While abstractions and interfaces can complicate the codebase, the flexibility is worth it.
|
||||
2. Drop is secure. The nature of Drop means an instance can never be accessible without authentication. In line with #1, Drop also supports a huge variety of authentication mechanisms, from username/password to SSO.
|
||||
3. Drop is user-friendly. The interface is designed to be clean and simple to use, with advanced features available to users who want them.
|
||||
1. Drop is flexible. While abstractions and interfaces can make the codebase more complicated, the flexibility is worth it.
|
||||
2. Drop is secure. The nature of Drop means an instance can never be accessible without authentication. In line with #1, Drop also supports a huge variety of authentication mechanisms, from a username/password to SSO.
|
||||
3. Drop is user-friendly. The interface is designed to be clean and simple to use, with complexity available to the users who want it.
|
||||
|
||||
## Deployment
|
||||
|
||||
See our documentation on how to [deploy Drop](https://droposs.org/docs/admin/quickstart) for more information.
|
||||
To just deploy Drop, we've set up a simple docker compose file in deploy-template.
|
||||
|
||||
1. Generate a [GiantBomb API Key](https://www.giantbomb.com/api/)
|
||||
2. Navigate to the deploy-template directory in your terminal (`cd deploy-template`)
|
||||
3. Edit the compose.yml file (`nano compose.yml`) and copy your GiamtBomb API Key into the GIANT_BOMB_API_KEY environment variable
|
||||
4. Run `docker compose up -d`
|
||||
|
||||
Your drop server should now be running. To register the admin user, navigate to http://your.drop.server.ip:3000/register?id=admin
|
||||
and fill in the required forms
|
||||
|
||||
### Adding a game
|
||||
|
||||
To add a game to the drop library, do as follows:
|
||||
|
||||
1. Ensure that the current user owns the library folder with `sudo chown -R $(id -u $(whoami)) library`
|
||||
2. `cd library`
|
||||
3. `mkdir <GAME_NAME>` with the name of the game which you would like to register
|
||||
4. `cd <GAME_NAME>`
|
||||
5. `mkdir <VERSION_NAME>` Upload files for the specific game version to this folder
|
||||
6. Navigate to http://your.drop.server.ip:3000/
|
||||
7. Import game metadata (uses GiantBomb API Key) by selecting the game and specifying which entry to import
|
||||
8. Navigate to http://your.drop.server.ip:3000/admin/library
|
||||
9. You should see the game which you have just imported listed in this menu. There should be a notification that "Drop has detected you have new verions of this game to import". Select import here.
|
||||
10. Select the game version to import and thus fill in fields as required.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
This repo uses the Nuxt 3 + TailwindCSS stack, with the `yarn` package manager.
|
||||
|
||||
For the database, Drop uses Prisma connected to PostgreSQL.
|
||||
|
||||
## Development
|
||||
|
||||
To get started with development, you need `yarn --optional` and `docker compose` installed (or know how to set up a PostgreSQL database).
|
||||
|
||||
### Note: `--optional` flag is **REQUIRED**
|
||||
|
||||
Drop uses a utility package called droplet that's written in Rust. It has builts for Linux (GNU) and Windows, and they are set up as optional packages. `npm` installs these by default, but `yarn` needs the `--optional` flag.
|
||||
|
||||
Steps:
|
||||
|
||||
1. Run `git submodule update --init --recursive` to setup submodules
|
||||
1. Copy the `.env.example` to `.env` and add your GiantBomb metadata key (more metadata providers coming)
|
||||
1. Create the `.data` directory with `mkdir .data`
|
||||
1. Ensure that your user owns the `.data` directory with `sudo chown -R $(id -u $(whoami))`
|
||||
1. Open up a terminal and navigate to `dev-tools`, and run `docker compose up`
|
||||
1. Open up another terminal in the root directory of the project and run `yarn` and then `yarn dev` to start the dev server
|
||||
|
||||
As part of the first-time bootstrap, Drop creates an invitation with the fixed id of 'admin'. So, to create an admin account, go to:
|
||||
|
||||
http://localhost:3000/auth/register?id=admin
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see the [in-depth contributing guide](CONTRIBUTING.md). The guide includes information on how to set up the project, how to contribute code, how to report issues, and even how to effectively translate Drop.
|
||||
|
||||
[](https://translate.droposs.org/engage/drop/)
|
||||
Please see the [in-depth contributing guide](CONTRIBUTING.md)
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
To report a vulnerability, please DO NOT create an issue for it
|
||||
as this may lead to the vulnerability being exploited before it
|
||||
can be fixed. Instead, please email [security@droposs.org](mailto:security@droposs.org)
|
||||
can be fixed. Instead, please email [security@deepcore.dev](mailto:security@deepcore.dev)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<NuxtLoadingIndicator color="#2563eb" />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<ModalStack />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
await updateUser();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* You can customise the default animation here. */
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "@tailwindcss/forms";
|
||||
@config "../tailwind.config.js";
|
||||
@@ -1 +0,0 @@
|
||||
/bin
|
||||
@@ -1,19 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func connect() {
|
||||
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close(context.Background())
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module drop/core
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.1 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
)
|
||||
@@ -1,15 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,5 +0,0 @@
|
||||
module drop
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require github.com/gorilla/mux v1.8.1
|
||||
@@ -1,2 +0,0 @@
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
@@ -1,3 +0,0 @@
|
||||
go 1.26.1
|
||||
|
||||
use ./core
|
||||
@@ -1,9 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,37 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func handler(res http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(res, "G'day there mate")
|
||||
}
|
||||
func routingMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
url := *r.URL
|
||||
url.Path = strings.TrimSuffix(r.URL.Path, "/")
|
||||
r.URL = &url
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter().StrictSlash(true)
|
||||
r.Use(routingMiddleware)
|
||||
|
||||
r.HandleFunc("/api/v1", handler)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":3433",
|
||||
Handler: r,
|
||||
}
|
||||
log.Printf("starting drop server on :3433")
|
||||
srv.ListenAndServe()
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
# This file starts up the Drop server by running migrations and then starting the executable
|
||||
echo "[Drop] performing migrations..."
|
||||
pnpm prisma migrate deploy
|
||||
ls ./prisma/migrations/
|
||||
prisma migrate deploy
|
||||
|
||||
# Actually start the application
|
||||
node /app/app/server/index.mjs
|
||||
node /app/app/server/index.mjs
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
## Release 0.2.0-beta
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix recursive dirs util #02d6346
|
||||
- Fix username length requirement #0a5a649
|
||||
- remove dynamic imports #0f10626
|
||||
- fix for missing developers or publishers #25fc957
|
||||
- split prisma schemas #2859005
|
||||
- results are returned alphabetically #33d3770
|
||||
- update prisma schemas #36776cc
|
||||
- removed global flag #43e32b4
|
||||
- properly disconnect websockets from task handler #5358f1f
|
||||
- follow best practices #54c5d55
|
||||
- future lenience #5c78b20
|
||||
- fix width of token breaking things #61d88c3
|
||||
- fixed websocket authentication #62ea9a1
|
||||
- fix delta manifest generation #6df560c
|
||||
- admin invitation w/ system user #8463e35
|
||||
- properly import icons #8945196
|
||||
- prisma create footprint #952ece8
|
||||
- game panel now always shows 3 lines exactly #9c2249e
|
||||
- remove unnecessary import #a361c38
|
||||
- fix disconnect code #a8f2106
|
||||
- fix types #b511b40
|
||||
- add drop-base as git submodule #b75ebd1
|
||||
- Update README.md with discord link #c6bb21d
|
||||
- fix expires requirement in the admin endpoint #c7b675f
|
||||
- fix always being created as admin #c7eb11a
|
||||
- moved icons and created PlatformClient so we can use the enum on the frontend #cada630
|
||||
- recurse submodules #db103de
|
||||
- fix FATAL: "root"... message #dbb315a
|
||||
- only show versions that are directories #ef8f3ae
|
||||
|
||||
### Features
|
||||
|
||||
- update prisma & delete games #089c3e0
|
||||
- manual handshake #12e3125
|
||||
- fetch game endpoint #1f4d075
|
||||
- under the hood organisation and consolidation #26a31f6
|
||||
- 'no images' slide on image carousel #28baabc
|
||||
- improve feedback when metadata fails #2c19e13
|
||||
- introduction of 'system user' #2c21a23
|
||||
- change name, description and icon #2cfe75a
|
||||
- 'manual' metadata provider #2f52a16
|
||||
- add disabled state #38fc6b8
|
||||
- overhauled version importing #39d7ce7
|
||||
- automatically create library folder if it doesn't exist #39fe9d5
|
||||
- smoother bar in admin task ui #4488ae2
|
||||
- add noWrapper option #4f9b949
|
||||
- add version metadata route #5393db3
|
||||
- completed admin UI, with minor changes to backend #599da0e
|
||||
- adjust gradient #5a1f841
|
||||
- keep track of last connected #69e4c25
|
||||
- added notification system w/ interwoven refactoring #6e6f09d
|
||||
- content length header for chunk downloads #76bceb1
|
||||
- add title to tab #7b0756c
|
||||
- add button to open in admin panel #7b3b919
|
||||
- client capability framework + peer API configuration #7d72a86
|
||||
- customisable image carousel and new layout #937954f
|
||||
- support more types #9b12d45
|
||||
- generate a server certificate for mtls APIs #9c4b6f3
|
||||
- new endpoints, ui and beginnings of main store page #9cbdcbc
|
||||
- backend #a309651
|
||||
- more subtle design improvements #a815542
|
||||
- add aden's carousel pagination design #a86045c
|
||||
- add header #a8a152e
|
||||
- client side search #b50e27f
|
||||
- new ws handler #bc0c47c
|
||||
- user widget now redirects to actual page #bfafe02
|
||||
- require lowercase usernames #d7160ab
|
||||
- more ui improvements #e408ac5
|
||||
- add modifying game descriptions #e505e58
|
||||
- mobile nav #e5cf13f
|
||||
- slightly improved game page #e796b46
|
||||
- game carousel #ecc819e
|
||||
- add enum dictionary type #f2e0182
|
||||
- improved ux #f3ed0f6
|
||||
- cleanup and raw accessors #f7d767d
|
||||
- add support for overriding UMU id #fd4a7d1
|
||||
- add .sh for linux #fe9373a
|
||||
|
||||
### Other Changes
|
||||
|
||||
- quexeky <git@quexeky.dev>
|
||||
- fixed manifest generation #03a37f7
|
||||
- manual ci/cd #03b0b0c
|
||||
- ability to fetch client certs for p2p #0a715fe
|
||||
- disable tls in build #0f80fcd
|
||||
- Updated README.md #17971e0
|
||||
- Merge pull request #18 from Drop-OSS/develop
|
||||
- initial work on metadata system #196f87c
|
||||
- more ui #1bd19ad
|
||||
- remove log statements #1d5e1bd
|
||||
- small fixes & SSR disabled #1f575b2
|
||||
- update information and setup guide #2236622
|
||||
- metadata engine #22ac7f6
|
||||
- Update CONTRIBUTING.md #2309407
|
||||
- slight bug fixes and clean up #24a0d11
|
||||
- almst complete admin ui and initial store designs #27070b6
|
||||
- handshakes #2b4382d
|
||||
- user mobile header #2e44ef3
|
||||
- more consistent naming for globals #305de9f
|
||||
- replaced markdown-it with micromark #31e8359
|
||||
- fixes to store page for mobile clients #328b9ba
|
||||
- game version re-ordering #329c74d
|
||||
- verbose yarn install #36568c3
|
||||
- patch for no version check in manifest generation #395219d
|
||||
- migrate bcrypt to bcryptjs #3a51c9c
|
||||
- added download chunk endpoint #3dd6062
|
||||
- Update README.md #425934d
|
||||
- build only ci #4273a20
|
||||
- object storage + full permission system + testing #435551c
|
||||
- rename admin socket session map #44c6028
|
||||
- bump droplet and add vue carousel #46551f9
|
||||
- version importing #46c8f0c
|
||||
- back to yarn, with nuxt telemetry force disabled #46d35ad
|
||||
- finished object endpoints #486bce8
|
||||
- update dependencies and add note about optional dependencies #4fa771a
|
||||
- use configuration from docs for ci/cd #52315d0
|
||||
- slight fixes to register logic #583301f
|
||||
- removed yarn.lock #584bcf1
|
||||
- Version bump #5f29c28
|
||||
- immutable application settings framework #5fe2036
|
||||
- fixed docker daemon location #62a111b
|
||||
- copy autodevops configuration #6328c24
|
||||
- Delete .gitlab-ci.yml #69f341b
|
||||
- admin ui shell #6b5e48d
|
||||
- bump @drop/droplet version for windows developers #6ba5cdd
|
||||
- Add LICENSE #6e2dc89
|
||||
- custom dind #716eac7
|
||||
- task API #718f5ba
|
||||
- use gitlab ci variable declaration #7194d35
|
||||
- move icons into dedicated folder #74fa671
|
||||
- another stage of client authentication #7523e53
|
||||
- refactoring #7869043
|
||||
- moved windows logo into logos dir #789d3ba
|
||||
- updated text colours across app #7a88f4c
|
||||
- starting docs infra #7d2a1c6
|
||||
- more cleaning #7e17626
|
||||
- slight patch to rename query to be more consistent #7f4db0c
|
||||
- move to raw docker #803752e
|
||||
- server side and user client side completed for registration #848a611
|
||||
- beginnings of download implementation #8674ac7
|
||||
- more consistent naming for object handler #87230fb
|
||||
- use autodevops build stage #886beb6
|
||||
- Updated tailwind config #88c95d6
|
||||
- change name of store file #8999303
|
||||
- split prisma schemas #9011cf5
|
||||
- client initiate #909432a
|
||||
- more client routes to support Drop app update #91b7e10
|
||||
- additional polish and QoL features #93bc143
|
||||
- upload images to games #9b7ee4e
|
||||
- migrate to pnpm due to ci/cd issues with yarn #9cb2d6d
|
||||
- run yarn install in CI/CD non interactively #a208fbe
|
||||
- completed game importing; partial work on version importing #a7c33e7
|
||||
- remove canvas from dependencies #a8f58eb
|
||||
- fix registry authentication #ad25d3e
|
||||
- consolidate type utils #adb4b73
|
||||
- Updated README.md #b0ef675
|
||||
- add proper carousel to store page #b2ab827
|
||||
- move to yarn v2 #b744671
|
||||
- remove client API deadweight #b9ae26c
|
||||
- add expires field #be6c30d
|
||||
- ca groundwork #bfafd2a
|
||||
- cleanup & polish #c355f6f
|
||||
- remove bcrypt (debug) #c3914cc
|
||||
- non rounded bottom #c4391d3
|
||||
- failed gracefully on invalid chunk index #c4a3e4e
|
||||
- update deploy template #c4a419f
|
||||
- migrate to new droplet ca system #c4d8113
|
||||
- docker based deployment #c5d00b4
|
||||
- updated CONTRIBUTING.md #cd0d2bf
|
||||
- update prisma version #ce0a9ab
|
||||
- README update #ceacd84
|
||||
- patch metadata handler #cf578bd
|
||||
- Added SECURITY.md #d3d93b0
|
||||
- finalised client APIs and authentication method #d4e2dc8
|
||||
- Update README.md #db916bf
|
||||
- object storage interface + utility functions #de388a9
|
||||
- initial commit #e1a789f
|
||||
- fixed task system #e1c1d7e
|
||||
- Update file chunk.get.ts #e4339c3
|
||||
- ui groundwork #e52f072
|
||||
- Update changelog #eadcaa1
|
||||
- check for no version in manifest generation #eb3f9f9
|
||||
- break into single column store on lg devices #ecb381e
|
||||
- better server side signin redirects #ef13b68
|
||||
- patch signin #f3672f8
|
||||
|
||||
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
|
||||
|
||||
## Release 0.1.0-beta
|
||||
|
||||
### Fixes
|
||||
|
||||
- remove dynamic imports #0f10626
|
||||
- fix for missing developers or publishers #25fc957
|
||||
- split prisma schemas #2859005
|
||||
- results are returned alphabetically #33d3770
|
||||
- properly disconnect websockets from task handler #5358f1f
|
||||
- follow best practices #54c5d55
|
||||
- future lenience #5c78b20
|
||||
- fixed websocket authentication #62ea9a1
|
||||
- fix delta manifest generation #6df560c
|
||||
- admin invitation w/ system user #8463e35
|
||||
- properly import icons #8945196
|
||||
- prisma create footprint #952ece8
|
||||
- game panel now always shows 3 lines exactly #9c2249e
|
||||
- remove unnecessary import #a361c38
|
||||
- fix types #b511b40
|
||||
- fix expires requirement in the admin endpoint #c7b675f
|
||||
- moved icons and created PlatformClient so we can use the enum on the frontend #cada630
|
||||
- only show versions that are directories #ef8f3ae
|
||||
|
||||
### Features
|
||||
|
||||
- update prisma & delete games #089c3e0
|
||||
- fetch game endpoint #1f4d075
|
||||
- under the hood organisation and consolidation #26a31f6
|
||||
- introduction of 'system user' #2c21a23
|
||||
- automatically create library folder if it doesn't exist #39fe9d5
|
||||
- smoother bar in admin task ui #4488ae2
|
||||
- add version metadata route #5393db3
|
||||
- completed admin UI, with minor changes to backend #599da0e
|
||||
- keep track of last connected #69e4c25
|
||||
- added notification system w/ interwoven refactoring #6e6f09d
|
||||
- content length header for chunk downloads #76bceb1
|
||||
- add title to tab #7b0756c
|
||||
- add button to open in admin panel #7b3b919
|
||||
- client capability framework + peer API configuration #7d72a86
|
||||
- generate a server certificate for mtls APIs #9c4b6f3
|
||||
- new endpoints, ui and beginnings of main store page #9cbdcbc
|
||||
- more subtle design improvements #a815542
|
||||
- add header #a8a152e
|
||||
- client side search #b50e27f
|
||||
- new ws handler #bc0c47c
|
||||
- user widget now redirects to actual page #bfafe02
|
||||
- require lowercase usernames #d7160ab
|
||||
- more ui improvements #e408ac5
|
||||
- slightly improved game page #e796b46
|
||||
- game carousel #ecc819e
|
||||
- add enum dictionary type #f2e0182
|
||||
- cleanup and raw accessors #f7d767d
|
||||
- add support for overriding UMU id #fd4a7d1
|
||||
|
||||
### Other Changes
|
||||
|
||||
- quexeky <git@quexeky.dev>
|
||||
- fixed manifest generation #03a37f7
|
||||
- manual ci/cd #03b0b0c
|
||||
- ability to fetch client certs for p2p #0a715fe
|
||||
- disable tls in build #0f80fcd
|
||||
- Updated README.md #17971e0
|
||||
- initial work on metadata system #196f87c
|
||||
- more ui #1bd19ad
|
||||
- remove log statements #1d5e1bd
|
||||
- small fixes & SSR disabled #1f575b2
|
||||
- update information and setup guide #2236622
|
||||
- metadata engine #22ac7f6
|
||||
- Update CONTRIBUTING.md #2309407
|
||||
- slight bug fixes and clean up #24a0d11
|
||||
- almst complete admin ui and initial store designs #27070b6
|
||||
- handshakes #2b4382d
|
||||
- user mobile header #2e44ef3
|
||||
- more consistent naming for globals #305de9f
|
||||
- replaced markdown-it with micromark #31e8359
|
||||
- fixes to store page for mobile clients #328b9ba
|
||||
- game version re-ordering #329c74d
|
||||
- verbose yarn install #36568c3
|
||||
- patch for no version check in manifest generation #395219d
|
||||
- migrate bcrypt to bcryptjs #3a51c9c
|
||||
- added download chunk endpoint #3dd6062
|
||||
- Update README.md #425934d
|
||||
- build only ci #4273a20
|
||||
- object storage + full permission system + testing #435551c
|
||||
- rename admin socket session map #44c6028
|
||||
- bump droplet and add vue carousel #46551f9
|
||||
- version importing #46c8f0c
|
||||
- back to yarn, with nuxt telemetry force disabled #46d35ad
|
||||
- finished object endpoints #486bce8
|
||||
- update dependencies and add note about optional dependencies #4fa771a
|
||||
- use configuration from docs for ci/cd #52315d0
|
||||
- slight fixes to register logic #583301f
|
||||
- removed yarn.lock #584bcf1
|
||||
- Version bump #5f29c28
|
||||
- immutable application settings framework #5fe2036
|
||||
- fixed docker daemon location #62a111b
|
||||
- copy autodevops configuration #6328c24
|
||||
- Delete .gitlab-ci.yml #69f341b
|
||||
- admin ui shell #6b5e48d
|
||||
- bump @drop/droplet version for windows developers #6ba5cdd
|
||||
- Add LICENSE #6e2dc89
|
||||
- task API #718f5ba
|
||||
- use gitlab ci variable declaration #7194d35
|
||||
- move icons into dedicated folder #74fa671
|
||||
- another stage of client authentication #7523e53
|
||||
- refactoring #7869043
|
||||
- moved windows logo into logos dir #789d3ba
|
||||
- updated text colours across app #7a88f4c
|
||||
- starting docs infra #7d2a1c6
|
||||
- more cleaning #7e17626
|
||||
- slight patch to rename query to be more consistent #7f4db0c
|
||||
- move to raw docker #803752e
|
||||
- server side and user client side completed for registration #848a611
|
||||
- beginnings of download implementation #8674ac7
|
||||
- more consistent naming for object handler #87230fb
|
||||
- use autodevops build stage #886beb6
|
||||
- Updated tailwind config #88c95d6
|
||||
- change name of store file #8999303
|
||||
- split prisma schemas #9011cf5
|
||||
- client initiate #909432a
|
||||
- more client routes to support Drop app update #91b7e10
|
||||
- additional polish and QoL features #93bc143
|
||||
- upload images to games #9b7ee4e
|
||||
- migrate to pnpm due to ci/cd issues with yarn #9cb2d6d
|
||||
- run yarn install in CI/CD non interactively #a208fbe
|
||||
- completed game importing; partial work on version importing #a7c33e7
|
||||
- remove canvas from dependencies #a8f58eb
|
||||
- fix registry authentication #ad25d3e
|
||||
- consolidate type utils #adb4b73
|
||||
- Updated README.md #b0ef675
|
||||
- add proper carousel to store page #b2ab827
|
||||
- move to yarn v2 #b744671
|
||||
- remove client API deadweight #b9ae26c
|
||||
- add expires field #be6c30d
|
||||
- ca groundwork #bfafd2a
|
||||
- cleanup & polish #c355f6f
|
||||
- remove bcrypt (debug) #c3914cc
|
||||
- non rounded bottom #c4391d3
|
||||
- failed gracefully on invalid chunk index #c4a3e4e
|
||||
- update deploy template #c4a419f
|
||||
- migrate to new droplet ca system #c4d8113
|
||||
- docker based deployment #c5d00b4
|
||||
- updated CONTRIBUTING.md #cd0d2bf
|
||||
- update prisma version #ce0a9ab
|
||||
- README update #ceacd84
|
||||
- patch metadata handler #cf578bd
|
||||
- Added SECURITY.md #d3d93b0
|
||||
- finalised client APIs and authentication method #d4e2dc8
|
||||
- Update README.md #db916bf
|
||||
- object storage interface + utility functions #de388a9
|
||||
- initial commit #e1a789f
|
||||
- fixed task system #e1c1d7e
|
||||
- Update file chunk.get.ts #e4339c3
|
||||
- ui groundwork #e52f072
|
||||
- check for no version in manifest generation #eb3f9f9
|
||||
- break into single column store on lg devices #ecb381e
|
||||
- better server side signin redirects #ef13b68
|
||||
- patch signin #f3672f8
|
||||
|
||||
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
|
||||
@@ -1 +0,0 @@
|
||||
use flake
|
||||
@@ -1,4 +0,0 @@
|
||||
/target
|
||||
logs/
|
||||
.vscode
|
||||
.direnv
|
||||
Generated
-3396
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "downpour"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
async-trait = "0.1.89"
|
||||
chrono = "0.4.43"
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
console = "0.16.2"
|
||||
dialoguer = "0.12.0"
|
||||
dirs = "6.0.0"
|
||||
droplet-rs = { path = "../libraries/droplet" }
|
||||
fern = { version = "0.7.1", features = ["colored"] }
|
||||
futures = "0.3.31"
|
||||
indicatif = "0.18.3"
|
||||
log = "0.4.29"
|
||||
opendal = { version = "0.55.0", features = ["services-s3"] }
|
||||
rand = "0.9.2"
|
||||
reqwest = { version = "0.13.1", features = ["json"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.148"
|
||||
tokio = { version = "1.48.0", features = ["fs", "macros"] }
|
||||
tokio-util = { version = "0.7.18", features = ["compat"] }
|
||||
url = "2.5.8"
|
||||
webbrowser = "1.0.6"
|
||||
@@ -1,3 +0,0 @@
|
||||
# CLI (`downpour`)
|
||||
|
||||
The cli way to access Drop. Used for admin tasks that require local access, like uploading game content.
|
||||
Generated
-96
@@ -1,96 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768564909,
|
||||
"narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1744536153,
|
||||
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768704795,
|
||||
"narHash": "sha256-Y33TAp2BHEcuspYvcmBXXD0qdvjftv73PwyKTDOjoSY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "4b7472a78857ac789fb26616040f55cfcbd36c6e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
description = "Drop-OSS app development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
rust-overlay,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
libraries = with pkgs; [
|
||||
glib
|
||||
glibc
|
||||
openssl
|
||||
];
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
git
|
||||
rust-bin.nightly.latest.default
|
||||
rust-analyzer
|
||||
cargo-expand
|
||||
];
|
||||
|
||||
|
||||
buildInputs = libraries;
|
||||
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH="${
|
||||
pkgs.lib.makeLibraryPath libraries
|
||||
}:$LD_LIBRARY_PATH"
|
||||
echo "Downpour development environment loaded"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
# Downpour CLI spec
|
||||
`downpour [command] --opts`
|
||||
## Commands:
|
||||
- new <path/s3 name> <public endpoint> - creates/initalizes a depot at the endpoint. Creates manifest.json and speedtest
|
||||
- connect <s3 endpoint> <key> <secret> [name] - connects to an s3 endpoint and saves the endpoint to some sort of credentials file. Name is either as provided or the hostname of the endpoint
|
||||
- upload <game id> <localpath> <path/s3 name> - uploads game as described before. Should fail if depot isn't initialized with new from above
|
||||
- copy <game id> <version id> <src path/s3 name> <dest path/s3 name> - copies between two depots
|
||||
- mark [exists/absent] <game id> <version id> <path/s3 name> - modifies depot's manifest.json to show content exists or is absent without copying (for third party copies)
|
||||
- rename <public endpoint> <new public endpoint> - renames an endpoint [NEEDS API ROUTES - can't do yet]
|
||||
- delete <public endpoint> - delete an endpoint [NEEDS API ROUTES - can't do yet]
|
||||
@@ -1,69 +0,0 @@
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
|
||||
use crate::{commands::connect::config_option::ConfigOptionCli, interactive_variable};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
|
||||
/// Specify data file path
|
||||
#[arg(short, long)]
|
||||
pub data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Configures downpour endpoints
|
||||
Connect {
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
#[command(subcommand)]
|
||||
option: ConfigOptionCli,
|
||||
},
|
||||
/// Uploads new game version to depot
|
||||
Upload {
|
||||
#[clap(flatten)]
|
||||
info: UploadInfoCli,
|
||||
#[arg(short, long)]
|
||||
/// Alias of a given connection
|
||||
name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct UploadInfo {
|
||||
pub path: String,
|
||||
pub game_id: String,
|
||||
pub version_id: String,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct UploadInfoCli {
|
||||
/// Relative path to new version files
|
||||
#[arg(short, long, default_value_t = String::from("."))]
|
||||
pub path: String,
|
||||
/// ID of game to attach to
|
||||
#[arg(short, long)]
|
||||
pub game_id: Option<String>,
|
||||
/// Version ID to attach to
|
||||
#[arg(short, long)]
|
||||
pub version_id: Option<String>,
|
||||
}
|
||||
impl UploadInfoCli {
|
||||
pub fn interactive_configure(self) -> UploadInfo {
|
||||
let path = self.path;
|
||||
interactive_variable!(self, game_id, "Game ID");
|
||||
interactive_variable!(self, version_id, "Version ID");
|
||||
UploadInfo {
|
||||
path,
|
||||
game_id,
|
||||
version_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum UploadStyle {
|
||||
S3,
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
use crate::{
|
||||
commands::connect::{
|
||||
config_option::{ConfigOption, ConfigOptionCli},
|
||||
configurable::Configure,
|
||||
speedtest::{SPEEDTEST_PATH, Speedtest},
|
||||
},
|
||||
manifest::DepotManifest,
|
||||
};
|
||||
use dialoguer::{Confirm, theme::ColorfulTheme};
|
||||
use futures::AsyncWriteExt;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{debug, info};
|
||||
use opendal::Operator;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fs, ops::Not};
|
||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
|
||||
const CONFIG_DIR: &str = "downpour/config.json";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
configurations: HashMap<String, ConfigOption>,
|
||||
active: Option<String>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
configurations: HashMap::new(),
|
||||
active: None,
|
||||
}
|
||||
}
|
||||
pub fn exists(&self, name: &String) -> bool {
|
||||
self.configurations.contains_key(name)
|
||||
}
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let json = serde_json::to_string(self)?;
|
||||
let save_path = dirs::config_dir()
|
||||
.expect("Apparently your home directory doesn't exist") // Should probably formalise that error
|
||||
.join(CONFIG_DIR);
|
||||
fs::create_dir_all(save_path.parent().unwrap())?;
|
||||
fs::write(save_path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn read() -> Self {
|
||||
let save_path = dirs::config_dir()
|
||||
.expect("Apparently your home directory doesn't exist") // Should probably formalise that error
|
||||
.join(CONFIG_DIR);
|
||||
if fs::exists(&save_path)
|
||||
.unwrap_or_else(|_| panic!("Could not read save path {:#?}", &save_path))
|
||||
{
|
||||
serde_json::from_str(&fs::read_to_string(save_path).unwrap()).unwrap()
|
||||
} else {
|
||||
Config::new()
|
||||
}
|
||||
}
|
||||
pub fn add_item(&mut self, name: String, object: ConfigOption) {
|
||||
if matches!(object, ConfigOption::S3(..)) {
|
||||
self.active = Some(name.clone())
|
||||
}
|
||||
self.configurations.insert(name, object);
|
||||
self.save().expect("Failed to save config");
|
||||
}
|
||||
|
||||
pub fn get_active(&self) -> Option<&ConfigOption> {
|
||||
if let Some(active) = &self.active {
|
||||
self.configurations.get(active)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn get<T: AsRef<str>>(&self, name: T) -> Option<&ConfigOption> {
|
||||
self.configurations.get(name.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn manage_configuration(
|
||||
config: &mut Config,
|
||||
name: Option<String>,
|
||||
option: ConfigOptionCli,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut name = name;
|
||||
if let Some(name) = &name
|
||||
&& config.exists(name)
|
||||
{
|
||||
let confirm = Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(format!(
|
||||
"An entry already exists with the name \"{}\". Would you like to overwrite it?",
|
||||
name
|
||||
))
|
||||
.interact()?;
|
||||
if !confirm {
|
||||
return Err(anyhow::anyhow!("User cancelled action"));
|
||||
}
|
||||
}
|
||||
let config_option = match option {
|
||||
ConfigOptionCli::S3(s3_config_cli) => s3_config_cli.clone().configure(&mut name).await?,
|
||||
};
|
||||
let name = name.expect("Default name was not provided by ConfigOption. This is a bug");
|
||||
config.add_item(name, config_option.clone());
|
||||
let operator = config_option.build()?;
|
||||
|
||||
generate_manifest(&operator).await?;
|
||||
info!("Finished uploading manifest");
|
||||
generate_speedtest(&operator).await?;
|
||||
info!("Finished uploading speedtest");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_speedtest(operator: &Operator) -> anyhow::Result<()> {
|
||||
// Workaround to operator.exists("...") also logging a 404 warning
|
||||
let lister = operator.list_with(SPEEDTEST_PATH).limit(1).await?;
|
||||
if lister.is_empty().not() {
|
||||
info!("Speedtest already exists on Depot. Skipping speedtest upload...");
|
||||
return Ok(());
|
||||
}
|
||||
let mut writer = operator
|
||||
.writer(SPEEDTEST_PATH)
|
||||
.await?
|
||||
.into_futures_async_write()
|
||||
.compat_write();
|
||||
|
||||
let progress_bar = ProgressBar::new(10_000).with_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%")
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut reader = Speedtest::new(|progress| {
|
||||
let progress_int = (progress * 100f32).round() as u64;
|
||||
progress_bar.set_position(progress_int);
|
||||
});
|
||||
let written = tokio::io::copy(&mut reader, &mut writer).await?;
|
||||
progress_bar.finish();
|
||||
debug!("Wrote {} bytes to {:?}", written, operator.info());
|
||||
writer.into_inner().close().await?;
|
||||
debug!("Closed writer");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_manifest(operator: &Operator) -> anyhow::Result<()> {
|
||||
let lister = operator.list_with("manifest.json").limit(1).await?;
|
||||
if lister.is_empty().not() {
|
||||
info!("Manifest already exists on Depot. Skipping manifest upload...");
|
||||
return Ok(());
|
||||
}
|
||||
let data = DepotManifest::new();
|
||||
operator
|
||||
.write("manifest.json", serde_json::to_string(&data)?)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use clap::Subcommand;
|
||||
use opendal::{Operator, layers::LoggingLayer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
commands::connect::s3::{S3Config, S3ConfigCli},
|
||||
operator_builder::OperatorBuilder,
|
||||
};
|
||||
|
||||
#[derive(Subcommand, Clone)]
|
||||
pub enum ConfigOptionCli {
|
||||
// Connect to any S3-compatible endpoint
|
||||
S3(S3ConfigCli),
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub enum ConfigOption {
|
||||
S3(S3Config),
|
||||
}
|
||||
|
||||
impl ConfigOption {
|
||||
pub fn build(&self) -> anyhow::Result<Operator> {
|
||||
Ok(match self {
|
||||
ConfigOption::S3(s3_config) => s3_config.build()?,
|
||||
}
|
||||
.layer(LoggingLayer::default()))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
use crate::commands::connect::config_option::ConfigOption;
|
||||
|
||||
pub trait Configure {
|
||||
async fn configure(self, name: &mut Option<String>) -> anyhow::Result<ConfigOption>;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use dialoguer::{Input, theme::ColorfulTheme};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! interactive_variable {
|
||||
($value:ident, $var:ident, $prompt:expr) => {
|
||||
let $var = if let Some($var) = $value.$var {
|
||||
$var
|
||||
} else {
|
||||
$crate::commands::connect::interactive::query_variable($prompt).unwrap()
|
||||
};
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! interactive_optional_variable {
|
||||
($value:ident, $var:ident, $prompt:expr) => {
|
||||
let $var = if let Some($var) = $value.$var {
|
||||
Some($var)
|
||||
} else {
|
||||
$crate::commands::connect::interactive::query_optional_variable($prompt).unwrap()
|
||||
};
|
||||
};
|
||||
}
|
||||
pub fn query_variable<T: Clone + FromStr + ToString>(prompt: impl ToString) -> dialoguer::Result<T>
|
||||
where
|
||||
<T as FromStr>::Err: ToString,
|
||||
{
|
||||
Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt.to_string())
|
||||
.interact_text()
|
||||
}
|
||||
pub fn query_optional_variable<T: Clone + FromStr + ToString>(
|
||||
prompt: impl ToString,
|
||||
) -> dialoguer::Result<Option<T>>
|
||||
where
|
||||
<T as FromStr>::Err: ToString,
|
||||
{
|
||||
let input: T = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt.to_string())
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
if input.to_string().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(input))
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod config;
|
||||
pub mod configurable;
|
||||
pub mod s3;
|
||||
#[macro_use]
|
||||
pub mod interactive;
|
||||
pub mod config_option;
|
||||
pub mod speedtest;
|
||||
@@ -1,67 +0,0 @@
|
||||
use clap::Args;
|
||||
use opendal::Operator;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
commands::connect::{config_option::ConfigOption, configurable::Configure},
|
||||
interactive_variable,
|
||||
operator_builder::OperatorBuilder,
|
||||
};
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
pub struct S3ConfigCli {
|
||||
key_id: Option<String>,
|
||||
secret_key: Option<String>,
|
||||
endpoint: Option<String>,
|
||||
region: Option<String>,
|
||||
bucket_name: Option<String>,
|
||||
root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct S3Config {
|
||||
key_id: String,
|
||||
secret_key: String,
|
||||
endpoint: String,
|
||||
region: String,
|
||||
bucket_name: String,
|
||||
root: Option<String>,
|
||||
}
|
||||
|
||||
impl Configure for S3ConfigCli {
|
||||
async fn configure(self, name: &mut Option<String>) -> anyhow::Result<ConfigOption> {
|
||||
interactive_variable!(self, key_id, "S3 Key ID");
|
||||
interactive_variable!(self, secret_key, "S3 Secret Key");
|
||||
interactive_variable!(self, region, "S3 Region");
|
||||
interactive_variable!(self, bucket_name, "S3 Bucket Name");
|
||||
interactive_variable!(self, endpoint, "S3 Endpoint");
|
||||
if let None = name {
|
||||
*name = Some(endpoint.clone());
|
||||
}
|
||||
Ok(ConfigOption::S3(S3Config {
|
||||
secret_key,
|
||||
key_id,
|
||||
region,
|
||||
bucket_name,
|
||||
endpoint,
|
||||
root: self.root,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl OperatorBuilder for S3Config {
|
||||
fn build(&self) -> anyhow::Result<Operator> {
|
||||
let builder = opendal::services::S3::default()
|
||||
.access_key_id(&self.key_id)
|
||||
.secret_access_key(&self.secret_key)
|
||||
.region(&self.region)
|
||||
.endpoint(&self.endpoint)
|
||||
.root(self.root.as_deref().unwrap_or("/"))
|
||||
.bucket(&self.bucket_name)
|
||||
.disable_config_load();
|
||||
|
||||
let op: Operator = Operator::new(builder)?.finish();
|
||||
|
||||
Ok(op)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use rand::{RngCore, SeedableRng, rng, rngs::StdRng};
|
||||
use tokio::io::AsyncRead;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Speedtest<F: Fn(f32)> {
|
||||
core: rand::rngs::StdRng,
|
||||
to_write: usize,
|
||||
callback: Box<F>,
|
||||
}
|
||||
pub const SPEEDTEST_BYTES: usize = 64 * 1024 * 1024;
|
||||
pub const SPEEDTEST_PATH: &str = "speedtest";
|
||||
|
||||
impl<F: Fn(f32)> AsyncRead for Speedtest<F> {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let mut s = self;
|
||||
let to_write = buf.remaining().min(s.to_write);
|
||||
|
||||
let filled = {
|
||||
let fill_slice = buf.initialize_unfilled_to(to_write);
|
||||
s.core.fill_bytes(fill_slice);
|
||||
fill_slice.len()
|
||||
};
|
||||
s.to_write = s.to_write.saturating_sub(filled);
|
||||
(s.callback)((1f32 - (s.to_write as f32 / SPEEDTEST_BYTES as f32)) * 100f32);
|
||||
buf.advance(filled);
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
impl<F: Fn(f32)> Speedtest<F> {
|
||||
pub fn new(callback: F) -> Self {
|
||||
Self {
|
||||
core: StdRng::from_rng(&mut rng()),
|
||||
to_write: SPEEDTEST_BYTES,
|
||||
callback: Box::new(callback),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod connect;
|
||||
pub mod upload;
|
||||
@@ -1,79 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
cli::UploadInfo,
|
||||
commands::connect::{config::Config, config_option::ConfigOption},
|
||||
manifest::{ClosureFactory, CompressionOption, DepotManifest, generate_v2_manifest},
|
||||
operator_builder::OperatorBuilder,
|
||||
};
|
||||
use futures::AsyncWriteExt;
|
||||
use log::info;
|
||||
use opendal::{FuturesAsyncWriter, Operator};
|
||||
use tokio_util::compat::{Compat, FuturesAsyncWriteCompatExt};
|
||||
|
||||
pub async fn upload(
|
||||
upload_info: &UploadInfo,
|
||||
config: Config,
|
||||
name: &Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let game_id = upload_info.game_id.clone();
|
||||
let path = upload_info.path.clone();
|
||||
let version_id = upload_info.version_id.clone();
|
||||
|
||||
let operator = get_operator(config, name)?;
|
||||
|
||||
let mut existing_depot_manifest = get_depot_manifest(&operator).await?;
|
||||
|
||||
info!("Uploading chunks");
|
||||
|
||||
let v2_manifest = generate_v2_manifest(
|
||||
Path::new(&path),
|
||||
ClosureFactory::new(
|
||||
async move |id: String| {
|
||||
info!("Uploading chunk id {id}");
|
||||
let writer = operator
|
||||
.writer(&format!("{game_id}/{version_id}/{id}"))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_futures_async_write()
|
||||
.compat_write();
|
||||
writer
|
||||
},
|
||||
|writer: Compat<FuturesAsyncWriter>| async {
|
||||
writer.into_inner().close().await.unwrap();
|
||||
},
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Finished uploading chunks");
|
||||
|
||||
existing_depot_manifest.append(
|
||||
upload_info.game_id.to_string(),
|
||||
upload_info.version_id.to_string(),
|
||||
CompressionOption::None,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_depot_manifest(operator: &Operator) -> Result<DepotManifest, anyhow::Error> {
|
||||
let existing_depot_manifest = operator.read("manifest.json").await?.to_bytes();
|
||||
let existing_depot_manifest: DepotManifest =
|
||||
serde_json::from_slice(existing_depot_manifest.as_ref())?;
|
||||
Ok(existing_depot_manifest)
|
||||
}
|
||||
|
||||
fn get_operator(config: Config, name: &Option<String>) -> anyhow::Result<Operator> {
|
||||
let operator = match if let Some(name) = name {
|
||||
config
|
||||
.get(name)
|
||||
.ok_or(anyhow::anyhow!("Name does not exist"))?
|
||||
} else {
|
||||
config.get_active().ok_or(anyhow::anyhow!(
|
||||
"No active connection set. Please specify with --name"
|
||||
))?
|
||||
} {
|
||||
ConfigOption::S3(s3_config) => s3_config.build()?,
|
||||
};
|
||||
Ok(operator)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod interface;
|
||||
@@ -1,53 +0,0 @@
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use log::LevelFilter;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
|
||||
pub fn configure_logging() -> anyhow::Result<()> {
|
||||
let log_level = env::var("RUST_LOG")
|
||||
.unwrap_or_else(|_| "info".to_string())
|
||||
.parse::<LevelFilter>()?;
|
||||
|
||||
let log_dir = env::var("LOG_FILE_DIR").unwrap_or_else(|_| "logs".to_string());
|
||||
|
||||
fs::create_dir_all(&log_dir)?;
|
||||
|
||||
let colors = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
.info(Color::Blue)
|
||||
.debug(Color::Green)
|
||||
.trace(Color::Magenta);
|
||||
|
||||
fern::Dispatch::new()
|
||||
.chain(
|
||||
fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}: {}",
|
||||
chrono::Local::now().format("%H:%M:%S%.3f"),
|
||||
colors.color(record.level()),
|
||||
message
|
||||
))
|
||||
})
|
||||
.chain(io::stdout()),
|
||||
)
|
||||
.chain(
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {} {} - {}",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.chain(fern::log_file(format!("{}/app.log", log_dir))?),
|
||||
)
|
||||
.level(log_level)
|
||||
.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#![feature(async_fn_traits)]
|
||||
|
||||
use crate::commands::connect::config::manage_configuration;
|
||||
use crate::{
|
||||
cli::{Cli, Commands},
|
||||
commands::connect::config::Config,
|
||||
commands::upload,
|
||||
};
|
||||
use clap::Parser;
|
||||
mod cli;
|
||||
mod commands;
|
||||
mod logging;
|
||||
mod manifest;
|
||||
mod operator_builder;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
crate::logging::configure_logging()?;
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let mut config = Config::read();
|
||||
match cli.command {
|
||||
Commands::Connect { name, option } => {
|
||||
manage_configuration(&mut config, name, option).await?
|
||||
}
|
||||
Commands::Upload { info, name } => {
|
||||
let info = info.interactive_configure();
|
||||
upload::interface::upload(&info, config, &name).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use droplet_rs::manifest::{Manifest, ManifestWriterFactory, generate_manifest_rusty};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncWrite;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DepotManifest {
|
||||
content: HashMap<String, DepotManifestGameData>,
|
||||
}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DepotManifestGameData {
|
||||
version_id: String,
|
||||
compression: CompressionOption,
|
||||
}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum CompressionOption {
|
||||
None,
|
||||
Gzip,
|
||||
Zstd,
|
||||
}
|
||||
impl DepotManifest {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
content: HashMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn append(&mut self, game_id: String, version_id: String, compression: CompressionOption) {
|
||||
self.content.insert(
|
||||
game_id,
|
||||
DepotManifestGameData {
|
||||
version_id,
|
||||
compression,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClosureFactory<Writer, Factory, Closer>
|
||||
where
|
||||
Writer: AsyncWrite + Unpin,
|
||||
Factory: AsyncFn(String) -> Writer,
|
||||
Closer: AsyncFn(Writer),
|
||||
{
|
||||
writer: Factory,
|
||||
closer: Closer,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<
|
||||
W: AsyncWrite + Unpin + Send + Sync,
|
||||
F: AsyncFn(String) -> W + Send + Sync + 'static,
|
||||
C: AsyncFn(W) + Send + Sync,
|
||||
> ManifestWriterFactory for ClosureFactory<W, F, C>
|
||||
where
|
||||
for<'a> F::CallRefFuture<'a>: Send,
|
||||
for<'b> C::CallRefFuture<'b>: Send,
|
||||
{
|
||||
type Writer = W;
|
||||
|
||||
async fn create(&self, id: String) -> anyhow::Result<Self::Writer> {
|
||||
let func = &self.writer;
|
||||
let output = func(id).await;
|
||||
Ok(output)
|
||||
}
|
||||
async fn close(&self, writer: Self::Writer) -> anyhow::Result<()> {
|
||||
let func = &self.closer;
|
||||
func(writer).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
W: AsyncWrite + Unpin + Send + Sync,
|
||||
F: AsyncFn(String) -> W + Send + Sync + 'static,
|
||||
C: AsyncFn(W) + Sync,
|
||||
> ClosureFactory<W, F, C>
|
||||
where
|
||||
for<'a> F::CallRefFuture<'a>: Send,
|
||||
for<'b> C::CallRefFuture<'b>: Send,
|
||||
{
|
||||
pub fn new(f: F, c: C) -> Self {
|
||||
Self {
|
||||
writer: f,
|
||||
closer: c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_v2_manifest<Factory>(dir: &Path, factory: Factory) -> anyhow::Result<Manifest>
|
||||
where
|
||||
Factory: ManifestWriterFactory,
|
||||
{
|
||||
let progress_bar = ProgressBar::new(10_000).with_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%")
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
generate_manifest_rusty(
|
||||
dir,
|
||||
|progress| {
|
||||
let progress_int = (progress * 100f32).round() as u64;
|
||||
progress_bar.set_position(progress_int);
|
||||
},
|
||||
|log| progress_bar.suspend(|| info!("{}", log)),
|
||||
Some(&factory),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
use opendal::Operator;
|
||||
|
||||
pub trait OperatorBuilder {
|
||||
fn build(&self) -> anyhow::Result<Operator>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto px-6 py-4">
|
||||
<span class="inline-flex items-center gap-x-2 font-semibold text-zinc-100">
|
||||
<UserIcon class="size-5" /> {{ $t("account.title") }}
|
||||
<UserIcon class="size-5" /> Account Settings
|
||||
</span>
|
||||
<nav class="flex flex-1 flex-col">
|
||||
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
||||
@@ -45,55 +45,40 @@ import {
|
||||
LockClosedIcon,
|
||||
DevicePhoneMobileIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
CodeBracketIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import { UserIcon } from "@heroicons/vue/24/solid";
|
||||
import type { Component } from "vue";
|
||||
|
||||
const notifications = useNotifications();
|
||||
const { t } = useI18n();
|
||||
|
||||
const navigation: Ref<
|
||||
(NavigationItem & { icon: Component; count?: number })[]
|
||||
> = computed(() => [
|
||||
const navigation: (NavigationItem & { icon: Component; count?: number })[] = [
|
||||
{ label: "Home", route: "/account", icon: HomeIcon, prefix: "/account" },
|
||||
{
|
||||
label: t("account.home.title"),
|
||||
route: "/account",
|
||||
icon: HomeIcon,
|
||||
prefix: "/account",
|
||||
},
|
||||
{
|
||||
label: t("account.security.title"),
|
||||
label: "Security",
|
||||
route: "/account/security",
|
||||
prefix: "/account/security",
|
||||
icon: LockClosedIcon,
|
||||
},
|
||||
{
|
||||
label: t("account.devices.title"),
|
||||
label: "Devices",
|
||||
route: "/account/devices",
|
||||
prefix: "/account/devices",
|
||||
icon: DevicePhoneMobileIcon,
|
||||
},
|
||||
{
|
||||
label: t("account.token.title"),
|
||||
route: "/account/tokens",
|
||||
prefix: "/account/tokens",
|
||||
icon: CodeBracketIcon,
|
||||
},
|
||||
{
|
||||
label: t("account.notifications.notifications"),
|
||||
label: "Notifications",
|
||||
route: "/account/notifications",
|
||||
prefix: "/account/notifications",
|
||||
icon: BellIcon,
|
||||
count: notifications.value.length,
|
||||
},
|
||||
{
|
||||
label: t("account.settings"),
|
||||
label: "Settings",
|
||||
route: "/account/settings",
|
||||
prefix: "/account/settings",
|
||||
icon: WrenchScrewdriverIcon,
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation.value);
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
class="transition w-full inline-flex items-center justify-center h-full gap-x-2 rounded-none rounded-l-md bg-white/10 hover:bg-white/20 text-zinc-100 backdrop-blur px-5 py-3 active:scale-95"
|
||||
@click="() => toggleLibrary()"
|
||||
>
|
||||
{{ inLibrary ? $t("library.inLib") : $t("library.addToLib") }}
|
||||
{{ inLibrary ? "In Library" : "Add to Library" }}
|
||||
<CheckIcon v-if="inLibrary" class="-mr-0.5 h-5 w-5" aria-hidden="true" />
|
||||
<PlusIcon v-else class="-mr-0.5 h-5 w-5" aria-hidden="true" />
|
||||
</LoadingButton>
|
||||
@@ -36,7 +36,7 @@
|
||||
<div
|
||||
class="font-display uppercase px-3 py-2 text-sm font-semibold text-zinc-500"
|
||||
>
|
||||
{{ $t("library.collection.collections") }}
|
||||
Collections
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-y-2 py-1 max-h-[150px] overflow-y-auto"
|
||||
@@ -45,7 +45,7 @@
|
||||
v-if="collections.length === 0"
|
||||
class="px-3 py-2 text-sm text-zinc-500"
|
||||
>
|
||||
{{ $t("library.collection.noCollections") }}
|
||||
No collections
|
||||
</div>
|
||||
<MenuItem
|
||||
v-for="(collection, collectionIdx) in collections"
|
||||
@@ -75,7 +75,7 @@
|
||||
@click="createCollectionModal = true"
|
||||
>
|
||||
<PlusIcon class="mr-2 h-4 w-4" />
|
||||
{{ $t("library.collection.addToNew") }}
|
||||
Add to new collection
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<ModalCreateCollection
|
||||
<CreateCollectionModal
|
||||
v-model="createCollectionModal"
|
||||
:game-id="props.gameId"
|
||||
/>
|
||||
@@ -100,7 +100,6 @@ const props = defineProps<{
|
||||
|
||||
const isLibraryLoading = ref(false);
|
||||
|
||||
const { t } = useI18n();
|
||||
const createCollectionModal = ref(false);
|
||||
const collections = await useCollections();
|
||||
const library = await useLibrary();
|
||||
@@ -122,9 +121,18 @@ async function toggleLibrary() {
|
||||
body: {
|
||||
id: props.gameId,
|
||||
},
|
||||
failTitle: t("errors.library.add.title"),
|
||||
});
|
||||
await refreshLibrary();
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Failed to add game to library",
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
description: `Drop couldn't add this game to your library: ${e?.statusMessage}`,
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
} finally {
|
||||
isLibraryLoading.value = false;
|
||||
}
|
||||
@@ -136,18 +144,24 @@ async function toggleCollection(id: string) {
|
||||
if (!collection) return;
|
||||
const index = collection.entries.findIndex((e) => e.gameId == props.gameId);
|
||||
|
||||
await $dropFetch(`/api/v1/collection/:id/entry`, {
|
||||
await $dropFetch(`/api/v1/collection/${id}/entry`, {
|
||||
method: index == -1 ? "POST" : "DELETE",
|
||||
params: { id },
|
||||
body: {
|
||||
id: props.gameId,
|
||||
},
|
||||
failTitle: t("errors.library.add.title"),
|
||||
});
|
||||
|
||||
await refreshCollection(id);
|
||||
} finally {
|
||||
/* empty */
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Failed to add game to library",
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
description: `Drop couldn't add this game to your library: ${e?.statusMessage}`,
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<a
|
||||
href="/auth/oidc"
|
||||
class="transition rounded-md grow inline-flex items-center justify-center bg-white/10 px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-white/20"
|
||||
>
|
||||
Sign in with external provider →
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,7 +4,7 @@
|
||||
<label
|
||||
for="username"
|
||||
class="block text-sm font-medium leading-6 text-zinc-300"
|
||||
>{{ $t("auth.username") }}</label
|
||||
>Username</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
@@ -12,7 +12,7 @@
|
||||
v-model="username"
|
||||
name="username"
|
||||
type="username"
|
||||
autocomplete="username webauthn"
|
||||
autocomplete="username"
|
||||
required
|
||||
class="block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
@@ -23,7 +23,7 @@
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm font-medium leading-6 text-zinc-300"
|
||||
>{{ $t("auth.password") }}</label
|
||||
>Password</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
@@ -50,23 +50,19 @@
|
||||
<label
|
||||
for="remember-me"
|
||||
class="ml-3 block text-sm leading-6 text-zinc-400"
|
||||
>{{ $t("auth.signin.rememberMe") }}</label
|
||||
>Remember me</label
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="text-sm leading-6">
|
||||
<NuxtLink
|
||||
to="#"
|
||||
class="font-semibold text-blue-600 hover:text-blue-500"
|
||||
>{{ $t("auth.signin.forgot") }}</NuxtLink
|
||||
<NuxtLink to="#" class="font-semibold text-blue-600 hover:text-blue-500"
|
||||
>Forgot password?</NuxtLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LoadingButton class="w-full" :loading="loading">{{
|
||||
$t("auth.signin.signin")
|
||||
}}</LoadingButton>
|
||||
<LoadingButton class="w-full" :loading="loading"> Sign in</LoadingButton>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
@@ -86,78 +82,35 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/20/solid";
|
||||
import {
|
||||
startAuthentication,
|
||||
browserSupportsWebAuthn,
|
||||
} from "@simplewebauthn/browser";
|
||||
import { FetchError } from "ofetch";
|
||||
import type { User } from "~/prisma/client";
|
||||
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const rememberMe = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
async function passkeyAutofill() {
|
||||
let silentWebauthnOptions;
|
||||
try {
|
||||
silentWebauthnOptions = await $dropFetch("/api/v1/auth/passkey/start", {
|
||||
method: "POST",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await startAuthentication({
|
||||
optionsJSON: silentWebauthnOptions,
|
||||
useBrowserAutofill: true,
|
||||
});
|
||||
|
||||
loading.value = true;
|
||||
|
||||
await $dropFetch("/api/v1/auth/passkey/finish", {
|
||||
method: "POST",
|
||||
body: result,
|
||||
});
|
||||
|
||||
await completeSignin();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (browserSupportsWebAuthn()) {
|
||||
try {
|
||||
await passkeyAutofill();
|
||||
} catch (response) {
|
||||
const message = (response as FetchError).message || t("errors.unknown");
|
||||
error.value = message;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const error = ref<string | undefined>();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
async function signin_wrapper() {
|
||||
function signin_wrapper() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await signin();
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError) {
|
||||
error.value = e.data.message || t("errors.unknown");
|
||||
} else {
|
||||
error.value = e as string;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
signin()
|
||||
.then(() => {
|
||||
router.push(route.query.redirect?.toString() ?? "/");
|
||||
})
|
||||
.catch((response) => {
|
||||
const message = response.statusMessage || "An unknown error occurred";
|
||||
error.value = message;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function signin() {
|
||||
const { result } = await $dropFetch("/api/v1/auth/signin/simple", {
|
||||
await $dropFetch("/api/v1/auth/signin/simple", {
|
||||
method: "POST",
|
||||
body: {
|
||||
username: username.value,
|
||||
@@ -165,11 +118,7 @@ async function signin() {
|
||||
rememberMe: rememberMe.value,
|
||||
},
|
||||
});
|
||||
if (result == "2fa") {
|
||||
router.push({ query: route.query, path: "/auth/mfa" });
|
||||
return;
|
||||
}
|
||||
|
||||
await completeSignin();
|
||||
const user = useUser();
|
||||
user.value = await $dropFetch<User | null>("/api/v1/user");
|
||||
}
|
||||
</script>
|
||||
@@ -4,10 +4,9 @@
|
||||
v-for="(_, i) in amount"
|
||||
:key="i"
|
||||
:class="[
|
||||
carousel.currentSlide === i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
|
||||
carousel.currentSlide == i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
|
||||
'transition-all cursor-pointer h-2 rounded-full',
|
||||
]"
|
||||
@click="slideTo(i)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -19,8 +18,8 @@ const carousel = inject(injectCarousel)!;
|
||||
|
||||
const amount = carousel.maxSlide - carousel.minSlide + 1;
|
||||
|
||||
function slideTo(index: number) {
|
||||
const offsetIndex = index + carousel.minSlide;
|
||||
carousel.nav.slideTo(offsetIndex);
|
||||
}
|
||||
// function slideTo(index: number) {
|
||||
// const offsetIndex = index + carousel.minSlide;
|
||||
// carousel.nav.slideTo(offsetIndex);
|
||||
// }
|
||||
</script>
|
||||
+20
-9
@@ -3,10 +3,11 @@
|
||||
<template #default>
|
||||
<div>
|
||||
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-white">
|
||||
{{ $t("library.collection.create") }}
|
||||
Create collection
|
||||
</DialogTitle>
|
||||
<p class="mt-1 text-zinc-400 text-sm">
|
||||
{{ $t("library.collection.createDesc") }}
|
||||
Collections can used to organise your games and find them more easily,
|
||||
especially if you have a large library.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
@@ -14,7 +15,7 @@
|
||||
<input
|
||||
v-model="collectionName"
|
||||
type="text"
|
||||
:placeholder="$t('library.collection.namePlaceholder')"
|
||||
placeholder="Collection name"
|
||||
class="block w-full rounded-md border-0 bg-zinc-800 py-1.5 text-white shadow-sm ring-1 ring-inset ring-zinc-700 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
<button class="hidden" type="submit" />
|
||||
@@ -29,7 +30,7 @@
|
||||
class="w-full sm:w-fit"
|
||||
@click="() => createCollection()"
|
||||
>
|
||||
{{ $t("common.create") }}
|
||||
Create
|
||||
</LoadingButton>
|
||||
<button
|
||||
ref="cancelButtonRef"
|
||||
@@ -37,7 +38,7 @@
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="() => close()"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
@@ -46,7 +47,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { DialogTitle } from "@headlessui/vue";
|
||||
import type { CollectionEntryModel, GameModel } from "~/prisma/client/models";
|
||||
import type { CollectionEntry, Game } from "~/prisma/client";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -73,17 +74,15 @@ async function createCollection() {
|
||||
const response = await $dropFetch("/api/v1/collection", {
|
||||
method: "POST",
|
||||
body: { name: collectionName.value },
|
||||
failTitle: "Failed to create collection",
|
||||
});
|
||||
|
||||
// Add the game if provided
|
||||
if (props.gameId) {
|
||||
const entry = await $dropFetch<
|
||||
CollectionEntryModel & { game: SerializeObject<GameModel> }
|
||||
CollectionEntry & { game: SerializeObject<Game> }
|
||||
>(`/api/v1/collection/${response.id}/entry`, {
|
||||
method: "POST",
|
||||
body: { id: props.gameId },
|
||||
failTitle: "Failed to add game to collection",
|
||||
});
|
||||
response.entries.push(entry);
|
||||
}
|
||||
@@ -95,6 +94,18 @@ async function createCollection() {
|
||||
open.value = false;
|
||||
|
||||
emit("created", response.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to create collection:", error);
|
||||
|
||||
const err = error as { statusMessage?: string };
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Failed to create collection",
|
||||
description: `Drop couldn't create your collection: ${err?.statusMessage}`,
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
} finally {
|
||||
createCollectionLoading.value = false;
|
||||
}
|
||||
+12
-17
@@ -6,13 +6,13 @@
|
||||
as="h3"
|
||||
class="text-lg font-bold font-display text-zinc-100"
|
||||
>
|
||||
{{ $t("library.collection.delete") }}
|
||||
Delete Collection
|
||||
</DialogTitle>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
{{ $t("common.deleteConfirm", [collection?.name]) }}
|
||||
Are you sure you want to delete "{{ collection?.name }}"?
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-bold text-red-500">
|
||||
{{ $t("common.cannotUndo") }}
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,38 +22,35 @@
|
||||
class="bg-red-600 text-white hover:bg-red-500"
|
||||
@click="() => deleteCollection()"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
Delete
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
@click="() => (collection = undefined)"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CollectionModel } from "~/prisma/client/models";
|
||||
import type { Collection } from "~/prisma/client";
|
||||
import { DialogTitle } from "@headlessui/vue";
|
||||
|
||||
const collection = defineModel<CollectionModel | undefined>();
|
||||
const collection = defineModel<Collection | undefined>();
|
||||
const deleteLoading = ref(false);
|
||||
|
||||
const collections = await useCollections();
|
||||
const { t } = useI18n();
|
||||
|
||||
async function deleteCollection() {
|
||||
try {
|
||||
if (!collection.value) return;
|
||||
|
||||
deleteLoading.value = true;
|
||||
await $dropFetch(`/api/v1/collection/:id`, {
|
||||
await $dropFetch(`/api/v1/collection/${collection.value.id}`, {
|
||||
// @ts-expect-error not documented
|
||||
method: "DELETE",
|
||||
params: {
|
||||
id: collection.value.id,
|
||||
},
|
||||
});
|
||||
const index = collections.value.findIndex(
|
||||
(e) => e.id == collection.value?.id,
|
||||
@@ -65,11 +62,9 @@ async function deleteCollection() {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: t("errors.library.add.title"),
|
||||
description: t("errors.library.add.desc", [
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
e?.statusMessage ?? t("errors.unknown"),
|
||||
]),
|
||||
title: "Failed to add game to library",
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
description: `Drop couldn't add this game to your library: ${e?.statusMessage}`,
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
@@ -6,13 +6,13 @@
|
||||
as="h3"
|
||||
class="text-lg font-bold font-display text-zinc-100"
|
||||
>
|
||||
{{ $t("news.delete") }}
|
||||
Delete Article
|
||||
</DialogTitle>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
{{ $t("common.deleteConfirm", [article?.title]) }}
|
||||
Are you sure you want to delete "{{ article?.title }}"?
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-bold text-red-500">
|
||||
{{ $t("common.cannotUndo") }}
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,13 +22,13 @@
|
||||
class="bg-red-600 text-white hover:bg-red-500"
|
||||
@click="() => deleteArticle()"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
Delete
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
@click="() => (article = undefined)"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
@@ -45,7 +45,6 @@ interface Article {
|
||||
const article = defineModel<Article | undefined>();
|
||||
const deleteLoading = ref(false);
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const news = useNews();
|
||||
if (!news.value) {
|
||||
news.value = await fetchNews();
|
||||
@@ -69,11 +68,9 @@ async function deleteArticle() {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: t("errors.news.article.delete.title"),
|
||||
description: t("errors.news.article.delete.desc", [
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
e?.statusMessage ?? t("errors.unknown"),
|
||||
]),
|
||||
title: "Failed to delete article",
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
description: `Drop couldn't delete this article: ${e?.statusMessage}`,
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
@@ -1,17 +1,14 @@
|
||||
<svg
|
||||
<template>
|
||||
<svg
|
||||
class="text-blue-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<style>
|
||||
:root {
|
||||
color: oklch(70.7% 0.165 254.624)
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M4 13.5C4 11.0008 5.38798 8.76189 7.00766 7C8.43926 5.44272 10.0519 4.25811 11.0471 3.5959C11.6287 3.20893 12.3713 3.20893 12.9529 3.5959C13.9481 4.25811 15.5607 5.44272 16.9923 7C18.612 8.76189 20 11.0008 20 13.5C20 17.9183 16.4183 21.5 12 21.5C7.58172 21.5 4 17.9183 4 13.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
</svg>
|
||||
</template>
|
||||
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 503 B |
@@ -10,18 +10,9 @@
|
||||
d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z"
|
||||
/>
|
||||
</svg>
|
||||
<ApplicationLogo aria-hidden="true" class="h-6" />
|
||||
<span class="text-blue-400 font-display font-bold text-xl uppercase">
|
||||
<template v-if="serverName">
|
||||
{{ serverName }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("drop.drop") }}
|
||||
</template>
|
||||
</span>
|
||||
<DropLogo class="h-6" />
|
||||
<span class="text-blue-400 font-display font-bold text-xl uppercase"
|
||||
>Drop</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { serverName } = await $dropFetch("/api/v1");
|
||||
</script>
|
||||
@@ -7,11 +7,7 @@
|
||||
:key="gameIdx"
|
||||
class="justify-start"
|
||||
>
|
||||
<GamePanel
|
||||
:game="game"
|
||||
:href="game ? `/store/${game.id}` : undefined"
|
||||
:show-title-description="showGamePanelTextDecoration"
|
||||
/>
|
||||
<GamePanel :game="game" />
|
||||
</VueSlide>
|
||||
|
||||
<template #addons>
|
||||
@@ -35,23 +31,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GameModel } from "~/prisma/client/models";
|
||||
import type { Game } from "~/prisma/client";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
const props = defineProps<{
|
||||
items: Array<SerializeObject<GameModel>>;
|
||||
items: Array<SerializeObject<Game>>;
|
||||
min?: number;
|
||||
width?: number;
|
||||
}>();
|
||||
|
||||
const {
|
||||
store: { showGamePanelTextDecoration },
|
||||
} = await $dropFetch(`/api/v1/settings`);
|
||||
|
||||
const currentComponent = ref<HTMLDivElement>();
|
||||
|
||||
const min = computed(() => Math.max(props.min ?? 8, props.items.length));
|
||||
const games: Ref<Array<SerializeObject<GameModel> | undefined>> = computed(() =>
|
||||
const games: Ref<Array<SerializeObject<Game> | undefined>> = computed(() =>
|
||||
Array(min.value)
|
||||
.fill(0)
|
||||
.map((_, i) => props.items[i]),
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
v-if="game"
|
||||
:href="props.href ?? `/store/${game.id}`"
|
||||
class="group relative w-48 h-64 rounded-lg overflow-hidden transition-all duration-300 text-left hover:scale-[1.02] hover:shadow-lg hover:-translate-y-0.5"
|
||||
@click="active = game.id"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 transition-all duration-300 group-hover:scale-110"
|
||||
>
|
||||
<img
|
||||
:src="useObject(game.mCoverObjectId)"
|
||||
class="w-full h-full object-cover brightness-[90%]"
|
||||
:class="{ active: active === game.id }"
|
||||
:alt="game.mName"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-zinc-950/80 via-zinc-950/20 to-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full p-3">
|
||||
<h1
|
||||
class="text-zinc-100 text-sm font-bold font-display group-hover:text-white transition-colors"
|
||||
>
|
||||
{{ game.mName }}
|
||||
</h1>
|
||||
<p
|
||||
class="text-zinc-400 text-xs line-clamp-2 group-hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<SkeletonCard v-else message="no game" />>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
const props = defineProps<{
|
||||
game:
|
||||
| SerializeObject<{
|
||||
id: string;
|
||||
mCoverObjectId: string;
|
||||
mName: string;
|
||||
mShortDescription: string;
|
||||
}>
|
||||
| undefined;
|
||||
href?: string;
|
||||
}>();
|
||||
|
||||
const active = useState();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
img.active {
|
||||
view-transition-name: selected-game;
|
||||
contain: layout;
|
||||
}
|
||||
</style>
|
||||
+3
-7
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<img
|
||||
:src="rawIcon ? game.icon : useObject(game.icon)"
|
||||
class="w-12 h-12 rounded-sm object-cover"
|
||||
/>
|
||||
<img :src="game.icon" class="w-12 h-12 rounded-sm object-cover" />
|
||||
<div class="flex flex-col items-left">
|
||||
<h1 class="font-semibold font-display text-lg text-zinc-100">
|
||||
{{ game.name }}
|
||||
@@ -21,8 +18,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
|
||||
const { game, rawIcon = true } = defineProps<{
|
||||
game: Omit<GameMetadataSearchResult, "year"> & { sourceName?: string };
|
||||
rawIcon?: boolean;
|
||||
const { game } = defineProps<{
|
||||
game: GameMetadataSearchResult & { sourceName?: string };
|
||||
}>();
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex grow flex-col overflow-y-auto px-6 py-4">
|
||||
<span class="inline-flex items-center gap-x-2 font-semibold text-zinc-100">
|
||||
<Bars3Icon class="size-6" /> {{ $t("userHeader.links.library") }}
|
||||
<Bars3Icon class="size-6" /> Library
|
||||
</span>
|
||||
|
||||
<!-- Search bar -->
|
||||
@@ -13,7 +13,7 @@
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
class="block w-full rounded-md bg-zinc-900 py-2 pl-9 pr-2 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600"
|
||||
:placeholder="$t('library.search')"
|
||||
placeholder="Search library..."
|
||||
/>
|
||||
<MagnifyingGlassIcon
|
||||
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400"
|
||||
@@ -31,11 +31,11 @@
|
||||
<li v-for="game in filteredLibrary" :key="game.id" class="flex">
|
||||
<NuxtLink
|
||||
:to="`/library/game/${game.id}`"
|
||||
class="flex flex-row items-center w-full p-2 rounded-md transition-all duration-200 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg active:scale-95"
|
||||
class="flex flex-row items-center w-full p-1 rounded-md transition-all duration-200 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg active:scale-95"
|
||||
>
|
||||
<img
|
||||
:src="useObject(game.mIconObjectId)"
|
||||
class="h-5 flex-shrink-0 rounded transition-all duration-300 group-hover:scale-105 hover:rotate-[-2deg] hover:shadow-lg"
|
||||
:src="useObject(game.mCoverObjectId)"
|
||||
class="h-9 w-9 flex-shrink-0 rounded transition-all duration-300 group-hover:scale-105 hover:rotate-[-2deg] hover:shadow-lg"
|
||||
alt=""
|
||||
/>
|
||||
<div class="min-w-0 flex-1 pl-2.5">
|
||||
@@ -53,7 +53,7 @@
|
||||
v-else
|
||||
class="text-zinc-600 text-sm font-display font-bold uppercase text-center mt-8"
|
||||
>
|
||||
{{ !!searchQuery ? $t("common.noResults") : $t("library.noGames") }}
|
||||
{{ !!searchQuery ? "No results" : "No games in library" }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
+29
-75
@@ -11,18 +11,18 @@
|
||||
class="h-5 w-5 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': modalOpen }"
|
||||
/>
|
||||
<span>{{ $t("news.article.new") }}</span>
|
||||
<span>New article</span>
|
||||
</button>
|
||||
|
||||
<ModalTemplate v-model="modalOpen" size-class="sm:max-w-[80vw]">
|
||||
<h3 class="text-lg font-semibold text-zinc-100 mb-4">
|
||||
{{ $t("news.article.create") }}
|
||||
Create New Article
|
||||
</h3>
|
||||
<form class="space-y-4" @submit.prevent="() => createArticle()">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-zinc-400">{{
|
||||
$t("news.article.titles")
|
||||
}}</label>
|
||||
<label for="title" class="block text-sm font-medium text-zinc-400"
|
||||
>Title</label
|
||||
>
|
||||
<input
|
||||
id="title"
|
||||
v-model="newArticle.title"
|
||||
@@ -34,10 +34,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="excerpt"
|
||||
class="block text-sm font-medium text-zinc-400"
|
||||
>{{ $t("news.article.shortDesc") }}</label
|
||||
<label for="excerpt" class="block text-sm font-medium text-zinc-400"
|
||||
>Short description</label
|
||||
>
|
||||
<input
|
||||
id="excerpt"
|
||||
@@ -49,10 +47,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="content"
|
||||
class="block text-sm font-medium text-zinc-400"
|
||||
>{{ $t("news.article.content") }}</label
|
||||
<label for="content" class="block text-sm font-medium text-zinc-400"
|
||||
>Content (Markdown)</label
|
||||
>
|
||||
<div class="mt-1 flex flex-col gap-4">
|
||||
<!-- Markdown shortcuts -->
|
||||
@@ -73,9 +69,7 @@
|
||||
>
|
||||
<!-- Editor -->
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-zinc-500 mb-2">{{
|
||||
$t("news.article.editor")
|
||||
}}</span>
|
||||
<span class="text-sm text-zinc-500 mb-2">Editor</span>
|
||||
<textarea
|
||||
id="content"
|
||||
ref="contentEditor"
|
||||
@@ -88,9 +82,7 @@
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-zinc-500 mb-2">{{
|
||||
$t("news.article.preview")
|
||||
}}</span>
|
||||
<span class="text-sm text-zinc-500 mb-2">Preview</span>
|
||||
<div
|
||||
class="flex-1 p-4 rounded-md bg-zinc-900 border border-zinc-700 overflow-y-auto"
|
||||
>
|
||||
@@ -103,7 +95,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-500">
|
||||
{{ $t("news.article.editorGuide") }}
|
||||
Use the shortcuts above or write Markdown directly. Supports
|
||||
**bold**, *italic*, [links](url), and more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +114,7 @@
|
||||
/>
|
||||
<span
|
||||
class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500"
|
||||
>{{ $t("news.article.uploadCover") }}</span
|
||||
>Upload cover image</span
|
||||
>
|
||||
<p v-if="currentFile" class="mt-1 text-xs text-zinc-400">
|
||||
{{ currentFile.name }}
|
||||
@@ -132,14 +125,14 @@
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
type="file"
|
||||
@change="(e: Event) => (file = (e.target as any)?.files)"
|
||||
@change="(e) => (file = (e.target as any)?.files)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-400 mb-2">{{
|
||||
$t("common.tags")
|
||||
}}</label>
|
||||
<label class="block text-sm font-medium text-zinc-400 mb-2"
|
||||
>Tags</label
|
||||
>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
<span
|
||||
v-for="tag in newArticle.tags"
|
||||
@@ -160,7 +153,7 @@
|
||||
<input
|
||||
v-model="newTagInput"
|
||||
type="text"
|
||||
:placeholder="$t('news.article.tagPlaceholder')"
|
||||
placeholder="Add a tag..."
|
||||
class="mt-1 block w-full rounded-md bg-zinc-900 border-zinc-700 text-zinc-100 shadow-sm focus:border-primary-500 focus:ring-primary-500"
|
||||
@keydown.enter.prevent="addTag"
|
||||
/>
|
||||
@@ -169,7 +162,7 @@
|
||||
class="mt-1 px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 hover:bg-zinc-700"
|
||||
@click="addTag"
|
||||
>
|
||||
{{ $t("news.article.add") }}
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,16 +186,15 @@
|
||||
<LoadingButton
|
||||
:loading="loading"
|
||||
class="bg-blue-600 text-white hover:bg-blue-500"
|
||||
:disabled="!isValidArticle"
|
||||
@click="() => createArticle()"
|
||||
>
|
||||
{{ $t("news.article.submit") }}
|
||||
Submit
|
||||
</LoadingButton>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
|
||||
@click="() => (modalOpen = !modalOpen)"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
@@ -236,13 +228,6 @@ const newArticle = ref({
|
||||
tags: [] as string[],
|
||||
});
|
||||
|
||||
const isValidArticle = computed(
|
||||
() =>
|
||||
newArticle.value.title &&
|
||||
newArticle.value.description &&
|
||||
newArticle.value.content,
|
||||
);
|
||||
|
||||
const markdownPreview = computed(() => {
|
||||
// TODO: maybe?? add https://github.com/cure53/DOMPurify
|
||||
// micromark says its safe, but this is straight html we are injecting
|
||||
@@ -251,49 +236,18 @@ const markdownPreview = computed(() => {
|
||||
|
||||
const file = ref<FileList | undefined>();
|
||||
const currentFile = computed(() => file.value?.item(0));
|
||||
const { t } = useI18n();
|
||||
|
||||
const error = ref<string | undefined>();
|
||||
|
||||
const contentEditor = ref<HTMLTextAreaElement>();
|
||||
|
||||
const markdownShortcuts = [
|
||||
{
|
||||
label: t("editor.bold"),
|
||||
prefix: "**",
|
||||
suffix: "**",
|
||||
placeholder: t("editor.boldPlaceholder"),
|
||||
},
|
||||
{
|
||||
label: t("editor.italic"),
|
||||
prefix: "_",
|
||||
suffix: "_",
|
||||
placeholder: t("editor.italicPlaceholder"),
|
||||
},
|
||||
{
|
||||
label: t("editor.link"),
|
||||
prefix: "[",
|
||||
suffix: "](url)",
|
||||
placeholder: t("editor.linkPlaceholder"),
|
||||
},
|
||||
{
|
||||
label: t("editor.code"),
|
||||
prefix: "`",
|
||||
suffix: "`",
|
||||
placeholder: t("editor.codePlaceholder"),
|
||||
},
|
||||
{
|
||||
label: t("editor.listItem"),
|
||||
prefix: "- ",
|
||||
suffix: "",
|
||||
placeholder: t("editor.listItemPlaceholder"),
|
||||
},
|
||||
{
|
||||
label: t("editor.heading"),
|
||||
prefix: "## ",
|
||||
suffix: "",
|
||||
placeholder: t("editor.headingPlaceholder"),
|
||||
},
|
||||
{ label: "Bold", prefix: "**", suffix: "**", placeholder: "bold text" },
|
||||
{ label: "Italic", prefix: "_", suffix: "_", placeholder: "italic text" },
|
||||
{ label: "Link", prefix: "[", suffix: "](url)", placeholder: "link text" },
|
||||
{ label: "Code", prefix: "`", suffix: "`", placeholder: "code" },
|
||||
{ label: "List Item", prefix: "- ", suffix: "", placeholder: "list item" },
|
||||
{ label: "Heading", prefix: "## ", suffix: "", placeholder: "heading" },
|
||||
];
|
||||
|
||||
function handleContentKeydown(e: KeyboardEvent) {
|
||||
@@ -415,7 +369,7 @@ async function createArticle() {
|
||||
modalOpen.value = false;
|
||||
} catch (e) {
|
||||
// @ts-expect-error attempt to get statusMessage on error
|
||||
error.value = e?.statusMessage ?? t("errors.unknown");
|
||||
error.value = e?.statusMessage ?? "An unknown error occured.";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- Search and filters -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="search" class="sr-only">{{ $t("news.search") }}</label>
|
||||
<label for="search" class="sr-only">Search articles</label>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||
@@ -21,35 +21,31 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="block w-full rounded-md border-0 bg-zinc-800 py-2.5 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||
:placeholder="$t('news.searchPlaceholder')"
|
||||
placeholder="Search articles..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<label
|
||||
for="date"
|
||||
class="block text-sm font-medium text-zinc-400 mb-2"
|
||||
>{{ $t("common.date") }}</label
|
||||
<label for="date" class="block text-sm font-medium text-zinc-400 mb-2"
|
||||
>Date</label
|
||||
>
|
||||
<select
|
||||
id="date"
|
||||
v-model="dateFilter"
|
||||
class="mt-1 block w-full rounded-md border-0 bg-zinc-800 py-2 pl-3 pr-10 text-zinc-100 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<option value="all">{{ $t("news.filter.all") }}</option>
|
||||
<option value="today">{{ $t("common.today") }}</option>
|
||||
<option value="week">{{ $t("news.filter.week") }}</option>
|
||||
<option value="month">{{ $t("news.filter.month") }}</option>
|
||||
<option value="year">{{ $t("news.filter.year") }}</option>
|
||||
<option value="all">All time</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="week">This week</option>
|
||||
<option value="month">This month</option>
|
||||
<option value="year">This year</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-400 mb-2">
|
||||
{{ $t("common.tags") }}
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-zinc-400 mb-2">Tags</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in availableTags"
|
||||
@@ -91,7 +87,9 @@
|
||||
:src="useObject(article.imageObjectId)"
|
||||
class="absolute blur-sm inset-0 w-full h-full object-cover transition-all duration-200 group-hover:scale-110"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-zinc-900/50" />
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-transparent to-zinc-800 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="relative text-sm font-medium text-zinc-100">
|
||||
@@ -104,9 +102,9 @@
|
||||
<div
|
||||
class="relative mt-2 flex items-center gap-x-2 text-xs text-zinc-500"
|
||||
>
|
||||
<time :datetime="article.publishedAt">
|
||||
{{ $d(new Date(article.publishedAt), "short") }}
|
||||
</time>
|
||||
<time :datetime="article.publishedAt">{{
|
||||
formatDate(article.publishedAt)
|
||||
}}</time>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
@@ -148,9 +146,20 @@ const toggleTag = (tag: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatExcerpt = (excerpt: string) => {
|
||||
// Convert markdown to HTML, micromark is safe
|
||||
return micromark(excerpt);
|
||||
// TODO: same as one in NewsArticleCreateButton
|
||||
// Convert markdown to HTML
|
||||
const html = micromark(excerpt);
|
||||
// Strip HTML tags using regex
|
||||
return html.replace(/<[^>]*>/g, "");
|
||||
};
|
||||
|
||||
const filteredArticles = computed(() => {
|
||||
@@ -24,6 +24,7 @@
|
||||
>
|
||||
{{ name }}
|
||||
</NuxtLink>
|
||||
<!-- todo -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex shrink-0">
|
||||
@@ -32,7 +33,7 @@
|
||||
class="inline-flex rounded-md text-zinc-400 hover:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
@click="() => deleteMe()"
|
||||
>
|
||||
<span class="sr-only">{{ $t("common.close") }}</span>
|
||||
<span class="sr-only">Close</span>
|
||||
<XMarkIcon class="size-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -43,19 +44,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { NotificationModel } from "~/prisma/client/models";
|
||||
import type { Notification } from "~/prisma/client";
|
||||
|
||||
const props = defineProps<{
|
||||
notification: SerializeObject<NotificationModel>;
|
||||
}>();
|
||||
const props = defineProps<{ notification: Notification }>();
|
||||
|
||||
async function deleteMe() {
|
||||
await $dropFetch(`/api/v1/notifications/:id`, {
|
||||
await $dropFetch(`/api/v1/notifications/${props.notification.id}`, {
|
||||
method: "DELETE",
|
||||
params: {
|
||||
id: props.notification.id,
|
||||
},
|
||||
});
|
||||
const notifications = useNotifications();
|
||||
const indexOfMe = notifications.value.findIndex(
|
||||
@@ -15,7 +15,7 @@
|
||||
/>
|
||||
<span class="ml-3 block truncate">{{ model }}</span>
|
||||
</span>
|
||||
<span v-else>{{ $t("library.admin.import.selectPlatform") }}</span>
|
||||
<span v-else>Please select a platform...</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2"
|
||||
>
|
||||
@@ -32,7 +32,7 @@
|
||||
class="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-950 ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="[name, value] in values"
|
||||
v-for="[name, value] in Object.entries(values)"
|
||||
:key="value"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
@@ -82,11 +82,10 @@ import {
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { Platform } from "~/prisma/client/enums";
|
||||
|
||||
const model = defineModel<Platform | undefined>();
|
||||
const model = defineModel<PlatformClient | undefined>();
|
||||
|
||||
const typedModel = computed<Platform | null>({
|
||||
const typedModel = computed<PlatformClient | null>({
|
||||
get() {
|
||||
return model.value || null;
|
||||
},
|
||||
@@ -96,5 +95,5 @@ const typedModel = computed<Platform | null>({
|
||||
},
|
||||
});
|
||||
|
||||
const values = Object.entries(Platform);
|
||||
const values = Object.fromEntries(Object.entries(PlatformClient));
|
||||
</script>
|
||||
@@ -49,38 +49,31 @@
|
||||
/>
|
||||
<span
|
||||
class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500"
|
||||
>{{ $t("uploadFile") }}</span
|
||||
>Upload file</span
|
||||
>
|
||||
<div v-if="currentFileList">
|
||||
<p
|
||||
v-for="currentFile in currentFileList"
|
||||
:key="currentFile"
|
||||
class="mt-1 text-[10px] text-zinc-500 whitespace-nowrap"
|
||||
>
|
||||
{{ currentFile }}
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="currentFile" class="mt-1 text-xs text-zinc-400">
|
||||
{{ currentFile.name }}
|
||||
</p>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
:accept="props.accept"
|
||||
class="hidden"
|
||||
type="file"
|
||||
:multiple="props.multiple"
|
||||
@change="(e: Event) => (file = (e.target as any)?.files)"
|
||||
@change="(e) => (file = (e.target as any)?.files)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<LoadingButton
|
||||
:disabled="currentFiles == undefined"
|
||||
:disabled="currentFile == undefined"
|
||||
type="button"
|
||||
:loading="uploadLoading"
|
||||
:class="['inline-flex w-full shadow-sm sm:ml-3 sm:w-auto']"
|
||||
@click="() => uploadFile_wrapper()"
|
||||
>
|
||||
{{ $t("upload") }}
|
||||
Upload
|
||||
</LoadingButton>
|
||||
<button
|
||||
ref="cancelButtonRef"
|
||||
@@ -88,7 +81,7 @@
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="open = false"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="uploadError" class="mt-3 rounded-md bg-red-600/10 p-4">
|
||||
@@ -129,21 +122,11 @@ const open = defineModel<boolean>({
|
||||
required: true,
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const file = ref<FileList | undefined>();
|
||||
const currentFiles = computed(() => file.value);
|
||||
const currentFileList = computed(() => {
|
||||
if (!currentFiles.value) return undefined;
|
||||
const list = [];
|
||||
for (const file of currentFiles.value) {
|
||||
list.push(file.name);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
const currentFile = computed(() => file.value?.item(0));
|
||||
const props = defineProps<{
|
||||
endpoint: string;
|
||||
accept: string;
|
||||
multiple?: boolean;
|
||||
options?: { [key: string]: string };
|
||||
}>();
|
||||
const emit = defineEmits(["upload"]);
|
||||
@@ -151,12 +134,10 @@ const emit = defineEmits(["upload"]);
|
||||
const uploadLoading = ref(false);
|
||||
const uploadError = ref<string | undefined>();
|
||||
async function uploadFile() {
|
||||
if (!currentFiles.value) return;
|
||||
if (!currentFile.value) return;
|
||||
|
||||
const form = new FormData();
|
||||
for (const file of currentFiles.value) {
|
||||
form.append(file.name, file);
|
||||
}
|
||||
form.append("file", currentFile.value);
|
||||
|
||||
if (props.options) {
|
||||
for (const [key, value] of Object.entries(props.options)) {
|
||||
@@ -177,7 +158,7 @@ function uploadFile_wrapper() {
|
||||
uploadLoading.value = true;
|
||||
uploadFile()
|
||||
.catch((error) => {
|
||||
uploadError.value = error.statusMessage ?? t("errors.unknown");
|
||||
uploadError.value = error.statusMessage ?? "An unknown error occurred.";
|
||||
})
|
||||
.finally(() => {
|
||||
uploadLoading.value = false;
|
||||
@@ -1,17 +1,14 @@
|
||||
<template>
|
||||
<footer class="bg-zinc-950" aria-labelledby="footer-heading">
|
||||
<h2 id="footer-heading" class="sr-only">{{ $t("footer.footer") }}</h2>
|
||||
<h2 id="footer-heading" class="sr-only">Footer</h2>
|
||||
<div class="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
||||
<!-- Drop Info -->
|
||||
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||
<div class="space-y-8">
|
||||
<DropWordmark class="h-10" />
|
||||
<p class="text-sm leading-6 text-zinc-300">
|
||||
{{ $t("drop.desc") }}
|
||||
An open-source game distribution platform built for speed,
|
||||
flexibility and beauty.
|
||||
</p>
|
||||
|
||||
<SelectorLanguage />
|
||||
|
||||
<div class="flex space-x-6">
|
||||
<NuxtLink
|
||||
v-for="item in navigation.social"
|
||||
@@ -25,14 +22,10 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foot links -->
|
||||
<div class="mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
|
||||
<div class="md:grid md:grid-cols-2 md:gap-8">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold leading-6 text-white">
|
||||
{{ $t("footer.games") }}
|
||||
</h3>
|
||||
<h3 class="text-sm font-semibold leading-6 text-white">Games</h3>
|
||||
<ul role="list" class="mt-6 space-y-4">
|
||||
<li v-for="item in navigation.games" :key="item.name">
|
||||
<NuxtLink
|
||||
@@ -45,7 +38,7 @@
|
||||
</div>
|
||||
<div class="mt-10 md:mt-0">
|
||||
<h3 class="text-sm font-semibold leading-6 text-white">
|
||||
{{ $t("userHeader.links.community") }}
|
||||
Community
|
||||
</h3>
|
||||
<ul role="list" class="mt-6 space-y-4">
|
||||
<li v-for="item in navigation.community" :key="item.name">
|
||||
@@ -61,7 +54,7 @@
|
||||
<div class="md:grid md:grid-cols-2 md:gap-8">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold leading-6 text-white">
|
||||
{{ $t("footer.documentation") }}
|
||||
Documentation
|
||||
</h3>
|
||||
<ul role="list" class="mt-6 space-y-4">
|
||||
<li v-for="item in navigation.documentation" :key="item.name">
|
||||
@@ -74,9 +67,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-10 md:mt-0">
|
||||
<h3 class="text-sm font-semibold leading-6 text-white">
|
||||
{{ $t("footer.about") }}
|
||||
</h3>
|
||||
<h3 class="text-sm font-semibold leading-6 text-white">About</h3>
|
||||
<ul role="list" class="mt-6 space-y-4">
|
||||
<li v-for="item in navigation.about" :key="item.name">
|
||||
<NuxtLink
|
||||
@@ -89,22 +80,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center xl:col-span-3 mt-8">
|
||||
<NuxtLink
|
||||
:to="`https://github.com/Drop-OSS/drop/releases/tag/${versionInfo.version}`"
|
||||
class="text-xs text-zinc-700 hover:text-zinc-400 transition-colors duration-200 cursor-default select-none"
|
||||
>
|
||||
<i18n-t keypath="footer.version" tag="span" scope="global">
|
||||
<template #version>
|
||||
<span>{{ versionInfo.version }}</span>
|
||||
</template>
|
||||
<template #gitRef>
|
||||
<span>{{ versionInfo.gitRef }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -113,49 +88,45 @@
|
||||
<script setup lang="ts">
|
||||
import { IconsDiscordLogo, IconsGithubLogo } from "#components";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const versionInfo = await $dropFetch("/api/v1");
|
||||
|
||||
const navigation = computed(() => ({
|
||||
const navigation = {
|
||||
games: [
|
||||
{ name: t("store.recentlyAdded"), href: "#" },
|
||||
{ name: t("store.recentlyReleased"), href: "#" },
|
||||
{ name: t("footer.topSellers"), href: "#" },
|
||||
{ name: t("footer.findGame"), href: "#" },
|
||||
{ name: "Newly Added", href: "#" },
|
||||
{ name: "New Releases", href: "#" },
|
||||
{ name: "Top Sellers", href: "#" },
|
||||
{ name: "Find a Game", href: "#" },
|
||||
],
|
||||
community: [
|
||||
{ name: t("common.friends"), href: "#" },
|
||||
{ name: t("common.groups"), href: "#" },
|
||||
{ name: t("common.servers"), href: "#" },
|
||||
{ name: "Friends", href: "#" },
|
||||
{ name: "Groups", href: "#" },
|
||||
{ name: "Servers", href: "#" },
|
||||
],
|
||||
documentation: [
|
||||
// TODO: public API docs
|
||||
// { name: t("footer.api"), href: "https://api.droposs.org/" },
|
||||
{ name: "API", href: "https://api.droposs.org/" },
|
||||
{
|
||||
name: t("footer.docs.server"),
|
||||
href: "https://droposs.org/docs/admin/quickstart",
|
||||
name: "Server Docs",
|
||||
href: "https://wiki.droposs.org/guides/quickstart.html",
|
||||
},
|
||||
{
|
||||
name: t("footer.docs.client"),
|
||||
href: "https://droposs.org/docs/user",
|
||||
name: "Client Docs",
|
||||
href: "https://wiki.droposs.org/guides/client.html",
|
||||
},
|
||||
],
|
||||
about: [
|
||||
{ name: t("footer.aboutDrop"), href: "https://droposs.org/" },
|
||||
{ name: t("footer.comparison"), href: "https://droposs.org/comparison" },
|
||||
{ name: "About Drop", href: "https://droposs.org/" },
|
||||
{ name: "Features", href: "https://droposs.org/features" },
|
||||
{ name: "FAQ", href: "https://droposs.org/faq" },
|
||||
],
|
||||
social: [
|
||||
{
|
||||
name: t("footer.social.github"),
|
||||
name: "GitHub",
|
||||
href: "https://github.com/Drop-OSS",
|
||||
icon: IconsGithubLogo,
|
||||
},
|
||||
{
|
||||
name: t("footer.social.discord"),
|
||||
name: "Discord",
|
||||
href: "https://discord.gg/NHx46XKJWA",
|
||||
icon: IconsDiscordLogo,
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
@@ -76,7 +76,7 @@
|
||||
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<span class="sr-only">{{ $t("header.openSidebar") }}</span>
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -125,9 +125,7 @@
|
||||
class="-m-2.5 p-2.5"
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="sr-only">{{
|
||||
$t("userHeader.closeSidebar")
|
||||
}}</span>
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<XMarkIcon class="h-6 w-6 text-zinc-400" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -138,7 +136,7 @@
|
||||
>
|
||||
<div class="flex shrink-0 h-16 items-center justify-between">
|
||||
<NuxtLink :to="homepageURL">
|
||||
<ApplicationLogo class="h-8 w-auto" />
|
||||
<DropLogo class="h-8 w-auto" />
|
||||
</NuxtLink>
|
||||
|
||||
<UserHeaderUserWidget />
|
||||
@@ -174,9 +172,6 @@
|
||||
<BellIcon class="h-5" />
|
||||
</UserHeaderWidget>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<UserHeaderWidget class="w-full" />
|
||||
</li>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -203,33 +198,32 @@ import { Bars3Icon } from "@heroicons/vue/24/outline";
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const homepageURL = "/store";
|
||||
const navigation: Ref<Array<NavigationItem>> = computed(() => [
|
||||
const navigation: Array<NavigationItem> = [
|
||||
{
|
||||
prefix: "/store",
|
||||
route: "/store",
|
||||
label: t("store.title"),
|
||||
label: "Store",
|
||||
},
|
||||
{
|
||||
prefix: "/library",
|
||||
route: "/library",
|
||||
label: t("userHeader.links.library"),
|
||||
label: "Library",
|
||||
},
|
||||
{
|
||||
prefix: "/community",
|
||||
route: "/community",
|
||||
label: t("userHeader.links.community"),
|
||||
label: "Community",
|
||||
},
|
||||
{
|
||||
prefix: "/news",
|
||||
route: "/news",
|
||||
label: t("userHeader.links.news"),
|
||||
label: "News",
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation.value);
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
|
||||
const notifications = useNotifications();
|
||||
const unreadNotifications = computed(() =>
|
||||
+5
-16
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<div class="ml-4 mt-2">
|
||||
<h3 class="text-base font-semibold text-zinc-100 text-sm">
|
||||
{{ $t("account.notifications.unread") }}
|
||||
Unread notifications
|
||||
</h3>
|
||||
</div>
|
||||
<div class="ml-4 mt-2 shrink-0">
|
||||
@@ -15,15 +15,7 @@
|
||||
type="button"
|
||||
class="text-sm text-zinc-400"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="account.notifications.all"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
View all →
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,16 +32,13 @@
|
||||
v-if="props.notifications.length == 0"
|
||||
class="text-sm text-zinc-400 p-3 text-center w-full"
|
||||
>
|
||||
{{ $t("account.notifications.none") }}
|
||||
No notifications
|
||||
</div>
|
||||
</PanelWidget>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { NotificationModel } from "~/prisma/client/models";
|
||||
import type { Notification } from "~/prisma/client";
|
||||
|
||||
const props = defineProps<{
|
||||
notifications: Array<SerializeObject<NotificationModel>>;
|
||||
}>();
|
||||
const props = defineProps<{ notifications: Array<Notification> }>();
|
||||
</script>
|
||||
+25
-33
@@ -46,30 +46,29 @@
|
||||
hydrate-on-visible
|
||||
as="div"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="nav.route"
|
||||
<!-- TODO: think this would work better as a NuxtLink instead of a button -->
|
||||
<button
|
||||
:href="nav.route"
|
||||
:class="[
|
||||
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
|
||||
'w-full text-left transition block px-4 py-2 text-sm',
|
||||
]"
|
||||
@click="close"
|
||||
@click="() => navigateTo(nav.route, close)"
|
||||
>
|
||||
{{ nav.label }}
|
||||
</NuxtLink>
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active, close }" hydrate-on-visible as="div">
|
||||
<NuxtLink
|
||||
to="/auth/signout"
|
||||
<MenuItem v-slot="{ active }" hydrate-on-visible as="div">
|
||||
<!-- TODO: think this would work better as a NuxtLink instead of a button -->
|
||||
<a
|
||||
:class="[
|
||||
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
|
||||
'w-full text-left transition block px-4 py-2 text-sm',
|
||||
]"
|
||||
:data-comment="'external=true is required because we implemented the signout as a route on the server for performance'"
|
||||
:external="true"
|
||||
@click="close"
|
||||
href="/auth/signout"
|
||||
>
|
||||
{{ $t("auth.signout") }}
|
||||
</NuxtLink>
|
||||
Signout
|
||||
</a>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</PanelWidget>
|
||||
@@ -86,25 +85,18 @@ import type { NavigationItem } from "~/composables/types";
|
||||
|
||||
const user = useUser();
|
||||
|
||||
const navigation = computed<NavigationItem[]>(() =>
|
||||
[
|
||||
user.value?.admin
|
||||
? {
|
||||
label: $t("userHeader.profile.admin"),
|
||||
route: "/admin",
|
||||
prefix: "",
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
label: $t("userHeader.profile.settings"),
|
||||
route: "/account",
|
||||
prefix: "",
|
||||
},
|
||||
{
|
||||
label: "Authorize client",
|
||||
route: "/client/code",
|
||||
prefix: "",
|
||||
},
|
||||
].filter((e) => e !== undefined),
|
||||
);
|
||||
const navigation: NavigationItem[] = [
|
||||
user.value?.admin
|
||||
? {
|
||||
label: "Admin Dashboard",
|
||||
route: "/admin",
|
||||
prefix: "",
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
label: "Account settings",
|
||||
route: "/account",
|
||||
prefix: "",
|
||||
},
|
||||
].filter((e) => e !== undefined);
|
||||
</script>
|
||||
@@ -1,12 +1,8 @@
|
||||
import type {
|
||||
CollectionModel,
|
||||
CollectionEntryModel,
|
||||
GameModel,
|
||||
} from "~/prisma/client/models";
|
||||
import type { Collection, CollectionEntry, Game } from "~/prisma/client";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
type FullCollection = CollectionModel & {
|
||||
entries: Array<CollectionEntryModel & { game: SerializeObject<GameModel> }>;
|
||||
type FullCollection = Collection & {
|
||||
entries: Array<CollectionEntry & { game: SerializeObject<Game> }>;
|
||||
};
|
||||
|
||||
export const useCollections = async () => {
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
import type { NavigationItem } from "./types";
|
||||
|
||||
export const useCurrentNavigationIndex = (
|
||||
@@ -8,7 +9,7 @@ export const useCurrentNavigationIndex = (
|
||||
|
||||
const currentNavigation = ref(-1);
|
||||
|
||||
function calculateCurrentNavIndex(to: typeof route) {
|
||||
function calculateCurrentNavIndex(to: RouteLocationNormalized) {
|
||||
const validOptions = navigation
|
||||
.map((e, i) => ({ ...e, index: i }))
|
||||
.filter((e) => to.fullPath.startsWith(e.prefix));
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IconsLinuxLogo, IconsWindowsLogo, IconsMacLogo } from "#components";
|
||||
import { PlatformClient } from "./types";
|
||||
|
||||
export const PLATFORM_ICONS = {
|
||||
[PlatformClient.Linux]: IconsLinuxLogo,
|
||||
[PlatformClient.Windows]: IconsWindowsLogo,
|
||||
[PlatformClient.macOS]: IconsMacLogo,
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ArticleModel } from "~/prisma/client/models";
|
||||
import type { Article } from "~/prisma/client";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
export const useNews = () =>
|
||||
useState<
|
||||
| Array<
|
||||
SerializeObject<
|
||||
ArticleModel & {
|
||||
Article & {
|
||||
tags: Array<{ id: string; name: string }>;
|
||||
author: { displayName: string; id: string } | null;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Notification } from "~/prisma/client";
|
||||
|
||||
const ws = new WebSocketHandler("/api/v1/notifications/ws");
|
||||
|
||||
export const useNotifications = () =>
|
||||
useState<Array<Notification>>("notifications", () => []);
|
||||
|
||||
ws.listen((e) => {
|
||||
const notification = JSON.parse(e) as Notification;
|
||||
const notifications = useNotifications();
|
||||
notifications.value.push(notification);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import type {
|
||||
ExtractedRouteMethod,
|
||||
NitroFetchOptions,
|
||||
NitroFetchRequest,
|
||||
TypedInternalResponse,
|
||||
} from "nitropack/types";
|
||||
|
||||
interface DropFetch<
|
||||
DefaultT = unknown,
|
||||
DefaultR extends NitroFetchRequest = NitroFetchRequest,
|
||||
> {
|
||||
<
|
||||
T = DefaultT,
|
||||
R extends NitroFetchRequest = DefaultR,
|
||||
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
|
||||
>(
|
||||
request: R,
|
||||
opts?: O,
|
||||
): Promise<
|
||||
// sometimes there is an error, other times there isn't
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
TypedInternalResponse<
|
||||
R,
|
||||
T,
|
||||
NitroFetchOptions<R> extends O ? "get" : ExtractedRouteMethod<R, O>
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
export const $dropFetch: DropFetch = async (request, opts) => {
|
||||
if (!getCurrentInstance()?.proxy) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Excessive stack depth comparing types
|
||||
return await $fetch(request, opts);
|
||||
}
|
||||
const id = request.toString();
|
||||
|
||||
const state = useState(id);
|
||||
if (state.value) {
|
||||
// Deep copy
|
||||
const object = JSON.parse(JSON.stringify(state.value));
|
||||
// Never use again on client
|
||||
state.value = undefined;
|
||||
return object;
|
||||
}
|
||||
|
||||
const headers = useRequestHeaders(["cookie", "authorization"]);
|
||||
const data = await $fetch(request, {
|
||||
...opts,
|
||||
headers: { ...opts?.headers, ...headers },
|
||||
});
|
||||
if (import.meta.server) state.value = data;
|
||||
return data;
|
||||
};
|
||||
@@ -52,7 +52,6 @@ websocketHandler.listen((message) => {
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
log: [],
|
||||
actions: [],
|
||||
};
|
||||
state.value.error = { title, description };
|
||||
break;
|
||||
@@ -11,3 +11,9 @@ export type QuickActionNav = {
|
||||
notifications?: Ref<number>;
|
||||
action: () => Promise<void>;
|
||||
};
|
||||
|
||||
export enum PlatformClient {
|
||||
Windows = "Windows",
|
||||
Linux = "Linux",
|
||||
macOS = "macOS",
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user