feat: Global Account Controller implented with Context's

This commit is contained in:
LynithDev 2024-07-28 21:43:43 +02:00
parent 6d9f145ada
commit dab365f048
11 changed files with 275 additions and 197 deletions

View file

@ -10,6 +10,8 @@ fn main() {
"get_users",
"get_user",
"remove_user",
"get_default_user",
"set_default_user",
// Cluster
"create_cluster",
"remove_cluster",

View file

@ -6,6 +6,8 @@ permissions = [
"allow-get-users",
"allow-get-user",
"allow-remove-user",
"allow-get-default-user",
"allow-set-default-user",
# Cluster
"allow-create-cluster",

View file

@ -25,6 +25,8 @@ macro_rules! collect_commands {
auth_login,
get_users,
get_user,
get_default_user,
set_default_user,
remove_user,
// Cluster
create_cluster,
@ -212,6 +214,24 @@ pub async fn get_user(uuid: Uuid) -> Result<MinecraftCredentials, String> {
Ok(minecraft::get_user(uuid).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_default_user() -> Result<Option<MinecraftCredentials>, String> {
let uuid = minecraft::get_default_user().await?;
match uuid {
Some(uuid) => Ok(Some(minecraft::get_user(uuid).await?)),
None => Ok(None),
}
}
#[specta::specta]
#[tauri::command]
pub async fn set_default_user(uuid: Uuid) -> Result<(), String> {
minecraft::set_default_user(uuid).await?;
Ok(())
}
#[specta::specta]
#[tauri::command]
pub async fn auth_login(handle: AppHandle) -> Result<Option<MinecraftCredentials>, String> {

View file

@ -8,6 +8,7 @@ import NotificationOverlay from './components/overlay/notifications/Notification
import { SettingsProvider } from './hooks/useSettings';
import { ClusterModalController } from './components/overlay/cluster/ClusterCreationModal';
import { MultiProvider } from './components/MultiProvider';
import { AccountControllerProvider } from './components/overlay/account/AddAccountModal';
import { AppInfo } from '~utils/program-info';
function App(props: ParentProps) {
@ -50,6 +51,7 @@ function GlobalContexts(props: ParentProps) {
values={[
SettingsProvider,
ClusterModalController,
AccountControllerProvider,
]}
>
{props.children}

View file

@ -3,13 +3,13 @@ import { createSignal } from 'solid-js';
import { Bell01Icon, Cloud01Icon, Settings01Icon, TerminalBrowserIcon } from '@untitled-theme/icons-solid';
import { open } from '@tauri-apps/plugin-shell';
import { WEBSITE } from '@onelauncher/client';
import useAccount from '../hooks/useAccount';
import PolyfrostFull from './logos/PolyfrostFull';
import AccountPopup from './overlay/AccountPopup';
import AccountPopup from './overlay/account/AccountsPopup';
import PlayerHead from './game/PlayerHead';
import Button from './base/Button';
import NotificationPopup from './overlay/notifications/NotificationPopup';
import Popup from './overlay/Popup';
import useAccountController from './overlay/account/AddAccountModal';
interface NavbarLinkProps {
path: string;
@ -33,9 +33,9 @@ function NavbarLink(props: NavbarLinkProps) {
function Navbar() {
const [profileMenuOpen, setProfileMenuOpen] = createSignal(false);
const [notificationMenuOpen, setNotificationMenuOpen] = createSignal(false);
const navigate = useNavigate();
const controller = useAccountController();
const account = useAccount();
const navigate = useNavigate();
let profileButton!: HTMLButtonElement;
let notificationButton!: HTMLButtonElement;
@ -81,7 +81,7 @@ function Navbar() {
ref={profileButton}
onClick={() => setProfileMenuOpen(!profileMenuOpen())}
>
<PlayerHead class="w-[30px] h-[30px] rounded-md hover:opacity-70" uuid={account.uuid} />
<PlayerHead class="w-[30px] h-[30px] rounded-md hover:opacity-70" uuid={controller.defaultAccount()?.id} />
</button>
</div>

View file

@ -3,7 +3,7 @@ import type { JSX } from 'solid-js';
import steveSrc from '../../../assets/images/steve.png';
type PlayerHeadProps = JSX.IntrinsicElements['img'] & {
uuid: string;
uuid: string | null | undefined;
onError?: () => any;
};
@ -24,13 +24,15 @@ function PlayerHead(props: PlayerHeadProps) {
{...rest}
/>
</Show>
<img
src={headSrc(split.uuid)}
onLoad={() => setLoading(false)}
onError={() => props.onError && props.onError()}
class={`image-render-pixel ${isLoading() ? 'hidden' : ''} ${split.class}`}
{...rest}
/>
<Show when={props.uuid !== null && props.uuid !== undefined}>
<img
src={headSrc(split.uuid!)}
onLoad={() => setLoading(false)}
onError={() => props.onError && props.onError()}
class={`image-render-pixel ${isLoading() ? 'hidden' : ''} ${split.class}`}
{...rest}
/>
</Show>
</>
);
}

View file

@ -1,75 +0,0 @@
import { LogOut01Icon, PlusIcon, Settings01Icon } from '@untitled-theme/icons-solid';
import { useNavigate } from '@solidjs/router';
import Button from '../base/Button';
import PlayerHead from '../game/PlayerHead';
import useAccount from '../../hooks/useAccount';
import Popup from './Popup';
interface AccountComponentProps {
username: string;
uuid: string;
loggedIn?: boolean;
}
function AccountComponent(props: AccountComponentProps) {
return (
<div class={`flex flex-row justify-between p-2 rounded-lg ${!props.loggedIn && 'hover:bg-gray-05 active:bg-gray-10 hover:text-fg-primary-hover'}`}>
<div class="flex flex-row justify-start flex-1 gap-x-3">
<PlayerHead class="w-8 h-8 rounded-md" uuid={props.uuid} />
<div class="flex flex-col items-center justify-center">
<div class="flex flex-col items-start justify-between">
<p class="font-semibold h-[18px]">{props.username}</p>
{props.loggedIn && <p class="text-xs">Logged in</p>}
</div>
</div>
</div>
{props.loggedIn && (
<Button buttonStyle="icon">
<LogOut01Icon class=" stroke-danger" />
</Button>
)}
</div>
);
}
function AccountPopup(props: Popup.PopupProps) {
const loggedInAccount = useAccount();
const navigate = useNavigate();
return (
<Popup {...props}>
<div class="bg-secondary rounded-xl border border-gray-10 w-72 p-2 shadow-md shadow-black/30">
<div class="flex flex-col gap-y-2 text-fg-primary">
<AccountComponent username={loggedInAccount.username} uuid={loggedInAccount.uuid} loggedIn />
<div class="w-full h-px bg-gray-05 rounded-md" />
<AccountComponent username="Wyvest" uuid="a5331404-0e77-440e-8bef-24c071dac1ae" />
<div class="w-full h-px bg-gray-05 rounded-md" />
<div class="flex flex-row justify-between">
<div>
<Button buttonStyle="ghost" iconLeft={<PlusIcon />}>Add Account</Button>
</div>
<div class="flex flex-row">
<Button
buttonStyle="icon"
large
onClick={() => {
props.setVisible(false);
navigate('/settings/accounts');
}}
>
<Settings01Icon class="stroke-fg-primary" />
</Button>
</div>
</div>
</div>
</div>
</Popup>
);
}
export default AccountPopup;

View file

@ -0,0 +1,94 @@
import { For, Show } from 'solid-js';
import { LogOut01Icon, PlusIcon, Settings01Icon } from '@untitled-theme/icons-solid';
import { useNavigate } from '@solidjs/router';
import Button from '../../base/Button';
import PlayerHead from '../../game/PlayerHead';
import Popup from '../Popup';
import useAccountController from './AddAccountModal';
import type { MinecraftCredentials } from '~bindings';
interface AccountComponentProps {
account: MinecraftCredentials | null | undefined;
loggedIn?: boolean;
}
function AccountComponent(props: AccountComponentProps) {
return (
<Show when={props.account}>
<div class={`flex flex-row justify-between p-2 rounded-lg ${!props.loggedIn && 'hover:bg-gray-05 active:bg-gray-10 hover:text-fg-primary-hover'}`}>
<div class="flex flex-row justify-start flex-1 gap-x-3">
<PlayerHead class="w-8 h-8 rounded-md" uuid={props.account!.id} />
<div class="flex flex-col items-center justify-center">
<div class="flex flex-col items-start justify-between">
<p class="font-semibold h-[18px]">{props.account!.username}</p>
{props.loggedIn && <p class="text-xs">Logged in</p>}
</div>
</div>
</div>
{props.loggedIn && (
<Button buttonStyle="icon">
<LogOut01Icon class=" stroke-danger" />
</Button>
)}
</div>
</Show>
);
}
function AccountPopup(props: Popup.PopupProps) {
const controller = useAccountController();
const filteredAccounts = () => controller.accounts()?.filter(account => account.id !== controller.defaultAccount()?.id) ?? [];
const navigate = useNavigate();
return (
<Popup {...props}>
<div class="bg-secondary rounded-xl border border-gray-10 w-72 p-2 shadow-md shadow-black/30">
<div class="flex flex-col gap-y-2 text-fg-primary">
<AccountComponent account={controller.defaultAccount()} loggedIn />
<Show when={filteredAccounts().length !== 0}>
<div class="w-full h-px bg-gray-05 rounded-md" />
<For each={filteredAccounts()}>
{account => (
<AccountComponent account={account} />
)}
</For>
<div class="w-full h-px bg-gray-05 rounded-md" />
</Show>
<div class="flex flex-row justify-between">
<div>
<Button
buttonStyle="ghost"
iconLeft={<PlusIcon />}
onClick={() => {
props.setVisible(false);
controller.displayAddAccount();
}}
>
Add Account
</Button>
</div>
<div class="flex flex-row">
<Button
buttonStyle="icon"
large
onClick={() => {
props.setVisible(false);
navigate('/settings/accounts');
}}
>
<Settings01Icon class="stroke-fg-primary" />
</Button>
</div>
</div>
</div>
</div>
</Popup>
);
}
export default AccountPopup;

View file

@ -0,0 +1,127 @@
import { type Accessor, type Context, Match, type ParentProps, type Resource, type Setter, Switch, createContext, createSignal, useContext } from 'solid-js';
import { LinkExternal01Icon } from '@untitled-theme/icons-solid';
import Modal from '../Modal';
import type { MinecraftCredentials } from '~bindings';
import { bridge } from '~imports';
import useCommand, { tryResult } from '~ui/hooks/useCommand';
import Button from '~ui/components/base/Button';
interface AccountControllerContextFunc {
displayAddAccount: () => void;
refetch: () => void;
accounts: Resource<MinecraftCredentials[]>;
defaultAccount: Resource<MinecraftCredentials | null>;
setDefaultAccount: (uuid: string) => Promise<void>;
removeAccount: (uuid: string) => Promise<void>;
}
const AccountControllerContext = createContext<AccountControllerContextFunc>() as Context<AccountControllerContextFunc>;
export function AccountControllerProvider(props: ParentProps) {
const [accounts, { refetch: refetchAccounts }] = useCommand(bridge.commands.getUsers);
const [defaultAccount, { refetch: refetchDefaultAccount }] = useCommand(bridge.commands.getDefaultUser);
const [visible, setVisible] = createSignal(false);
function refetch() {
refetchAccounts();
refetchDefaultAccount();
}
async function setDefaultAccount(uuid: string) {
await tryResult(bridge.commands.setDefaultUser, uuid);
}
async function removeAccount(uuid: string) {
await tryResult(bridge.commands.removeUser, uuid);
refetch();
}
const func: AccountControllerContextFunc = {
displayAddAccount: () => setVisible(true),
refetch,
accounts,
defaultAccount,
setDefaultAccount,
removeAccount,
};
return (
<AccountControllerContext.Provider value={func}>
{props.children}
<AddAccountModal visible={visible} setVisible={setVisible} refetch={refetch} />
</AccountControllerContext.Provider>
);
}
export function useAccountController() {
return useContext(AccountControllerContext);
}
export default useAccountController;
enum ModalStage {
Tasks,
WaitingForCode,
LoggingIn,
}
interface AddAccountModalProps {
visible: Accessor<boolean>;
setVisible: Setter<boolean>;
refetch: () => any;
}
function AddAccountModal(props: AddAccountModalProps) {
const [stage, setStage] = createSignal(ModalStage.Tasks);
function start() {
setStage(ModalStage.WaitingForCode);
// tryResult(bridge.commands.beginMsa).then((res) => {
// console.log(res);
// });
}
// function finish() {
// props.setVisible(false);
// props.refetch();
// }
return (
<Modal.Simple
title="Add Account"
visible={props.visible}
setVisible={props.setVisible}
buttons={[
<Button
buttonStyle="secondary"
children="Cancel"
onClick={() => props.setVisible(false)}
/>,
<Button
buttonStyle="primary"
children="Add"
iconLeft={<LinkExternal01Icon />}
onClick={start}
disabled={stage() !== 0}
/>,
]}
>
<div class="flex flex-col gap-y-3 max-w-120 line-height-normal">
<Switch>
<Match when={stage() !== ModalStage.LoggingIn}>
<p>
Pressing the "Add" button will open your browser with a Microsoft login page.
On this page, you login to your chosen Microsoft account and end up being asked whether you want to add the OneLauncher application.
</p>
</Match>
<Match when={stage() === ModalStage.LoggingIn}>
<p>Proceeding with Microsoft auth steps...</p>
</Match>
</Switch>
</div>
</Modal.Simple>
);
}

View file

@ -1,11 +0,0 @@
function useAccount() {
// TODO return the currently logged in account
return {
username: 'Lynith',
uuid: 'f247be7c-5b82-41c6-9148-793ded77e71f',
skins: [],
};
}
export default useAccount;

View file

@ -1,36 +1,25 @@
import { InfoCircleIcon, LinkExternal01Icon, Trash01Icon, UserPlus02Icon } from '@untitled-theme/icons-solid';
import { type Accessor, For, Match, type Setter, Show, Switch, createEffect, createSignal, mergeProps } from 'solid-js';
import { bridge } from '~imports';
import { InfoCircleIcon, Trash01Icon, UserPlus02Icon } from '@untitled-theme/icons-solid';
import { For, Match, Show, Switch, createSignal, mergeProps } from 'solid-js';
import Button from '~ui/components/base/Button';
import Tooltip from '~ui/components/base/Tooltip';
import PlayerHead from '~ui/components/game/PlayerHead';
import Modal from '~ui/components/overlay/Modal';
import useAccountController from '~ui/components/overlay/account/AddAccountModal';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import useCommand from '~ui/hooks/useCommand';
function SettingsAccounts() {
const [current, setCurrent] = createSignal<string>();
const [accounts, { refetch }] = useCommand(bridge.commands.getUsers);
const [modalVisible, setModalVisible] = createSignal(false);
const controller = useAccountController();
createEffect(() => {
const users = accounts();
if (users === undefined || users.length < 1 || current() !== undefined)
return;
setCurrent(users[0]!.id);
});
function setCurrent(uuid: string) {
controller.setDefaultAccount(uuid);
}
function onDelete(uuid: string) {
bridge.commands.removeUser(uuid)
.finally(() => {
refetch();
});
controller.removeAccount(uuid);
}
function showModal() {
setModalVisible(true);
controller.displayAddAccount();
}
return (
@ -39,19 +28,19 @@ function SettingsAccounts() {
<ScrollableContainer>
<Switch>
<Match when={accounts()?.length === 0}>
<Match when={controller.accounts()?.length === 0}>
<div class="flex flex-col h-full gap-y-4 max-h-64 justify-center items-center">
<span class="text-lg font-bold uppercase text-fg-secondary">No accounts added.</span>
<span class="text-xl font-bold">Add one with the Add Account button.</span>
</div>
</Match>
<Match when={accounts()?.length !== 0}>
<For each={accounts()}>
<Match when={controller.accounts()?.length !== 0}>
<For each={controller.accounts()}>
{account => (
<AccountRow
username={account.username}
uuid={account.id}
current={current() === account.id}
current={controller.defaultAccount()?.id === account.id}
onClick={setCurrent}
onDelete={onDelete}
/>
@ -69,84 +58,10 @@ function SettingsAccounts() {
onClick={showModal}
/>
</div>
<AddAccountModal
setVisible={setModalVisible}
visible={modalVisible}
refetch={refetch}
/>
</Sidebar.Page>
);
}
enum ModalStage {
Tasks,
WaitingForCode,
LoggingIn,
}
interface AddAccountModalProps {
visible: Accessor<boolean>;
setVisible: Setter<boolean>;
refetch: () => any;
}
function AddAccountModal(props: AddAccountModalProps) {
const [stage, setStage] = createSignal(ModalStage.Tasks);
function start() {
setStage(ModalStage.WaitingForCode);
// tryResult(bridge.commands.beginMsa).then((res) => {
// console.log(res);
// });
}
function finish() {
props.setVisible(false);
props.refetch();
}
return (
<Modal.Simple
title="Add Account"
visible={props.visible}
setVisible={props.setVisible}
buttons={[
<Button
buttonStyle="secondary"
children="Cancel"
onClick={() => props.setVisible(false)}
/>,
<Button
buttonStyle="primary"
children="Add"
iconLeft={<LinkExternal01Icon />}
onClick={start}
disabled={stage() !== 0}
/>,
]}
>
<div class="flex flex-col gap-y-3 max-w-120 line-height-normal">
<Switch>
<Match when={stage() !== ModalStage.LoggingIn}>
<p>
Pressing the "Add" button will open your browser with a Microsoft login page.
On this page, you login to your chosen Microsoft account and end up being asked whether you want to add the OneLauncher application.
</p>
</Match>
<Match when={stage() === ModalStage.LoggingIn}>
<p>Proceeding with Microsoft auth steps...</p>
</Match>
</Switch>
</div>
</Modal.Simple>
);
}
interface AccountRowProps {
username: string;
uuid: string;