mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-08-20 04:27:29 +00:00
fix(android): playtime tracking and music stopping on unfocus
This commit is contained in:
parent
7a2650bd48
commit
17c2000b97
File diff suppressed because one or more lines are too long
|
|
@ -54,11 +54,15 @@ pub async fn launch_game(
|
|||
}
|
||||
ensure_server_list(&working_dir, servers);
|
||||
|
||||
crate::android_runtime::launch_bridge(
|
||||
let result = crate::android_runtime::launch_bridge(
|
||||
working_dir.to_string_lossy().to_string(),
|
||||
crate::android_runtime::BridgeAction::Play,
|
||||
extra_args,
|
||||
)
|
||||
);
|
||||
if result.is_ok() {
|
||||
playtime::start_session(&app, &instance_id);
|
||||
}
|
||||
result
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
launch_game_desktop(app, state, instance_id, servers, extra_args).await
|
||||
|
|
|
|||
|
|
@ -157,6 +157,18 @@ pub fn run() {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let handle = app_handle.clone();
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::Focused(true) = event {
|
||||
playtime::finish_active_session(&handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ pub struct PlaytimeData {
|
|||
pub sessions: HashMap<String, Vec<PlaytimeSession>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ActiveSession {
|
||||
instance_id: String,
|
||||
start: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaytimeResponse {
|
||||
|
|
@ -54,6 +60,34 @@ pub fn record_session(app: &AppHandle, instance_id: &str, start: u64, end: u64)
|
|||
save(app, &data);
|
||||
}
|
||||
|
||||
const MAX_SESSION_SECONDS: u64 = 12 * 60 * 60;
|
||||
fn active_session_path(app: &AppHandle) -> PathBuf {
|
||||
util::get_app_dir(app).join("active_session.json")
|
||||
}
|
||||
|
||||
pub fn start_session(app: &AppHandle, instance_id: &str) {
|
||||
let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
let active = ActiveSession { instance_id: instance_id.to_string(), start };
|
||||
let path = active_session_path(app);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if let Ok(content) = serde_json::to_string(&active) {
|
||||
let _ = std::fs::write(&path, content);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_active_session(app: &AppHandle) {
|
||||
let path = active_session_path(app);
|
||||
let Ok(content) = std::fs::read_to_string(&path) else { return };
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let Ok(active) = serde_json::from_str::<ActiveSession>(&content) else { return };
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
if now > active.start && now - active.start <= MAX_SESSION_SECONDS {
|
||||
record_session(app, &active.instance_id, active.start, now);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_playtime(app: &AppHandle, instance_id: &str) -> PlaytimeResponse {
|
||||
let data = load(app);
|
||||
let sessions = data.sessions.get(instance_id);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from "react";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { useAppConfig } from "../hooks/useAppConfig";
|
||||
import { TauriService } from "../services/TauriService";
|
||||
import { useAudioController } from "../hooks/useAudioController";
|
||||
|
|
@ -29,10 +36,18 @@ interface UIContextType {
|
|||
clearUpdateMessage: () => void;
|
||||
}
|
||||
const UIContext = createContext<UIContextType | undefined>(undefined);
|
||||
export const ConfigContext = createContext<ReturnType<typeof useAppConfig> | undefined>(undefined);
|
||||
export const AudioContext = createContext<ReturnType<typeof useAudioController> | undefined>(undefined);
|
||||
export const GameContext = createContext<ReturnType<typeof useGameManager> | undefined>(undefined);
|
||||
export const SkinContext = createContext<ReturnType<typeof useSkinSync> | undefined>(undefined);
|
||||
export const ConfigContext = createContext<
|
||||
ReturnType<typeof useAppConfig> | undefined
|
||||
>(undefined);
|
||||
export const AudioContext = createContext<
|
||||
ReturnType<typeof useAudioController> | undefined
|
||||
>(undefined);
|
||||
export const GameContext = createContext<
|
||||
ReturnType<typeof useGameManager> | undefined
|
||||
>(undefined);
|
||||
export const SkinContext = createContext<
|
||||
ReturnType<typeof useSkinSync> | undefined
|
||||
>(undefined);
|
||||
export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
const [showIntro, setShowIntro] = useState(true);
|
||||
const [logoAnimDone, setLogoAnimDone] = useState(false);
|
||||
|
|
@ -55,7 +70,11 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
setCustomizations: configRaw.setCustomizations,
|
||||
extraLaunchArgs: configRaw.extraLaunchArgs,
|
||||
});
|
||||
const skinSync = useSkinSync({ username: configRaw.username, profile: configRaw.profile, editions: gameRaw.editions });
|
||||
const skinSync = useSkinSync({
|
||||
username: configRaw.username,
|
||||
profile: configRaw.profile,
|
||||
editions: gameRaw.editions,
|
||||
});
|
||||
const audioRaw = useAudioController({
|
||||
musicVol: configRaw.musicVol,
|
||||
sfxVol: configRaw.sfxVol,
|
||||
|
|
@ -63,33 +82,78 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
isWindowVisible,
|
||||
});
|
||||
|
||||
const config = useMemo(() => configRaw, [
|
||||
configRaw.username, configRaw.theme, configRaw.layout, configRaw.vfxEnabled,
|
||||
configRaw.rpcEnabled, configRaw.musicVol, configRaw.sfxVol, configRaw.isDayTime,
|
||||
configRaw.profile, configRaw.linuxRunner, configRaw.perfBoost, configRaw.customEditions,
|
||||
configRaw.customPaths,
|
||||
configRaw.customizations,
|
||||
configRaw.legacyMode, configRaw.animationsEnabled, configRaw.mangohudEnabled,
|
||||
configRaw.extraLaunchArgs, configRaw.launchPrefix, configRaw.launchEnvVars, configRaw.startFullscreen,
|
||||
configRaw.skipIntro, configRaw.instanceLaunchArgs,
|
||||
]);
|
||||
const config = useMemo(
|
||||
() => configRaw,
|
||||
[
|
||||
configRaw.username,
|
||||
configRaw.theme,
|
||||
configRaw.layout,
|
||||
configRaw.vfxEnabled,
|
||||
configRaw.rpcEnabled,
|
||||
configRaw.musicVol,
|
||||
configRaw.sfxVol,
|
||||
configRaw.isDayTime,
|
||||
configRaw.profile,
|
||||
configRaw.linuxRunner,
|
||||
configRaw.perfBoost,
|
||||
configRaw.customEditions,
|
||||
configRaw.customPaths,
|
||||
configRaw.customizations,
|
||||
configRaw.legacyMode,
|
||||
configRaw.animationsEnabled,
|
||||
configRaw.mangohudEnabled,
|
||||
configRaw.extraLaunchArgs,
|
||||
configRaw.launchPrefix,
|
||||
configRaw.launchEnvVars,
|
||||
configRaw.startFullscreen,
|
||||
configRaw.skipIntro,
|
||||
configRaw.instanceLaunchArgs,
|
||||
],
|
||||
);
|
||||
|
||||
const game = useMemo(() => gameRaw, [
|
||||
gameRaw.installs, gameRaw.isGameRunning, gameRaw.downloadProgress,
|
||||
gameRaw.downloadingIds, gameRaw.editions, gameRaw.isRunnerDownloading,
|
||||
gameRaw.runnerDownloadProgress, gameRaw.error, gameRaw.updateCustomEdition,
|
||||
gameRaw.handleUninstall, gameRaw.handleCancelDownload, gameRaw.gameUpdateMessage, configRaw.profile,
|
||||
gameRaw.updatesAvailable, gameRaw.addToSteam, gameRaw.steamSuccessMessage,
|
||||
gameRaw.cycleBranch, gameRaw.toggleInstall, gameRaw.checkInstalls,
|
||||
gameRaw.handleLaunch, gameRaw.stopGame, gameRaw.addCustomEdition,
|
||||
gameRaw.deleteCustomEdition, gameRaw.downloadRunner,
|
||||
gameRaw.customizations, gameRaw.updateCustomization,
|
||||
gameRaw.gameLog, gameRaw.clearGameLog,
|
||||
]);
|
||||
const game = useMemo(
|
||||
() => gameRaw,
|
||||
[
|
||||
gameRaw.installs,
|
||||
gameRaw.isGameRunning,
|
||||
gameRaw.downloadProgress,
|
||||
gameRaw.downloadingIds,
|
||||
gameRaw.editions,
|
||||
gameRaw.isRunnerDownloading,
|
||||
gameRaw.runnerDownloadProgress,
|
||||
gameRaw.error,
|
||||
gameRaw.updateCustomEdition,
|
||||
gameRaw.handleUninstall,
|
||||
gameRaw.handleCancelDownload,
|
||||
gameRaw.gameUpdateMessage,
|
||||
configRaw.profile,
|
||||
gameRaw.updatesAvailable,
|
||||
gameRaw.addToSteam,
|
||||
gameRaw.steamSuccessMessage,
|
||||
gameRaw.cycleBranch,
|
||||
gameRaw.toggleInstall,
|
||||
gameRaw.checkInstalls,
|
||||
gameRaw.handleLaunch,
|
||||
gameRaw.stopGame,
|
||||
gameRaw.addCustomEdition,
|
||||
gameRaw.deleteCustomEdition,
|
||||
gameRaw.downloadRunner,
|
||||
gameRaw.customizations,
|
||||
gameRaw.updateCustomization,
|
||||
gameRaw.gameLog,
|
||||
gameRaw.clearGameLog,
|
||||
],
|
||||
);
|
||||
|
||||
const audio = useMemo(() => audioRaw, [
|
||||
audioRaw.currentTrack, audioRaw.splashIndex, audioRaw.tracks, audioRaw.splashes
|
||||
]);
|
||||
const audio = useMemo(
|
||||
() => audioRaw,
|
||||
[
|
||||
audioRaw.currentTrack,
|
||||
audioRaw.splashIndex,
|
||||
audioRaw.tracks,
|
||||
audioRaw.splashes,
|
||||
],
|
||||
);
|
||||
|
||||
useDiscordRPC({
|
||||
rpcEnabled: config.rpcEnabled,
|
||||
|
|
@ -143,32 +207,50 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
extraLaunchArgs: config.extraLaunchArgs,
|
||||
launchPrefix: config.launchPrefix,
|
||||
launchEnvVars: config.launchEnvVars,
|
||||
startFullscreen: config.startFullscreen,
|
||||
skipIntro: config.skipIntro,
|
||||
startFullscreen: config.startFullscreen,
|
||||
skipIntro: config.skipIntro,
|
||||
instanceLaunchArgs: config.instanceLaunchArgs,
|
||||
}).catch(console.error);
|
||||
}
|
||||
}, [
|
||||
config.username, skinSync.skinBase64, config.theme, config.linuxRunner,
|
||||
config.perfBoost, config.customEditions, config.profile,
|
||||
config.username,
|
||||
skinSync.skinBase64,
|
||||
config.theme,
|
||||
config.linuxRunner,
|
||||
config.perfBoost,
|
||||
config.customEditions,
|
||||
config.profile,
|
||||
config.customPaths,
|
||||
config.customizations, config.vfxEnabled, config.animationsEnabled,
|
||||
config.rpcEnabled, config.musicVol, config.sfxVol, config.legacyMode,
|
||||
config.mangohudEnabled, config.extraLaunchArgs, config.launchPrefix,
|
||||
config.launchEnvVars, config.isLoaded, config.startFullscreen,
|
||||
config.skipIntro, config.instanceLaunchArgs,
|
||||
config.customizations,
|
||||
config.vfxEnabled,
|
||||
config.animationsEnabled,
|
||||
config.rpcEnabled,
|
||||
config.musicVol,
|
||||
config.sfxVol,
|
||||
config.legacyMode,
|
||||
config.mangohudEnabled,
|
||||
config.extraLaunchArgs,
|
||||
config.launchPrefix,
|
||||
config.launchEnvVars,
|
||||
config.isLoaded,
|
||||
config.startFullscreen,
|
||||
config.skipIntro,
|
||||
config.instanceLaunchArgs,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const setupVisibilityDetection = async () => {
|
||||
try {
|
||||
const { listen } = await import("@tauri-apps/api/event");
|
||||
const unlistenClose = await listen("tauri://close-requested", async () => {
|
||||
setIsWindowVisible(false);
|
||||
if (config.rpcEnabled) {
|
||||
await RpcService.StopRPC();
|
||||
}
|
||||
});
|
||||
const unlistenClose = await listen(
|
||||
"tauri://close-requested",
|
||||
async () => {
|
||||
setIsWindowVisible(false);
|
||||
if (config.rpcEnabled) {
|
||||
await RpcService.StopRPC();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const unlistenShow = await listen("tauri://window-shown", () => {
|
||||
setIsWindowVisible(true);
|
||||
|
|
@ -179,14 +261,19 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
});
|
||||
|
||||
const unlistenBlur = await listen("tauri://blur", () => {
|
||||
console.log("Window blurred - checking visibility");
|
||||
setIsWindowVisible(false);
|
||||
});
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
setIsWindowVisible(!document.hidden);
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
return () => {
|
||||
unlistenClose();
|
||||
unlistenShow();
|
||||
unlistenFocus();
|
||||
unlistenBlur();
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to setup visibility detection:", error);
|
||||
|
|
@ -197,14 +284,41 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
setupVisibilityDetection();
|
||||
}, [config.rpcEnabled]);
|
||||
|
||||
const uiValue = useMemo(() => ({
|
||||
activeView, setActiveView, showIntro, setShowIntro,
|
||||
logoAnimDone, setLogoAnimDone, isUiHidden, setIsUiHidden,
|
||||
isWindowVisible,
|
||||
focusSection, setFocusSection,
|
||||
onNavigateToSkin, onNavigateToMenu, connected,
|
||||
updateMessage, updateUrl, clearUpdateMessage
|
||||
}), [activeView, showIntro, logoAnimDone, isUiHidden, isWindowVisible, focusSection, onNavigateToSkin, onNavigateToMenu, connected, updateMessage, updateUrl, clearUpdateMessage]);
|
||||
const uiValue = useMemo(
|
||||
() => ({
|
||||
activeView,
|
||||
setActiveView,
|
||||
showIntro,
|
||||
setShowIntro,
|
||||
logoAnimDone,
|
||||
setLogoAnimDone,
|
||||
isUiHidden,
|
||||
setIsUiHidden,
|
||||
isWindowVisible,
|
||||
focusSection,
|
||||
setFocusSection,
|
||||
onNavigateToSkin,
|
||||
onNavigateToMenu,
|
||||
connected,
|
||||
updateMessage,
|
||||
updateUrl,
|
||||
clearUpdateMessage,
|
||||
}),
|
||||
[
|
||||
activeView,
|
||||
showIntro,
|
||||
logoAnimDone,
|
||||
isUiHidden,
|
||||
isWindowVisible,
|
||||
focusSection,
|
||||
onNavigateToSkin,
|
||||
onNavigateToMenu,
|
||||
connected,
|
||||
updateMessage,
|
||||
updateUrl,
|
||||
clearUpdateMessage,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<UIContext.Provider value={uiValue}>
|
||||
|
|
@ -221,8 +335,28 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
export const useUI = () => { const c = useContext(UIContext); if (!c) throw new Error("useUI must be used within LauncherProvider"); return c; };
|
||||
export const useConfig = () => { const c = useContext(ConfigContext); if (!c) throw new Error("useConfig must be used within LauncherProvider"); return c; };
|
||||
export const useAudio = () => { const c = useContext(AudioContext); if (!c) throw new Error("useAudio must be used within LauncherProvider"); return c; };
|
||||
export const useGame = () => { const c = useContext(GameContext); if (!c) throw new Error("useGame must be used within LauncherProvider"); return c; };
|
||||
export const useSkin = () => { const c = useContext(SkinContext); if (!c) throw new Error("useSkin must be used within LauncherProvider"); return c; };
|
||||
export const useUI = () => {
|
||||
const c = useContext(UIContext);
|
||||
if (!c) throw new Error("useUI must be used within LauncherProvider");
|
||||
return c;
|
||||
};
|
||||
export const useConfig = () => {
|
||||
const c = useContext(ConfigContext);
|
||||
if (!c) throw new Error("useConfig must be used within LauncherProvider");
|
||||
return c;
|
||||
};
|
||||
export const useAudio = () => {
|
||||
const c = useContext(AudioContext);
|
||||
if (!c) throw new Error("useAudio must be used within LauncherProvider");
|
||||
return c;
|
||||
};
|
||||
export const useGame = () => {
|
||||
const c = useContext(GameContext);
|
||||
if (!c) throw new Error("useGame must be used within LauncherProvider");
|
||||
return c;
|
||||
};
|
||||
export const useSkin = () => {
|
||||
const c = useContext(SkinContext);
|
||||
if (!c) throw new Error("useSkin must be used within LauncherProvider");
|
||||
return c;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue