NeoScan_Radiologist/app/modules/PatientCare/components/ImageViewer.tsx
2025-08-12 18:50:19 +05:30

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.
*/