Skip to content

Commit b85fdca

Browse files
authored
Merge pull request #18 from 0xtbug/dev
feat(auth-files): implement download and upload functionality
2 parents b7f067a + 1a28d36 commit b85fdca

11 files changed

Lines changed: 228 additions & 24 deletions

File tree

src-tauri/capabilities/default.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"dialog:default",
1111
"updater:default",
1212
"process:allow-restart",
13-
"fs:default"
13+
"fs:default",
14+
"fs:allow-read-text-file",
15+
"fs:allow-write-text-file"
1416
]
1517
}

src/features/providers/ProvidersPage.tsx

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
EyeOff,
1717
ChevronDown,
1818
ChevronRight,
19-
Key
19+
Key,
20+
Download,
21+
Upload,
2022
} from 'lucide-react';
2123
import {
2224
Dialog,
@@ -68,6 +70,9 @@ export function ProvidersPage() {
6870
submitCallback,
6971
updateProviderState,
7072
copyToClipboard,
73+
downloadAuthFile,
74+
downloadAllAuthFiles,
75+
uploadAuthFile,
7176
isPrivacyMode,
7277
togglePrivacyMode,
7378
openInBrowser,
@@ -173,28 +178,47 @@ export function ProvidersPage() {
173178
<CheckCircle className="h-5 w-5" />
174179
{t('providers.connectedAccounts')} ({files.length})
175180
</h2>
176-
{files.length > 0 && (
177-
<div className="flex items-center gap-2">
178-
<Button
179-
variant="outline"
180-
size="sm"
181-
onClick={openCopyAllModal}
182-
className="h-8 text-xs"
183-
>
184-
<Key className="mr-2 h-3.5 w-3.5" />
185-
{t('common.copyAll', 'Copy All')}
186-
</Button>
187-
<Button
188-
variant="outline"
189-
size="sm"
190-
onClick={() => setShowDeleteAllConfirmation(true)}
191-
className="h-8 text-xs bg-red-500/10 text-red-500 hover:bg-red-500/20 shadow-none border border-red-500/20"
192-
>
193-
<Trash2 className="mr-2 h-3.5 w-3.5" />
194-
{t('common.deleteAll', 'Delete All')}
195-
</Button>
196-
</div>
197-
)}
181+
<div className="flex items-center gap-2">
182+
<Button
183+
variant="outline"
184+
size="sm"
185+
onClick={openCopyAllModal}
186+
className="h-8 text-xs"
187+
disabled={files.length === 0}
188+
>
189+
<Key className="mr-2 h-3.5 w-3.5" />
190+
{t('common.copyAll', 'Copy All')}
191+
</Button>
192+
<Button
193+
variant="outline"
194+
size="sm"
195+
onClick={downloadAllAuthFiles}
196+
className="h-8 text-xs"
197+
disabled={files.length === 0}
198+
>
199+
<Download className="mr-2 h-3.5 w-3.5" />
200+
{t('providers.downloadAll', 'Download All')}
201+
</Button>
202+
<Button
203+
variant="outline"
204+
size="sm"
205+
onClick={uploadAuthFile}
206+
className="h-8 text-xs"
207+
>
208+
<Upload className="mr-2 h-3.5 w-3.5" />
209+
{t('providers.upload', 'Upload')}
210+
</Button>
211+
<Button
212+
variant="outline"
213+
size="sm"
214+
onClick={() => setShowDeleteAllConfirmation(true)}
215+
className="h-8 text-xs bg-red-500/10 text-red-500 hover:bg-red-500/20 shadow-none border border-red-500/20 disabled:opacity-50"
216+
disabled={files.length === 0}
217+
>
218+
<Trash2 className="mr-2 h-3.5 w-3.5" />
219+
{t('common.deleteAll', 'Delete All')}
220+
</Button>
221+
</div>
198222
</div>
199223

200224
{filesError && (
@@ -293,6 +317,15 @@ export function ProvidersPage() {
293317
</div>
294318

295319
<div className="flex items-center gap-1">
320+
<Button
321+
size="icon"
322+
variant="ghost"
323+
className="h-8 w-8 text-primary hover:bg-primary/10 opacity-80 group-hover:opacity-100"
324+
onClick={() => downloadAuthFile(file.name || file.filename || file.id)}
325+
title={t('providers.download', 'Download')}
326+
>
327+
<Download className="h-4 w-4" />
328+
</Button>
296329
<Button
297330
size="icon"
298331
variant="ghost"

src/features/providers/useProvidersPresenter.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export function useProvidersPresenter() {
5656

5757
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
5858
const [files, setFiles] = useState<AuthFile[]>([]);
59+
const filesRef = useRef<AuthFile[]>(files);
60+
61+
useEffect(() => {
62+
filesRef.current = files;
63+
}, [files]);
64+
5965
const [loadingFiles, setLoadingFiles] = useState(false);
6066
const [filesError, setFilesError] = useState<string | null>(null);
6167

@@ -444,6 +450,113 @@ export function useProvidersPresenter() {
444450
}
445451
}, [t]);
446452

453+
const downloadAuthFile = useCallback(async (filename: string | undefined | null) => {
454+
try {
455+
if (!filename) throw new Error('Filename is missing');
456+
let name = filename;
457+
if (!name.toLowerCase().endsWith('.json')) name = `${name}.json`;
458+
459+
const { save } = await import('@tauri-apps/plugin-dialog');
460+
const { writeTextFile } = await import('@tauri-apps/plugin-fs');
461+
462+
const data = await authFilesApi.download(name);
463+
464+
const filePath = await save({
465+
defaultPath: name,
466+
filters: [{ name: 'JSON', extensions: ['json'] }],
467+
});
468+
469+
if (filePath) {
470+
await writeTextFile(filePath, JSON.stringify(data, null, 2));
471+
toast.success(t('providers.downloadSuccess', 'File downloaded successfully'));
472+
}
473+
} catch (err) {
474+
toast.error(t('providers.downloadFailed', 'Failed to download file') + `: ${(err as Error).message}`);
475+
}
476+
}, [t]);
477+
478+
const downloadAllAuthFiles = useCallback(async () => {
479+
try {
480+
const { open } = await import('@tauri-apps/plugin-dialog');
481+
const { writeTextFile } = await import('@tauri-apps/plugin-fs');
482+
const { join } = await import('@tauri-apps/api/path');
483+
484+
const dirPath = await open({
485+
directory: true,
486+
multiple: false,
487+
title: 'Select Destination Folder'
488+
});
489+
490+
if (!dirPath || typeof dirPath !== 'string') return;
491+
492+
let successCount = 0;
493+
const currentFiles = filesRef.current;
494+
495+
console.log('Downloading all files. Count:', currentFiles.length);
496+
497+
for (const file of currentFiles) {
498+
try {
499+
const name = file.name || file.filename || file.id;
500+
if (!name) continue;
501+
502+
let fileName = name;
503+
if (!fileName.toLowerCase().endsWith('.json')) fileName = `${fileName}.json`;
504+
505+
const data = await authFilesApi.download(fileName);
506+
const fullPath = await join(dirPath, fileName);
507+
await writeTextFile(fullPath, JSON.stringify(data, null, 2));
508+
successCount++;
509+
} catch (e) {
510+
console.error('Failed to download file:', file.id, e);
511+
}
512+
}
513+
514+
toast.success(t('providers.downloadSuccess', 'Downloaded {{count}} files successfully', { count: successCount }));
515+
} catch (err) {
516+
toast.error(t('providers.downloadFailed', 'Failed to download files') + `: ${(err as Error).message}`);
517+
}
518+
}, [t]);
519+
520+
const uploadAuthFile = useCallback(async () => {
521+
try {
522+
const { open } = await import('@tauri-apps/plugin-dialog');
523+
const { readTextFile } = await import('@tauri-apps/plugin-fs');
524+
const { basename } = await import('@tauri-apps/api/path');
525+
526+
const filePaths = await open({
527+
multiple: true,
528+
filters: [{ name: 'JSON', extensions: ['json'] }],
529+
});
530+
531+
if (!filePaths || filePaths.length === 0) return;
532+
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
533+
534+
let uploads = 0;
535+
for (const path of paths) {
536+
try {
537+
const content = await readTextFile(path);
538+
const name = await basename(path);
539+
540+
const blob = new Blob([content], { type: 'application/json' });
541+
const formData = new FormData();
542+
formData.append('file', blob, name);
543+
544+
await authFilesApi.upload(formData);
545+
uploads++;
546+
} catch (e) {
547+
console.error('Failed to upload file:', path, e);
548+
}
549+
}
550+
551+
if (uploads > 0) {
552+
toast.success(t('providers.uploadSuccess', 'Files uploaded successfully'));
553+
loadFiles();
554+
}
555+
} catch (err) {
556+
toast.error(t('providers.uploadFailed', 'Failed to upload files') + `: ${(err as Error).message}`);
557+
}
558+
}, [t, loadFiles]);
559+
447560
const togglePrivacyMode = useCallback(() => {
448561
setIsPrivacyMode(prev => !prev);
449562
}, []);
@@ -493,6 +606,9 @@ export function useProvidersPresenter() {
493606
updateProviderState,
494607
copyToClipboard,
495608
copyRefreshToken,
609+
downloadAuthFile,
610+
downloadAllAuthFiles,
611+
uploadAuthFile,
496612

497613
// Privacy
498614
isPrivacyMode,

src/i18n/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"authSuccess": "Provider connected successfully!",
7777
"deleteAllConfirm": "This will permanently delete all connected accounts. This action cannot be undone.",
7878
"deleteAllSuccess": "All accounts deleted successfully",
79+
"downloadAll": "Download All",
80+
"download": "Download",
81+
"downloadSuccess": "File downloaded successfully",
82+
"downloadFailed": "Failed to download file",
83+
"upload": "Upload",
84+
"uploadSuccess": "Files uploaded successfully",
85+
"uploadFailed": "Failed to upload files",
7986
"copyAllTitle": "Copy All Refresh Tokens",
8087
"selectProviders": "Select the providers you want to include in the copy.",
8188
"copySuccess": "Refresh tokens copied to clipboard",

src/i18n/locales/id.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"authSuccess": "Penyedia berhasil terhubung!",
7777
"deleteAllConfirm": "Ini akan menghapus semua akun yang terhubung secara permanen. Tindakan ini tidak dapat dibatalkan.",
7878
"deleteAllSuccess": "Semua akun berhasil dihapus",
79+
"downloadAll": "Unduh Semua",
80+
"download": "Unduh",
81+
"downloadSuccess": "File berhasil diunduh",
82+
"downloadFailed": "Gagal mengunduh file",
83+
"upload": "Unggah",
84+
"uploadSuccess": "File berhasil diunggah",
85+
"uploadFailed": "Gagal mengunggah file",
7986
"copyAllTitle": "Salin Semua Token Penyegaran",
8087
"selectProviders": "Pilih penyedia yang ingin Anda sertakan dalam penyalinan.",
8188
"copySuccess": "Token penyegaran disalin ke papan klip",

src/i18n/locales/ja.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"authSuccess": "プロバイダーの接続に成功しました!",
7777
"deleteAllConfirm": "接続されているすべてのアカウントが完全に削除されます。この操作は取り消せません。",
7878
"deleteAllSuccess": "すべてのアカウントが正常に削除されました",
79+
"downloadAll": "すべてダウンロード",
80+
"download": "ダウンロード",
81+
"downloadSuccess": "ファイルが正常にダウンロードされました",
82+
"downloadFailed": "ファイルのダウンロードに失敗しました",
83+
"upload": "アップロード",
84+
"uploadSuccess": "ファイルが正常にアップロードされました",
85+
"uploadFailed": "ファイルのアップロードに失敗しました",
7986
"copyAllTitle": "すべてのリフレッシュトークンをコピー",
8087
"selectProviders": "コピーに含めたいプロバイダーを選択してください。",
8188
"copySuccess": "リフレッシュトークンがクリップボードにコピーされました",

src/i18n/locales/ko.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"authSuccess": "제공자가 성공적으로 연결되었습니다!",
7777
"deleteAllConfirm": "연결된 모든 계정이 영구적으로 삭제됩니다. 이 작업은 취소할 수 없습니다.",
7878
"deleteAllSuccess": "모든 계정이 성공적으로 삭제되었습니다",
79+
"downloadAll": "모두 다운로드",
80+
"download": "다운로드",
81+
"downloadSuccess": "파일이 성공적으로 다운로드되었습니다",
82+
"downloadFailed": "파일 다운로드 실패",
83+
"upload": "업로드",
84+
"uploadSuccess": "파일이 성공적으로 업로드되었습니다",
85+
"uploadFailed": "파일 업로드 실패",
7986
"copyAllTitle": "모든 갱신 토큰 복사",
8087
"selectProviders": "복사에 포함할 공급자를 선택하세요.",
8188
"copySuccess": "갱신 토큰이 클립보드에 복사되었습니다",

src/i18n/locales/th.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"authSuccess": "เชื่อมต่อผู้ให้บริการสำเร็จ!",
7777
"deleteAllConfirm": "บัญชีที่เชื่อมต่อทั้งหมดจะถูกลบอย่างถาวร การดำเนินการนี้ไม่สามารถยกเลิกได้",
7878
"deleteAllSuccess": "ลบบัญชีทั้งหมดเรียบร้อยแล้ว",
79+
"downloadAll": "ดาวน์โหลดทั้งหมด",
80+
"download": "ดาวน์โหลด",
81+
"downloadSuccess": "ดาวน์โหลดไฟล์สำเร็จแล้ว",
82+
"downloadFailed": "ดาวน์โหลดไฟล์ล้มเหลว",
83+
"upload": "อัปโหลด",
84+
"uploadSuccess": "อัปโหลดไฟล์สำเร็จแล้ว",
85+
"uploadFailed": "อัปโหลดไฟล์ล้มเหลว",
7986
"copyAllTitle": "คัดลอกโทเค็นรีเฟรชทั้งหมด",
8087
"selectProviders": "เลือกผู้ให้บริการที่คุณต้องการรวมในการคัดลอก",
8188
"copySuccess": "คัดลอกโทเค็นรีเฟรชไปยังคลิปบอร์ดแล้ว",

src/i18n/locales/vi.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"authSuccess": "Kết nối nhà cung cấp thành công!",
7777
"deleteAllConfirm": "Việc này sẽ xóa vĩnh viễn tất cả các tài khoản đã kết nối. Hành động này không thể hoàn tác.",
7878
"deleteAllSuccess": "Đã xóa tất cả tài khoản thành công",
79+
"downloadAll": "Tải xuống tất cả",
80+
"download": "Tải xuống",
81+
"downloadSuccess": "Đã tải tệp xuống thành công",
82+
"downloadFailed": "Tải tệp xuống thất bại",
83+
"upload": "Tải lên",
84+
"uploadSuccess": "Đã tải tệp lên thành công",
85+
"uploadFailed": "Tải tệp lên thất bại",
7986
"copyAllTitle": "Sao chép tất cả Token làm mới",
8087
"selectProviders": "Chọn nhà cung cấp bạn muốn sao chép.",
8188
"copySuccess": "Đã sao chép token làm mới vào clipboard",

src/i18n/locales/zh-CN.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"authSuccess": "提供商连接成功!",
7777
"deleteAllConfirm": "这将永久删除所有连接的帐户。此操作无法撤销。",
7878
"deleteAllSuccess": "所有帐户已成功删除",
79+
"downloadAll": "全部下载",
80+
"download": "下载",
81+
"downloadSuccess": "文件下载成功",
82+
"downloadFailed": "文件下载失败",
83+
"upload": "上传",
84+
"uploadSuccess": "文件上传成功",
85+
"uploadFailed": "文件上传失败",
7986
"copyAllTitle": "复制所有刷新令牌",
8087
"selectProviders": "选择您要包含在复制中的提供商。",
8188
"copySuccess": "刷新令牌已复制到剪贴板",

0 commit comments

Comments
 (0)