304 lines
10 KiB
Vue

<template>
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/40">
<!-- Header -->
<div class="bg-white/80 backdrop-blur-md border-b border-white/60 sticky top-0 z-10">
<div class="max-w-3xl mx-auto px-6 py-4">
<div class="flex items-center gap-4">
<div class="relative">
<img :src="rebreakIcon" alt="ReBreak" class="w-11 h-11 rounded-xl shadow-lg shadow-blue-500/20">
<div class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 bg-green-500 rounded-full border-2 border-white" />
</div>
<div class="flex-1 min-w-0">
<h1 class="text-lg font-extrabold text-gray-900 truncate">
{{ profile?.nickname || 'ReBreak Magic' }}
</h1>
<div class="flex items-center gap-2 text-sm text-gray-500">
<UBadge v-if="profile?.plan" color="primary" variant="subtle" size="xs" class="font-bold">
{{ profile.plan }}
</UBadge>
<span v-if="session?.label" class="truncate">{{ session.label }}</span>
</div>
</div>
<UButton
icon="i-heroicons-arrow-right-start-on-rectangle"
color="neutral"
variant="ghost"
size="sm"
@click="logout"
>
Abmelden
</UButton>
</div>
</div>
</div>
<div class="max-w-3xl mx-auto px-6 py-8 space-y-6">
<!-- Overall status card -->
<UCard class="shadow-sm border-gray-100 overflow-hidden">
<div class="flex items-start gap-4">
<div
class="w-14 h-14 rounded-2xl bg-gradient-to-br flex items-center justify-center shrink-0"
:class="statusCardClass"
>
<UIcon
:name="protection.status.value.overall === 'protected'
? 'i-heroicons-shield-check'
: protection.status.value.overall === 'partial'
? 'i-heroicons-shield-exclamation'
: 'i-heroicons-shield-slash'"
class="w-7 h-7 text-white"
/>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<h2 class="text-lg font-bold text-gray-900">{{ protection.status.value.label }}</h2>
<UBadge
:color="badgeColorForOverall"
variant="subtle"
size="xs"
>
{{ overallBadgeLabel }}
</UBadge>
</div>
<p class="text-sm text-gray-500 mt-0.5">{{ protection.status.value.message }}</p>
</div>
<div v-if="protection.lastUpdated.value" class="hidden sm:block text-xs text-gray-400 text-right">
Aktualisiert<br>{{ formatTime(protection.lastUpdated.value) }}
</div>
</div>
</UCard>
<!-- Hero section -->
<section>
<h2 class="text-sm font-bold text-gray-500 uppercase tracking-wider mb-3">Aktives Gerät</h2>
<DeviceHeroCard
v-if="currentBackendDevice"
:device="currentBackendDevice"
:is-current="true"
:ios-stars="currentBackendDevice.platform === 'ios' ? iosStars : null"
@open="openDevice"
@toggle-protection="toggleProtection"
/>
<!-- Empty / not registered state -->
<div v-else class="rounded-2xl bg-white ring-1 ring-gray-100 shadow-sm p-6 text-center">
<div class="w-16 h-16 mx-auto rounded-2xl bg-blue-50 flex items-center justify-center mb-4">
<UIcon name="i-heroicons-shield-exclamation" class="w-8 h-8 text-[var(--rebreak-primary)]" />
</div>
<h3 class="text-lg font-bold text-gray-900">Dieses Gerät ist nicht geschützt</h3>
<p class="text-sm text-gray-500 mt-1 mb-4">Aktiviere den Schutz für das Gerät, auf dem Magic läuft.</p>
<UButton color="primary" icon="i-heroicons-shield-check" to="/desktop-enroll">
Dieses Gerät schützen
</UButton>
</div>
</section>
<!-- Other devices list -->
<section>
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-bold text-gray-500 uppercase tracking-wider">Weitere Geräte</h2>
<UButton
icon="i-heroicons-arrow-path"
color="neutral"
variant="ghost"
size="xs"
:loading="loading"
@click="refresh"
>
Aktualisieren
</UButton>
</div>
<div v-if="error" class="text-sm text-red-600 mb-4 p-4 bg-red-50 rounded-xl">{{ error }}</div>
<div v-if="loading && otherDevices.length === 0" class="py-12 text-center text-gray-500">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin mx-auto mb-3 text-[var(--rebreak-primary)]" />
<p class="font-semibold">Lade Geräte</p>
</div>
<div v-else-if="otherDevices.length === 0" class="py-10 text-center rounded-2xl bg-white ring-1 ring-gray-100">
<p class="text-sm text-gray-500">Keine weiteren Geräte registriert.</p>
</div>
<div v-else class="space-y-3">
<DeviceListItem
v-for="device in otherDevices"
:key="device.deviceId"
:device="device"
:is-current="false"
@open="openDevice"
/>
</div>
</section>
</div>
<DeviceDetailSheet
v-model:open="sheetOpen"
:device="selectedDevice"
:ios-stars="selectedDevice?.platform === 'ios' && selectedDevice?.isCurrent ? iosStars : null"
@close="sheetOpen = false"
@toggle-protection="toggleProtection"
@start-cooldown="startCooldown"
@cancel-cooldown="cancelCooldown"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useTauri, type UserProfile } from "~/composables/useTauri";
import { useMagicSession, useMagicDevices, useIphoneDevice } from "~/composables/useMagicState";
import { useProtectionStatus } from "~/composables/useProtectionStatus";
import { useDeviceStatus, type ComputedDevice } from "~/composables/useDeviceStatus";
import rebreakIcon from "~/assets/rebreak-icon.png";
const {
getMagicDevices,
startCooldown: apiStartCooldown,
cancelCooldown: apiCancelCooldown,
logoutMagic,
fetchMe,
setDesktopProtectionStatus,
getPlatform,
} = useTauri();
const session = useMagicSession();
const devices = useMagicDevices();
const iphone = useIphoneDevice();
const protection = useProtectionStatus();
const profile = ref<UserProfile | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const sheetOpen = ref(false);
const selectedDevice = ref<ComputedDevice | null>(null);
const platformInfo = ref<{ platform: string } | null>(null);
// Share localHostname from protection composable with device status logic.
const localHostname = protection.localHostname;
const { currentBackendDevice, otherDevices, iosStars } = useDeviceStatus(devices, localHostname, iphone);
const statusCardClass = computed(() => {
switch (protection.status.value.overall) {
case "protected": return "from-emerald-500 to-teal-500";
case "partial": return "from-amber-500 to-orange-500";
case "unprotected": return "from-slate-400 to-slate-500";
default: return "from-blue-500 to-indigo-500";
}
});
const badgeColorForOverall = computed(() => {
switch (protection.status.value.overall) {
case "protected": return "success";
case "partial": return "warning";
case "unprotected": return "neutral";
default: return "primary";
}
});
const overallBadgeLabel = computed(() => {
switch (protection.status.value.overall) {
case "protected": return "Aktiv";
case "partial": return "Teilweise";
case "unprotected": return "Inaktiv";
default: return "Unbekannt";
}
});
onMounted(async () => {
await loadProfile();
await refresh();
try {
const info = await getPlatform();
platformInfo.value = { platform: info.platform };
} catch (e) {
platformInfo.value = null;
}
});
async function loadProfile() {
try {
profile.value = await fetchMe();
} catch (e: any) {
console.error("Failed to load profile:", e);
}
}
async function refresh() {
loading.value = true;
error.value = null;
try {
await protection.refresh();
devices.value = await getMagicDevices();
} catch (e: any) {
error.value = e?.message ?? "Geräte konnten nicht geladen werden";
} finally {
loading.value = false;
}
}
function formatTime(date: Date) {
return date.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
function openDevice(device: ComputedDevice) {
selectedDevice.value = device;
sheetOpen.value = true;
}
async function toggleProtection(device: ComputedDevice) {
if (!device.isCurrent) return;
if (device.platform === "mac" || device.platform === "windows") {
if (protection.status.value.desktopProtected) {
try {
await setDesktopProtectionStatus(false, device.platform);
await refresh();
} catch (e: any) {
error.value = e?.message ?? "Schutz konnte nicht deaktiviert werden";
}
} else {
await navigateTo("/desktop-enroll");
}
}
}
async function startCooldown(device: ComputedDevice, minutes: number) {
if (!device.isCurrent) return;
loading.value = true;
try {
await apiStartCooldown(device.deviceId, minutes);
await refresh();
} catch (e: any) {
error.value = e?.message ?? "Cooldown konnte nicht gestartet werden";
} finally {
loading.value = false;
}
}
async function cancelCooldown(device: ComputedDevice) {
if (!device.isCurrent) return;
loading.value = true;
try {
await apiCancelCooldown(device.deviceId);
await refresh();
} catch (e: any) {
error.value = e?.message ?? "Cooldown konnte nicht beendet werden";
} finally {
loading.value = false;
}
}
async function logout() {
try {
await logoutMagic();
session.value = null;
devices.value = [];
await navigateTo("/");
} catch (e: any) {
error.value = e?.message ?? "Abmelden fehlgeschlagen";
}
}
</script>