Merge installers: Velopack packaging on all platforms
Per-user installers with in-app auto-updates (Velopack Setup.exe / macOS pkg / Linux AppImage) built by the Gitea release workflow. macOS GUI switched to Qt6 WebEngine (modern Chromium). Diagnostics section in settings (version, browser engine, log view, client error capture).
This commit is contained in:
+24
-3
@@ -190,9 +190,7 @@ export async function fetchResumePositions(): Promise<Record<string, ResumePosit
|
||||
const rawEpisodes = (entry as { episodes?: unknown }).episodes
|
||||
if (rawEpisodes && typeof rawEpisodes === "object") {
|
||||
const watches: Record<string, EpisodeWatchEntry> = {}
|
||||
for (const [key, watch] of Object.entries(
|
||||
rawEpisodes as Record<string, unknown>,
|
||||
)) {
|
||||
for (const [key, watch] of Object.entries(rawEpisodes as Record<string, unknown>)) {
|
||||
if (!watch || typeof watch !== "object") continue
|
||||
const w = watch as { pos?: unknown; done?: unknown }
|
||||
if (typeof w.pos !== "number" || !Number.isFinite(w.pos)) continue
|
||||
@@ -438,3 +436,26 @@ export async function pickFolderAndAddRoot(): Promise<string | null> {
|
||||
const folder: string | null = await api.pick_folder()
|
||||
return folder
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the installed MediaHive version.
|
||||
*/
|
||||
export async function getVersion(): Promise<string> {
|
||||
const response = await fetch("/api/version")
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load version: ${response.statusText}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
return data.version || "dev"
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the tail of the application log.
|
||||
*/
|
||||
export async function getLog(): Promise<string> {
|
||||
const response = await fetch("/api/log")
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load log: ${response.statusText}`)
|
||||
}
|
||||
return response.text()
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
<!-- Detail mode: show current category + Details -->
|
||||
<template v-else>
|
||||
<button class="header-nav-item" v-bind="navAttrs(navRow, 0)" @focus="goToCategory">
|
||||
{{ currentView === "search" ? "Search" : currentView === "movies" ? "Movies" : "Series" }}
|
||||
{{
|
||||
currentView === "search" ? "Search" : currentView === "movies" ? "Movies" : "Series"
|
||||
}}
|
||||
</button>
|
||||
<button class="header-nav-item active" v-bind="navAttrs(navRow, 1, 1)">Details</button>
|
||||
</template>
|
||||
@@ -208,7 +210,9 @@
|
||||
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section-title">Preferred Format</h2>
|
||||
<p class="settings-section-desc">Preferred format when multiple versions are available.</p>
|
||||
<p class="settings-section-desc">
|
||||
Preferred format when multiple versions are available.
|
||||
</p>
|
||||
|
||||
<div class="format-grid">
|
||||
<div class="format-row format-row-stack">
|
||||
@@ -295,6 +299,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section-title">Diagnostics</h2>
|
||||
<p class="settings-section-desc">
|
||||
Version info and application log for troubleshooting.
|
||||
</p>
|
||||
|
||||
<div class="diag-rows">
|
||||
<div class="diag-row">
|
||||
<span class="diag-label">App version</span>
|
||||
<span class="diag-value">{{ appVersion || "…" }}</span>
|
||||
</div>
|
||||
<div class="diag-row">
|
||||
<span class="diag-label">Browser engine</span>
|
||||
<span class="diag-value">{{ browserEngine }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="diag-log-header">
|
||||
<span class="diag-label">Application log</span>
|
||||
<button class="diag-refresh-btn" @click="refreshLog">Refresh</button>
|
||||
</div>
|
||||
<pre class="diag-log">{{ appLog }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,7 +334,7 @@ import { ref, watch, computed, onMounted, onUnmounted } from "vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { navAttrs } from "../composables/useKeyboardNavigation"
|
||||
import logoUrl from "../assets/mediahive.webp"
|
||||
import { replaceRoots, pickFolderAndAddRoot, fetchPlayers } from "../api"
|
||||
import { replaceRoots, pickFolderAndAddRoot, fetchPlayers, getVersion, getLog } from "../api"
|
||||
import type { PlayerInfo } from "../api"
|
||||
import HexKeyboard from "./HexKeyboard.vue"
|
||||
import {
|
||||
@@ -409,6 +437,30 @@ async function refreshPlayers() {
|
||||
}
|
||||
}
|
||||
|
||||
const appVersion = ref("")
|
||||
const appLog = ref("")
|
||||
const browserEngine = navigator.userAgent
|
||||
let diagnosticsFetched = false
|
||||
|
||||
async function refreshLog() {
|
||||
try {
|
||||
appLog.value = await getLog()
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch log:", e)
|
||||
appLog.value = "Failed to load log."
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDiagnostics() {
|
||||
try {
|
||||
appVersion.value = await getVersion()
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch version:", e)
|
||||
appVersion.value = "unknown"
|
||||
}
|
||||
await refreshLog()
|
||||
}
|
||||
|
||||
async function removeRoot(rootId: string) {
|
||||
const filtered = roots.value.filter((r) => r.root_id !== rootId)
|
||||
const newRoots = Object.fromEntries(filtered.map((r) => [r.root_id, r.path]))
|
||||
@@ -438,6 +490,10 @@ async function addRoot() {
|
||||
watch(showSettings, (visible) => {
|
||||
if (visible) {
|
||||
void refreshPlayers()
|
||||
if (!diagnosticsFetched) {
|
||||
diagnosticsFetched = true
|
||||
void refreshDiagnostics()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -868,4 +924,69 @@ onUnmounted(() => {
|
||||
font-size: 0.8rem;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.diag-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.diag-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.diag-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.diag-value {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.diag-log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.diag-refresh-btn {
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.diag-refresh-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.diag-log {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
font-size: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
width: 80ch;
|
||||
max-width: 100%;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,38 @@ import { installKeyboardNavigation } from "./composables/useKeyboardNavigation"
|
||||
import { installGamepadNavigation } from "./composables/useGamepadNavigation"
|
||||
import { installInputModalityTracking } from "./composables/useInputModality"
|
||||
|
||||
function postClientError(payload: {
|
||||
message: string
|
||||
stack: string | null
|
||||
source: string | null
|
||||
}) {
|
||||
fetch("/api/client-log", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function installErrorCapture() {
|
||||
window.addEventListener("error", (event) => {
|
||||
const source =
|
||||
event.filename != null ? `${event.filename}:${event.lineno ?? 0}:${event.colno ?? 0}` : null
|
||||
postClientError({
|
||||
message: event.message || String(event.error ?? "Unknown error"),
|
||||
stack: event.error?.stack ?? null,
|
||||
source,
|
||||
})
|
||||
})
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
const reason = event.reason
|
||||
postClientError({
|
||||
message: reason instanceof Error ? reason.message : `Unhandled rejection: ${String(reason)}`,
|
||||
stack: reason instanceof Error ? (reason.stack ?? null) : null,
|
||||
source: "unhandledrejection",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function installReloadShortcut() {
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
@@ -27,6 +59,7 @@ installInputModalityTracking()
|
||||
installKeyboardNavigation()
|
||||
installGamepadNavigation()
|
||||
installReloadShortcut()
|
||||
installErrorCapture()
|
||||
|
||||
// Unregister any legacy service workers — MediaHive no longer uses a PWA/SW.
|
||||
if ("serviceWorker" in navigator) {
|
||||
|
||||
Reference in New Issue
Block a user