Skip to content

Commit

Permalink
Backup & Cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
GedasFX committed Jan 7, 2024
1 parent c1dd9f6 commit b3e42fc
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 147 deletions.
45 changes: 42 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os

import asyncio
import subprocess
import app_config
import decky_plugin
from ludusavi import Ludusavi
Expand All @@ -10,6 +10,8 @@
class Plugin:
ludusavi: Ludusavi

backup_task: asyncio.Task

# Check plugin for initialization
async def get_ludusavi_version(self):
decky_plugin.logger.debug("Executing: get_ludusavi_version()")
Expand All @@ -26,7 +28,44 @@ async def set_config(self, key: str, value: str):
async def verify_game_exists(self, game_name: str):
decky_plugin.logger.debug("Executing: verify_game_exists(%s)", game_name)
return { "exists": self.ludusavi.check_game(game_name) }

async def backup_game(self, game_name: str):
decky_plugin.logger.debug("Executing: backup_game(%s)", game_name)
self.backup_task = asyncio.create_task(self.ludusavi.backup_game_async(game_name))

async def backup_game_sync(self, game_name: str):
decky_plugin.logger.debug("Executing: backup_game_sync(%s)", game_name)
# self.ludusavi.backup_game(game_name)
cmd = [
'/var/lib/flatpak/exports/bin/com.github.mtkennerly.ludusavi',
'backup',
'--api',
'--force',
'Pokemon - Emerald Version (U)'
]

try:
# Run the command securely
process = subprocess.run(cmd, check=True, capture_output=True, text=True, shell=True)

# Print the output and errors if needed
decky_plugin.logger.debug("STDOUT: %s", process.stdout)
decky_plugin.logger.debug("STDERR: %s", process.stderr)

decky_plugin.logger.debug("wat")

except subprocess.CalledProcessError as e:
# Handle errors, if any
decky_plugin.logger.debug("Error: %s", e)

async def backup_game_check_finished(self):
decky_plugin.logger.debug("Executing: backup_game_check_finished()")
if not self.backup_task.done():
return { "completed": False }

result = { "completed": True, "result": self.backup_task.result() }
self.backup_task = None
return result

# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
async def _main(self):
Expand All @@ -38,4 +77,4 @@ async def _unload(self):

# Migrations that should be performed before entering `_main()`.
async def _migration(self):
pass
app_config.migrate()
4 changes: 4 additions & 0 deletions py_modules/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

cfg_property_file = Path(decky_plugin.DECKY_PLUGIN_SETTINGS_DIR) / "plugin.properties"

def migrate():
if not cfg_property_file.is_file():
cfg_property_file.touch()

def get_config():
with open(decky_plugin.DECKY_PLUGIN_SETTINGS_DIR) as f:
lines = f.readlines()
Expand Down
30 changes: 6 additions & 24 deletions py_modules/ludusavi.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ async def backup_game_async(self, game_name: str):
cmd = [self.bin_path, 'backup', '--api', '--force', game_name]
api_logger.info("Running command: %s", subprocess.list2cmdline(cmd))

process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
# Workaround for flatpacks
l_env = os.environ.copy()
if 'XDG_RUNTIME_DIR' not in l_env:
l_env['XDG_RUNTIME_DIR'] = '/run/user/1000'

process = await asyncio.create_subprocess_exec(*cmd, env=l_env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, _ = await process.communicate()

result = stdout.decode()
Expand All @@ -70,26 +75,3 @@ def _check_initialized(self, bin_path: str) -> bool:
return True
except FileNotFoundError:
return False


# # Check the installation. 'or' operand checks from left to right, so that it will not check later if one was found.
# # com.github.mtkennerly.ludusavi comes from flatpak.
# __check_initialized('com.github.mtkennerly.ludusavi') or __check_initialized('ludusavi') or __check_initialized('ludusavi.exe')
# LudusaviState.initialized = True

# # backup_game("Pokemon Emerald")

# # backup_game()

# task = None
# async def lemain():
# task = asyncio.get_event_loop().create_task(backup_game_async("Pokemon Emerald"))
# while not task.done():
# await asyncio.sleep(0.2)

# a = 5
# b = task.result()
# c = 5

# asyncio.get_event_loop().run_until_complete(lemain())
# asyncio.run(lemain())
26 changes: 26 additions & 0 deletions src/components/sidebar/ConfigurationSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PanelSectionRow, ToggleField } from "decky-frontend-lib";
import { setAppState, useAppState } from "../../util/state";

export default function ConfigurationSection() {
const appState = useAppState();

return (
<>
<PanelSectionRow>
<ToggleField
label="Sync after closing a game"
checked={appState.auto_backup_enabled === "true"}
onChange={(e) => setAppState("auto_backup_enabled", e ? "true" : "false", true)}
/>
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
disabled={appState.auto_backup_enabled !== "true"}
label="Toast after auto sync"
checked={appState.auto_backup_toast_enabled === "true"}
onChange={(e) => setAppState("auto_backup_toast_enabled", e ? "true" : "false", true)}
/>
</PanelSectionRow>
</>
);
}
10 changes: 10 additions & 0 deletions src/components/sidebar/DeckyStoreButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React, { PropsWithChildren } from "react";

export default function DeckyStoreButton({ icon, children }: PropsWithChildren<{ icon: React.ReactElement }>) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
{icon}
<div>{children}</div>
</div>
);
}
88 changes: 60 additions & 28 deletions src/components/sidebar/SyncNowButton.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,74 @@
import {
ButtonItem,
DialogBody,
DialogControlsSection,
DialogControlsSectionHeader,
DialogHeader,
Dropdown,
ModalRoot,
SimpleModal,
showModal,
} from "decky-frontend-lib";
import { ButtonItem, ConfirmModal, Dropdown, showModal } from "decky-frontend-lib";
import { useAppState } from "../../util/state";
import { FC, VFC } from "react";
import { FC, VFC, useEffect, useMemo, useState } from "react";
import { backupGame } from "../../util/apiClient";
import DeckyStoreButton from "./DeckyStoreButton";
import { FaSave } from "react-icons/fa";

const SelectPreviousGameDropdown: FC<{ onChange: (gameName: string) => void }> = ({ onChange }) => {

const SelectPreviousGameDropdown: FC<{ onSelected: (gameName: string) => void }> = ({ onSelected }) => {
const { recent_games } = useAppState();
const [selected, setSelected] = useState<string>();

useEffect(() => {
if (recent_games[0]) update(recent_games[0]);
}, [recent_games]);

const update = (game: string) => {
setSelected(game);
onSelected(game);
};

const data = useMemo(() => {
if (recent_games.length === 0) {
return [{ label: "N/A - Open a supported game for it to show up here", data: undefined }];
}

return recent_games.map((g) => ({ label: g, data: g }));
}, [recent_games]);

return <Dropdown rgOptions={data} selectedOption={selected} onChange={(e) => update(e.data)} />;
};

const SyncConfirmationModal: VFC<{ closeModal: () => void }> = ({ closeModal }) => {
const SyncConfirmationModal: VFC<{ closeModal?: () => void }> = ({ closeModal }) => {
const [selectedGame, setSelectedGame] = useState<string>();

return (
<ModalRoot closeModal={closeModal}>
<DialogHeader>Select game to backup</DialogHeader>
<DialogBody>
<Dropdown rgOptions={[{ label: "", data: 1 }]} />
<DialogControlsSection>
<DialogControlsSectionHeader>Syncthing Process Log</DialogControlsSectionHeader>
<ButtonItem>Show Current Log</ButtonItem>
</DialogControlsSection>
</DialogBody>
</ModalRoot>
<ConfirmModal
strTitle="Select recently played game"
onOK={() => {
backupGame(selectedGame!);
}}
bOKDisabled={!selectedGame}
closeModal={closeModal}
>
<SelectPreviousGameDropdown onSelected={setSelectedGame} />
</ConfirmModal>
);
};

export default function SyncNowButton() {
const { ludusavi_enabled } = useAppState();
const { ludusavi_enabled, syncing } = useAppState();

return (
<ButtonItem layout="below" disabled={ludusavi_enabled === "false"} onClick={(e: Event) => showModal(<SyncConfirmationModal />)}>
Sync Now
<ButtonItem layout="below" disabled={ludusavi_enabled === "false" || syncing} onClick={() => showModal(<SyncConfirmationModal />)}>
<style>
{`
.dcs-rotate {
animation: dcsrotate 1s infinite cubic-bezier(0.46, 0.03, 0.52, 0.96);
}
@keyframes dcsrotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
`}
</style>
<DeckyStoreButton icon={<FaSave className={syncing ? "dcs-rotate" : ""} />}>Sync Now</DeckyStoreButton>
</ButtonItem>
);
}
80 changes: 12 additions & 68 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
import {
ButtonItem,
definePlugin,
DialogBody,
DialogButton,
DialogHeader,
LifetimeNotification,
Menu,
MenuItem,
ModalRoot,
PanelSection,
PanelSectionRow,
Router,
ServerAPI,
showContextMenu,
showModal,
SimpleModal,
staticClasses,
} from "decky-frontend-lib";
import { definePlugin, LifetimeNotification, PanelSection, PanelSectionRow, ServerAPI, staticClasses, ToggleField } from "decky-frontend-lib";
import { VFC } from "react";
import { FaShip } from "react-icons/fa";

import logo from "../assets/logo.png";
import LudusaviVersion from "./components/sidebar/LusudaviVersion";
import appState from "./util/state";
import SyncNowButton from "./components/sidebar/SyncNowButton";
import { verifyGameSyncable } from "./util/apiClient";
import { backupGame, verifyGameSyncable } from "./util/apiClient";
import ConfigurationSection from "./components/sidebar/ConfigurationSection";

// interface AddMethodArgs {
// left: number;
Expand All @@ -49,51 +31,17 @@ const Content: VFC = () => {

return (
<>
<PanelSection title="Version">
<PanelSectionRow>
<LudusaviVersion />
</PanelSectionRow>
</PanelSection>
<PanelSection title="Sync">
<PanelSectionRow>
<SyncNowButton />
</PanelSectionRow>
</PanelSection>
<PanelSection>
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={(e: Event) =>
showContextMenu(
<Menu label="Menu" cancelText="CAAAANCEL" onCancel={() => {}}>
<MenuItem onSelected={() => {}}>Item #1</MenuItem>
<MenuItem onSelected={() => {}}>Item #2</MenuItem>
<MenuItem onSelected={() => {}}>Item #3</MenuItem>
</Menu>,
e.currentTarget ?? window
)
}
>
Server says yolo
</ButtonItem>
</PanelSectionRow>

<PanelSectionRow>
<div style={{ display: "flex", justifyContent: "center" }}>
<img src={logo} />
</div>
</PanelSectionRow>

<PanelSection title="Configuration">
<ConfigurationSection />
</PanelSection>
<PanelSection title="Version">
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={() => {
Router.CloseSideMenus();
Router.Navigate("/decky-plugin-test");
}}
>
Router
</ButtonItem>
<LudusaviVersion />
</PanelSectionRow>
</PanelSection>
</>
Expand All @@ -104,10 +52,6 @@ export default definePlugin((serverApi: ServerAPI) => {
appState.initialize(serverApi);

const { unregister: removeGameExitListener } = SteamClient.GameSessions.RegisterForAppLifetimeNotifications(async (e: LifetimeNotification) => {
console.warn("Lud", e);

if (!appState.currentState.auto_backup_enabled) return;

// On Start
if (e.bRunning) {
const x = await Promise.all([SteamClient.Apps.GetLaunchOptionsForApp(e.unAppID), SteamClient.Apps.GetShortcutData([e.unAppID])]);
Expand All @@ -118,7 +62,7 @@ export default definePlugin((serverApi: ServerAPI) => {
if (gameName && (await verifyGameSyncable(gameName))) {
appState.pushRecentGame(gameName);
} else {
console.error("Ludusavi: game not suppported", gameName)
console.error("Ludusavi: game not suppported", gameName);
appState.serverApi.toaster.toast({
title: "Ludusavi",
body: `Game '${gameName}' not supported. Click to learn more.`,
Expand All @@ -136,9 +80,6 @@ export default definePlugin((serverApi: ServerAPI) => {
// </ModalRoot>
// );
// },
onClick: () => {
console.warn("awdawdawdawd")
},
});
}

Expand All @@ -150,6 +91,9 @@ export default definePlugin((serverApi: ServerAPI) => {

// On Exit
else {
if (appState.currentState.ludusavi_enabled === "true" && appState.currentState.auto_backup_enabled) {
backupGame(appState.currentState.recent_games[0]);
}
}
});

Expand Down
Loading

0 comments on commit b3e42fc

Please sign in to comment.