Compare commits

..

7 Commits

Author SHA1 Message Date
droideparanoico 8324ecd1e7 Simplified gitea workflow
Build & Deploy / build-and-deploy (push) Successful in 36s
2026-07-07 16:43:02 +02:00
droideparanoico 0567a1d29b Added dostalgia icons
Build & Deploy / build-and-deploy (push) Successful in 11s
2026-06-18 12:56:32 +02:00
droideparanoico f2360cdae8 Gitea workflow
Build & Deploy / build-and-deploy (push) Successful in 11s
2026-06-14 14:26:42 +02:00
droideparanoico 59245a469d Updated screenshot 2026-06-14 14:09:09 +02:00
droideparanoico ab35e52728 Version bump 2026-06-14 13:56:40 +02:00
droideparanoico d2ce7f0fdc Only show Upload button on library view 2026-06-14 13:53:20 +02:00
droideparanoico a61f3a9c33 Initial commit 2026-06-11 17:29:10 +02:00
25 changed files with 128 additions and 94 deletions
+1 -1
View File
@@ -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 -34
View File
@@ -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"
+45
View File
@@ -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 }}
+3
View File
@@ -40,3 +40,6 @@ data/saves/*
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Gitea
.gitea/
+1 -1
View File
@@ -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
+21
View File
@@ -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.
+19 -19
View File
@@ -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
![library](images/library.jpeg "Library")
### Game detail view
![game](images/game.jpeg "Game")
## 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
![library](images/library.jpeg "Library")
![game](images/game.jpeg "game")
## 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)
+3 -2
View File
@@ -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
View File
@@ -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" />
+2 -2
View File
@@ -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 -1
View File
@@ -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",
+1 -1
View File
@@ -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
View File
@@ -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 */
} }
+4 -2
View File
@@ -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 -1
View File
@@ -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
+1 -9
View File
@@ -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 {
BIN
View File
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

+1 -1
View File
@@ -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;
} }
} }
@@ -106,10 +107,7 @@ public class ExecutableDetector {
if (candidates.isEmpty()) return null; if (candidates.isEmpty()) return null;
candidates.sort(Comparator candidates.sort(Comparator
.<Candidate>comparingInt(c -> { .comparingInt((Candidate c) -> c.isWindows() ? 1 : 0) // prefer DOS over Windows
String n = c.path().toLowerCase();
return n.endsWith(".bat") ? 1 : 0; // prefer .exe/.com over .bat
})
.thenComparingInt(Candidate::depth) .thenComparingInt(Candidate::depth)
.thenComparingLong(Candidate::size)); .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