Skip to content

Commit

Permalink
feat: implement tab visibility snapshots
Browse files Browse the repository at this point in the history
  • Loading branch information
jessebofill committed Dec 30, 2023
1 parent be905c8 commit 58d7431
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 5 deletions.
17 changes: 17 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ async def get_friends_games(self) -> dict[int, list[int]] | None:
friends_games = Plugin.users_dict[Plugin.user_id]["friendsGames"]
log(f"Got {len(friends_games)} friendsGames")
return friends_games or {}

async def get_snapshots(self) -> dict[str, list[str]] | None:
"""
Waits until users_dict is loaded, then returns snapshots
:return: Users tab visibility snapshots
"""
while Plugin.users_dict is None:
await asyncio.sleep(0.1)

snapshots = Plugin.users_dict[Plugin.user_id]["snapshots"]
log(f"Got snapshots {snapshots}")
return snapshots or {}

# Plugin settings setters
async def set_tabs(self, tabs: dict[str, dict]):
Expand All @@ -165,6 +178,10 @@ async def set_friends_games(self, friends_games: dict[str, list[int]]):
Plugin.users_dict[Plugin.user_id]["friendsGames"] = friends_games
await Plugin.set_setting(self, "usersDict", Plugin.users_dict)

async def set_snapshots(self, snapshots: dict[str, list[str]]):
Plugin.users_dict[Plugin.user_id]["snapshots"] = snapshots
await Plugin.set_setting(self, "usersDict", Plugin.users_dict)

async def get_docs(self):
for docsFileName in os.listdir(self.docsDirPath):
with open(os.path.join(self.docsDirPath, docsFileName), 'r') as docFile:
Expand Down
2 changes: 2 additions & 0 deletions src/components/menus/LibraryMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PresetMenuItems } from './PresetMenu';
import { CustomTabContainer } from '../CustomTabContainer';
import { TabListLabel } from '../TabListLabel';
import { MicroSDeckInterop } from '../../lib/controllers/MicroSDeckInterop';
import { ApplySnapshotMenuGroup } from './SnapshotMenu';

export interface LibraryMenuProps {
closeMenu: () => void;
Expand Down Expand Up @@ -65,6 +66,7 @@ const LibraryMenuItems: VFC<LibraryMenuItemsProps> = ({ selectedTabId, closeMenu
<PresetMenuItems tabMasterManager={tabMasterManager} isMicroSDeckInstalled={isMicroSDeckInstalled} />
</MenuGroup>
<div className={gamepadContextMenuClasses.ContextMenuSeparator} />
<ApplySnapshotMenuGroup label='Apply Visibilty Snapshot' tabMasterManager={tabMasterManager}/>
<MenuGroup label='Reorder Tabs'>
<Focusable style={{ width: '240px', background: "#23262e", margin: '0' }} className='tab-master-library-menu-reorderable-group' onOKActionDescription=''>
<ReorderableList<TabIdEntryType>
Expand Down
65 changes: 65 additions & 0 deletions src/components/menus/SnapshotMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Menu, MenuGroup, MenuItem, showModal } from 'decky-frontend-lib';
import { VFC, Fragment } from 'react';
import { TabMasterManager } from '../../state/TabMasterManager';
import { CreateSnapshotModal, OverwriteSnapshotModal } from '../modals/SnapshotModals';
import { gamepadContextMenuClasses } from '../../lib/GamepadContextMenuClasses';

interface SnapshotMenuProps {
tabMasterManager: TabMasterManager,
}

export const SnapshotMenu: VFC<SnapshotMenuProps> = ({ tabMasterManager }) => {
return <Menu label='Manage Tab Visibility Snapshots'>
<SnapshotMenuItems tabMasterManager={tabMasterManager} />
</Menu>;
};


export const SnapshotMenuItems: VFC<SnapshotMenuProps> = ({ tabMasterManager }) => {
return (
<>
<MenuItem onClick={() => showModal(<CreateSnapshotModal tabMasterManager={tabMasterManager} />)}>
New
</MenuItem>
<OverwriteSnapshotMenuGroup tabMasterManager={tabMasterManager} />
<div className={gamepadContextMenuClasses.ContextMenuSeparator} />
<ApplySnapshotMenuGroup label='Apply' tabMasterManager={tabMasterManager} />
</>
);
};

export const OverwriteSnapshotMenuGroup: VFC<SnapshotMenuProps> = ({ tabMasterManager }) => {

return (
<MenuGroup label='Overwrite'>
{Object.keys(tabMasterManager.snapshotManager?.snapshots ?? {}).map(snapshotName => {
return (
<MenuItem onClick={() => showModal(<OverwriteSnapshotModal snapshotName={snapshotName} tabMasterManager={tabMasterManager} />)}>
{snapshotName}
</MenuItem>
);
})}
</MenuGroup>
);
};

interface ApplySnapshotMenuGroupProps extends SnapshotMenuProps {
label: string;
}

export const ApplySnapshotMenuGroup: VFC<ApplySnapshotMenuGroupProps> = ({ label, tabMasterManager }) => {

return (
<MenuGroup label={label}>
{Object.keys(tabMasterManager.snapshotManager?.snapshots ?? {}).map(snapshotName => {
return (
<MenuItem onClick={() => tabMasterManager.snapshotManager?.apply(snapshotName, tabMasterManager)}>
{snapshotName}
</MenuItem>
);
})}
</MenuGroup>
);
};


57 changes: 57 additions & 0 deletions src/components/modals/SnapshotModals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ConfirmModal, TextField } from 'decky-frontend-lib';
import { VFC, useState } from 'react';
import { TabMasterManager } from '../../state/TabMasterManager';

export interface CreateSnapshotModalProps {
tabMasterManager: TabMasterManager,
closeModal?: () => void,
}

export const CreateSnapshotModal: VFC<CreateSnapshotModalProps> = ({ tabMasterManager, closeModal }) => {
const [name, setName] = useState<string>('');
const visibleTabs = tabMasterManager.getTabs().visibleTabsList;

return (
<ConfirmModal
onOK={() => {
tabMasterManager.snapshotManager?.write(name, visibleTabs.map(tabContainer => tabContainer.id));
closeModal!();
}}
onCancel={() => closeModal!()}
>
<TextField value={name} placeholder="The name for this snapshot" onChange={e => setName(e?.target.value)} />
{visibleTabs.map(tabContainer => <div>{tabContainer.title}</div>)}
</ConfirmModal>
);
};

export interface OverwriteSnapshotModalProps extends CreateSnapshotModalProps {
snapshotName: string;
}

export const OverwriteSnapshotModal: VFC<OverwriteSnapshotModalProps> = ({ snapshotName, tabMasterManager, closeModal }) => {
const { visibleTabsList, tabsMap } = tabMasterManager.getTabs();
const existingTabs = tabMasterManager.snapshotManager!.snapshots[snapshotName].map(tabId => tabsMap.get(tabId));

return (
<ConfirmModal
onOK={() => {
tabMasterManager.snapshotManager?.write(snapshotName, visibleTabsList.map(tabContainer => tabContainer.id));
closeModal!();
}}
onCancel={() => closeModal!()}
>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<div>
New Tabs:
{visibleTabsList.map(tabContainer => <div>{tabContainer.title}</div>)}
</div>
<div>
Existing Tabs:
{existingTabs.map(tabContainer => <div>{tabContainer?.title}</div>)}
</div>
</div>
</ConfirmModal>
);
};

16 changes: 15 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
showContextMenu,
SidebarNavigation,
staticClasses,
GamepadEvent,
GamepadButton
} from "decky-frontend-lib";
import { VFC, ReactNode, useState } from "react";

Expand Down Expand Up @@ -41,6 +43,7 @@ import { MicroSDeck } from "@cebbinghaus/microsdeck";
import { MicroSDeckInstallState, MicroSDeckInterop, microSDeckLibVersion } from './lib/controllers/MicroSDeckInterop';
import { MicroSDeckNotice } from './components/MicroSDeckNotice';
import { CustomTabContainer } from './components/CustomTabContainer';
import { SnapshotMenu } from './components/menus/SnapshotMenu';

declare global {
let DeckyPluginLoader: { pluginReloadQueue: { name: string; version?: string; }[]; };
Expand Down Expand Up @@ -125,7 +128,18 @@ const Content: VFC<{}> = ({ }) => {
</div>
)}
<QamStyles />
<Focusable onMenuActionDescription='Open Docs' onMenuButton={() => { Navigation.CloseSideMenus(); Navigation.Navigate("/tab-master-docs"); }}>
<Focusable
actionDescriptionMap={{
[GamepadButton.START]: 'Open Docs',
[GamepadButton.SELECT]: 'Visibility Snapshots'
}}
onButtonDown={(evt: GamepadEvent) => {
if(evt.detail.button === GamepadButton.SELECT) {
showContextMenu(<SnapshotMenu tabMasterManager={tabMasterManager}/>);
}
}}
onMenuButton={() => { Navigation.CloseSideMenus(); Navigation.Navigate("/tab-master-docs"); }}
>
<div style={{ margin: "5px", marginTop: "0px" }}>
Here you can add, re-order, or remove tabs from the library.
</div>
Expand Down
34 changes: 32 additions & 2 deletions src/lib/controllers/PythonInterop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ServerAPI } from "decky-frontend-lib";
import { validateTabs } from "../Utils";
import { SnapshotDictionary } from '../../state/SnapshotManager';

/**
* Class for frontend -> backend communication.
Expand Down Expand Up @@ -135,8 +136,8 @@ export class PythonInterop {
}

/**
* Gets the store tabs.
* @returns A promise resolving to the store tabs.
* Gets the store tags.
* @returns A promise resolving to the store tags.
*/
static async getTags(): Promise<TagResponse[] | Error> {
let result = await PythonInterop.serverAPI.callPluginMethod<{}, TagResponse[]>("get_tags", {});
Expand Down Expand Up @@ -180,6 +181,20 @@ export class PythonInterop {
}
}

/**
* Gets the visible tab snapshots.
* @returns A promise resolving the snapshots.
*/
static async getSnapshots(): Promise<SnapshotDictionary | Error> {
let result = await PythonInterop.serverAPI.callPluginMethod<{}, SnapshotDictionary>("get_snapshots", {});

if (result.success) {
return result.result;
} else {
return new Error(result.result);
};
}

/**
* Sets the plugin's tabs.
* @param tabs The plugin's tabsDictionary.
Expand Down Expand Up @@ -251,6 +266,21 @@ export class PythonInterop {
};
}

/**
* Sets the visible tab snapshots.
* @param snapshots The snapshots.
* @returns A promise resolving to whether or not the snapshots were successfully set.
*/
static async setSnapshots(snapshots: SnapshotDictionary): Promise<void | Error> {
let result = await PythonInterop.serverAPI.callPluginMethod<{ snapshots: SnapshotDictionary }, void>("set_snapshots", { snapshots: snapshots });

if (result.success) {
return result.result;
} else {
return new Error(result.result);
};
}

/**
* Shows a toast message.
* @param title The title of the toast.
Expand Down
29 changes: 29 additions & 0 deletions src/state/SnapshotManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PythonInterop } from '../lib/controllers/PythonInterop';
import { TabMasterManager } from './TabMasterManager';

export type SnapshotDictionary = {
[name: string]: string[]; //array of ordered tab ids
};

export class SnapshotManager {
snapshots: SnapshotDictionary;

constructor(snapshots: SnapshotDictionary) {
this.snapshots = snapshots;
}

write(snapshotName: string, tabIds: string[]) {
this.snapshots[snapshotName] = tabIds;
this.save();
}


apply(snapshotName: string, tabMasterManager: TabMasterManager) {
tabMasterManager.getTabs().tabsMap.forEach(tabContainer => tabContainer.position = -1);
tabMasterManager.reorderTabs(this.snapshots[snapshotName]);
}

private save() {
PythonInterop.setSnapshots(this.snapshots);
}
}
13 changes: 11 additions & 2 deletions src/state/TabMasterManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { LogController } from "../lib/controllers/LogController";
import { PresetName, PresetOptions, getPreset } from '../presets/presets';
import { MicroSDeckInterop } from '../lib/controllers/MicroSDeckInterop';
import { TabErrorController } from '../lib/controllers/TabErrorController';
import { SnapshotDictionary, SnapshotManager } from './SnapshotManager';

/**
* Converts a list of filters into a 1D array.
Expand Down Expand Up @@ -66,6 +67,8 @@ export class TabMasterManager {

private collectionRemoveReaction: IReactionDisposer | undefined;

public snapshotManager: SnapshotManager | undefined;

/**
* Creates a new TabMasterManager.
*/
Expand Down Expand Up @@ -483,8 +486,6 @@ export class TabMasterManager {
}
}



/**
* Loads the user's tabs from the backend.
*/
Expand Down Expand Up @@ -522,6 +523,14 @@ export class TabMasterManager {
}
}
});
PythonInterop.getSnapshots().then((res: SnapshotDictionary | Error) => {
if (res instanceof Error) {
LogController.log("TabMaster couldn't load tab visibilty snapshots");
LogController.error(res.message);
} else {
this.snapshotManager = new SnapshotManager(res);
}
});

if (settings instanceof Error) {
LogController.log("TabMaster couldn't load tab settings");
Expand Down

0 comments on commit 58d7431

Please sign in to comment.