enhancement: fit mobile layout

This commit is contained in:
Faisal Amir 2025-09-22 15:06:11 +07:00
parent 003598204e
commit f639ec70d4
13 changed files with 382 additions and 298 deletions

View File

@ -1,11 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en" class="bg-app">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/jan-logo.png" /> <link
<link rel="icon" type="image/png" sizes="16x16" href="/images/jan-logo.png" /> rel="icon"
type="image/png"
sizes="32x32"
href="/images/jan-logo.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/images/jan-logo.png"
/>
<link rel="apple-touch-icon" href="/images/jan-logo.png" /> <link rel="apple-touch-icon" href="/images/jan-logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-visual"
/>
<title>Jan</title> <title>Jan</title>
</head> </head>
<body> <body>

View File

@ -122,7 +122,7 @@ const LeftPanel = () => {
) { ) {
if (currentIsSmallScreen && open) { if (currentIsSmallScreen && open) {
setLeftPanel(false) setLeftPanel(false)
} else if(!open) { } else if (!open) {
setLeftPanel(true) setLeftPanel(true)
} }
prevScreenSizeRef.current = currentIsSmallScreen prevScreenSizeRef.current = currentIsSmallScreen
@ -141,7 +141,7 @@ const LeftPanel = () => {
return () => { return () => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
}, [setLeftPanel]) }, [open, setLeftPanel])
const currentPath = useRouterState({ const currentPath = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
@ -184,7 +184,7 @@ const LeftPanel = () => {
return ( return (
<> <>
{/* Backdrop overlay for small screens */} {/* Backdrop overlay for small screens */}
{isSmallScreen && open && ( {isSmallScreen && open && !IS_IOS && !IS_ANDROID && (
<div <div
className="fixed inset-0 bg-black/50 backdrop-blur z-30" className="fixed inset-0 bg-black/50 backdrop-blur z-30"
onClick={(e) => { onClick={(e) => {
@ -207,7 +207,7 @@ const LeftPanel = () => {
isResizableContext && 'h-full w-full', isResizableContext && 'h-full w-full',
// Small screen context: fixed positioning and styling // Small screen context: fixed positioning and styling
isSmallScreen && isSmallScreen &&
'fixed h-[calc(100%-16px)] bg-app z-50 rounded-sm border border-left-panel-fg/10 m-2 px-1 w-48', 'fixed h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))] bg-main-view z-50 md:border border-left-panel-fg/10 px-1 w-full md:w-48',
// Default context: original styling // Default context: original styling
!isResizableContext && !isResizableContext &&
!isSmallScreen && !isSmallScreen &&
@ -266,7 +266,8 @@ const LeftPanel = () => {
<div <div
className={cn( className={cn(
'flex flex-col', 'flex flex-col',
Object.keys(downloads).length > 0 || localDownloadingModels.size > 0 Object.keys(downloads).length > 0 ||
localDownloadingModels.size > 0
? 'h-[calc(100%-200px)]' ? 'h-[calc(100%-200px)]'
: 'h-[calc(100%-140px)]' : 'h-[calc(100%-140px)]'
)} )}
@ -379,7 +380,9 @@ const LeftPanel = () => {
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end"> <DropdownMenuContent side="bottom" align="end">
<DeleteAllThreadsDialog onDeleteAll={deleteAllThreads} /> <DeleteAllThreadsDialog
onDeleteAll={deleteAllThreads}
/>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@ -32,7 +32,10 @@ const SettingsMenu = () => {
if (!provider.active) return false if (!provider.active) return false
// On web version, hide llamacpp provider // On web version, hide llamacpp provider
if (!PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && provider.provider === 'llama.cpp') { if (
!PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] &&
provider.provider === 'llama.cpp'
) {
return false return false
} }
@ -92,7 +95,7 @@ const SettingsMenu = () => {
title: 'common:keyboardShortcuts', title: 'common:keyboardShortcuts',
route: route.settings.shortcuts, route: route.settings.shortcuts,
hasSubMenu: false, hasSubMenu: false,
isEnabled: true, isEnabled: PlatformFeatures[PlatformFeature.SHORTCUT],
}, },
{ {
title: 'common:hardware', title: 'common:hardware',
@ -137,7 +140,7 @@ const SettingsMenu = () => {
return ( return (
<> <>
<button <button
className="fixed top-4 right-4 sm:hidden size-5 cursor-pointer items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10 z-20" className="fixed top-[calc(10px+env(safe-area-inset-top))] right-4 sm:hidden size-5 cursor-pointer items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10 z-20"
onClick={toggleMenu} onClick={toggleMenu}
aria-label="Toggle settings menu" aria-label="Toggle settings menu"
> >
@ -152,7 +155,7 @@ const SettingsMenu = () => {
'h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5 bg-main-view', 'h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5 bg-main-view',
'sm:flex', 'sm:flex',
isMenuOpen isMenuOpen
? 'flex fixed sm:hidden top-0 z-10 m-1 h-[calc(100%-8px)] border-r-0 border-l bg-main-view right-0 py-8 rounded-tr-lg rounded-br-lg' ? 'flex fixed sm:hidden top-[calc(10px+env(safe-area-inset-top))] z-10 m-1 h-[calc(100%-8px)] border-r-0 border-l bg-main-view right-0 py-8 rounded-tr-lg rounded-br-lg'
: 'hidden' : 'hidden'
)} )}
> >
@ -162,77 +165,82 @@ const SettingsMenu = () => {
return null return null
} }
return ( return (
<div key={menu.title}> <div key={menu.title}>
<Link <Link
to={menu.route} to={menu.route}
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5" className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-main-view-fg/80">{t(menu.title)}</span> <span className="text-main-view-fg/80">
{menu.hasSubMenu && ( {t(menu.title)}
<button </span>
onClick={(e) => { {menu.hasSubMenu && (
e.preventDefault() <button
e.stopPropagation() onClick={(e) => {
toggleProvidersExpansion() e.preventDefault()
}} e.stopPropagation()
className="text-main-view-fg/60 hover:text-main-view-fg/80" toggleProvidersExpansion()
> }}
{expandedProviders ? ( className="text-main-view-fg/60 hover:text-main-view-fg/80"
<IconChevronDown size={16} /> >
) : ( {expandedProviders ? (
<IconChevronRight size={16} /> <IconChevronDown size={16} />
)} ) : (
</button> <IconChevronRight size={16} />
)} )}
</div> </button>
</Link> )}
</div>
</Link>
{/* Sub-menu for model providers */} {/* Sub-menu for model providers */}
{menu.hasSubMenu && expandedProviders && ( {menu.hasSubMenu && expandedProviders && (
<div className="ml-2 mt-1 space-y-1 first-step-setup-remote-provider"> <div className="ml-2 mt-1 space-y-1 first-step-setup-remote-provider">
{activeProviders.map((provider) => { {activeProviders.map((provider) => {
const isActive = matches.some( const isActive = matches.some(
(match) => (match) =>
match.routeId === '/settings/providers/$providerName' && match.routeId ===
'providerName' in match.params && '/settings/providers/$providerName' &&
match.params.providerName === provider.provider 'providerName' in match.params &&
) match.params.providerName === provider.provider
)
return ( return (
<div key={provider.provider}> <div key={provider.provider}>
<div <div
className={cn( className={cn(
'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80', 'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80',
isActive && 'bg-main-view-fg/5', isActive && 'bg-main-view-fg/5',
// hidden for llama.cpp provider for setup remote provider // hidden for llama.cpp provider for setup remote provider
provider.provider === 'llama.cpp' && provider.provider === 'llama.cpp' &&
stepSetupRemoteProvider && stepSetupRemoteProvider &&
'hidden' 'hidden'
)} )}
onClick={() => onClick={() =>
navigate({ navigate({
to: route.settings.providers, to: route.settings.providers,
params: { params: {
providerName: provider.provider, providerName: provider.provider,
}, },
...(stepSetupRemoteProvider ...(stepSetupRemoteProvider
? { search: { step: 'setup_remote_provider' } } ? {
: {}), search: { step: 'setup_remote_provider' },
}) }
} : {}),
> })
<ProvidersAvatar provider={provider} /> }
<div className="truncate"> >
<span>{getProviderTitle(provider.provider)}</span> <ProvidersAvatar provider={provider} />
<div className="truncate">
<span>{getProviderTitle(provider.provider)}</span>
</div>
</div> </div>
</div> </div>
</div> )
) })}
})} </div>
</div> )}
)} </div>
</div>
) )
})} })}
</div> </div>

View File

@ -6,6 +6,8 @@ import HeaderPage from './HeaderPage'
import { isProd } from '@/lib/version' import { isProd } from '@/lib/version'
import { useTranslation } from '@/i18n/react-i18next-compat' import { useTranslation } from '@/i18n/react-i18next-compat'
import { localStorageKey } from '@/constants/localStorage' import { localStorageKey } from '@/constants/localStorage'
import { PlatformFeatures } from '@/lib/platform/const'
import { PlatformFeature } from '@/lib/platform'
function SetupScreen() { function SetupScreen() {
const { t } = useTranslation() const { t } = useTranslation()
@ -21,7 +23,7 @@ function SetupScreen() {
<div className="flex h-full flex-col flex-justify-center"> <div className="flex h-full flex-col flex-justify-center">
<HeaderPage></HeaderPage> <HeaderPage></HeaderPage>
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center "> <div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center ">
<div className="w-4/6 mx-auto"> <div className="w-full lg:w-4/6 mx-auto">
<div className="mb-8 text-left"> <div className="mb-8 text-left">
<h1 className="font-editorialnew text-main-view-fg text-4xl"> <h1 className="font-editorialnew text-main-view-fg text-4xl">
{t('setup:welcome')} {t('setup:welcome')}
@ -31,22 +33,24 @@ function SetupScreen() {
</p> </p>
</div> </div>
<div className="flex gap-4 flex-col"> <div className="flex gap-4 flex-col">
<Card {PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && (
header={ <Card
<Link header={
to={route.hub.index} <Link
search={{ to={route.hub.index}
...(!isProd ? { step: 'setup_local_provider' } : {}), search={{
}} ...(!isProd ? { step: 'setup_local_provider' } : {}),
> }}
<div> >
<h1 className="text-main-view-fg font-medium text-base"> <div>
{t('setup:localModel')} <h1 className="text-main-view-fg font-medium text-base">
</h1> {t('setup:localModel')}
</div> </h1>
</Link> </div>
} </Link>
></Card> }
/>
)}
<Card <Card
header={ header={
<Link <Link
@ -65,7 +69,7 @@ function SetupScreen() {
</h1> </h1>
</Link> </Link>
} }
></Card> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -21,7 +21,7 @@ export function PromptAnalytic() {
} }
return ( return (
<div className="fixed bottom-4 right-4 z-50 p-4 shadow-lg bg-main-view w-100 border border-main-view-fg/8 rounded-lg"> <div className="fixed bottom-4 right-4 z-50 p-4 shadow-lg bg-main-view w-4/5 md:w-100 border border-main-view-fg/8 rounded-lg">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<IconFileTextShield className="text-accent" /> <IconFileTextShield className="text-accent" />
<h2 className="font-medium text-main-view-fg/80"> <h2 className="font-medium text-main-view-fg/80">
@ -45,7 +45,9 @@ export function PromptAnalytic() {
> >
{t('deny')} {t('deny')}
</Button> </Button>
<Button onClick={() => handleProductAnalytics(true)}>{t('allow')}</Button> <Button onClick={() => handleProductAnalytics(true)}>
{t('allow')}
</Button>
</div> </div>
</div> </div>
) )

View File

@ -56,6 +56,13 @@
@layer base { @layer base {
body { body {
@apply overflow-hidden; @apply overflow-hidden;
background-color: white;
min-height: 100vh;
min-height: -webkit-fill-available;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;
height: 6px; height: 6px;

View File

@ -4,7 +4,7 @@
*/ */
import { PlatformFeature } from './types' import { PlatformFeature } from './types'
import { isPlatformTauri } from './utils' import { isPlatformTauri, isPlatformIOS, isPlatformAndroid } from './utils'
/** /**
* Platform Features Configuration * Platform Features Configuration
@ -12,28 +12,35 @@ import { isPlatformTauri } from './utils'
*/ */
export const PlatformFeatures: Record<PlatformFeature, boolean> = { export const PlatformFeatures: Record<PlatformFeature, boolean> = {
// Hardware monitoring and GPU usage // Hardware monitoring and GPU usage
[PlatformFeature.HARDWARE_MONITORING]: isPlatformTauri(), [PlatformFeature.HARDWARE_MONITORING]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Local model inference (llama.cpp) // Local model inference (llama.cpp)
[PlatformFeature.LOCAL_INFERENCE]: isPlatformTauri(), [PlatformFeature.LOCAL_INFERENCE]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Local API server // Local API server
[PlatformFeature.LOCAL_API_SERVER]: isPlatformTauri(), [PlatformFeature.LOCAL_API_SERVER]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Hub/model downloads // Hub/model downloads
[PlatformFeature.MODEL_HUB]: isPlatformTauri(), [PlatformFeature.MODEL_HUB]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// System integrations (logs, file explorer, etc.) // System integrations (logs, file explorer, etc.)
[PlatformFeature.SYSTEM_INTEGRATIONS]: isPlatformTauri(), [PlatformFeature.SYSTEM_INTEGRATIONS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// HTTPS proxy // HTTPS proxy
[PlatformFeature.HTTPS_PROXY]: isPlatformTauri(), [PlatformFeature.HTTPS_PROXY]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Default model providers (OpenAI, Anthropic, etc.) - disabled for web-only Jan builds // Default model providers (OpenAI, Anthropic, etc.) - disabled for web-only Jan builds
[PlatformFeature.DEFAULT_PROVIDERS]: isPlatformTauri(), [PlatformFeature.DEFAULT_PROVIDERS]: isPlatformTauri(),
// Analytics and telemetry - disabled for web // Analytics and telemetry - disabled for web
[PlatformFeature.ANALYTICS]: isPlatformTauri(), [PlatformFeature.ANALYTICS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Web-specific automatic model selection from jan provider - enabled for web only // Web-specific automatic model selection from jan provider - enabled for web only
[PlatformFeature.WEB_AUTO_MODEL_SELECTION]: !isPlatformTauri(), [PlatformFeature.WEB_AUTO_MODEL_SELECTION]: !isPlatformTauri(),
@ -45,10 +52,12 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
[PlatformFeature.MCP_AUTO_APPROVE_TOOLS]: !isPlatformTauri(), [PlatformFeature.MCP_AUTO_APPROVE_TOOLS]: !isPlatformTauri(),
// MCP servers settings page - disabled for web // MCP servers settings page - disabled for web
[PlatformFeature.MCP_SERVERS_SETTINGS]: isPlatformTauri(), [PlatformFeature.MCP_SERVERS_SETTINGS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Extensions settings page - disabled for web // Extensions settings page - disabled for web
[PlatformFeature.EXTENSIONS_SETTINGS]: isPlatformTauri(), [PlatformFeature.EXTENSIONS_SETTINGS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Assistant functionality - disabled for web // Assistant functionality - disabled for web
[PlatformFeature.ASSISTANTS]: isPlatformTauri(), [PlatformFeature.ASSISTANTS]: isPlatformTauri(),
@ -60,5 +69,9 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
[PlatformFeature.GOOGLE_ANALYTICS]: !isPlatformTauri(), [PlatformFeature.GOOGLE_ANALYTICS]: !isPlatformTauri(),
// Alternate shortcut bindings - enabled for web only (to avoid browser conflicts) // Alternate shortcut bindings - enabled for web only (to avoid browser conflicts)
[PlatformFeature.ALTERNATE_SHORTCUT_BINDINGS]: !isPlatformTauri(), [PlatformFeature.ALTERNATE_SHORTCUT_BINDINGS]:
!isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Shortcut
[PlatformFeature.SHORTCUT]: !isPlatformIOS() && !isPlatformAndroid(),
} }

View File

@ -6,7 +6,7 @@
/** /**
* Supported platforms * Supported platforms
*/ */
export type Platform = 'tauri' | 'web' export type Platform = 'tauri' | 'web' | 'ios' | 'android'
/** /**
* Platform Feature Enum * Platform Feature Enum
@ -16,6 +16,8 @@ export enum PlatformFeature {
// Hardware monitoring and GPU usage // Hardware monitoring and GPU usage
HARDWARE_MONITORING = 'hardwareMonitoring', HARDWARE_MONITORING = 'hardwareMonitoring',
SHORTCUT = 'shortcut',
// Local model inference (llama.cpp) // Local model inference (llama.cpp)
LOCAL_INFERENCE = 'localInference', LOCAL_INFERENCE = 'localInference',

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Platform, PlatformFeature } from './types' import { Platform, PlatformFeature } from './types'
declare const IS_WEB_APP: boolean declare const IS_WEB_APP: boolean
@ -12,7 +13,25 @@ export const isPlatformTauri = (): boolean => {
return true return true
} }
export const isPlatformIOS = (): boolean => {
if (typeof navigator === 'undefined') return false
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
)
}
export const isPlatformAndroid = (): boolean => {
if (typeof navigator === 'undefined') return false
return /Android/.test(navigator.userAgent)
}
export const isIOS = (): boolean => isPlatformIOS()
export const isAndroid = (): boolean => isPlatformAndroid()
export const getCurrentPlatform = (): Platform => { export const getCurrentPlatform = (): Platform => {
if (isPlatformIOS()) return 'ios'
if (isPlatformAndroid()) return 'android'
return isPlatformTauri() ? 'tauri' : 'web' return isPlatformTauri() ? 'tauri' : 'web'
} }

View File

@ -111,13 +111,17 @@ const AppLayout = () => {
return ( return (
<Fragment> <Fragment>
<AnalyticProvider /> <AnalyticProvider />
{PlatformFeatures[PlatformFeature.GOOGLE_ANALYTICS] && <GoogleAnalyticsProvider />} {PlatformFeatures[PlatformFeature.GOOGLE_ANALYTICS] && (
<GoogleAnalyticsProvider />
)}
<KeyboardShortcutsProvider /> <KeyboardShortcutsProvider />
<main className="relative h-svh text-sm antialiased select-none bg-app"> <main className="relative h-svh text-sm antialiased select-none bg-app">
{/* Fake absolute panel top to enable window drag */} {/* Fake absolute panel top to enable window drag */}
<div className="absolute w-full h-10 z-10" data-tauri-drag-region /> <div className="absolute w-full h-10 z-10" data-tauri-drag-region />
<DialogAppUpdater /> <DialogAppUpdater />
{PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && <BackendUpdater />} {PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && (
<BackendUpdater />
)}
{/* Use ResizablePanelGroup only on larger screens */} {/* Use ResizablePanelGroup only on larger screens */}
{!isSmallScreen && isLeftPanelOpen ? ( {!isSmallScreen && isLeftPanelOpen ? (
@ -158,11 +162,11 @@ const AppLayout = () => {
{/* Main content panel */} {/* Main content panel */}
<div <div
className={cn( className={cn(
'h-full flex w-full p-1 ', 'h-svh flex w-full md:p-1',
isLeftPanelOpen && 'w-full md:w-[calc(100%-198px)]' isLeftPanelOpen && 'w-full md:w-[calc(100%-198px)]'
)} )}
> >
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden"> <div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full md:rounded-lg overflow-hidden">
<Outlet /> <Outlet />
</div> </div>
</div> </div>

View File

@ -31,7 +31,7 @@ function Appareances() {
const { resetCodeBlockStyle } = useCodeblock() const { resetCodeBlockStyle } = useCodeblock()
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))]">
<HeaderPage> <HeaderPage>
<h1 className="font-medium">{t('common:settings')}</h1> <h1 className="font-medium">{t('common:settings')}</h1>
</HeaderPage> </HeaderPage>

View File

@ -161,7 +161,7 @@ function General() {
}, [t, checkForUpdate]) }, [t, checkForUpdate])
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))]">
<HeaderPage> <HeaderPage>
<h1 className="font-medium">{t('common:settings')}</h1> <h1 className="font-medium">{t('common:settings')}</h1>
</HeaderPage> </HeaderPage>
@ -181,28 +181,29 @@ function General() {
} }
/> />
)} )}
{!AUTO_UPDATER_DISABLED && PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && ( {!AUTO_UPDATER_DISABLED &&
<CardItem PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
title={t('settings:general.checkForUpdates')} <CardItem
description={t('settings:general.checkForUpdatesDesc')} title={t('settings:general.checkForUpdates')}
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2" description={t('settings:general.checkForUpdatesDesc')}
actions={ className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
<Button actions={
variant="link" <Button
size="sm" variant="link"
className="p-0" size="sm"
onClick={handleCheckForUpdate} className="p-0"
disabled={isCheckingUpdate} onClick={handleCheckForUpdate}
> disabled={isCheckingUpdate}
<div className="cursor-pointer rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1"> >
{isCheckingUpdate <div className="cursor-pointer rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
? t('settings:general.checkingForUpdates') {isCheckingUpdate
: t('settings:general.checkForUpdates')} ? t('settings:general.checkingForUpdates')
</div> : t('settings:general.checkForUpdates')}
</Button> </div>
} </Button>
/> }
)} />
)}
<CardItem <CardItem
title={t('common:language')} title={t('common:language')}
actions={<LanguageSwitcher />} actions={<LanguageSwitcher />}
@ -211,165 +212,173 @@ function General() {
{/* Data folder - Desktop only */} {/* Data folder - Desktop only */}
{PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && ( {PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
<Card title={t('common:dataFolder')}> <Card title={t('common:dataFolder')}>
<CardItem <CardItem
title={t('settings:dataFolder.appData', { title={t('settings:dataFolder.appData', {
ns: 'settings', ns: 'settings',
})} })}
align="start" align="start"
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2" className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
description={ description={
<> <>
<span> <span>
{t('settings:dataFolder.appDataDesc', { {t('settings:dataFolder.appDataDesc', {
ns: 'settings', ns: 'settings',
})} })}
&nbsp; &nbsp;
</span> </span>
<div className="flex items-center gap-2 mt-1 "> <div className="flex items-center gap-2 mt-1 ">
<div className=""> <div className="">
<span <span
title={janDataFolder} title={janDataFolder}
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80 line-clamp-1 w-fit" className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80 line-clamp-1 w-fit"
>
{janDataFolder}
</span>
</div>
<button
onClick={() =>
janDataFolder && copyToClipboard(janDataFolder)
}
className="cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out p-1"
title={
isCopied
? t('settings:general.copied')
: t('settings:general.copyPath')
}
> >
{janDataFolder} {isCopied ? (
</span> <div className="flex items-center gap-1">
<IconCopyCheck
size={12}
className="text-accent"
/>
<span className="text-xs leading-0">
{t('settings:general.copied')}
</span>
</div>
) : (
<IconCopy
size={12}
className="text-main-view-fg/50"
/>
)}
</button>
</div> </div>
<button </>
onClick={() => }
janDataFolder && copyToClipboard(janDataFolder) actions={
} <>
className="cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out p-1" <Button
title={ variant="link"
isCopied size="sm"
? t('settings:general.copied') className="p-0"
: t('settings:general.copyPath') title={t('settings:dataFolder.appData')}
} onClick={handleDataFolderChange}
> >
{isCopied ? ( <div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<div className="flex items-center gap-1"> <IconFolder
<IconCopyCheck size={12} className="text-accent" />
<span className="text-xs leading-0">
{t('settings:general.copied')}
</span>
</div>
) : (
<IconCopy
size={12} size={12}
className="text-main-view-fg/50" className="text-main-view-fg/50"
/> />
)} <span>{t('settings:general.changeLocation')}</span>
</button> </div>
</div> </Button>
</> {selectedNewPath && (
} <ChangeDataFolderLocation
actions={ currentPath={janDataFolder || ''}
<> newPath={selectedNewPath}
<Button onConfirm={confirmDataFolderChange}
variant="link" open={isDialogOpen}
size="sm" onOpenChange={(open) => {
className="p-0" setIsDialogOpen(open)
title={t('settings:dataFolder.appData')} if (!open) {
onClick={handleDataFolderChange} setSelectedNewPath(null)
> }
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1"> }}
<IconFolder >
size={12} <div />
className="text-main-view-fg/50" </ChangeDataFolderLocation>
/> )}
<span>{t('settings:general.changeLocation')}</span> </>
</div> }
</Button> />
{selectedNewPath && ( <CardItem
<ChangeDataFolderLocation title={t('settings:dataFolder.appLogs', {
currentPath={janDataFolder || ''} ns: 'settings',
newPath={selectedNewPath} })}
onConfirm={confirmDataFolderChange} description={t('settings:dataFolder.appLogsDesc')}
open={isDialogOpen} className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
onOpenChange={(open) => { actions={
setIsDialogOpen(open) <div className="flex items-center gap-2">
if (!open) { <Button
setSelectedNewPath(null) variant="link"
size="sm"
className="p-0"
onClick={handleOpenLogs}
title={t('settings:dataFolder.appLogs')}
>
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconLogs
size={12}
className="text-main-view-fg/50"
/>
<span>{t('settings:general.openLogs')}</span>
</div>
</Button>
<Button
variant="link"
size="sm"
className="p-0"
onClick={async () => {
if (janDataFolder) {
try {
const logsPath = `${janDataFolder}/logs`
await serviceHub
.opener()
.revealItemInDir(logsPath)
} catch (error) {
console.error(
'Failed to reveal logs folder:',
error
)
}
} }
}} }}
title={t('settings:general.revealLogs')}
> >
<div /> <div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
</ChangeDataFolderLocation> <IconFolder
)} size={12}
</> className="text-main-view-fg/50"
} />
/> <span>{openFileTitle()}</span>
<CardItem </div>
title={t('settings:dataFolder.appLogs', { </Button>
ns: 'settings', </div>
})} }
description={t('settings:dataFolder.appLogsDesc')} />
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2" </Card>
actions={
<div className="flex items-center gap-2">
<Button
variant="link"
size="sm"
className="p-0"
onClick={handleOpenLogs}
title={t('settings:dataFolder.appLogs')}
>
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconLogs size={12} className="text-main-view-fg/50" />
<span>{t('settings:general.openLogs')}</span>
</div>
</Button>
<Button
variant="link"
size="sm"
className="p-0"
onClick={async () => {
if (janDataFolder) {
try {
const logsPath = `${janDataFolder}/logs`
await serviceHub.opener().revealItemInDir(logsPath)
} catch (error) {
console.error(
'Failed to reveal logs folder:',
error
)
}
}
}}
title={t('settings:general.revealLogs')}
>
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconFolder
size={12}
className="text-main-view-fg/50"
/>
<span>{openFileTitle()}</span>
</div>
</Button>
</div>
}
/>
</Card>
)} )}
{/* Advanced - Desktop only */} {/* Advanced - Desktop only */}
{PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && ( {PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
<Card title="Advanced"> <Card title="Advanced">
<CardItem <CardItem
title={t('settings:others.resetFactory', { title={t('settings:others.resetFactory', {
ns: 'settings', ns: 'settings',
})} })}
description={t('settings:others.resetFactoryDesc', { description={t('settings:others.resetFactoryDesc', {
ns: 'settings', ns: 'settings',
})} })}
actions={ actions={
<FactoryResetDialog onReset={resetApp}> <FactoryResetDialog onReset={resetApp}>
<Button variant="destructive" size="sm"> <Button variant="destructive" size="sm">
{t('common:reset')} {t('common:reset')}
</Button> </Button>
</FactoryResetDialog> </FactoryResetDialog>
} }
/> />
</Card> </Card>
)} )}
{/* Other */} {/* Other */}

View File

@ -443,7 +443,7 @@ function ProviderDetail() {
return ( return (
<> <>
<Joyride <Joyride
run={isSetup} run={IS_IOS || IS_ANDROID ? false : isSetup}
floaterProps={{ floaterProps={{
hideArrow: true, hideArrow: true,
}} }}
@ -465,7 +465,7 @@ function ProviderDetail() {
skip: t('providers:joyride.skip'), skip: t('providers:joyride.skip'),
}} }}
/> />
<div className="flex flex-col h-full"> <div className="flex flex-col h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))]">
<HeaderPage> <HeaderPage>
<h1 className="font-medium">{t('common:settings')}</h1> <h1 className="font-medium">{t('common:settings')}</h1>
</HeaderPage> </HeaderPage>