Compare commits
7 Commits
1db2f3394c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8324ecd1e7 | |||
| 0567a1d29b | |||
| f2360cdae8 | |||
| 59245a469d | |||
| ab35e52728 | |||
| d2ce7f0fdc | |||
| a61f3a9c33 |
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Dostalgia — Environment Configuration
|
# DOStalgia — Environment Configuration
|
||||||
# Copy this file to .env and fill in your values:
|
# Copy this file to .env and fill in your values:
|
||||||
# cp .env.example .env
|
# cp .env.example .env
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# This workflow builds and deploys Dostalgia whenever code is pushed to main.
|
# This workflow builds DOStalgia whenever code is pushed to main.
|
||||||
# The runner has Docker socket access so it can manage containers on the host.
|
# The runner has Docker socket access so it can manage containers on the host.
|
||||||
# Parameters match the docker-compose at:
|
|
||||||
# /appdata/dockhand/stacks/unraid/dostalgia/compose.yaml
|
|
||||||
# See: https://docs.gitea.com/usage/actions
|
# See: https://docs.gitea.com/usage/actions
|
||||||
|
|
||||||
name: Build & Deploy
|
name: Build & Deploy
|
||||||
@@ -13,8 +11,6 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: dostalgia
|
IMAGE_NAME: dostalgia
|
||||||
CONTAINER_NAME: dostalgia
|
|
||||||
DATA_VOLUME: /mnt/cache/appdata/dostalgia
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
@@ -29,34 +25,5 @@ jobs:
|
|||||||
docker build -t ${{ env.IMAGE_NAME }}:${{ gitea.sha }} .
|
docker build -t ${{ env.IMAGE_NAME }}:${{ gitea.sha }} .
|
||||||
docker tag ${{ env.IMAGE_NAME }}:${{ gitea.sha }} ${{ env.IMAGE_NAME }}:latest
|
docker tag ${{ env.IMAGE_NAME }}:${{ gitea.sha }} ${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
- name: Deploy container
|
|
||||||
run: |
|
|
||||||
docker stop ${{ env.CONTAINER_NAME }} 2>/dev/null || true
|
|
||||||
docker rm ${{ env.CONTAINER_NAME }} 2>/dev/null || true
|
|
||||||
docker run -d \
|
|
||||||
--name ${{ env.CONTAINER_NAME }} \
|
|
||||||
--restart unless-stopped \
|
|
||||||
--network dockernet \
|
|
||||||
-p 8765:8765/tcp \
|
|
||||||
-v ${{ env.DATA_VOLUME }}:/data \
|
|
||||||
-e TWITCH_CLIENT_ID='${{ secrets.TWITCH_CLIENT_ID }}' \
|
|
||||||
-e TWITCH_CLIENT_SECRET='${{ secrets.TWITCH_CLIENT_SECRET }}' \
|
|
||||||
${{ env.IMAGE_NAME }}:latest
|
|
||||||
|
|
||||||
- name: Verify container is running
|
|
||||||
run: |
|
|
||||||
sleep 5
|
|
||||||
STATUS=$(docker inspect -f '{{.State.Status}}' ${{ env.CONTAINER_NAME }})
|
|
||||||
if [ "$STATUS" = "running" ]; then
|
|
||||||
echo "✅ Dostalgia container is running"
|
|
||||||
echo "--- last logs ---"
|
|
||||||
docker logs ${{ env.CONTAINER_NAME }} --tail 5
|
|
||||||
echo "✅ Dostalgia deployed!"
|
|
||||||
else
|
|
||||||
echo "❌ Container status: $STATUS"
|
|
||||||
docker logs ${{ env.CONTAINER_NAME }} --tail 30
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Clean up old images
|
- name: Clean up old images
|
||||||
run: docker image prune -f --filter "until=24h"
|
run: docker image prune -f --filter "until=24h"
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Publish Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: droideparanoico/dostalgia
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -40,3 +40,6 @@ data/saves/*
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Gitea
|
||||||
|
.gitea/
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Dostalgia — Quarkus multi-stage build
|
# DOStalgia — Quarkus multi-stage build
|
||||||
|
|
||||||
# ─── 1. Build frontend ───────────────────────────
|
# ─── 1. Build frontend ───────────────────────────
|
||||||
FROM node:20-alpine AS frontend
|
FROM node:20-alpine AS frontend
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 droideparanoico
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
# DOSTALGIA 🕹️
|
# DOStalgia 🕹️
|
||||||
|
|
||||||
A nostalgic DOS game hub. Upload your old DOS games, auto-scrape artwork and metadata from IGDB, and play them directly in your browser via js-dos (DOSBox compiled to WebAssembly).
|
A nostalgic DOS game hub. Upload your old DOS games, auto-scrape artwork and metadata from IGDB, and play them directly in your browser via js-dos (DOSBox compiled to WebAssembly).
|
||||||
|
|
||||||
|
### Library view
|
||||||
|

|
||||||
|
|
||||||
|
### Game detail view
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🎮 Play in the browser
|
### 🎮 Play in the browser
|
||||||
@@ -10,18 +16,18 @@ Every uploaded game is packaged into a `.jsdos` bundle — a standard ZIP with e
|
|||||||
**Save states are automatic** — js-dos persists your game progress to the browser's storage. Come back anytime and pick up where you left off.
|
**Save states are automatic** — js-dos persists your game progress to the browser's storage. Come back anytime and pick up where you left off.
|
||||||
|
|
||||||
### 📦 Handles any file structure
|
### 📦 Handles any file structure
|
||||||
DOS games come in all shapes. Dostalgia handles them transparently:
|
DOS games come in all shapes. DOStalgia handles them transparently:
|
||||||
|
|
||||||
- **Flattening** — If your ZIP has a single root directory (e.g. `doom/` with all files inside), the extractor flattens it so the game files sit at the bundle root. No extra nesting.
|
- **Flattening** — If your ZIP has a single root directory (e.g. `doom/` with all files inside), the extractor flattens it so the game files sit at the bundle root. No extra nesting.
|
||||||
- **Subdirectory games** — If files are deeper, the autoexec automatically `cd`s to the right directory before launching the executable.
|
- **Subdirectory games** — If files are deeper, the autoexec automatically `cd`s to the right directory before launching the executable.
|
||||||
- **CD images** — Games shipped on CD-ROM often need the disc mounted. Dostalgia detects `.iso`, `.cue`, `.img`, `.ccd`, and `.bin` files, fixes broken CloneCD `.cue` references (where the referenced `.bin` is actually `.img`), and mounts them in both `dosbox.conf` and `jsdos.json`.
|
- **CD images** — Games shipped on CD-ROM often need the disc mounted. DOStalgia detects `.iso`, `.cue`, `.img`, `.ccd`, and `.bin` files, fixes broken CloneCD `.cue` references (where the referenced `.bin` is actually `.img`), and mounts them in both `dosbox.conf` and `jsdos.json`.
|
||||||
- **CD-only games** — If the archive contains only CD images with no executable, a CD-only bundle is created that mounts the disc and drops you at the DOS prompt.
|
- **CD-only games** — If the archive contains only CD images with no executable, a CD-only bundle is created that mounts the disc and drops you at the DOS prompt.
|
||||||
- **Hardcoded paths** — `ConfigPatcher` scans game config files for hardcoded absolute paths (e.g. `C:\FALLOUT1\MASTER.DAT`) that broke after flattening, and rewrites them to relative paths.
|
- **Hardcoded paths** — `ConfigPatcher` scans game config files for hardcoded absolute paths (e.g. `C:\FALLOUT1\MASTER.DAT`) that broke after flattening, and rewrites them to relative paths.
|
||||||
|
|
||||||
Other solutions such as [RomM](https://www.romm.app/) generate raw ZIP bundles and require you to write your own `dosbox.conf` with manual `mount` and `imgmount` commands. Dostalgia automates all of this for you.
|
Other solutions such as [RomM](https://www.romm.app/) generate raw ZIP bundles and require you to write your own `dosbox.conf` with manual `mount` and `imgmount` commands. DOStalgia automates all of this for you.
|
||||||
|
|
||||||
### 🔍 Smart executable detection
|
### 🔍 Smart executable detection
|
||||||
On upload, Dostalgia scans every `.exe`, `.com`, and `.bat` file and picks the best candidate as the main executable using a scoring system:
|
On upload, DOStalgia scans every `.exe`, `.com`, and `.bat` file and picks the best candidate as the main executable using a scoring system:
|
||||||
|
|
||||||
1. **Not an installer** — INSTALL/SETUP/CONFIG executables are deprioritised
|
1. **Not an installer** — INSTALL/SETUP/CONFIG executables are deprioritised
|
||||||
2. **DOS executables** — Pure DOS apps are preferred over Windows ones
|
2. **DOS executables** — Pure DOS apps are preferred over Windows ones
|
||||||
@@ -31,12 +37,12 @@ On upload, Dostalgia scans every `.exe`, `.com`, and `.bat` file and picks the b
|
|||||||
|
|
||||||
All discovered executables are stored and available in the **Edit** page, where you can pick a different one via radio buttons.
|
All discovered executables are stored and available in the **Edit** page, where you can pick a different one via radio buttons.
|
||||||
|
|
||||||
Again, RomM requires you to manually specify the executable in a custom `dosbox.conf`, while Dostalgia detects and configures it for you.
|
Again, RomM requires you to manually specify the executable in a custom `dosbox.conf`, while DOStalgia detects and configures it for you.
|
||||||
|
|
||||||
> **⚠️ Cache note:** If you change the executable, the `.jsdos` bundle is patched in-place. Your browser may serve a cached version of the old bundle — if the game doesn't launch with the new executable, **hard-refresh** the play page (Ctrl+Shift+R / Cmd+Shift+R).
|
> **⚠️ Cache note:** If you change the executable, the `.jsdos` bundle is patched in-place. Your browser may serve a cached version of the old bundle — if the game doesn't launch with the new executable, **hard-refresh** the play page (Ctrl+Shift+R / Cmd+Shift+R).
|
||||||
|
|
||||||
### 🛠 One-click Setup launcher
|
### 🛠 One-click Setup launcher
|
||||||
Many DOS games include a `SETUP.EXE`, `INSTALL.EXE`, or `CONFIG.EXE` used to configure sound, controls, and graphics. When Dostalgia detects one:
|
Many DOS games include a `SETUP.EXE`, `INSTALL.EXE`, or `CONFIG.EXE` used to configure sound, controls, and graphics. When DOStalgia detects one:
|
||||||
|
|
||||||
- A **🛠 Setup** button appears on the game detail page
|
- A **🛠 Setup** button appears on the game detail page
|
||||||
- Clicking it launches the setup executable *without modifying the main game bundle*
|
- Clicking it launches the setup executable *without modifying the main game bundle*
|
||||||
@@ -45,7 +51,7 @@ Many DOS games include a `SETUP.EXE`, `INSTALL.EXE`, or `CONFIG.EXE` used to con
|
|||||||
This is not supported in RomM, where you would have to run the setup manually from the DOS prompt every time.
|
This is not supported in RomM, where you would have to run the setup manually from the DOS prompt every time.
|
||||||
|
|
||||||
### 🪟 Windows game detection
|
### 🪟 Windows game detection
|
||||||
DOSBox can't run Windows executables. Dostalgia's `PlatformDetector` reads MZ/PE/NE headers to detect Windows executables:
|
DOSBox can't run Windows executables. DOStalgia's `PlatformDetector` reads MZ/PE/NE headers to detect Windows executables:
|
||||||
|
|
||||||
- The game detail page shows a **🪟 Requires Windows 3.1** badge
|
- The game detail page shows a **🪟 Requires Windows 3.1** badge
|
||||||
- The **Play** button is disabled with "Unplayable" text
|
- The **Play** button is disabled with "Unplayable" text
|
||||||
@@ -53,16 +59,16 @@ DOSBox can't run Windows executables. Dostalgia's `PlatformDetector` reads MZ/PE
|
|||||||
- Setup executables that are Windows-native are also filtered out from the Setup button
|
- Setup executables that are Windows-native are also filtered out from the Setup button
|
||||||
|
|
||||||
### 📡 IGDB metadata & media
|
### 📡 IGDB metadata & media
|
||||||
Dostalgia integrates with [IGDB](https://www.igdb.com/) (via Twitch OAuth2) to auto-populate game info:
|
DOStalgia integrates with [IGDB](https://www.igdb.com/) (via Twitch OAuth2) to auto-populate game info:
|
||||||
|
|
||||||
- **Auto Scrape** — On upload, Dostalgia searches IGDB by title and fills in year, genre, developer, publisher, description, and cover art. If IGDB finds a DOS result, it prioritises it.
|
- **Auto Scrape** — On upload, DOStalgia searches IGDB by title and fills in year, genre, developer, publisher, description, and cover art. If IGDB finds a DOS result, it prioritises it.
|
||||||
- **Manual Search** — From the game detail page you can search IGDB by any query, browse results with cover thumbnails and DOS badges, and apply the one you want.
|
- **Manual Search** — From the game detail page you can search IGDB by any query, browse results with cover thumbnails and DOS badges, and apply the one you want.
|
||||||
- **Media** — Videos (YouTube embeds) and screenshots (1080p) are fetched and displayed in a scrollable media gallery with a preview player.
|
- **Media** — Videos (YouTube embeds) and screenshots (1080p) are fetched and displayed in a scrollable media gallery with a preview player.
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
|
|
||||||
1. Go to https://dev.twitch.tv/console/apps → **Register Your Application**
|
1. Go to https://dev.twitch.tv/console/apps → **Register Your Application**
|
||||||
2. Name: `Dostalgia` (or anything), OAuth Redirect URL: `http://localhost`, Category: **Other**
|
2. Name: `dostalgia` (or anything), OAuth Redirect URL: `http://localhost`, Category: **Other**
|
||||||
3. Copy the **Client ID** and generate a **Client Secret**
|
3. Copy the **Client ID** and generate a **Client Secret**
|
||||||
4. Provide them via environment variables:
|
4. Provide them via environment variables:
|
||||||
|
|
||||||
@@ -74,11 +80,6 @@ Local dev | `export TWITCH_CLIENT_ID=xxx TWITCH_CLIENT_SECRET=yyy`
|
|||||||
|
|
||||||
IGDB features degrade gracefully — if credentials aren't set, uploads and metadata editing still work, just without auto-scrape.
|
IGDB features degrade gracefully — if credentials aren't set, uploads and metadata editing still work, just without auto-scrape.
|
||||||
|
|
||||||
### 📷 Screenshots
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### With Docker Compose (easiest)
|
### With Docker Compose (easiest)
|
||||||
@@ -88,7 +89,7 @@ IGDB features degrade gracefully — if credentials aren't set, uploads and meta
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with your Twitch credentials (see IGDB section below)
|
# Edit .env with your Twitch credentials (see IGDB section below)
|
||||||
|
|
||||||
# 2. Build & run
|
# 2. Pull & run
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -97,11 +98,10 @@ Open http://localhost:8765
|
|||||||
### With Docker
|
### With Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t dostalgia .
|
|
||||||
docker run -p 8765:8765 -v $(pwd)/data:/data \
|
docker run -p 8765:8765 -v $(pwd)/data:/data \
|
||||||
-e TWITCH_CLIENT_ID=your_id \
|
-e TWITCH_CLIENT_ID=your_id \
|
||||||
-e TWITCH_CLIENT_SECRET=your_secret \
|
-e TWITCH_CLIENT_SECRET=your_secret \
|
||||||
dostalgia
|
droideparanoico/dostalgia
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development (without Docker)
|
## Development (without Docker)
|
||||||
|
|||||||
+4
-3
@@ -1,13 +1,14 @@
|
|||||||
services:
|
services:
|
||||||
dostalgia:
|
dostalgia:
|
||||||
build: .
|
image: droideparanoico/dostalgia
|
||||||
container_name: dostalgia
|
container_name: dostalgia
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8765:8765"
|
- "8765:8765"
|
||||||
volumes:
|
volumes:
|
||||||
|
# Configure here your data directory if you want persistence.
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DOSTALGIA_DATA_DIR=/data
|
- DOSTALGIA_DATA_DIR=/data
|
||||||
restart: unless-stopped
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36'><rect x='4' y='6' width='28' height='22' rx='2' fill='%2333FF33' opacity='.15'/><rect x='6' y='8' width='24' height='16' fill='%230A0A0A'/><rect x='8' y='10' width='20' height='10' fill='%2333FF33' opacity='.3'/><rect x='10' y='12' width='6' height='4' fill='%2333FF33' opacity='.6'/><rect x='18' y='12' width='6' height='4' fill='%2333FF33' opacity='.4'/></svg>" />
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36'><rect x='4' y='6' width='28' height='22' rx='2' fill='%2333FF33' opacity='.15'/><rect x='6' y='8' width='24' height='16' fill='%230A0A0A'/><rect x='8' y='10' width='20' height='10' fill='%2333FF33' opacity='.3'/><rect x='10' y='12' width='6' height='4' fill='%2333FF33' opacity='.6'/><rect x='18' y='12' width='6' height='4' fill='%2333FF33' opacity='.4'/></svg>" />
|
||||||
<title>Dostalgia</title>
|
<title>DOStalgia</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "dostalgia-frontend",
|
"name": "dostalgia-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "dostalgia-frontend",
|
"name": "dostalgia-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@testing-library/svelte": "^5.3.1",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "dostalgia-frontend",
|
"name": "dostalgia-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header onUploadClick={() => uploadTriggered++} />
|
<Header onUploadClick={() => uploadTriggered++} hideUpload={route !== "/"} />
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
{#if route === "/"}
|
{#if route === "/"}
|
||||||
|
|||||||
+13
-8
@@ -98,7 +98,8 @@ h3 { font-size: 1.2rem; }
|
|||||||
/* ═══════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
BUTTONS
|
BUTTONS
|
||||||
═══════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════ */
|
||||||
.btn {
|
/* Only targets DOStalgia buttons, not js-dos emulator buttons inside .dos-container */
|
||||||
|
.btn.btn:not(.dos-container .btn) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -111,32 +112,36 @@ h3 { font-size: 1.2rem; }
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-transform: none;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
line-height: normal;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
.btn:hover {
|
.btn.btn:not(.dos-container .btn):hover {
|
||||||
background: var(--phosphor-burn);
|
background: var(--phosphor-burn);
|
||||||
border-color: var(--phosphor);
|
border-color: var(--phosphor);
|
||||||
box-shadow: 0 0 12px var(--phosphor-dark);
|
box-shadow: 0 0 12px var(--phosphor-dark);
|
||||||
}
|
}
|
||||||
.btn:active {
|
.btn.btn:not(.dos-container .btn):active {
|
||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary.btn-primary:not(.dos-container .btn) {
|
||||||
background: var(--phosphor-dark);
|
background: var(--phosphor-dark);
|
||||||
border-color: var(--phosphor);
|
border-color: var(--phosphor);
|
||||||
color: var(--phosphor-glow);
|
color: var(--phosphor-glow);
|
||||||
}
|
}
|
||||||
.btn-primary:hover {
|
.btn-primary.btn-primary:not(.dos-container .btn):hover {
|
||||||
background: var(--phosphor-dim);
|
background: var(--phosphor-dim);
|
||||||
color: var(--bg);
|
color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger.btn-danger:not(.dos-container .btn) {
|
||||||
border-color: #cc3333;
|
border-color: #cc3333;
|
||||||
color: #cc3333;
|
color: #cc3333;
|
||||||
}
|
}
|
||||||
.btn-danger:hover {
|
.btn-danger.btn-danger:not(.dos-container .btn):hover {
|
||||||
background: #331111;
|
background: #331111;
|
||||||
border-color: #ff4444;
|
border-color: #ff4444;
|
||||||
color: #ff4444;
|
color: #ff4444;
|
||||||
@@ -187,7 +192,7 @@ input::placeholder {
|
|||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
h1 { font-size: 1.5rem; }
|
h1 { font-size: 1.5rem; }
|
||||||
h2 { font-size: 1.25rem; }
|
h2 { font-size: 1.25rem; }
|
||||||
.btn { padding: 8px 14px; font-size: 0.85rem; min-height: 40px; }
|
.btn.btn:not(.dos-container .btn) { padding: 8px 14px; font-size: 0.85rem; min-height: 40px; }
|
||||||
input, textarea, select { font-size: 16px; } /* prevents iOS zoom */
|
input, textarea, select { font-size: 16px; } /* prevents iOS zoom */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
let { onUploadClick = () => {} } = $props();
|
let { onUploadClick = () => {}, hideUpload = false } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
@@ -17,7 +17,9 @@
|
|||||||
<span class="logo-text">DOSTALGIA</span>
|
<span class="logo-text">DOSTALGIA</span>
|
||||||
</a>
|
</a>
|
||||||
<nav>
|
<nav>
|
||||||
<button class="btn" onclick={onUploadClick}>+ Upload</button>
|
{#if !hideUpload}
|
||||||
|
<button class="btn" onclick={onUploadClick}>+ Upload</button>
|
||||||
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** API client for Dostalgia backend. */
|
/** API client for DOStalgia backend. */
|
||||||
|
|
||||||
const BASE = import.meta.env.PROD ? "" : ""; // Proxy handles it in dev
|
const BASE = import.meta.env.PROD ? "" : ""; // Proxy handles it in dev
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
← {isSetup ? "Back" : game.title}
|
← {isSetup ? "Back" : game.title}
|
||||||
</button>
|
</button>
|
||||||
{#if running}
|
{#if running}
|
||||||
<button class="btn stop-btn" onclick={stopEmulator}>⏹ Stop</button>
|
<button class="btn btn-danger" onclick={stopEmulator}>⏹ Stop</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -211,14 +211,6 @@
|
|||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
.link-button:hover { color: var(--phosphor); }
|
.link-button:hover { color: var(--phosphor); }
|
||||||
.stop-btn {
|
|
||||||
border-color: #cc3333;
|
|
||||||
color: #ff4444;
|
|
||||||
}
|
|
||||||
.stop-btn:hover {
|
|
||||||
background: #331111;
|
|
||||||
box-shadow: 0 0 12px #331111;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Emulator canvas — always mounted */
|
/* Emulator canvas — always mounted */
|
||||||
.dos-container {
|
.dos-container {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 261 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 488 B |
Binary file not shown.
|
After Width: | Height: | Size: 869 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>org.dostalgia</groupId>
|
<groupId>org.dostalgia</groupId>
|
||||||
<artifactId>dostalgia</artifactId>
|
<artifactId>dostalgia</artifactId>
|
||||||
<version>0.1.0</version>
|
<version>0.1.1</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.release>21</maven.compiler.release>
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ public class ExecutableDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String findSetup(Path dir) throws IOException {
|
public String findSetup(Path dir) throws IOException {
|
||||||
record Candidate(int depth, long size, String path) {}
|
record Candidate(int depth, boolean isWindows, long size, String path) {}
|
||||||
List<Candidate> candidates = new ArrayList<>();
|
List<Candidate> candidates = new ArrayList<>();
|
||||||
List<String> patterns = List.of("setup", "install", "config", "custom");
|
List<String> patterns = List.of("setup", "setmain", "install", "config", "custom");
|
||||||
|
|
||||||
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
|
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -95,8 +95,9 @@ public class ExecutableDetector {
|
|||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
for (String pat : patterns) {
|
for (String pat : patterns) {
|
||||||
if (stem.equals(pat) || stem.startsWith(pat + ".") || stem.startsWith(pat + "-")) {
|
if (stem.equals(pat) || stem.startsWith(pat + ".") || stem.startsWith(pat + "-")) {
|
||||||
|
boolean isWin = "windows".equals(platform.detect(f));
|
||||||
candidates.add(new Candidate(
|
candidates.add(new Candidate(
|
||||||
f.getNameCount() - dir.getNameCount(), a.size(), f.toString()));
|
f.getNameCount() - dir.getNameCount(), isWin, a.size(), f.toString()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +106,10 @@ public class ExecutableDetector {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (candidates.isEmpty()) return null;
|
if (candidates.isEmpty()) return null;
|
||||||
candidates.sort(Comparator.comparingInt(Candidate::depth).thenComparingLong(Candidate::size));
|
candidates.sort(Comparator
|
||||||
|
.comparingInt((Candidate c) -> c.isWindows() ? 1 : 0) // prefer DOS over Windows
|
||||||
|
.thenComparingInt(Candidate::depth)
|
||||||
|
.thenComparingLong(Candidate::size));
|
||||||
return candidates.getFirst().path();
|
return candidates.getFirst().path();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class PlatformDetector {
|
|||||||
|
|
||||||
/** Read enough PE header bytes from any InputStream (shared by Path and ZIP callers). */
|
/** Read enough PE header bytes from any InputStream (shared by Path and ZIP callers). */
|
||||||
static byte[] readHeaderBytes(InputStream is) throws IOException {
|
static byte[] readHeaderBytes(InputStream is) throws IOException {
|
||||||
byte[] buf = new byte[0x200];
|
byte[] buf = new byte[0x1000];
|
||||||
int read = is.readNBytes(buf, 0, buf.length);
|
int read = is.readNBytes(buf, 0, buf.length);
|
||||||
return read < buf.length ? java.util.Arrays.copyOf(buf, read) : buf;
|
return read < buf.length ? java.util.Arrays.copyOf(buf, read) : buf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class StaticResource {
|
|||||||
@GET
|
@GET
|
||||||
@jakarta.ws.rs.Path("/api/health")
|
@jakarta.ws.rs.Path("/api/health")
|
||||||
public Response health() {
|
public Response health() {
|
||||||
return Response.ok(Map.of("status", "ok", "version", "0.1.0")).build();
|
return Response.ok(Map.of("status", "ok", "version", "0.1.1")).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String guessContentType(String name) {
|
private String guessContentType(String name) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class StaticResourceTest {
|
|||||||
assertInstanceOf(Map.class, entity);
|
assertInstanceOf(Map.class, entity);
|
||||||
Map<?, ?> map = (Map<?, ?>) entity;
|
Map<?, ?> map = (Map<?, ?>) entity;
|
||||||
assertEquals("ok", map.get("status"));
|
assertEquals("ok", map.get("status"));
|
||||||
assertEquals("0.1.0", map.get("version"));
|
assertEquals("0.1.1", map.get("version"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user