enhancement: fit mobile layout
This commit is contained in:
parent
003598204e
commit
f639ec70d4
@ -1,11 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="bg-app">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link 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="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" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -122,7 +122,7 @@ const LeftPanel = () => {
|
||||
) {
|
||||
if (currentIsSmallScreen && open) {
|
||||
setLeftPanel(false)
|
||||
} else if(!open) {
|
||||
} else if (!open) {
|
||||
setLeftPanel(true)
|
||||
}
|
||||
prevScreenSizeRef.current = currentIsSmallScreen
|
||||
@ -141,7 +141,7 @@ const LeftPanel = () => {
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [setLeftPanel])
|
||||
}, [open, setLeftPanel])
|
||||
|
||||
const currentPath = useRouterState({
|
||||
select: (state) => state.location.pathname,
|
||||
@ -184,7 +184,7 @@ const LeftPanel = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop overlay for small screens */}
|
||||
{isSmallScreen && open && (
|
||||
{isSmallScreen && open && !IS_IOS && !IS_ANDROID && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur z-30"
|
||||
onClick={(e) => {
|
||||
@ -207,7 +207,7 @@ const LeftPanel = () => {
|
||||
isResizableContext && 'h-full w-full',
|
||||
// Small screen context: fixed positioning and styling
|
||||
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
|
||||
!isResizableContext &&
|
||||
!isSmallScreen &&
|
||||
@ -266,7 +266,8 @@ const LeftPanel = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'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%-140px)]'
|
||||
)}
|
||||
@ -379,7 +380,9 @@ const LeftPanel = () => {
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DeleteAllThreadsDialog onDeleteAll={deleteAllThreads} />
|
||||
<DeleteAllThreadsDialog
|
||||
onDeleteAll={deleteAllThreads}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@ -30,12 +30,15 @@ const SettingsMenu = () => {
|
||||
// On web: exclude llamacpp provider as it's not available
|
||||
const activeProviders = providers.filter((provider) => {
|
||||
if (!provider.active) return false
|
||||
|
||||
|
||||
// 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 true
|
||||
})
|
||||
|
||||
@ -92,7 +95,7 @@ const SettingsMenu = () => {
|
||||
title: 'common:keyboardShortcuts',
|
||||
route: route.settings.shortcuts,
|
||||
hasSubMenu: false,
|
||||
isEnabled: true,
|
||||
isEnabled: PlatformFeatures[PlatformFeature.SHORTCUT],
|
||||
},
|
||||
{
|
||||
title: 'common:hardware',
|
||||
@ -137,7 +140,7 @@ const SettingsMenu = () => {
|
||||
return (
|
||||
<>
|
||||
<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}
|
||||
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',
|
||||
'sm:flex',
|
||||
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'
|
||||
)}
|
||||
>
|
||||
@ -162,77 +165,82 @@ const SettingsMenu = () => {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div key={menu.title}>
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
||||
{menu.hasSubMenu && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleProvidersExpansion()
|
||||
}}
|
||||
className="text-main-view-fg/60 hover:text-main-view-fg/80"
|
||||
>
|
||||
{expandedProviders ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<div key={menu.title}>
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-main-view-fg/80">
|
||||
{t(menu.title)}
|
||||
</span>
|
||||
{menu.hasSubMenu && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleProvidersExpansion()
|
||||
}}
|
||||
className="text-main-view-fg/60 hover:text-main-view-fg/80"
|
||||
>
|
||||
{expandedProviders ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Sub-menu for model providers */}
|
||||
{menu.hasSubMenu && expandedProviders && (
|
||||
<div className="ml-2 mt-1 space-y-1 first-step-setup-remote-provider">
|
||||
{activeProviders.map((provider) => {
|
||||
const isActive = matches.some(
|
||||
(match) =>
|
||||
match.routeId === '/settings/providers/$providerName' &&
|
||||
'providerName' in match.params &&
|
||||
match.params.providerName === provider.provider
|
||||
)
|
||||
{/* Sub-menu for model providers */}
|
||||
{menu.hasSubMenu && expandedProviders && (
|
||||
<div className="ml-2 mt-1 space-y-1 first-step-setup-remote-provider">
|
||||
{activeProviders.map((provider) => {
|
||||
const isActive = matches.some(
|
||||
(match) =>
|
||||
match.routeId ===
|
||||
'/settings/providers/$providerName' &&
|
||||
'providerName' in match.params &&
|
||||
match.params.providerName === provider.provider
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={provider.provider}>
|
||||
<div
|
||||
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',
|
||||
isActive && 'bg-main-view-fg/5',
|
||||
// hidden for llama.cpp provider for setup remote provider
|
||||
provider.provider === 'llama.cpp' &&
|
||||
stepSetupRemoteProvider &&
|
||||
'hidden'
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: route.settings.providers,
|
||||
params: {
|
||||
providerName: provider.provider,
|
||||
},
|
||||
...(stepSetupRemoteProvider
|
||||
? { search: { step: 'setup_remote_provider' } }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
>
|
||||
<ProvidersAvatar provider={provider} />
|
||||
<div className="truncate">
|
||||
<span>{getProviderTitle(provider.provider)}</span>
|
||||
return (
|
||||
<div key={provider.provider}>
|
||||
<div
|
||||
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',
|
||||
isActive && 'bg-main-view-fg/5',
|
||||
// hidden for llama.cpp provider for setup remote provider
|
||||
provider.provider === 'llama.cpp' &&
|
||||
stepSetupRemoteProvider &&
|
||||
'hidden'
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: route.settings.providers,
|
||||
params: {
|
||||
providerName: provider.provider,
|
||||
},
|
||||
...(stepSetupRemoteProvider
|
||||
? {
|
||||
search: { step: 'setup_remote_provider' },
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
>
|
||||
<ProvidersAvatar provider={provider} />
|
||||
<div className="truncate">
|
||||
<span>{getProviderTitle(provider.provider)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,8 @@ import HeaderPage from './HeaderPage'
|
||||
import { isProd } from '@/lib/version'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { localStorageKey } from '@/constants/localStorage'
|
||||
import { PlatformFeatures } from '@/lib/platform/const'
|
||||
import { PlatformFeature } from '@/lib/platform'
|
||||
|
||||
function SetupScreen() {
|
||||
const { t } = useTranslation()
|
||||
@ -21,7 +23,7 @@ function SetupScreen() {
|
||||
<div className="flex h-full flex-col flex-justify-center">
|
||||
<HeaderPage></HeaderPage>
|
||||
<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">
|
||||
<h1 className="font-editorialnew text-main-view-fg text-4xl">
|
||||
{t('setup:welcome')}
|
||||
@ -31,22 +33,24 @@ function SetupScreen() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 flex-col">
|
||||
<Card
|
||||
header={
|
||||
<Link
|
||||
to={route.hub.index}
|
||||
search={{
|
||||
...(!isProd ? { step: 'setup_local_provider' } : {}),
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-main-view-fg font-medium text-base">
|
||||
{t('setup:localModel')}
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
></Card>
|
||||
{PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && (
|
||||
<Card
|
||||
header={
|
||||
<Link
|
||||
to={route.hub.index}
|
||||
search={{
|
||||
...(!isProd ? { step: 'setup_local_provider' } : {}),
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-main-view-fg font-medium text-base">
|
||||
{t('setup:localModel')}
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Card
|
||||
header={
|
||||
<Link
|
||||
@ -65,7 +69,7 @@ function SetupScreen() {
|
||||
</h1>
|
||||
</Link>
|
||||
}
|
||||
></Card>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -21,7 +21,7 @@ export function PromptAnalytic() {
|
||||
}
|
||||
|
||||
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">
|
||||
<IconFileTextShield className="text-accent" />
|
||||
<h2 className="font-medium text-main-view-fg/80">
|
||||
@ -45,7 +45,9 @@ export function PromptAnalytic() {
|
||||
>
|
||||
{t('deny')}
|
||||
</Button>
|
||||
<Button onClick={() => handleProductAnalytics(true)}>{t('allow')}</Button>
|
||||
<Button onClick={() => handleProductAnalytics(true)}>
|
||||
{t('allow')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -56,6 +56,13 @@
|
||||
@layer base {
|
||||
body {
|
||||
@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 {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { PlatformFeature } from './types'
|
||||
import { isPlatformTauri } from './utils'
|
||||
import { isPlatformTauri, isPlatformIOS, isPlatformAndroid } from './utils'
|
||||
|
||||
/**
|
||||
* Platform Features Configuration
|
||||
@ -12,28 +12,35 @@ import { isPlatformTauri } from './utils'
|
||||
*/
|
||||
export const PlatformFeatures: Record<PlatformFeature, boolean> = {
|
||||
// Hardware monitoring and GPU usage
|
||||
[PlatformFeature.HARDWARE_MONITORING]: isPlatformTauri(),
|
||||
[PlatformFeature.HARDWARE_MONITORING]:
|
||||
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
|
||||
|
||||
// Local model inference (llama.cpp)
|
||||
[PlatformFeature.LOCAL_INFERENCE]: isPlatformTauri(),
|
||||
[PlatformFeature.LOCAL_INFERENCE]:
|
||||
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
|
||||
|
||||
// Local API server
|
||||
[PlatformFeature.LOCAL_API_SERVER]: isPlatformTauri(),
|
||||
[PlatformFeature.LOCAL_API_SERVER]:
|
||||
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
|
||||
|
||||
// Hub/model downloads
|
||||
[PlatformFeature.MODEL_HUB]: isPlatformTauri(),
|
||||
[PlatformFeature.MODEL_HUB]:
|
||||
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
|
||||
|
||||
// System integrations (logs, file explorer, etc.)
|
||||
[PlatformFeature.SYSTEM_INTEGRATIONS]: isPlatformTauri(),
|
||||
[PlatformFeature.SYSTEM_INTEGRATIONS]:
|
||||
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
|
||||
|
||||
// HTTPS proxy
|
||||
[PlatformFeature.HTTPS_PROXY]: isPlatformTauri(),
|
||||
|
||||
[PlatformFeature.HTTPS_PROXY]:
|
||||
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
|
||||
|
||||
// Default model providers (OpenAI, Anthropic, etc.) - disabled for web-only Jan builds
|
||||
[PlatformFeature.DEFAULT_PROVIDERS]: isPlatformTauri(),
|
||||
|
||||
// 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
|
||||
[PlatformFeature.WEB_AUTO_MODEL_SELECTION]: !isPlatformTauri(),
|
||||
@ -45,10 +52,12 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
|
||||
[PlatformFeature.MCP_AUTO_APPROVE_TOOLS]: !isPlatformTauri(),
|
||||
|
||||
// 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
|
||||
[PlatformFeature.EXTENSIONS_SETTINGS]: isPlatformTauri(),
|
||||
[PlatformFeature.EXTENSIONS_SETTINGS]:
|
||||
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
|
||||
|
||||
// Assistant functionality - disabled for web
|
||||
[PlatformFeature.ASSISTANTS]: isPlatformTauri(),
|
||||
@ -60,5 +69,9 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
|
||||
[PlatformFeature.GOOGLE_ANALYTICS]: !isPlatformTauri(),
|
||||
|
||||
// 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(),
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
/**
|
||||
* Supported platforms
|
||||
*/
|
||||
export type Platform = 'tauri' | 'web'
|
||||
export type Platform = 'tauri' | 'web' | 'ios' | 'android'
|
||||
|
||||
/**
|
||||
* Platform Feature Enum
|
||||
@ -16,6 +16,8 @@ export enum PlatformFeature {
|
||||
// Hardware monitoring and GPU usage
|
||||
HARDWARE_MONITORING = 'hardwareMonitoring',
|
||||
|
||||
SHORTCUT = 'shortcut',
|
||||
|
||||
// Local model inference (llama.cpp)
|
||||
LOCAL_INFERENCE = 'localInference',
|
||||
|
||||
@ -30,16 +32,16 @@ export enum PlatformFeature {
|
||||
|
||||
// HTTPS proxy
|
||||
HTTPS_PROXY = 'httpsProxy',
|
||||
|
||||
|
||||
// Default model providers (OpenAI, Anthropic, etc.)
|
||||
DEFAULT_PROVIDERS = 'defaultProviders',
|
||||
|
||||
|
||||
// Analytics and telemetry
|
||||
ANALYTICS = 'analytics',
|
||||
|
||||
|
||||
// Web-specific automatic model selection from jan provider
|
||||
WEB_AUTO_MODEL_SELECTION = 'webAutoModelSelection',
|
||||
|
||||
|
||||
// Model provider settings page management
|
||||
MODEL_PROVIDER_SETTINGS = 'modelProviderSettings',
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Platform, PlatformFeature } from './types'
|
||||
|
||||
declare const IS_WEB_APP: boolean
|
||||
@ -12,7 +13,25 @@ export const isPlatformTauri = (): boolean => {
|
||||
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 => {
|
||||
if (isPlatformIOS()) return 'ios'
|
||||
if (isPlatformAndroid()) return 'android'
|
||||
return isPlatformTauri() ? 'tauri' : 'web'
|
||||
}
|
||||
|
||||
|
||||
@ -111,13 +111,17 @@ const AppLayout = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<AnalyticProvider />
|
||||
{PlatformFeatures[PlatformFeature.GOOGLE_ANALYTICS] && <GoogleAnalyticsProvider />}
|
||||
{PlatformFeatures[PlatformFeature.GOOGLE_ANALYTICS] && (
|
||||
<GoogleAnalyticsProvider />
|
||||
)}
|
||||
<KeyboardShortcutsProvider />
|
||||
<main className="relative h-svh text-sm antialiased select-none bg-app">
|
||||
{/* Fake absolute panel top to enable window drag */}
|
||||
<div className="absolute w-full h-10 z-10" data-tauri-drag-region />
|
||||
<DialogAppUpdater />
|
||||
{PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && <BackendUpdater />}
|
||||
{PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && (
|
||||
<BackendUpdater />
|
||||
)}
|
||||
|
||||
{/* Use ResizablePanelGroup only on larger screens */}
|
||||
{!isSmallScreen && isLeftPanelOpen ? (
|
||||
@ -158,11 +162,11 @@ const AppLayout = () => {
|
||||
{/* Main content panel */}
|
||||
<div
|
||||
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)]'
|
||||
)}
|
||||
>
|
||||
<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 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,7 @@ function Appareances() {
|
||||
const { resetCodeBlockStyle } = useCodeblock()
|
||||
|
||||
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>
|
||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||
</HeaderPage>
|
||||
|
||||
@ -161,7 +161,7 @@ function General() {
|
||||
}, [t, checkForUpdate])
|
||||
|
||||
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>
|
||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||
</HeaderPage>
|
||||
@ -181,28 +181,29 @@ function General() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!AUTO_UPDATER_DISABLED && PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
|
||||
<CardItem
|
||||
title={t('settings:general.checkForUpdates')}
|
||||
description={t('settings:general.checkForUpdatesDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0"
|
||||
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
|
||||
? t('settings:general.checkingForUpdates')
|
||||
: t('settings:general.checkForUpdates')}
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!AUTO_UPDATER_DISABLED &&
|
||||
PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
|
||||
<CardItem
|
||||
title={t('settings:general.checkForUpdates')}
|
||||
description={t('settings:general.checkForUpdatesDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0"
|
||||
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
|
||||
? t('settings:general.checkingForUpdates')
|
||||
: t('settings:general.checkForUpdates')}
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CardItem
|
||||
title={t('common:language')}
|
||||
actions={<LanguageSwitcher />}
|
||||
@ -211,165 +212,173 @@ function General() {
|
||||
|
||||
{/* Data folder - Desktop only */}
|
||||
{PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
|
||||
<Card title={t('common:dataFolder')}>
|
||||
<CardItem
|
||||
title={t('settings:dataFolder.appData', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
align="start"
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{t('settings:dataFolder.appDataDesc', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
|
||||
</span>
|
||||
<div className="flex items-center gap-2 mt-1 ">
|
||||
<div className="">
|
||||
<span
|
||||
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"
|
||||
<Card title={t('common:dataFolder')}>
|
||||
<CardItem
|
||||
title={t('settings:dataFolder.appData', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
align="start"
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{t('settings:dataFolder.appDataDesc', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
|
||||
</span>
|
||||
<div className="flex items-center gap-2 mt-1 ">
|
||||
<div className="">
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{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}
|
||||
</span>
|
||||
{isCopied ? (
|
||||
<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>
|
||||
<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')
|
||||
}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0"
|
||||
title={t('settings:dataFolder.appData')}
|
||||
onClick={handleDataFolderChange}
|
||||
>
|
||||
{isCopied ? (
|
||||
<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
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0"
|
||||
title={t('settings:dataFolder.appData')}
|
||||
onClick={handleDataFolderChange}
|
||||
>
|
||||
<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>{t('settings:general.changeLocation')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
{selectedNewPath && (
|
||||
<ChangeDataFolderLocation
|
||||
currentPath={janDataFolder || ''}
|
||||
newPath={selectedNewPath}
|
||||
onConfirm={confirmDataFolderChange}
|
||||
open={isDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsDialogOpen(open)
|
||||
if (!open) {
|
||||
setSelectedNewPath(null)
|
||||
<span>{t('settings:general.changeLocation')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
{selectedNewPath && (
|
||||
<ChangeDataFolderLocation
|
||||
currentPath={janDataFolder || ''}
|
||||
newPath={selectedNewPath}
|
||||
onConfirm={confirmDataFolderChange}
|
||||
open={isDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsDialogOpen(open)
|
||||
if (!open) {
|
||||
setSelectedNewPath(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div />
|
||||
</ChangeDataFolderLocation>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<CardItem
|
||||
title={t('settings:dataFolder.appLogs', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
description={t('settings:dataFolder.appLogsDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
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 />
|
||||
</ChangeDataFolderLocation>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<CardItem
|
||||
title={t('settings:dataFolder.appLogs', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
description={t('settings:dataFolder.appLogsDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
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>
|
||||
<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 */}
|
||||
{PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
|
||||
<Card title="Advanced">
|
||||
<CardItem
|
||||
title={t('settings:others.resetFactory', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
description={t('settings:others.resetFactoryDesc', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
actions={
|
||||
<FactoryResetDialog onReset={resetApp}>
|
||||
<Button variant="destructive" size="sm">
|
||||
{t('common:reset')}
|
||||
</Button>
|
||||
</FactoryResetDialog>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<Card title="Advanced">
|
||||
<CardItem
|
||||
title={t('settings:others.resetFactory', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
description={t('settings:others.resetFactoryDesc', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
actions={
|
||||
<FactoryResetDialog onReset={resetApp}>
|
||||
<Button variant="destructive" size="sm">
|
||||
{t('common:reset')}
|
||||
</Button>
|
||||
</FactoryResetDialog>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Other */}
|
||||
|
||||
@ -443,7 +443,7 @@ function ProviderDetail() {
|
||||
return (
|
||||
<>
|
||||
<Joyride
|
||||
run={isSetup}
|
||||
run={IS_IOS || IS_ANDROID ? false : isSetup}
|
||||
floaterProps={{
|
||||
hideArrow: true,
|
||||
}}
|
||||
@ -465,7 +465,7 @@ function ProviderDetail() {
|
||||
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>
|
||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||
</HeaderPage>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user