mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Focus-mode for mobile
This commit is contained in:
@@ -30,7 +30,7 @@ import { createErrorContentFragment, createTextContentFragment, DMessageAttachme
|
||||
import { gcChatImageAssets } from '~/common/stores/chat/chat.gc';
|
||||
import { getChatLLMId } from '~/common/stores/llms/store-llms';
|
||||
import { getConversation, getConversationSystemPurposeId, useConversation } from '~/common/stores/chat/store-chats';
|
||||
import { optimaActions, optimaOpenModels, optimaOpenPreferences } from '~/common/layout/optima/useOptima';
|
||||
import { optimaActions, optimaOpenModels, optimaOpenPreferences, useOptimaChromeless } from '~/common/layout/optima/useOptima';
|
||||
import { useFolderStore } from '~/common/stores/folders/store-chat-folders';
|
||||
import { useIsMobile, useIsTallScreen } from '~/common/components/useMatchMedia';
|
||||
import { useLLM } from '~/common/stores/llms/llms.hooks';
|
||||
@@ -209,7 +209,8 @@ export function AppChat() {
|
||||
});
|
||||
|
||||
// Composer Auto-hiding
|
||||
const forceComposerHide = !!beamOpenStoreInFocusedPane /* || !focusedPaneConversationId */; // auto-hide when no chat (the 'please select a conversation...' state) doesn't feel good
|
||||
const isChromeless = useOptimaChromeless() && isMobile; // auto-hide on Chromeless too
|
||||
const forceComposerHide = isChromeless || !!beamOpenStoreInFocusedPane /* || !focusedPaneConversationId */; // auto-hide when no chat (the 'please select a conversation...' state) doesn't feel good
|
||||
const composerAutoHide = useComposerAutoHide(forceComposerHide, composerHasContent);
|
||||
|
||||
// Window actions
|
||||
@@ -492,6 +493,7 @@ export function AppChat() {
|
||||
|
||||
const focusedChatPanelContent = React.useMemo(() => !focusedPaneConversationId ? null :
|
||||
<ChatPane
|
||||
isMobile={isMobile}
|
||||
conversationId={focusedPaneConversationId}
|
||||
disableItems={!focusedPaneConversationId || isFocusedChatEmpty}
|
||||
hasConversations={hasConversations}
|
||||
@@ -768,7 +770,7 @@ export function AppChat() {
|
||||
</Box>
|
||||
|
||||
{/* Hover zone for auto-hide */}
|
||||
{!forceComposerHide && composerAutoHide.isHidden && <Box {...composerAutoHide.detectorProps} />}
|
||||
{!isChromeless && !forceComposerHide && composerAutoHide.isHidden && <Box {...composerAutoHide.detectorProps} />}
|
||||
|
||||
{/* Diagrams */}
|
||||
{!!diagramConfig && (
|
||||
|
||||
@@ -13,6 +13,7 @@ import SettingsSuggestOutlinedIcon from '@mui/icons-material/SettingsSuggestOutl
|
||||
import UnarchiveOutlinedIcon from '@mui/icons-material/UnarchiveOutlined';
|
||||
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { ChromelessItemButton } from '~/common/layout/optima/ChromelessItemButton';
|
||||
import { CodiconSplitHorizontal } from '~/common/components/icons/CodiconSplitHorizontal';
|
||||
import { CodiconSplitHorizontalRemove } from '~/common/components/icons/CodiconSplitHorizontalRemove';
|
||||
import { CodiconSplitVertical } from '~/common/components/icons/CodiconSplitVertical';
|
||||
@@ -37,6 +38,7 @@ function VariformPaneFrame() {
|
||||
|
||||
|
||||
export function ChatPane(props: {
|
||||
isMobile: boolean,
|
||||
conversationId: DConversationId | null,
|
||||
disableItems: boolean,
|
||||
hasConversations: boolean,
|
||||
@@ -143,6 +145,8 @@ export function ChatPane(props: {
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
{props.isMobile && <ChromelessItemButton />}
|
||||
|
||||
</OptimaPanelGroupedList>
|
||||
|
||||
{/* Chat Actions group */}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { IconButton } from '@mui/joy';
|
||||
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
|
||||
import { LayoutSidebarRight } from '~/common/components/icons/LayoutSidebarRight';
|
||||
|
||||
import { optimaExitChromeless, optimaOpenDrawer, optimaOpenPanel } from './useOptima';
|
||||
|
||||
|
||||
const buttonSx: SxProps = {
|
||||
position: 'fixed',
|
||||
top: '0.5rem',
|
||||
zIndex: 25,
|
||||
// backdropFilter: 'blur(8px)',
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'md',
|
||||
borderRadius: '50%',
|
||||
} as const;
|
||||
|
||||
|
||||
export function ChromelessFloatingButtons() {
|
||||
return <>
|
||||
|
||||
{/* Left — where the drawer toggle usually is */}
|
||||
<IconButton aria-label='Open Drawer' variant='soft' onClick={optimaOpenDrawer} style={{ left: '0.5rem' }} sx={buttonSx}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* Center — exit chromeless (styled like the scroll-to-bottom button) */}
|
||||
<IconButton aria-label='Exit Chrome-less' variant='soft' onClick={optimaExitChromeless} sx={buttonSx} style={{ left: '50%', transform: 'translateX(-50%)' }}>
|
||||
<FullscreenExitIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* Right — where the panel toggle usually is */}
|
||||
<IconButton aria-label='Open Menu' variant='soft' onClick={optimaOpenPanel} style={{ right: '0.5rem' }} sx={buttonSx}>
|
||||
<LayoutSidebarRight />
|
||||
</IconButton>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ListItemButton, ListItemDecorator } from '@mui/joy';
|
||||
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
|
||||
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
||||
|
||||
import { optimaToggleChromeless, useOptimaChromeless } from './useOptima';
|
||||
|
||||
|
||||
export function ChromelessItemButton() {
|
||||
const isChromeless = useOptimaChromeless();
|
||||
return (
|
||||
<ListItemButton onClick={optimaToggleChromeless}>
|
||||
<ListItemDecorator>{isChromeless ? <FullscreenExitIcon /> : <FullscreenIcon />}</ListItemDecorator>
|
||||
{isChromeless ? 'Exit Focus-mode' : 'Focus mode'}
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,13 @@ import { Box } from '@mui/joy';
|
||||
|
||||
import { themeBgApp, themeZIndexPageBar } from '~/common/app.theme';
|
||||
import type { NavItemApp } from '~/common/app.nav';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
|
||||
// import { MobileNav } from './MobileNav';
|
||||
import { OptimaBar } from '~/common/layout/optima/bar/OptimaBar';
|
||||
import { optimaHasMOTD, OptimaMOTD } from '~/common/layout/optima/OptimaMOTD';
|
||||
import { ChromelessFloatingButtons } from './ChromelessFloatingButtons';
|
||||
import { useOptimaChromeless } from './useOptima';
|
||||
|
||||
|
||||
const pageCoreSx: SxProps = {
|
||||
@@ -44,8 +47,12 @@ export const PageCore = (props: {
|
||||
isFull: boolean,
|
||||
isMobile: boolean,
|
||||
children: React.ReactNode,
|
||||
}) =>
|
||||
<Box
|
||||
}) => {
|
||||
|
||||
// external state
|
||||
const isChromeless = useOptimaChromeless();
|
||||
|
||||
return <Box
|
||||
component={props.component}
|
||||
sx={props.currentApp?.pageBrighter ? pageCoreBrighterSx : props.isFull ? pageCoreFullSx : pageCoreSx}
|
||||
>
|
||||
@@ -53,13 +60,17 @@ export const PageCore = (props: {
|
||||
{/* Optional deployment MOTD */}
|
||||
{optimaHasMOTD && <OptimaMOTD />}
|
||||
|
||||
{/* Responsive page bar (pluggable App Center Items and App Menu) */}
|
||||
<OptimaBar
|
||||
component='header'
|
||||
currentApp={props.currentApp}
|
||||
isMobile={props.isMobile}
|
||||
sx={pageCoreBarSx}
|
||||
/>
|
||||
{/* Responsive page bar (pluggable App Center Items and App Menu) - collapsible for chromeless mode */}
|
||||
<ExpanderControlledBox expanded={!isChromeless}>
|
||||
<OptimaBar
|
||||
component='header'
|
||||
currentApp={props.currentApp}
|
||||
isMobile={props.isMobile}
|
||||
sx={pageCoreBarSx}
|
||||
/>
|
||||
</ExpanderControlledBox>
|
||||
{/* Chromeless alternative to the OptimaBar */}
|
||||
{isChromeless && <ChromelessFloatingButtons />}
|
||||
|
||||
{/* Page (NextJS) must make the assumption they're in a flex-col layout */}
|
||||
{props.children}
|
||||
@@ -74,4 +85,5 @@ export const PageCore = (props: {
|
||||
{/* />*/}
|
||||
{/*)}*/}
|
||||
|
||||
</Box>;
|
||||
</Box>;
|
||||
};
|
||||
@@ -16,7 +16,7 @@ export type ModelOptionsContext = 'full' | 'parameters';
|
||||
interface OptimaState {
|
||||
|
||||
// modes
|
||||
// isFocusedMode: boolean; // when active, the Mobile App menu is not displayed
|
||||
isChromeless: boolean; // when active, the top bar and composer are hidden, with floating buttons
|
||||
|
||||
// panes
|
||||
drawerIsOpen: boolean;
|
||||
@@ -62,7 +62,7 @@ const modalsClosedState = {
|
||||
const initialState: OptimaState = {
|
||||
|
||||
// modes
|
||||
// isFocusedMode: false,
|
||||
isChromeless: false,
|
||||
|
||||
// panes
|
||||
drawerIsOpen: initialDrawerOpen(),
|
||||
@@ -81,7 +81,7 @@ const initialState: OptimaState = {
|
||||
|
||||
export interface OptimaActions {
|
||||
|
||||
// setIsFocusedMode: (isFocusedMode: boolean) => void;
|
||||
setChromeless: (isChromeless: boolean) => void;
|
||||
|
||||
closeDrawer: () => void;
|
||||
openDrawer: () => void;
|
||||
@@ -168,7 +168,7 @@ export const useLayoutOptimaStore = create<OptimaState & OptimaActions>((_set, _
|
||||
|
||||
...initialState,
|
||||
|
||||
// setIsFocusedMode: (isFocusedMode) => _set({ isFocusedMode }),
|
||||
setChromeless: (isChromeless) => _set({ isChromeless }),
|
||||
|
||||
closeDrawer: () => {
|
||||
// prevent accidental immediate close (e.g. double-click, animation protection)
|
||||
|
||||
@@ -6,6 +6,22 @@ import { NavItemApp } from '~/common/app.nav';
|
||||
import { useOptimaPortalHasInputs } from '~/common/layout/optima/portals/useOptimaPortalHasInputs';
|
||||
|
||||
|
||||
// Chromeless Mode
|
||||
|
||||
export function optimaToggleChromeless() {
|
||||
const state = useLayoutOptimaStore.getState();
|
||||
state.setChromeless(!state.isChromeless);
|
||||
}
|
||||
|
||||
export function optimaExitChromeless() {
|
||||
useLayoutOptimaStore.getState().setChromeless(false);
|
||||
}
|
||||
|
||||
export function useOptimaChromeless() {
|
||||
return useLayoutOptimaStore(({ isChromeless }) => isChromeless);
|
||||
}
|
||||
|
||||
|
||||
// Drawer
|
||||
|
||||
export function optimaCloseDrawer() {
|
||||
|
||||
Reference in New Issue
Block a user