7185dca1a2
* feat(reader): add save/share button to image gallery toolbar Add a button to the top-right toolbar of the fullscreen image viewer that saves the currently viewed image to the device. It uses the native or web Share flow where available (iOS/Android/macOS, navigator.share) and falls back to a save dialog or browser download otherwise, reusing the existing export path via appService.saveFile. The button icon and label reflect the active flow (share vs save). Adds dataUrlToBytes/imageExtensionFromMime helpers, unit and component tests, and translations for the new strings across all locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(share): write shareable file to a Temp subdirectory to avoid 0-byte share On Android, Tauri's Temp dir is the app cache dir, and the sharekit plugin copies the shared file to <cacheDir>/<name> before firing the share intent. When saveFile wrote the shareable file to the Temp root, that copy became a copy onto itself whose output stream truncated the source to 0 bytes, so the shared image (and any shared export) arrived as a 0 KB file. Write the file to a Temp subdirectory instead so the plugin's copy has a distinct source. Verified on a Xiaomi device: sharing a file in the Temp root truncated it to 0 bytes, while sharing from the subdirectory produced a real, non-empty copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): save image to system gallery on Android The Android share sheet cannot save an image to a file (no file manager registers as an ACTION_SEND target), so the Save Image button now writes the image straight into the system photo gallery via MediaStore. It lands in Pictures/Readest, visible in Gallery and the Files app, with no picker and no storage permission on Android 10+. Adds a save_image_to_gallery command to the native-bridge plugin (Rust + Kotlin MediaStore insert) and an appService.saveImageToGallery method. On Android the Save button uses it; iOS/macOS/desktop/web keep the existing share/export flow, and the button label/icon reflect the actual action. Also includes local agent memory notes that were staged alongside. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
IoClose,
|
|
IoExpand,
|
|
IoAdd,
|
|
IoRemove,
|
|
IoShareOutline,
|
|
IoDownloadOutline,
|
|
} from 'react-icons/io5';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
import { useThemeStore } from '@/store/themeStore';
|
|
import { Insets } from '@/types/misc';
|
|
|
|
interface ZoomControlsProps {
|
|
gridInsets: Insets;
|
|
// Save/Share is image-specific; omit `onSave` (e.g. the table viewer) to hide it.
|
|
canShare?: boolean;
|
|
onClose: () => void;
|
|
onSave?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
onZoomIn: () => void;
|
|
onZoomOut: () => void;
|
|
onReset: () => void;
|
|
}
|
|
|
|
const ZoomControls: React.FC<ZoomControlsProps> = ({
|
|
gridInsets,
|
|
canShare,
|
|
onClose,
|
|
onSave,
|
|
onZoomIn,
|
|
onZoomOut,
|
|
onReset,
|
|
}) => {
|
|
const _ = useTranslation();
|
|
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
|
return (
|
|
<div
|
|
className='absolute right-4 top-2 z-10 grid grid-cols-1 gap-4 text-white'
|
|
style={{
|
|
marginTop: systemUIVisible
|
|
? `${Math.max(gridInsets.top, statusBarHeight)}px`
|
|
: `${gridInsets.top}px`,
|
|
}}
|
|
>
|
|
<button
|
|
onClick={onClose}
|
|
className='eink-bordered flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70'
|
|
aria-label={_('Close')}
|
|
title={_('Close')}
|
|
>
|
|
<IoClose className='h-6 w-6' />
|
|
</button>
|
|
|
|
{onSave && (
|
|
<button
|
|
onClick={onSave}
|
|
className='eink-bordered flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70'
|
|
aria-label={canShare ? _('Share Image') : _('Save Image')}
|
|
title={canShare ? _('Share Image') : _('Save Image')}
|
|
>
|
|
{canShare ? (
|
|
<IoShareOutline className='h-6 w-6' />
|
|
) : (
|
|
<IoDownloadOutline className='h-6 w-6' />
|
|
)}
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={onZoomIn}
|
|
className='eink-bordered flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70'
|
|
aria-label={_('Zoom In')}
|
|
title={_('Zoom In')}
|
|
>
|
|
<IoAdd className='h-6 w-6' />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onZoomOut}
|
|
className='eink-bordered flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70'
|
|
aria-label={_('Zoom Out')}
|
|
title={_('Zoom Out')}
|
|
>
|
|
<IoRemove className='h-6 w-6' />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onReset}
|
|
className='eink-bordered flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70'
|
|
aria-label={_('Reset Zoom')}
|
|
title={_('Reset Zoom')}
|
|
>
|
|
<IoExpand className='h-6 w-6' />
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ZoomControls;
|