mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-25 01:20:55 +00:00
feat: playtime counting
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TauriService, PlaytimeResponse } from "../../services/TauriService";
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
if (h > 0) return `${h}h ${m}m ${s}s`;
|
||||
if (m > 0) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
}
|
||||
|
||||
export default function PlaytimeModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
instanceId,
|
||||
instanceName,
|
||||
playBackSound,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
instanceId: string;
|
||||
instanceName: string;
|
||||
playBackSound: (s?: string) => void;
|
||||
}) {
|
||||
const [playtime, setPlaytime] = useState<PlaytimeResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setPlaytime(null);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
TauriService.getPlaytime(instanceId)
|
||||
.then(setPlaytime)
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, [isOpen, instanceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
playBackSound("close_click.wav");
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKey);
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
}, [isOpen, onClose, playBackSound]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md outline-none border-none"
|
||||
>
|
||||
<div
|
||||
className="relative w-[380px] p-6 flex flex-col items-center shadow-2xl"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
|
||||
Playtime
|
||||
</h2>
|
||||
|
||||
<p className="text-white text-sm mc-text-shadow mb-5 text-center">
|
||||
{instanceName}
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-gray-400 text-sm mc-text-shadow mb-4">Loading...</div>
|
||||
) : playtime ? (
|
||||
<div className="w-full flex flex-col gap-3 mb-4">
|
||||
<div className="flex justify-between items-center bg-black/30 px-4 py-3 border border-[#373737]">
|
||||
<span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider">
|
||||
Total
|
||||
</span>
|
||||
<span className="text-white text-lg mc-text-shadow font-bold">
|
||||
{formatTime(playtime.totalSeconds)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center bg-black/30 px-4 py-3 border border-[#373737]">
|
||||
<span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider">
|
||||
This Week
|
||||
</span>
|
||||
<span className="text-white text-lg mc-text-shadow font-bold">
|
||||
{formatTime(playtime.weekSeconds)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center bg-black/30 px-4 py-3 border border-[#373737]">
|
||||
<span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider">
|
||||
Today
|
||||
</span>
|
||||
<span className="text-white text-lg mc-text-shadow font-bold">
|
||||
{formatTime(playtime.daySeconds)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-red-400 text-sm mc-text-shadow mb-4">Failed to load playtime data.</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
playBackSound("close_click.wav");
|
||||
onClose();
|
||||
}}
|
||||
className="w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none text-white"
|
||||
style={{
|
||||
backgroundImage: "url('/images/button_highlighted.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect, useRef, memo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
import { TauriService, PlaytimeResponse } from "../../services/TauriService";
|
||||
import CustomTUModal from "../modals/CustomTUModal";
|
||||
import SetUidModal from "../modals/SetUidModal";
|
||||
import ImportWorldModal from "../modals/ImportWorldModal";
|
||||
import PlaytimeModal from "../modals/PlaytimeModal";
|
||||
import {
|
||||
useUI,
|
||||
useConfig,
|
||||
@@ -46,6 +47,14 @@ const DeleteConfirmButton = memo(function DeleteConfirmButton({
|
||||
);
|
||||
});
|
||||
|
||||
function formatPlaytime(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
if (m > 0) return `${m}m`;
|
||||
return seconds > 0 ? `${seconds}s` : "";
|
||||
}
|
||||
|
||||
const VersionsView = memo(function VersionsView() {
|
||||
const { setActiveView } = useUI();
|
||||
const {
|
||||
@@ -78,6 +87,9 @@ const VersionsView = memo(function VersionsView() {
|
||||
const [editingEdition, setEditingEdition] = useState<Edition | null>(null);
|
||||
const [isImportWorldModalOpen, setIsImportWorldModalOpen] = useState(false);
|
||||
const [importWorldTarget, setImportWorldTarget] = useState<{ id: string; name: string } | null>(null);
|
||||
const [isPlaytimeModalOpen, setIsPlaytimeModalOpen] = useState(false);
|
||||
const [playtimeTarget, setPlaytimeTarget] = useState<{ id: string; name: string } | null>(null);
|
||||
const [playtimeMap, setPlaytimeMap] = useState<Record<string, PlaytimeResponse>>({});
|
||||
const [initialPath, setInitialPath] = useState<string>("");
|
||||
const [hoveredBtn, setHoveredBtn] = useState<{
|
||||
row: number;
|
||||
@@ -189,6 +201,21 @@ const VersionsView = memo(function VersionsView() {
|
||||
}
|
||||
}, [focusIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlaytimes = async () => {
|
||||
const map: Record<string, PlaytimeResponse> = {};
|
||||
await Promise.all(installedVersions.map(async (id) => {
|
||||
try {
|
||||
map[id] = await TauriService.getPlaytime(id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}));
|
||||
setPlaytimeMap(map);
|
||||
};
|
||||
fetchPlaytimes();
|
||||
}, [installedVersions]);
|
||||
|
||||
const handleEditionClick = (edition: Edition, index: number) => {
|
||||
const isInstalled = installedVersions.includes(edition.instanceId);
|
||||
if (isInstalled) {
|
||||
@@ -331,6 +358,36 @@ const VersionsView = memo(function VersionsView() {
|
||||
>
|
||||
{edition.name}
|
||||
</span>
|
||||
{isInstalled && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
setPlaytimeTarget({ id: edition.instanceId, name: edition.name });
|
||||
setIsPlaytimeModalOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-2 py-1 bg-black/60 border border-[#555] hover:border-[#FFFF55] group transition-colors flex-shrink-0"
|
||||
title="View playtime"
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#AAAAAA"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="group-hover:stroke-[#FFFF55] transition-colors"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span className="text-xs text-[#AAAAAA] group-hover:text-[#FFFF55] leading-none transition-colors">
|
||||
{playtimeMap[edition.instanceId] ? formatPlaytime(playtimeMap[edition.instanceId].totalSeconds) : ""}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{edition.category &&
|
||||
edition.category.map((cat: string) => (
|
||||
<span
|
||||
@@ -769,6 +826,14 @@ const VersionsView = memo(function VersionsView() {
|
||||
targetInstanceName={importWorldTarget?.name ?? ""}
|
||||
/>
|
||||
|
||||
<PlaytimeModal
|
||||
isOpen={isPlaytimeModalOpen}
|
||||
onClose={() => { setIsPlaytimeModalOpen(false); setPlaytimeTarget(null); }}
|
||||
playBackSound={playBackSound}
|
||||
instanceId={playtimeTarget?.id ?? ""}
|
||||
instanceName={playtimeTarget?.name ?? ""}
|
||||
/>
|
||||
|
||||
{deleteConfirmEdition && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user