504 lines
12 KiB
TypeScript
504 lines
12 KiB
TypeScript
/*
|
|
* File: ImageViewer.tsx
|
|
* Description: Full-screen DICOM image viewer with zoom, pan, and navigation
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Dimensions,
|
|
Image,
|
|
ScrollView,
|
|
StatusBar,
|
|
SafeAreaView,
|
|
} from 'react-native';
|
|
import { theme } from '../../../theme/theme';
|
|
import Icon from 'react-native-vector-icons/Feather';
|
|
import { API_CONFIG } from '../../../shared/utils';
|
|
|
|
// Get screen dimensions
|
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
|
|
// ============================================================================
|
|
// INTERFACES
|
|
// ============================================================================
|
|
|
|
interface ImageViewerProps {
|
|
visible: boolean;
|
|
images: string[];
|
|
initialIndex: number;
|
|
onClose: () => void;
|
|
patientName?: string;
|
|
seriesInfo?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// IMAGE VIEWER COMPONENT
|
|
// ============================================================================
|
|
|
|
/**
|
|
* ImageViewer Component
|
|
*
|
|
* Purpose: Full-screen DICOM image viewer with advanced viewing capabilities
|
|
*
|
|
* Features:
|
|
* - Full-screen image display
|
|
* - Image navigation (previous/next)
|
|
* - Zoom and pan functionality
|
|
* - Patient information display
|
|
* - Series information
|
|
* - Touch gestures for navigation
|
|
* - Professional medical imaging interface
|
|
*/
|
|
const ImageViewer: React.FC<ImageViewerProps> = ({
|
|
visible,
|
|
images,
|
|
initialIndex,
|
|
onClose,
|
|
patientName = 'Unknown Patient',
|
|
seriesInfo = 'DICOM Series',
|
|
}) => {
|
|
// ============================================================================
|
|
// STATE MANAGEMENT
|
|
// ============================================================================
|
|
|
|
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
const [scale, setScale] = useState(1);
|
|
const [isZoomed, setIsZoomed] = useState(false);
|
|
|
|
// ============================================================================
|
|
// EVENT HANDLERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Handle Previous Image
|
|
*
|
|
* Purpose: Navigate to previous image in series
|
|
*/
|
|
const handlePrevious = useCallback(() => {
|
|
if (currentIndex > 0) {
|
|
setCurrentIndex(currentIndex - 1);
|
|
setScale(1);
|
|
setIsZoomed(false);
|
|
}
|
|
}, [currentIndex]);
|
|
|
|
/**
|
|
* Handle Next Image
|
|
*
|
|
* Purpose: Navigate to next image in series
|
|
*/
|
|
const handleNext = useCallback(() => {
|
|
if (currentIndex < images.length - 1) {
|
|
setCurrentIndex(currentIndex + 1);
|
|
setScale(1);
|
|
setIsZoomed(false);
|
|
}
|
|
}, [currentIndex, images.length]);
|
|
|
|
/**
|
|
* Handle Zoom In
|
|
*
|
|
* Purpose: Increase image zoom level
|
|
*/
|
|
const handleZoomIn = useCallback(() => {
|
|
const newScale = Math.min(scale * 1.5, 3);
|
|
setScale(newScale);
|
|
setIsZoomed(newScale > 1);
|
|
}, [scale]);
|
|
|
|
/**
|
|
* Handle Zoom Out
|
|
*
|
|
* Purpose: Decrease image zoom level
|
|
*/
|
|
const handleZoomOut = useCallback(() => {
|
|
const newScale = Math.max(scale / 1.5, 0.5);
|
|
setScale(newScale);
|
|
setIsZoomed(newScale > 1);
|
|
}, [scale]);
|
|
|
|
/**
|
|
* Handle Reset Zoom
|
|
*
|
|
* Purpose: Reset image to original size
|
|
*/
|
|
const handleResetZoom = useCallback(() => {
|
|
setScale(1);
|
|
setIsZoomed(false);
|
|
}, []);
|
|
|
|
/**
|
|
* Handle Close
|
|
*
|
|
* Purpose: Close image viewer and return to previous screen
|
|
*/
|
|
const handleClose = useCallback(() => {
|
|
setScale(1);
|
|
setIsZoomed(false);
|
|
setCurrentIndex(initialIndex);
|
|
onClose();
|
|
}, [initialIndex, onClose]);
|
|
|
|
// ============================================================================
|
|
// RENDER HELPERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Render Header
|
|
*
|
|
* Purpose: Render image viewer header with patient info and controls
|
|
*/
|
|
const renderHeader = () => (
|
|
<View style={styles.header}>
|
|
<View style={styles.headerLeft}>
|
|
<TouchableOpacity
|
|
style={styles.closeButton}
|
|
onPress={handleClose}
|
|
>
|
|
<Icon name="x" size={24} color={theme.colors.background} />
|
|
</TouchableOpacity>
|
|
<View style={styles.patientInfo}>
|
|
<Text style={styles.patientName}>{patientName}</Text>
|
|
<Text style={styles.seriesInfo}>{seriesInfo}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.headerRight}>
|
|
<Text style={styles.imageCounter}>
|
|
{currentIndex + 1} of {images.length}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
|
|
/**
|
|
* Render Navigation Controls
|
|
*
|
|
* Purpose: Render image navigation controls
|
|
*/
|
|
const renderNavigationControls = () => (
|
|
<View style={styles.navigationControls}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.navButton,
|
|
currentIndex === 0 && styles.navButtonDisabled
|
|
]}
|
|
onPress={handlePrevious}
|
|
disabled={currentIndex === 0}
|
|
>
|
|
<Icon
|
|
name="chevron-left"
|
|
size={24}
|
|
color={currentIndex === 0 ? theme.colors.textMuted : theme.colors.background}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.navButton,
|
|
currentIndex === images.length - 1 && styles.navButtonDisabled
|
|
]}
|
|
onPress={handleNext}
|
|
disabled={currentIndex === images.length - 1}
|
|
>
|
|
<Icon
|
|
name="chevron-right"
|
|
size={24}
|
|
color={currentIndex === images.length - 1 ? theme.colors.textMuted : theme.colors.background}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
|
|
/**
|
|
* Render Zoom Controls
|
|
*
|
|
* Purpose: Render zoom control buttons
|
|
*/
|
|
const renderZoomControls = () => (
|
|
<View style={styles.zoomControls}>
|
|
<TouchableOpacity
|
|
style={styles.zoomButton}
|
|
onPress={handleZoomOut}
|
|
>
|
|
<Icon name="minus" size={20} color={theme.colors.background} />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={styles.zoomButton}
|
|
onPress={handleResetZoom}
|
|
>
|
|
<Text style={styles.zoomText}>{Math.round(scale * 100)}%</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={styles.zoomButton}
|
|
onPress={handleZoomIn}
|
|
>
|
|
<Icon name="plus" size={20} color={theme.colors.background} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
|
|
// ============================================================================
|
|
// MAIN RENDER
|
|
// ============================================================================
|
|
|
|
if (!visible || images.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<StatusBar barStyle="light-content" backgroundColor="#000000" />
|
|
|
|
{/* Header */}
|
|
{renderHeader()}
|
|
|
|
{/* Main Image Area */}
|
|
<View style={styles.imageContainer}>
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsHorizontalScrollIndicator={false}
|
|
showsVerticalScrollIndicator={false}
|
|
maximumZoomScale={3}
|
|
minimumZoomScale={0.5}
|
|
bounces={false}
|
|
>
|
|
<Image
|
|
source={{ uri: API_CONFIG.DICOM_BASE_URL + images[currentIndex] }}
|
|
style={[
|
|
styles.image,
|
|
{
|
|
transform: [{ scale }],
|
|
},
|
|
]}
|
|
resizeMode="contain"
|
|
/>
|
|
</ScrollView>
|
|
</View>
|
|
|
|
{/* Navigation Controls */}
|
|
{renderNavigationControls()}
|
|
|
|
{/* Zoom Controls */}
|
|
{renderZoomControls()}
|
|
|
|
{/* Thumbnail Strip */}
|
|
<View style={styles.thumbnailStrip}>
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.thumbnailContent}
|
|
>
|
|
{images.map((image, index) => (
|
|
<TouchableOpacity
|
|
key={index}
|
|
style={[
|
|
styles.thumbnail,
|
|
index === currentIndex && styles.activeThumbnail
|
|
]}
|
|
onPress={() => setCurrentIndex(index)}
|
|
>
|
|
<Image
|
|
source={{ uri: API_CONFIG.DICOM_BASE_URL + image }}
|
|
style={styles.thumbnailImage}
|
|
resizeMode="cover"
|
|
/>
|
|
{index === currentIndex && (
|
|
<View style={styles.activeIndicator}>
|
|
<Icon name="check" size={12} color={theme.colors.background} />
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// STYLES
|
|
// ============================================================================
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#000000',
|
|
},
|
|
|
|
// Header Styles
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.sm,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
},
|
|
headerLeft: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
},
|
|
closeButton: {
|
|
padding: theme.spacing.sm,
|
|
marginRight: theme.spacing.md,
|
|
},
|
|
patientInfo: {
|
|
flex: 1,
|
|
},
|
|
patientName: {
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
seriesInfo: {
|
|
fontSize: 12,
|
|
color: theme.colors.background,
|
|
opacity: 0.8,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
headerRight: {
|
|
alignItems: 'flex-end',
|
|
},
|
|
imageCounter: {
|
|
fontSize: 14,
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
|
|
// Image Container Styles
|
|
imageContainer: {
|
|
flex: 1,
|
|
// backgroundColor: '#000000',
|
|
},
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
flexGrow: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
image: {
|
|
width: screenWidth,
|
|
height: screenHeight * 0.7,
|
|
},
|
|
|
|
// Navigation Controls Styles
|
|
navigationControls: {
|
|
position: 'absolute',
|
|
top: '50%',
|
|
left: 0,
|
|
right: 0,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: theme.spacing.md,
|
|
transform: [{ translateY: -20 }],
|
|
},
|
|
navButton: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 24,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
shadowColor: '#000000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 4,
|
|
},
|
|
navButtonDisabled: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
},
|
|
|
|
// Zoom Controls Styles
|
|
zoomControls: {
|
|
position: 'absolute',
|
|
bottom: 100,
|
|
right: theme.spacing.md,
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
},
|
|
zoomButton: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
shadowColor: '#000000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 4,
|
|
},
|
|
zoomText: {
|
|
color: theme.colors.background,
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
|
|
// Thumbnail Strip Styles
|
|
thumbnailStrip: {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
paddingVertical: theme.spacing.sm,
|
|
},
|
|
thumbnailContent: {
|
|
paddingHorizontal: theme.spacing.md,
|
|
},
|
|
thumbnail: {
|
|
width: 60,
|
|
height: 60,
|
|
borderRadius: 8,
|
|
marginRight: theme.spacing.sm,
|
|
position: 'relative',
|
|
borderWidth: 2,
|
|
borderColor: 'transparent',
|
|
},
|
|
activeThumbnail: {
|
|
borderColor: theme.colors.primary,
|
|
},
|
|
thumbnailImage: {
|
|
width: '100%',
|
|
height: '100%',
|
|
borderRadius: 6,
|
|
},
|
|
activeIndicator: {
|
|
position: 'absolute',
|
|
top: -4,
|
|
right: -4,
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
backgroundColor: theme.colors.primary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
});
|
|
|
|
export default ImageViewer;
|
|
|
|
/*
|
|
* End of File: ImageViewer.tsx
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|