Skip to content

Commit 7a18ce1

Browse files
committed
feat: add export to JSON functionality with backend implementation
- Create backend export command in utils.rs that saves to Downloads folder - Add dirs dependency for proper file system access - Update Export and Clear All buttons to use shadcn Button components - Fix Time Saved stat card to be clickable - Remove border highlight from selected stats cards (background only) - Update Upgrade to Pro button to use shadcn Button component
1 parent 8bd2135 commit 7a18ce1

File tree

8 files changed

+162
-15
lines changed

8 files changed

+162
-15
lines changed

src-tauri/Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ rdev = "0.5.3"
3737
sha2 = "0.10.9"
3838
sha1 = "0.10.6"
3939
arboard = "3.6.0"
40+
dirs = "5"
4041
log = "0.4"
4142
env_logger = "0.11"
4243
tauri-plugin-log = { version = "2", features = ["colored"] }

src-tauri/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ pub mod permissions;
1010
pub mod reset;
1111
pub mod settings;
1212
pub mod text;
13+
pub mod utils;
1314
pub mod window;

src-tauri/src/commands/utils.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use tauri::AppHandle;
2+
use tauri_plugin_store::StoreExt;
3+
4+
#[tauri::command]
5+
pub async fn export_transcriptions(app: AppHandle) -> Result<String, String> {
6+
use std::fs;
7+
8+
log::info!("Exporting transcriptions to JSON");
9+
10+
// Get transcription history from the store
11+
let store = app.store("transcriptions").map_err(|e| e.to_string())?;
12+
13+
let mut entries: Vec<(String, serde_json::Value)> = Vec::new();
14+
15+
// Collect all entries with their timestamps
16+
for key in store.keys() {
17+
if let Some(value) = store.get(&key) {
18+
entries.push((key.to_string(), value));
19+
}
20+
}
21+
22+
// Sort by timestamp (newest first)
23+
entries.sort_by(|a, b| b.0.cmp(&a.0));
24+
25+
let history: Vec<serde_json::Value> = entries.into_iter().map(|(_, v)| v).collect();
26+
27+
if history.is_empty() {
28+
return Err("No transcriptions to export".to_string());
29+
}
30+
31+
// Create export data structure
32+
let export_data = serde_json::json!({
33+
"app": "VoiceTypr",
34+
"exportDate": chrono::Utc::now().to_rfc3339(),
35+
"totalTranscriptions": history.len(),
36+
"transcriptions": history
37+
});
38+
39+
// Get the Downloads folder path
40+
let download_dir = if cfg!(target_os = "macos") {
41+
// macOS specific
42+
dirs::download_dir()
43+
.or_else(|| dirs::home_dir().map(|h| h.join("Downloads")))
44+
} else {
45+
// Windows/Linux
46+
dirs::download_dir()
47+
};
48+
49+
let download_path = download_dir
50+
.ok_or_else(|| "Could not find Downloads folder".to_string())?;
51+
52+
// Create filename with current date
53+
let filename = format!(
54+
"voicetypr-transcriptions-{}.json",
55+
chrono::Local::now().format("%Y-%m-%d")
56+
);
57+
58+
let file_path = download_path.join(&filename);
59+
60+
// Write to file with pretty formatting
61+
let json_string = serde_json::to_string_pretty(&export_data)
62+
.map_err(|e| format!("Failed to serialize data: {}", e))?;
63+
64+
fs::write(&file_path, json_string)
65+
.map_err(|e| format!("Failed to write file: {}", e))?;
66+
67+
log::info!("Exported {} transcriptions to {:?}", history.len(), file_path);
68+
69+
// Return the full path as string
70+
Ok(file_path.to_string_lossy().to_string())
71+
}

src-tauri/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use commands::{
5252
reset::reset_app_data,
5353
settings::*,
5454
text::*,
55+
utils::export_transcriptions,
5556
window::*,
5657
};
5758
use state::unified_state::UnifiedRecordingState;
@@ -1266,6 +1267,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
12661267
get_transcription_history,
12671268
delete_transcription_entry,
12681269
clear_all_transcriptions,
1270+
export_transcriptions,
12691271
show_pill_widget,
12701272
hide_pill_widget,
12711273
close_pill_widget,

src/components/Sidebar.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SidebarMenuItem,
88
Sidebar as SidebarPrimitive,
99
} from "@/components/ui/sidebar";
10+
import { Button } from "@/components/ui/button";
1011
import { useLicense } from "@/contexts/LicenseContext";
1112
import { cn } from "@/lib/utils";
1213
import {
@@ -156,14 +157,19 @@ export function Sidebar({ activeSection, onSectionChange }: SidebarProps) {
156157
: "No License"}
157158
</span>
158159
</div>
159-
<a
160-
href="https://voicetypr.com/#pricing"
161-
target="_blank"
162-
rel="noopener noreferrer"
163-
className="w-full inline-flex items-center justify-center px-3 py-2 text-xs font-medium text-white bg-primary rounded-md hover:bg-primary/90 transition-colors"
160+
<Button
161+
asChild
162+
className="w-full text-sm"
163+
size="sm"
164164
>
165-
Upgrade to Pro
166-
</a>
165+
<a
166+
href="https://voicetypr.com/#pricing"
167+
target="_blank"
168+
rel="noopener noreferrer"
169+
>
170+
Upgrade to Pro
171+
</a>
172+
</Button>
167173
</>
168174
)}
169175
</div>

src/components/sections/RecentRecordings.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { ScrollArea } from "@/components/ui/scroll-area";
2+
import { Button } from "@/components/ui/button";
23
import { formatHotkey } from "@/lib/hotkey-utils";
34
import { TranscriptionHistory } from "@/types";
45
import { useCanRecord, useCanAutoInsert } from "@/contexts/ReadinessContext";
56
import { invoke } from "@tauri-apps/api/core";
67
import { ask } from "@tauri-apps/plugin-dialog";
7-
import { AlertCircle, Mic, Trash2, Search, Copy, Calendar, Clock } from "lucide-react";
8+
import { AlertCircle, Mic, Trash2, Search, Copy, Calendar, Clock, Download } from "lucide-react";
89
import { useState, useMemo } from "react";
910
import { toast } from "sonner";
1011
import { cn } from "@/lib/utils";
@@ -126,6 +127,33 @@ export function RecentRecordings({ history, hotkey = "Cmd+Shift+Space", onHistor
126127
}
127128
};
128129

130+
const handleExport = async () => {
131+
if (history.length === 0) return;
132+
133+
try {
134+
// Show confirmation dialog with location info
135+
const confirmed = await ask(
136+
`Export ${history.length} transcription${history.length !== 1 ? 's' : ''} to JSON?\n\nThe file will be saved to your Downloads folder.`,
137+
{
138+
title: "Export Transcriptions",
139+
kind: "info"
140+
}
141+
);
142+
143+
if (!confirmed) return;
144+
145+
// Call the backend export command
146+
const filePath = await invoke<string>("export_transcriptions");
147+
148+
toast.success(`Exported ${history.length} transcriptions`, {
149+
description: `Saved to Downloads folder`
150+
});
151+
} catch (error) {
152+
console.error("Failed to export transcriptions:", error);
153+
toast.error("Failed to export transcriptions");
154+
}
155+
};
156+
129157
return (
130158
<div className="h-full flex flex-col">
131159
{/* Header */}
@@ -138,15 +166,27 @@ export function RecentRecordings({ history, hotkey = "Cmd+Shift+Space", onHistor
138166
</p>
139167
</div>
140168
<div className="flex items-center gap-3">
169+
{history.length > 0 && (
170+
<Button
171+
onClick={handleExport}
172+
size="sm"
173+
title="Export transcriptions to JSON"
174+
>
175+
<Download className="h-3.5 w-3.5" />
176+
Export
177+
</Button>
178+
)}
141179
{history.length > 5 && (
142-
<button
180+
<Button
143181
onClick={handleClearAll}
144-
className="flex items-center gap-2 px-3 py-1.5 text-sm text-destructive hover:bg-destructive/10 rounded-md transition-colors"
182+
variant="ghost"
183+
size="sm"
184+
className="text-destructive hover:text-destructive"
145185
title="Clear all transcriptions"
146186
>
147187
<Trash2 className="h-3.5 w-3.5" />
148188
Clear All
149-
</button>
189+
</Button>
150190
)}
151191
</div>
152192
</div>

src/components/tabs/OverviewTab.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export function OverviewTab({ history }: OverviewTabProps) {
165165
<div
166166
className={cn(
167167
"p-4 rounded-lg bg-card border border-border/50 hover:border-border transition-all cursor-pointer",
168-
selectedPeriod === 'all' && "border-primary bg-primary/5"
168+
selectedPeriod === 'all' && "bg-primary/5"
169169
)}
170170
onClick={() => setSelectedPeriod('all')}
171171
title="Click to filter all time"
@@ -181,7 +181,7 @@ export function OverviewTab({ history }: OverviewTabProps) {
181181
<div
182182
className={cn(
183183
"p-4 rounded-lg bg-card border border-border/50 hover:border-border transition-all cursor-pointer",
184-
selectedPeriod === 'month' && "border-primary bg-primary/5"
184+
selectedPeriod === 'month' && "bg-primary/5"
185185
)}
186186
onClick={() => setSelectedPeriod('month')}
187187
title="Click to filter last 30 days"
@@ -195,7 +195,11 @@ export function OverviewTab({ history }: OverviewTabProps) {
195195
</div>
196196

197197
<div
198-
className="p-4 rounded-lg bg-card border border-border/50 hover:border-border transition-all cursor-pointer"
198+
className={cn(
199+
"p-4 rounded-lg bg-card border border-border/50 hover:border-border transition-all cursor-pointer",
200+
selectedPeriod === 'today' && "bg-primary/5"
201+
)}
202+
onClick={() => setSelectedPeriod('today')}
199203
title="Based on 40 WPM typing speed"
200204
>
201205
<Clock className="h-5 w-5 text-muted-foreground/50 mb-3" />
@@ -209,7 +213,7 @@ export function OverviewTab({ history }: OverviewTabProps) {
209213
<div
210214
className={cn(
211215
"p-4 rounded-lg bg-card border border-border/50 hover:border-border transition-all cursor-pointer",
212-
selectedPeriod === 'week' && "border-primary bg-primary/5"
216+
selectedPeriod === 'week' && "bg-primary/5"
213217
)}
214218
onClick={() => setSelectedPeriod('week')}
215219
title="Click to filter last 7 days"

0 commit comments

Comments
 (0)