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>
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
)

View File

@ -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;

View File

@ -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(),
}

View File

@ -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',

View File

@ -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'
}

View File

@ -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>

View File

@ -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>

View File

@ -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',
})}
&nbsp;
</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',
})}
&nbsp;
</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 */}

View File

@ -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>