Various improvements & Epub support
This commit is contained in:
@@ -21,6 +21,7 @@ export default function StoryDetailPage() {
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadStoryData = async () => {
|
||||
@@ -65,6 +66,53 @@ export default function StoryDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEPUBExport = async () => {
|
||||
if (!story) return;
|
||||
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const token = localStorage.getItem('auth-token');
|
||||
const response = await fetch(`/api/stories/${story.id}/epub`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
// Get filename from Content-Disposition header or create default
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = `${story.title}.epub`;
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (match && match[1]) {
|
||||
filename = match[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
alert('Authentication required. Please log in.');
|
||||
} else {
|
||||
throw new Error('Failed to export EPUB');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error exporting EPUB:', error);
|
||||
alert('Failed to export EPUB. Please try again.');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
@@ -358,6 +406,14 @@ export default function StoryDetailPage() {
|
||||
>
|
||||
📚 Start Reading
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleEPUBExport}
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
disabled={isExporting}
|
||||
>
|
||||
{isExporting ? 'Exporting...' : '📖 Export EPUB'}
|
||||
</Button>
|
||||
<Button
|
||||
href={`/stories/${story.id}/edit`}
|
||||
variant="ghost"
|
||||
|
||||
@@ -252,7 +252,7 @@ export default function EditStoryPage() {
|
||||
</label>
|
||||
<ImageUpload
|
||||
onImageSelect={setCoverImage}
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
accept="image/jpeg,image/png"
|
||||
maxSizeMB={5}
|
||||
aspectRatio="3:4"
|
||||
placeholder="Drop a new cover image here or click to select"
|
||||
|
||||
@@ -201,6 +201,7 @@ export default function StoryReadingPage() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const findNextStory = (): Story | null => {
|
||||
if (!story?.seriesId || seriesStories.length <= 1) return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user