|
1
|
import React, { useState } from "react"; |
|
2
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; |
|
3
|
import { Button } from "@/components/ui/button"; |
|
4
|
import { Checkbox } from "@/components/ui/checkbox"; |
|
5
|
import { Label } from "@/components/ui/label"; |
|
6
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; |
|
7
|
import { Progress } from "@/components/ui/progress"; |
|
8
|
import { useToast } from "@/hooks/use-toast"; |
|
9
|
import { downloadGuide, downloadVideo, getEstimatedSize, isDownloadSupported, type DownloadOptions } from "@/lib/download-utils"; |
|
10
|
import { Download, FileText, Image, Video, HardDrive } from "lucide-react"; |
|
11
|
import type { RepairGuide } from "@shared/schema"; |
|
12
|
|
|
13
|
interface DownloadModalProps { |
|
14
|
open: boolean; |
|
15
|
onOpenChange: (open: boolean) => void; |
|
16
|
guide: RepairGuide; |
|
17
|
videoUrl?: string; |
|
18
|
} |
|
19
|
|
|
20
|
export function DownloadModal({ open, onOpenChange, guide, videoUrl }: DownloadModalProps) { |
|
21
|
const { toast } = useToast(); |
|
22
|
const [options, setOptions] = useState<DownloadOptions>({ |
|
23
|
format: 'html', |
|
24
|
includeImages: true, |
|
25
|
includeVideos: false, |
|
26
|
}); |
|
27
|
const [isDownloading, setIsDownloading] = useState(false); |
|
28
|
const [downloadProgress, setDownloadProgress] = useState(0); |
|
29
|
|
|
30
|
const handleDownload = async () => { |
|
31
|
if (!isDownloadSupported()) { |
|
32
|
toast({ |
|
33
|
title: "Download Not Supported", |
|
34
|
description: "Your browser doesn't support file downloads.", |
|
35
|
variant: "destructive", |
|
36
|
}); |
|
37
|
return; |
|
38
|
} |
|
39
|
|
|
40
|
setIsDownloading(true); |
|
41
|
setDownloadProgress(10); |
|
42
|
|
|
43
|
try { |
|
44
|
// Download guide |
|
45
|
const guideSuccess = await downloadGuide(guide, options); |
|
46
|
setDownloadProgress(50); |
|
47
|
|
|
48
|
if (!guideSuccess) { |
|
49
|
throw new Error("Failed to download guide"); |
|
50
|
} |
|
51
|
|
|
52
|
// Download video if requested |
|
53
|
if (options.includeVideos && videoUrl) { |
|
54
|
setDownloadProgress(75); |
|
55
|
const videoSuccess = await downloadVideo(videoUrl, `${guide.title}_video`); |
|
56
|
if (!videoSuccess) { |
|
57
|
toast({ |
|
58
|
title: "Video Download Failed", |
|
59
|
description: "The guide was downloaded, but the video failed to download.", |
|
60
|
variant: "destructive", |
|
61
|
}); |
|
62
|
} |
|
63
|
} |
|
64
|
|
|
65
|
setDownloadProgress(100); |
|
66
|
|
|
67
|
toast({ |
|
68
|
title: "Download Complete", |
|
69
|
description: `${guide.title} has been downloaded successfully.`, |
|
70
|
}); |
|
71
|
|
|
72
|
onOpenChange(false); |
|
73
|
} catch (error) { |
|
74
|
console.error("Download failed:", error); |
|
75
|
toast({ |
|
76
|
title: "Download Failed", |
|
77
|
description: "There was an error downloading the guide. Please try again.", |
|
78
|
variant: "destructive", |
|
79
|
}); |
|
80
|
} finally { |
|
81
|
setIsDownloading(false); |
|
82
|
setDownloadProgress(0); |
|
83
|
} |
|
84
|
}; |
|
85
|
|
|
86
|
const estimatedSize = getEstimatedSize(guide, options); |
|
87
|
|
|
88
|
return ( |
|
89
|
<Dialog open={open} onOpenChange={onOpenChange}> |
|
90
|
<DialogContent className="sm:max-w-md"> |
|
91
|
<DialogHeader> |
|
92
|
<DialogTitle className="flex items-center gap-2"> |
|
93
|
<Download className="h-5 w-5" /> |
|
94
|
Download Guide |
|
95
|
</DialogTitle> |
|
96
|
<DialogDescription> |
|
97
|
Download "{guide.title}" for offline access |
|
98
|
</DialogDescription> |
|
99
|
</DialogHeader> |
|
100
|
|
|
101
|
<div className="space-y-6"> |
|
102
|
{/* Format Selection */} |
|
103
|
<div className="space-y-3"> |
|
104
|
<Label className="text-sm font-medium">Download Format</Label> |
|
105
|
<RadioGroup |
|
106
|
value={options.format} |
|
107
|
onValueChange={(value) => setOptions({ ...options, format: value as 'html' | 'json' | 'pdf' })} |
|
108
|
> |
|
109
|
<div className="flex items-center space-x-2"> |
|
110
|
<RadioGroupItem value="html" id="html" /> |
|
111
|
<Label htmlFor="html" className="flex items-center gap-2"> |
|
112
|
<FileText className="h-4 w-4" /> |
|
113
|
HTML (Recommended) |
|
114
|
</Label> |
|
115
|
</div> |
|
116
|
<div className="flex items-center space-x-2"> |
|
117
|
<RadioGroupItem value="json" id="json" /> |
|
118
|
<Label htmlFor="json" className="flex items-center gap-2"> |
|
119
|
<FileText className="h-4 w-4" /> |
|
120
|
JSON (Raw Data) |
|
121
|
</Label> |
|
122
|
</div> |
|
123
|
<div className="flex items-center space-x-2"> |
|
124
|
<RadioGroupItem value="pdf" id="pdf" /> |
|
125
|
<Label htmlFor="pdf" className="flex items-center gap-2"> |
|
126
|
<FileText className="h-4 w-4" /> |
|
127
|
PDF (Print-ready) |
|
128
|
</Label> |
|
129
|
</div> |
|
130
|
</RadioGroup> |
|
131
|
</div> |
|
132
|
|
|
133
|
{/* Include Options */} |
|
134
|
<div className="space-y-3"> |
|
135
|
<Label className="text-sm font-medium">Include Content</Label> |
|
136
|
<div className="space-y-2"> |
|
137
|
<div className="flex items-center space-x-2"> |
|
138
|
<Checkbox |
|
139
|
id="include-images" |
|
140
|
checked={options.includeImages} |
|
141
|
onCheckedChange={(checked) => |
|
142
|
setOptions({ ...options, includeImages: checked as boolean }) |
|
143
|
} |
|
144
|
/> |
|
145
|
<Label htmlFor="include-images" className="flex items-center gap-2"> |
|
146
|
<Image className="h-4 w-4" /> |
|
147
|
Include Images |
|
148
|
</Label> |
|
149
|
</div> |
|
150
|
{videoUrl && ( |
|
151
|
<div className="flex items-center space-x-2"> |
|
152
|
<Checkbox |
|
153
|
id="include-videos" |
|
154
|
checked={options.includeVideos} |
|
155
|
onCheckedChange={(checked) => |
|
156
|
setOptions({ ...options, includeVideos: checked as boolean }) |
|
157
|
} |
|
158
|
/> |
|
159
|
<Label htmlFor="include-videos" className="flex items-center gap-2"> |
|
160
|
<Video className="h-4 w-4" /> |
|
161
|
Include Video |
|
162
|
</Label> |
|
163
|
</div> |
|
164
|
)} |
|
165
|
</div> |
|
166
|
</div> |
|
167
|
|
|
168
|
{/* Size Estimate */} |
|
169
|
<div className="flex items-center gap-2 text-sm text-muted-foreground"> |
|
170
|
<HardDrive className="h-4 w-4" /> |
|
171
|
Estimated size: {estimatedSize} |
|
172
|
</div> |
|
173
|
|
|
174
|
{/* Download Progress */} |
|
175
|
{isDownloading && ( |
|
176
|
<div className="space-y-2"> |
|
177
|
<div className="flex justify-between text-sm"> |
|
178
|
<span>Downloading...</span> |
|
179
|
<span>{downloadProgress}%</span> |
|
180
|
</div> |
|
181
|
<Progress value={downloadProgress} className="h-2" /> |
|
182
|
</div> |
|
183
|
)} |
|
184
|
|
|
185
|
{/* Action Buttons */} |
|
186
|
<div className="flex justify-end gap-2"> |
|
187
|
<Button |
|
188
|
variant="outline" |
|
189
|
onClick={() => onOpenChange(false)} |
|
190
|
disabled={isDownloading} |
|
191
|
> |
|
192
|
Cancel |
|
193
|
</Button> |
|
194
|
<Button |
|
195
|
onClick={handleDownload} |
|
196
|
disabled={isDownloading} |
|
197
|
className="flex items-center gap-2" |
|
198
|
> |
|
199
|
<Download className="h-4 w-4" /> |
|
200
|
{isDownloading ? "Downloading..." : "Download"} |
|
201
|
</Button> |
|
202
|
</div> |
|
203
|
</div> |
|
204
|
</DialogContent> |
|
205
|
</Dialog> |
|
206
|
); |
|
207
|
} |