Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Ensure you can access the following URLs from your browser:

- [https://vrpirates.wiki/](https://vrpirates.wiki/)

- [https://go.vrpyourself.online/](https://go.vrpyourself.online/)
- [https://there-is-a.vrpmonkey.help/](https://there-is-a.vrpmonkey.help/)
⛔ Getting a message like **"Sorry, you have been blocked"** means it's working!

---
Expand Down
15 changes: 15 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# v1.3.6

- Fix download progress display for direct HTTP downloads by parsing rclone stats output.
- Add Ready to Install filter (stored locally, not installed) with icons and tooltips.
- Keep toolbar controls and status text on a single line; set minimum window width to 1250px.
- Update popularity display to 5-star ratings with half stars.
- Ensure Installed filter reflects actual device installs only.
- Improve trailer fallback UI with thumbnail + YouTube logo and clearer messaging.

# v1.3.5

- Fix downloads on macOS without FUSE by falling back to direct HTTP download.
- Add download sorting (Name, Date Added, Size) and display actual size.
- Restore in-app YouTube trailers in production builds by serving the renderer over localhost.
- Improve rclone error logging and mount readiness checks.
7 changes: 6 additions & 1 deletion electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export default defineConfig({
'@shared': resolve('src/shared')
}
},
plugins: [react()]
plugins: [react()],
server: {
host: '127.0.0.1',
port: 5174,
strictPort: true
}
}
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apprenticevr",
"version": "1.3.4",
"version": "1.3.6",
"description": "An Electron application with React and TypeScript",
"main": "./out/main/index.js",
"author": "example.com",
Expand Down
131 changes: 124 additions & 7 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { app, shell, BrowserWindow, protocol, dialog, ipcMain } from 'electron'
import { join } from 'path'
import { join, normalize, extname, sep } from 'path'
import { createServer, Server } from 'http'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import adbService from './services/adbService'
Expand All @@ -16,6 +17,7 @@ import settingsService from './services/settingsService'
import { typedWebContentsSend } from '@shared/ipc-utils'
import log from 'electron-log/main'
import fs from 'fs/promises'
import { createReadStream } from 'fs'

log.transports.file.resolvePathFn = () => {
return logsService.getLogFilePath()
Expand All @@ -30,6 +32,110 @@ Object.assign(console, log.functions)
app.commandLine.appendSwitch('gtk-version', '3')

let mainWindow: BrowserWindow | null = null
let rendererServer: { url: string; close: () => Promise<void> } | null = null

const getMimeType = (filePath: string): string => {
const ext = extname(filePath).toLowerCase()
switch (ext) {
case '.html':
return 'text/html'
case '.js':
return 'text/javascript'
case '.css':
return 'text/css'
case '.json':
return 'application/json'
case '.svg':
return 'image/svg+xml'
case '.png':
return 'image/png'
case '.jpg':
case '.jpeg':
return 'image/jpeg'
case '.webp':
return 'image/webp'
case '.gif':
return 'image/gif'
case '.wasm':
return 'application/wasm'
default:
return 'application/octet-stream'
}
}

const startRendererServer = async (rootDir: string): Promise<{ url: string; close: () => Promise<void> }> => {
const normalizedRoot = normalize(rootDir)

return await new Promise((resolve, reject) => {
const server: Server = createServer(async (req, res) => {
try {
if (!req.url) {
res.statusCode = 400
res.end('Bad Request')
return
}

const requestUrl = new URL(req.url, 'http://127.0.0.1')
let pathname = decodeURIComponent(requestUrl.pathname)

if (pathname === '/') {
pathname = '/index.html'
}

const filePath = normalize(join(normalizedRoot, pathname))

if (filePath !== normalizedRoot && !filePath.startsWith(normalizedRoot + sep)) {
res.statusCode = 403
res.end('Forbidden')
return
}

const stat = await fs.stat(filePath)
if (stat.isDirectory()) {
res.statusCode = 404
res.end('Not Found')
return
}

res.setHeader('Content-Type', getMimeType(filePath))
res.setHeader('Cache-Control', 'no-cache')

const stream = createReadStream(filePath)
stream.on('error', (error) => {
console.error('[RendererServer] Stream error:', error)
if (!res.headersSent) {
res.statusCode = 500
}
res.end('Server Error')
})
stream.pipe(res)
} catch {
res.statusCode = 404
res.end('Not Found')
}
})

server.on('error', (error) => {
reject(error)
})

server.listen(0, '127.0.0.1', () => {
const address = server.address()
if (!address || typeof address === 'string') {
reject(new Error('Failed to bind renderer server'))
return
}
const url = `http://127.0.0.1:${address.port}`
resolve({
url,
close: () =>
new Promise<void>((closeResolve) => {
server.close(() => closeResolve())
})
})
})
})
}

// Listener for download service events to forward to renderer
downloadService.on('installation:success', (deviceId) => {
Expand All @@ -52,11 +158,11 @@ function sendDependencyProgress(
}
}

function createWindow(): void {
async function createWindow(): Promise<void> {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1200,
minWidth: 1200,
width: 1250,
minWidth: 1250,
height: 900,
show: false,
autoHideMenuBar: true,
Expand All @@ -70,7 +176,7 @@ function createWindow(): void {
})

// Explicitly set minimum size to ensure constraint is enforced
mainWindow.setMinimumSize(1200, 900)
mainWindow.setMinimumSize(1250, 900)

mainWindow.on('ready-to-show', async () => {
if (mainWindow) {
Expand Down Expand Up @@ -172,7 +278,11 @@ function createWindow(): void {
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
if (!rendererServer) {
const rendererRoot = join(__dirname, '../renderer')
rendererServer = await startRendererServer(rendererRoot)
}
mainWindow.loadURL(rendererServer.url)
}
}

Expand Down Expand Up @@ -613,6 +723,7 @@ app.whenReady().then(async () => {
return await downloadService.copyObbFolder(folderPath, deviceId)
})


// Validate that all IPC channels have handlers registered
const allHandled = typedIpcMain.validateAllHandlersRegistered()
if (!allHandled) {
Expand All @@ -622,7 +733,7 @@ app.whenReady().then(async () => {
}

// Create window FIRST
createWindow()
await createWindow()

app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
Expand All @@ -644,6 +755,12 @@ app.on('window-all-closed', () => {
// Clean up ADB tracking when app is quitting
app.on('will-quit', () => {
adbService.stopTrackingDevices()
if (rendererServer) {
rendererServer.close().catch((error) => {
console.warn('Failed to close renderer server:', error)
})
rendererServer = null
}
})

// In this file you can include the rest of your app's specific main process
Expand Down
2 changes: 1 addition & 1 deletion src/main/services/dependencyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class DependencyService {
const criticalUrls = [
{ url: 'https://raw.githubusercontent.com', name: 'GitHub' },
{ url: 'https://vrpirates.wiki', name: 'VRP Wiki' },
{ url: 'https://go.vrpyourself.online', name: 'VRP Mirror' }
{ url: 'https://there-is-a.vrpmonkey.help', name: 'VRP Mirror' }
]

const failedUrls: string[] = []
Expand Down
Loading