diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index f03faf629..ef8932d4e 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -25,6 +25,7 @@ tauri = { version = "2.4.0", features = [ "protocol-asset", "macos-private-api",
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2.2.0"
tauri-plugin-os = "2.2.1"
+tauri-plugin-opener = "2.2.7"
flate2 = "1.0"
tar = "0.4"
rand = "0.8"
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 36a40f6f3..566118a7d 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -2,13 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
- "windows": [
- "main"
- ],
+ "windows": ["main"],
"remote": {
- "urls": [
- "http://*"
- ]
+ "urls": ["http://*"]
},
"permissions": [
"core:default",
@@ -19,6 +15,7 @@
"core:app:allow-set-app-theme",
"core:window:allow-set-focus",
"os:default",
+ "opener:default",
"log:default",
"updater:default",
"dialog:default",
@@ -77,4 +74,4 @@
},
"store:default"
]
-}
\ No newline at end of file
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 744fb383a..6e5b9e1c5 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -17,6 +17,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
+ .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_updater::Builder::new().build())
diff --git a/web-app/package.json b/web-app/package.json
index 8b8e736ad..d0941dd2b 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -31,6 +31,7 @@
"@tanstack/react-router-devtools": "^1.116.0",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
+ "@tauri-apps/plugin-opener": "^2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-updater": "^2.7.1",
"@types/react-syntax-highlighter": "^15.5.13",
diff --git a/web-app/src/components/ui/button.tsx b/web-app/src/components/ui/button.tsx
index ea53b16cd..bface0830 100644
--- a/web-app/src/components/ui/button.tsx
+++ b/web-app/src/components/ui/button.tsx
@@ -16,7 +16,7 @@ const buttonVariants = cva(
},
size: {
default: 'h-7 px-3 py-2 has-[>svg]:px-3 rounded-sm',
- sm: 'h-6 rounded gap-1.5 px-2 has-[>svg]:px-2.5',
+ sm: 'h-6 gap-1.5 px-2 has-[>svg]:px-2.5 rounded-sm',
lg: 'h-9 rounded-md px-4 has-[>svg]:px-4',
icon: 'size-8',
},
diff --git a/web-app/src/containers/dialogs/ChangeDataFolderLocation.tsx b/web-app/src/containers/dialogs/ChangeDataFolderLocation.tsx
new file mode 100644
index 000000000..501c1ce36
--- /dev/null
+++ b/web-app/src/containers/dialogs/ChangeDataFolderLocation.tsx
@@ -0,0 +1,81 @@
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { IconFolder } from '@tabler/icons-react'
+
+interface ChangeDataFolderLocationProps {
+ children: React.ReactNode
+ currentPath: string
+ newPath: string
+ onConfirm: () => void
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export default function ChangeDataFolderLocation({
+ children,
+ currentPath,
+ newPath,
+ onConfirm,
+ open,
+ onOpenChange,
+}: ChangeDataFolderLocationProps) {
+ return (
+
+ )
+}
diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx
index 533e8673d..4fc441e21 100644
--- a/web-app/src/routes/settings/general.tsx
+++ b/web-app/src/routes/settings/general.tsx
@@ -10,6 +10,8 @@ import { useTranslation } from 'react-i18next'
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
import { useEffect, useState } from 'react'
import { open } from '@tauri-apps/plugin-dialog'
+import { revealItemInDir } from '@tauri-apps/plugin-opener'
+import ChangeDataFolderLocation from '@/containers/dialogs/ChangeDataFolderLocation'
import {
Dialog,
@@ -32,19 +34,35 @@ import {
IconExternalLink,
IconFolder,
IconLogs,
+ IconCopy,
+ IconCopyCheck,
} from '@tabler/icons-react'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { windowKey } from '@/constants/windows'
+import { toast } from 'sonner'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.settings.general as any)({
component: General,
})
+const openFileTitle = (): string => {
+ if (IS_MACOS) {
+ return 'Show in Finder'
+ } else if (IS_WINDOWS) {
+ return 'Show in File Explorer'
+ } else {
+ return 'Open Containing Folder'
+ }
+}
+
function General() {
const { t } = useTranslation()
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
const [janDataFolder, setJanDataFolder] = useState()
+ const [isCopied, setIsCopied] = useState(false)
+ const [selectedNewPath, setSelectedNewPath] = useState(null)
+ const [isDialogOpen, setIsDialogOpen] = useState(false)
useEffect(() => {
const fetchDataFolder = async () => {
@@ -97,6 +115,52 @@ function General() {
}
}
+ const copyToClipboard = async (text: string) => {
+ try {
+ await navigator.clipboard.writeText(text)
+ setIsCopied(true)
+ setTimeout(() => setIsCopied(false), 2000) // Reset after 2 seconds
+ } catch (error) {
+ console.error('Failed to copy to clipboard:', error)
+ }
+ }
+
+ const handleDataFolderChange = async () => {
+ const selectedPath = await open({
+ multiple: false,
+ directory: true,
+ defaultPath: janDataFolder,
+ })
+
+ if (selectedPath === janDataFolder) return
+ if (selectedPath !== null) {
+ setSelectedNewPath(selectedPath)
+ setIsDialogOpen(true)
+ }
+ }
+
+ const confirmDataFolderChange = async () => {
+ if (selectedNewPath) {
+ try {
+ setJanDataFolder(selectedNewPath)
+ await relocateJanDataFolder(selectedNewPath)
+ // Only relaunch if relocation was successful
+ window.core?.api?.relaunch()
+ setSelectedNewPath(null)
+ setIsDialogOpen(false)
+ } catch (error) {
+ console.error('Failed to relocate data folder:', error)
+ // Revert the data folder path on error
+ const originalPath = await getJanDataFolder()
+ setJanDataFolder(originalPath)
+
+ toast.error(
+ 'Failed to relocate data folder. Please try again or choose a different location.'
+ )
+ }
+ }
+ }
+
return (
@@ -133,42 +197,71 @@ function General() {
{t('settings.dataFolder.appDataDesc', {
ns: 'settings',
})}
+
-
- {janDataFolder}
-
+
+
+ {janDataFolder}
+
+
+
>
}
actions={
-
+ <>
+
+ {selectedNewPath && (
+ {
+ setIsDialogOpen(open)
+ if (!open) {
+ setSelectedNewPath(null)
+ }
+ }}
+ >
+
+
+ )}
+ >
}
/>
- {/* Open Logs */}
-
-
-
-
+
+
+
+
}
/>
diff --git a/web-app/src/routes/settings/local-api-server.tsx b/web-app/src/routes/settings/local-api-server.tsx
index 7ad6227c0..d6a13ac36 100644
--- a/web-app/src/routes/settings/local-api-server.tsx
+++ b/web-app/src/routes/settings/local-api-server.tsx
@@ -140,11 +140,13 @@ function LocalAPIServer() {