Compare commits
2 Commits
80a1688e19
...
692a8156da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692a8156da | ||
|
|
413a1d74de |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-Bold.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-Bold.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-ExtraBold.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-ExtraLight.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-Light.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-Light.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-Medium.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-Medium.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-Regular.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-Regular.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-SemiBold.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/WorkSans-Thin.ttf
Normal file
BIN
android/app/src/main/assets/fonts/WorkSans-Thin.ttf
Normal file
Binary file not shown.
@ -2,36 +2,36 @@
|
||||
"migIndex": 1,
|
||||
"data": [
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Black.ttf",
|
||||
"sha1": "d1678489a8d5645f16486ec52d77b651ff0bf327"
|
||||
"path": "app/assets/fonts/WorkSans-Bold.ttf",
|
||||
"sha1": "ec84061651ead3c3c5cbb61c2d338aca0bacdc1e"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Bold.ttf",
|
||||
"sha1": "508c35dee818addce6cc6d1fb6e42f039da5a7cf"
|
||||
"path": "app/assets/fonts/WorkSans-ExtraBold.ttf",
|
||||
"sha1": "0b371d1dbfbdd15db880bbd129b239530c71accb"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-ExtraBold.ttf",
|
||||
"sha1": "3dbfd71b6fbcfbd8e7ee8a8dd033dc5aaad63249"
|
||||
"path": "app/assets/fonts/WorkSans-ExtraLight.ttf",
|
||||
"sha1": "74596e55487e2961b6c43993698d658e2ceee77b"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-ExtraLight.ttf",
|
||||
"sha1": "df556e64732e5c272349e13cb5f87591a1ae779b"
|
||||
"path": "app/assets/fonts/WorkSans-Light.ttf",
|
||||
"sha1": "293e11dae7e8b930bf5eea0b06ca979531f22189"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Light.ttf",
|
||||
"sha1": "318b44c0a32848f78bf11d4fbf3355d00647a796"
|
||||
"path": "app/assets/fonts/WorkSans-Medium.ttf",
|
||||
"sha1": "c281f8454dd193c2260e43ae2de171c5dd4086e4"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Medium.ttf",
|
||||
"sha1": "fa5192203f85ddb667579e1bdf26f12098bb873b"
|
||||
"path": "app/assets/fonts/WorkSans-Regular.ttf",
|
||||
"sha1": "5e0183b29b57c54595c62ac6bc223b21f1434226"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Regular.ttf",
|
||||
"sha1": "3bff51436aa7eb995d84cfc592cc63e1316bb400"
|
||||
"path": "app/assets/fonts/WorkSans-SemiBold.ttf",
|
||||
"sha1": "64b8fe156fafce221a0f66504255257053fc6062"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-SemiBold.ttf",
|
||||
"sha1": "9ca139684fe902c8310dd82991648376ac9838db"
|
||||
"path": "app/assets/fonts/WorkSans-Thin.ttf",
|
||||
"sha1": "a62251331038fdd079c47bc413a350efbf702db8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
502
app/assets/dicom/dicom-viewer.html
Normal file
502
app/assets/dicom/dicom-viewer.html
Normal file
@ -0,0 +1,502 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
|
||||
<title>DICOM Viewer</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: rgb(8, 0, 0);
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
#dicomImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: white;
|
||||
}
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #2196F3;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.error {
|
||||
color: #F44336;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.success {
|
||||
color: #4CAF50;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.debug {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
max-width: 300px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="dicomImage">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<div>Loading DICOM Viewer...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="debug" class="debug" style="display: none;">
|
||||
<strong>Debug Info:</strong><br>
|
||||
<div id="debugContent"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global variables
|
||||
let cornerstone = null;
|
||||
let cornerstoneWADOImageLoader = null;
|
||||
let dicomParser = null;
|
||||
let isLoaded = false;
|
||||
let currentDicomUrl = null;
|
||||
let debugMode = true;
|
||||
|
||||
// Debug logging function
|
||||
function debugLog(message, type = 'info') {
|
||||
if (debugMode) {
|
||||
console.log(`[DICOM Viewer] ${message}`);
|
||||
const debugContent = document.getElementById('debugContent');
|
||||
if (debugContent) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
debugContent.innerHTML += `<div>[${timestamp}] ${message}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show debug panel
|
||||
function showDebug() {
|
||||
if (debugMode) {
|
||||
document.getElementById('debug').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load external libraries with retry
|
||||
async function loadLibraries() {
|
||||
try {
|
||||
debugLog('Starting library loading process...');
|
||||
showDebug();
|
||||
|
||||
// Load DICOM Parser first
|
||||
await loadDicomParser();
|
||||
|
||||
// Load Cornerstone Core
|
||||
await loadCornerstoneCore();
|
||||
|
||||
// Load Cornerstone WADO Image Loader with fallback
|
||||
await loadCornerstoneWADO();
|
||||
|
||||
// Initialize viewer
|
||||
initializeViewer();
|
||||
|
||||
// If we already have a DICOM URL, load it
|
||||
if (currentDicomUrl) {
|
||||
loadDicomImage(currentDicomUrl);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
debugLog(`Error loading libraries: ${error.message}`, 'error');
|
||||
showError(`Failed to load DICOM viewer libraries: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Load DICOM Parser
|
||||
function loadDicomParser() {
|
||||
return new Promise((resolve, reject) => {
|
||||
debugLog('Loading DICOM Parser...');
|
||||
|
||||
const dicomParserScript = document.createElement('script');
|
||||
dicomParserScript.src = 'https://unpkg.com/dicom-parser@1.8.6/dist/dicomParser.min.js';
|
||||
dicomParserScript.onload = () => {
|
||||
dicomParser = window.dicomParser;
|
||||
debugLog('DICOM Parser loaded successfully');
|
||||
resolve();
|
||||
};
|
||||
dicomParserScript.onerror = (error) => {
|
||||
debugLog(`Failed to load DICOM Parser: ${error}`, 'error');
|
||||
reject(new Error('Failed to load DICOM Parser'));
|
||||
};
|
||||
|
||||
// Set timeout for loading
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('DICOM Parser loading timeout'));
|
||||
}, 10000);
|
||||
|
||||
dicomParserScript.onload = () => {
|
||||
clearTimeout(timeout);
|
||||
dicomParser = window.dicomParser;
|
||||
debugLog('DICOM Parser loaded successfully');
|
||||
resolve();
|
||||
};
|
||||
|
||||
document.head.appendChild(dicomParserScript);
|
||||
});
|
||||
}
|
||||
|
||||
// Load Cornerstone Core
|
||||
function loadCornerstoneCore() {
|
||||
return new Promise((resolve, reject) => {
|
||||
debugLog('Loading Cornerstone Core...');
|
||||
|
||||
const cornerstoneScript = document.createElement('script');
|
||||
cornerstoneScript.src = 'https://unpkg.com/cornerstone-core@2.3.0/dist/cornerstone.js';
|
||||
cornerstoneScript.onload = () => {
|
||||
cornerstone = window.cornerstone;
|
||||
debugLog('Cornerstone Core loaded successfully');
|
||||
resolve();
|
||||
};
|
||||
cornerstoneScript.onerror = (error) => {
|
||||
debugLog(`Failed to load Cornerstone Core: ${error}`, 'error');
|
||||
reject(new Error('Failed to load Cornerstone Core'));
|
||||
};
|
||||
|
||||
// Set timeout for loading
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Cornerstone Core loading timeout'));
|
||||
}, 10000);
|
||||
|
||||
cornerstoneScript.onload = () => {
|
||||
clearTimeout(timeout);
|
||||
cornerstone = window.cornerstone;
|
||||
debugLog('Cornerstone Core loaded successfully');
|
||||
resolve();
|
||||
};
|
||||
|
||||
document.head.appendChild(cornerstoneScript);
|
||||
});
|
||||
}
|
||||
|
||||
// Load Cornerstone WADO Image Loader with fallback
|
||||
function loadCornerstoneWADO() {
|
||||
return new Promise((resolve, reject) => {
|
||||
debugLog('Loading Cornerstone WADO Image Loader...');
|
||||
|
||||
// Try multiple sources for WADO loader
|
||||
const wadoSources = [
|
||||
'https://unpkg.com/cornerstone-wado-image-loader@4.17.1/dist/cornerstoneWADOImageLoader.js',
|
||||
'https://unpkg.com/cornerstone-wado-image-loader@4.16.0/dist/cornerstoneWADOImageLoader.js',
|
||||
'https://unpkg.com/cornerstone-wado-image-loader@4.15.0/dist/cornerstoneWADOImageLoader.js',
|
||||
'https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.0.1/dist/cornerstoneWADOImageLoader.js'
|
||||
];
|
||||
|
||||
let currentSourceIndex = 0;
|
||||
|
||||
function tryNextSource() {
|
||||
if (currentSourceIndex >= wadoSources.length) {
|
||||
reject(new Error('All WADO Image Loader sources failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSource = wadoSources[currentSourceIndex];
|
||||
debugLog(`Trying WADO source ${currentSourceIndex + 1}: ${currentSource}`);
|
||||
|
||||
const wadoScript = document.createElement('script');
|
||||
wadoScript.src = currentSource;
|
||||
|
||||
wadoScript.onload = () => {
|
||||
try {
|
||||
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
||||
if (cornerstoneWADOImageLoader && cornerstone) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
debugLog(`Cornerstone WADO Image Loader loaded successfully from: ${currentSource}`);
|
||||
resolve();
|
||||
} else {
|
||||
throw new Error('WADO loader not properly initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog(`WADO loader initialization failed: ${error.message}`, 'error');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}
|
||||
};
|
||||
|
||||
wadoScript.onerror = (error) => {
|
||||
debugLog(`Failed to load WADO from ${currentSource}: ${JSON.stringify(error)}`, 'error');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
};
|
||||
|
||||
// Set timeout for loading
|
||||
const timeout = setTimeout(() => {
|
||||
debugLog(`WADO loader timeout from ${currentSource}`, 'error');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}, 8000);
|
||||
|
||||
wadoScript.onload = () => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
||||
if (cornerstoneWADOImageLoader && cornerstone) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
debugLog(`Cornerstone WADO Image Loader loaded successfully from: ${currentSource}`);
|
||||
resolve();
|
||||
} else {
|
||||
throw new Error('WADO loader not properly initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog(`WADO loader initialization failed: ${error.message}`, 'error');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}
|
||||
};
|
||||
|
||||
document.head.appendChild(wadoScript);
|
||||
}
|
||||
|
||||
tryNextSource();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the viewer
|
||||
function initializeViewer() {
|
||||
try {
|
||||
const element = document.getElementById('dicomImage');
|
||||
cornerstone.enable(element);
|
||||
isLoaded = true;
|
||||
debugLog('Cornerstone viewer initialized successfully');
|
||||
} catch (error) {
|
||||
debugLog(`Error initializing viewer: ${error.message}`, 'error');
|
||||
showError(`Failed to initialize DICOM viewer: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Load DICOM image with better error handling
|
||||
async function loadDicomImage(dicomUrl) {
|
||||
if (!isLoaded) {
|
||||
debugLog('Libraries not loaded yet, storing URL');
|
||||
currentDicomUrl = dicomUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
debugLog(`Loading DICOM image from: ${dicomUrl}`);
|
||||
|
||||
// Test if URL is accessible first
|
||||
await testUrlAccessibility(dicomUrl);
|
||||
|
||||
// Try to load with DICOM Parser first for validation
|
||||
try {
|
||||
await validateDicomWithParser(dicomUrl);
|
||||
debugLog('DICOM file validated with parser');
|
||||
} catch (parserError) {
|
||||
debugLog(`DICOM parser validation failed: ${parserError.message}`, 'warning');
|
||||
// Continue anyway, as Cornerstone might still be able to handle it
|
||||
}
|
||||
|
||||
const element = document.getElementById('dicomImage');
|
||||
const image = await cornerstone.loadImage(`wadouri:${dicomUrl}`);
|
||||
cornerstone.displayImage(element, image);
|
||||
|
||||
showSuccess('DICOM image loaded successfully');
|
||||
debugLog('DICOM image loaded successfully');
|
||||
|
||||
// Send success message to React Native
|
||||
sendMessageToReactNative({
|
||||
type: 'success',
|
||||
message: 'DICOM image loaded successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
debugLog(`Error loading DICOM image: ${error.message}`, 'error');
|
||||
showError(`Failed to load DICOM image: ${error.message}`);
|
||||
|
||||
// Send error message to React Native
|
||||
sendMessageToReactNative({
|
||||
type: 'error',
|
||||
message: `Failed to load DICOM image: ${error.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DICOM file with parser
|
||||
async function validateDicomWithParser(url) {
|
||||
try {
|
||||
debugLog('Validating DICOM file with parser...');
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
if (!dicomParser) {
|
||||
throw new Error('DICOM Parser not available');
|
||||
}
|
||||
|
||||
const dataSet = dicomParser.parseDicom(arrayBuffer);
|
||||
debugLog(`DICOM file validated. Patient: ${dataSet.string('x00100010') || 'Unknown'}`);
|
||||
debugLog(`Modality: ${dataSet.string('x00080060') || 'Unknown'}`);
|
||||
debugLog(`Study Date: ${dataSet.string('x00080020') || 'Unknown'}`);
|
||||
|
||||
return dataSet;
|
||||
} catch (error) {
|
||||
debugLog(`DICOM parser validation failed: ${error.message}`, 'warning');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test URL accessibility
|
||||
async function testUrlAccessibility(url) {
|
||||
try {
|
||||
debugLog(`Testing URL accessibility: ${url}`);
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
debugLog(`Content-Type: ${contentType}`);
|
||||
|
||||
if (contentType && !contentType.includes('application/dicom') && !contentType.includes('image/')) {
|
||||
debugLog(`Warning: Unexpected content type: ${contentType}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
debugLog(`URL accessibility test failed: ${error.message}`, 'error');
|
||||
throw new Error(`URL not accessible: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Send message to React Native
|
||||
function sendMessageToReactNative(data) {
|
||||
if (window.ReactNativeWebView) {
|
||||
try {
|
||||
const message = JSON.stringify(data);
|
||||
window.ReactNativeWebView.postMessage(message);
|
||||
debugLog(`Message sent to React Native: ${message}`);
|
||||
} catch (error) {
|
||||
debugLog(`Failed to send message to React Native: ${error.message}`, 'error');
|
||||
}
|
||||
} else {
|
||||
debugLog('ReactNativeWebView not available');
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function showLoading() {
|
||||
const element = document.getElementById('dicomImage');
|
||||
element.innerHTML = `
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<div>Loading DICOM Image...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Show error state
|
||||
function showError(message) {
|
||||
const element = document.getElementById('dicomImage');
|
||||
element.innerHTML = `
|
||||
<div class="error">
|
||||
<h3>Error</h3>
|
||||
<p>${message}</p>
|
||||
<p>Please check the DICOM URL and try again.</p>
|
||||
<button onclick="retryLoading()" style="background: #2196F3; color: white; border: none; padding: 10px 20px; border-radius: 5px; margin-top: 10px;">Retry</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Show success state
|
||||
function showSuccess(message) {
|
||||
const element = document.getElementById('dicomImage');
|
||||
element.innerHTML = `
|
||||
<div class="success">
|
||||
<h3>Success</h3>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Clear success message after 2 seconds
|
||||
setTimeout(() => {
|
||||
if (element.querySelector('.success')) {
|
||||
element.innerHTML = '';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Retry loading
|
||||
function retryLoading() {
|
||||
if (currentDicomUrl) {
|
||||
debugLog('Retrying DICOM image loading...');
|
||||
loadDicomImage(currentDicomUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from React Native
|
||||
window.addEventListener('message', function(event) {
|
||||
try {
|
||||
const dicomUrl = event.data;
|
||||
debugLog(`Received DICOM URL from React Native: ${dicomUrl}`);
|
||||
|
||||
if (dicomUrl && typeof dicomUrl === 'string') {
|
||||
currentDicomUrl = dicomUrl;
|
||||
if (isLoaded) {
|
||||
loadDicomImage(dicomUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog(`Error processing message from React Native: ${error.message}`, 'error');
|
||||
showError('Invalid message received from app');
|
||||
}
|
||||
});
|
||||
|
||||
// Start loading libraries when page loads
|
||||
window.addEventListener('load', () => {
|
||||
debugLog('Page loaded, starting library loading...');
|
||||
loadLibraries();
|
||||
});
|
||||
|
||||
// Send ready message to React Native
|
||||
sendMessageToReactNative({
|
||||
type: 'ready',
|
||||
message: 'DICOM viewer HTML loaded with DICOM Parser support'
|
||||
});
|
||||
|
||||
// Global error handler
|
||||
window.addEventListener('error', function(event) {
|
||||
debugLog(`Global error: ${event.error?.message || event.message}`, 'error');
|
||||
});
|
||||
|
||||
// Unhandled promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
debugLog(`Unhandled promise rejection: ${event.reason}`, 'error');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
443
app/assets/dicom/test-dicom-viewer.html
Normal file
443
app/assets/dicom/test-dicom-viewer.html
Normal file
@ -0,0 +1,443 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DICOM Viewer Test</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.url-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.load-button {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.load-button:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
.viewer-container {
|
||||
margin-top: 20px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
min-height: 400px;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-weight: bold;
|
||||
}
|
||||
#dicomImage {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
}
|
||||
.error {
|
||||
color: #F44336;
|
||||
background: #FFEBEE;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.success {
|
||||
color: #4CAF50;
|
||||
background: #E8F5E8;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.warning {
|
||||
color: #FF9800;
|
||||
background: #FFF3E0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.sample-urls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.sample-url {
|
||||
background: #E3F2FD;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #BBDEFB;
|
||||
}
|
||||
.sample-url:hover {
|
||||
background: #BBDEFB;
|
||||
}
|
||||
.dicom-info {
|
||||
background: #F5F5F5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>DICOM Viewer Test</h1>
|
||||
<p>Test the DICOM viewer functionality in your browser before using it in React Native.</p>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Sample DICOM URLs</h3>
|
||||
<div class="sample-urls">
|
||||
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm')">
|
||||
<strong>Sample 1:</strong><br>
|
||||
LIDC-IDRI-0001
|
||||
</div>
|
||||
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm')">
|
||||
<strong>Sample 2:</strong><br>
|
||||
LIDC-IDRI-0001
|
||||
</div>
|
||||
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-003.dcm')">
|
||||
<strong>Sample 3:</strong><br>
|
||||
LIDC-IDRI-0001
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Custom DICOM URL</h3>
|
||||
<input type="text" id="customUrl" class="url-input" placeholder="Enter DICOM URL here..." />
|
||||
<button onclick="loadCustomUrl()" class="load-button">Load DICOM Image</button>
|
||||
</div>
|
||||
|
||||
<div class="viewer-container">
|
||||
<div class="status" id="status">Ready to load DICOM image</div>
|
||||
<div id="dicomImage">
|
||||
<div>Click a sample URL above or enter a custom URL to load a DICOM image</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dicomInfo" class="dicom-info" style="display: none;">
|
||||
<strong>DICOM Information:</strong><br>
|
||||
<div id="dicomInfoContent"></div>
|
||||
</div>
|
||||
|
||||
<div id="messages"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let cornerstone = null;
|
||||
let cornerstoneWADOImageLoader = null;
|
||||
let dicomParser = null;
|
||||
let isLoaded = false;
|
||||
|
||||
// Load sample URL
|
||||
function loadSampleUrl(url) {
|
||||
document.getElementById('customUrl').value = url;
|
||||
loadDicomImage(url);
|
||||
}
|
||||
|
||||
// Load custom URL
|
||||
function loadCustomUrl() {
|
||||
const url = document.getElementById('customUrl').value.trim();
|
||||
if (url) {
|
||||
loadDicomImage(url);
|
||||
} else {
|
||||
showMessage('Please enter a valid URL', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show message
|
||||
function showMessage(message, type = 'info') {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = type;
|
||||
messageDiv.textContent = message;
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
|
||||
// Remove message after 5 seconds
|
||||
setTimeout(() => {
|
||||
messageDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Update status
|
||||
function updateStatus(message) {
|
||||
document.getElementById('status').textContent = message;
|
||||
}
|
||||
|
||||
// Show DICOM info
|
||||
function showDicomInfo(info) {
|
||||
const infoDiv = document.getElementById('dicomInfo');
|
||||
const contentDiv = document.getElementById('dicomInfoContent');
|
||||
|
||||
if (info) {
|
||||
contentDiv.innerHTML = info;
|
||||
infoDiv.style.display = 'block';
|
||||
} else {
|
||||
infoDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Load libraries
|
||||
async function loadLibraries() {
|
||||
try {
|
||||
updateStatus('Loading DICOM viewer libraries...');
|
||||
|
||||
// Load DICOM Parser first
|
||||
await loadScript('https://unpkg.com/dicom-parser@1.8.6/dist/dicomParser.min.js');
|
||||
dicomParser = window.dicomParser;
|
||||
showMessage('DICOM Parser loaded successfully', 'success');
|
||||
|
||||
// Load Cornerstone Core
|
||||
await loadScript('https://unpkg.com/cornerstone-core@2.3.0/dist/cornerstone.js');
|
||||
cornerstone = window.cornerstone;
|
||||
|
||||
// Load Cornerstone WADO Image Loader with fallback
|
||||
await loadCornerstoneWADO();
|
||||
|
||||
isLoaded = true;
|
||||
updateStatus('Libraries loaded successfully');
|
||||
showMessage('All DICOM viewer libraries loaded successfully', 'success');
|
||||
|
||||
// Initialize viewer
|
||||
const element = document.getElementById('dicomImage');
|
||||
cornerstone.enable(element);
|
||||
|
||||
} catch (error) {
|
||||
updateStatus('Failed to load libraries');
|
||||
showMessage(`Failed to load libraries: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load script
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// Load Cornerstone WADO Image Loader with fallback
|
||||
function loadCornerstoneWADO() {
|
||||
return new Promise((resolve, reject) => {
|
||||
updateStatus('Loading Cornerstone WADO Image Loader...');
|
||||
|
||||
// Try multiple sources for WADO loader
|
||||
const wadoSources = [
|
||||
'https://unpkg.com/cornerstone-wado-image-loader@4.17.1/dist/cornerstoneWADOImageLoader.js',
|
||||
'https://unpkg.com/cornerstone-wado-image-loader@4.16.0/dist/cornerstoneWADOImageLoader.js',
|
||||
'https://unpkg.com/cornerstone-wado-image-loader@4.15.0/dist/cornerstoneWADOImageLoader.js',
|
||||
'https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.17.1/dist/cornerstoneWADOImageLoader.js'
|
||||
];
|
||||
|
||||
let currentSourceIndex = 0;
|
||||
|
||||
function tryNextSource() {
|
||||
if (currentSourceIndex >= wadoSources.length) {
|
||||
reject(new Error('All WADO Image Loader sources failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSource = wadoSources[currentSourceIndex];
|
||||
updateStatus(`Trying WADO source ${currentSourceIndex + 1}: ${currentSource.split('/').pop()}`);
|
||||
|
||||
const wadoScript = document.createElement('script');
|
||||
wadoScript.src = currentSource;
|
||||
|
||||
wadoScript.onload = () => {
|
||||
try {
|
||||
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
||||
if (cornerstoneWADOImageLoader && cornerstone) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
showMessage(`WADO Image Loader loaded successfully from: ${currentSource.split('/').pop()}`, 'success');
|
||||
resolve();
|
||||
} else {
|
||||
throw new Error('WADO loader not properly initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(`WADO loader initialization failed: ${error.message}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}
|
||||
};
|
||||
|
||||
wadoScript.onerror = (error) => {
|
||||
showMessage(`Failed to load WADO from ${currentSource.split('/').pop()}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
};
|
||||
|
||||
// Set timeout for loading
|
||||
const timeout = setTimeout(() => {
|
||||
showMessage(`WADO loader timeout from ${currentSource.split('/').pop()}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}, 8000);
|
||||
|
||||
wadoScript.onload = () => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
||||
if (cornerstoneWADOImageLoader && cornerstone) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
showMessage(`WADO Image Loader loaded successfully from: ${currentSource.split('/').pop()}`, 'success');
|
||||
resolve();
|
||||
} else {
|
||||
throw new Error('WADO loader not properly initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(`WADO loader initialization failed: ${error.message}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}
|
||||
};
|
||||
|
||||
document.head.appendChild(wadoScript);
|
||||
}
|
||||
|
||||
tryNextSource();
|
||||
});
|
||||
}
|
||||
|
||||
// Load DICOM image
|
||||
async function loadDicomImage(url) {
|
||||
if (!isLoaded) {
|
||||
showMessage('Libraries not loaded yet, please wait...', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateStatus('Loading DICOM image...');
|
||||
showMessage(`Loading DICOM image from: ${url}`, 'info');
|
||||
|
||||
// Test URL accessibility
|
||||
await testUrl(url);
|
||||
|
||||
// Validate DICOM with parser
|
||||
let dicomInfo = null;
|
||||
try {
|
||||
dicomInfo = await validateDicomWithParser(url);
|
||||
showMessage('DICOM file validated successfully', 'success');
|
||||
} catch (parserError) {
|
||||
showMessage(`DICOM validation warning: ${parserError.message}`, 'warning');
|
||||
}
|
||||
|
||||
const element = document.getElementById('dicomImage');
|
||||
const image = await cornerstone.loadImage(`wadouri:${url}`);
|
||||
cornerstone.displayImage(element, image);
|
||||
|
||||
updateStatus('DICOM image loaded successfully');
|
||||
showMessage('DICOM image loaded successfully!', 'success');
|
||||
|
||||
// Display DICOM information if available
|
||||
if (dicomInfo) {
|
||||
displayDicomInfo(dicomInfo);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
updateStatus('Failed to load DICOM image');
|
||||
showMessage(`Failed to load DICOM image: ${error.message}`, 'error');
|
||||
showDicomInfo(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DICOM with parser
|
||||
async function validateDicomWithParser(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
if (!dicomParser) {
|
||||
throw new Error('DICOM Parser not available');
|
||||
}
|
||||
|
||||
const dataSet = dicomParser.parseDicom(arrayBuffer);
|
||||
return dataSet;
|
||||
} catch (error) {
|
||||
throw new Error(`DICOM validation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Display DICOM information
|
||||
function displayDicomInfo(dataSet) {
|
||||
try {
|
||||
const info = [
|
||||
`Patient Name: ${dataSet.string('x00100010') || 'Unknown'}`,
|
||||
`Patient ID: ${dataSet.string('x00100020') || 'Unknown'}`,
|
||||
`Modality: ${dataSet.string('x00080060') || 'Unknown'}`,
|
||||
`Study Date: ${dataSet.string('x00080020') || 'Unknown'}`,
|
||||
`Study Description: ${dataSet.string('x00081030') || 'Unknown'}`,
|
||||
`Manufacturer: ${dataSet.string('x00080070') || 'Unknown'}`,
|
||||
`Image Size: ${dataSet.uint16('x00280010') || 'Unknown'} x ${dataSet.uint16('x00280011') || 'Unknown'}`,
|
||||
`Bits Allocated: ${dataSet.uint16('x00280100') || 'Unknown'}`,
|
||||
`Samples per Pixel: ${dataSet.uint16('x00280002') || 'Unknown'}`
|
||||
].join('<br>');
|
||||
|
||||
showDicomInfo(info);
|
||||
} catch (error) {
|
||||
console.error('Error displaying DICOM info:', error);
|
||||
showDicomInfo('Error displaying DICOM information');
|
||||
}
|
||||
}
|
||||
|
||||
// Test URL accessibility
|
||||
async function testUrl(url) {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
console.log('Content-Type:', contentType);
|
||||
|
||||
if (contentType && !contentType.includes('application/dicom') && !contentType.includes('image/')) {
|
||||
console.warn('Warning: Unexpected content type:', contentType);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`URL not accessible: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
window.addEventListener('load', loadLibraries);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/assets/fonts/WorkSans-Bold.ttf
Normal file
BIN
app/assets/fonts/WorkSans-Bold.ttf
Normal file
Binary file not shown.
BIN
app/assets/fonts/WorkSans-ExtraBold.ttf
Normal file
BIN
app/assets/fonts/WorkSans-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
app/assets/fonts/WorkSans-ExtraLight.ttf
Normal file
BIN
app/assets/fonts/WorkSans-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
app/assets/fonts/WorkSans-Light.ttf
Normal file
BIN
app/assets/fonts/WorkSans-Light.ttf
Normal file
Binary file not shown.
BIN
app/assets/fonts/WorkSans-Medium.ttf
Normal file
BIN
app/assets/fonts/WorkSans-Medium.ttf
Normal file
Binary file not shown.
BIN
app/assets/fonts/WorkSans-Regular.ttf
Normal file
BIN
app/assets/fonts/WorkSans-Regular.ttf
Normal file
Binary file not shown.
BIN
app/assets/fonts/WorkSans-SemiBold.ttf
Normal file
BIN
app/assets/fonts/WorkSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
app/assets/fonts/WorkSans-Thin.ttf
Normal file
BIN
app/assets/fonts/WorkSans-Thin.ttf
Normal file
Binary file not shown.
@ -13,7 +13,7 @@ import { theme } from '../../../theme';
|
||||
|
||||
// Import screens
|
||||
import { AIPredictionsScreen, AIPredictionDetailScreen } from '../screens';
|
||||
import { ComingSoonScreen } from '../../../shared/components';
|
||||
import { ComingSoonScreen, DicomViewer } from '../../../shared/components';
|
||||
|
||||
// Import types
|
||||
import type { AIPredictionStackParamList } from './navigationTypes';
|
||||
@ -152,7 +152,7 @@ const AIPredictionStackNavigator: React.FC = () => {
|
||||
{/* AI Prediction Details Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionDetails"
|
||||
component={AIPredictionDetailScreen}
|
||||
component={() => <DicomViewer dicomUrl={'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'} />}
|
||||
options={({ navigation, route }) => ({
|
||||
title: 'Create Suggestion',
|
||||
headerLeft: () => (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -365,7 +365,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
// Header section
|
||||
header: {
|
||||
marginBottom: theme.spacing.lg,
|
||||
// marginBottom: theme.spacing.lg,
|
||||
},
|
||||
|
||||
// Main title
|
||||
@ -465,10 +465,8 @@ const styles = StyleSheet.create({
|
||||
// Pie chart container
|
||||
pieChartContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing.lg,
|
||||
backgroundColor: theme.colors.background,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
padding: theme.spacing.md,
|
||||
},
|
||||
|
||||
// Legend container
|
||||
@ -478,7 +476,6 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
padding: theme.spacing.md,
|
||||
marginTop: theme.spacing.md,
|
||||
},
|
||||
|
||||
// Legend title
|
||||
|
||||
503
app/modules/PatientCare/components/ImageViewer.tsx
Normal file
503
app/modules/PatientCare/components/ImageViewer.tsx
Normal file
@ -0,0 +1,503 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@ -158,7 +158,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
const series = parseJsonSafely(patient.series);
|
||||
const series = parseJsonSafely(patientDetails.series);
|
||||
const typeConfig = getCaseTypeConfig(patient.type);
|
||||
|
||||
// ============================================================================
|
||||
@ -207,7 +207,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
patient.type === 'Critical' && styles.containerCritical,
|
||||
// patient.type === 'Critical' && styles.containerCritical,
|
||||
{ borderLeftColor: typeConfig.color }
|
||||
]}
|
||||
onPress={onPress}
|
||||
|
||||
@ -10,6 +10,7 @@ export { default as SearchBar } from './SearchBar';
|
||||
export { default as FilterTabs } from './FilterTabs';
|
||||
export { default as EmptyState } from './EmptyState';
|
||||
export { default as LoadingState } from './LoadingState';
|
||||
export { default as ImageViewer } from './ImageViewer';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* File: PatientCareStackNavigator.tsx
|
||||
* Description: Stack navigator for PatientCare module navigation
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
// Import screens
|
||||
import { PatientsScreen, PatientDetailsScreen } from '../screens';
|
||||
|
||||
// Import types
|
||||
import { PatientCareStackParamList } from './navigationTypes';
|
||||
|
||||
// ============================================================================
|
||||
// STACK NAVIGATOR
|
||||
// ============================================================================
|
||||
|
||||
const Stack = createStackNavigator<PatientCareStackParamList>();
|
||||
|
||||
/**
|
||||
* PatientCareStackNavigator Component
|
||||
*
|
||||
* Purpose: Provides stack navigation for PatientCare module
|
||||
*
|
||||
* Screens:
|
||||
* - PatientsScreen: Main patient list screen
|
||||
* - PatientDetailsScreen: Detailed patient information and DICOM images
|
||||
*
|
||||
* Navigation Flow:
|
||||
* PatientsScreen → PatientDetailsScreen (with patient data)
|
||||
*/
|
||||
const PatientCareStackNavigator: React.FC = () => {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName="PatientsScreen"
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
cardStyle: { backgroundColor: 'transparent' },
|
||||
cardOverlayEnabled: false,
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
}}
|
||||
>
|
||||
{/* Patients Screen - Main patient list */}
|
||||
<Stack.Screen
|
||||
name="PatientsScreen"
|
||||
component={PatientsScreen}
|
||||
options={{
|
||||
title: 'Patients',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Patient Details Screen - Comprehensive patient information */}
|
||||
<Stack.Screen
|
||||
name="PatientDetails"
|
||||
component={PatientDetailsScreen}
|
||||
options={{
|
||||
title: 'Patient Details',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default PatientCareStackNavigator;
|
||||
|
||||
/*
|
||||
* End of File: PatientCareStackNavigator.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
15
app/modules/PatientCare/navigation/index.ts
Normal file
15
app/modules/PatientCare/navigation/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Barrel export for PatientCare navigation components
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export { default as PatientCareStackNavigator } from './PatientCareStackNavigator';
|
||||
export * from './navigationTypes';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
118
app/modules/PatientCare/navigation/navigationTypes.ts
Normal file
118
app/modules/PatientCare/navigation/navigationTypes.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* File: navigationTypes.ts
|
||||
* Description: TypeScript types for PatientCare module navigation
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { MedicalCase } from '../../../shared/types';
|
||||
|
||||
// ============================================================================
|
||||
// NAVIGATION PARAMETER LISTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* PatientCareStackParamList - Defines the parameter list for PatientCare stack navigator
|
||||
*
|
||||
* This interface defines all the screens available in the PatientCare module
|
||||
* and their associated navigation parameters.
|
||||
*/
|
||||
export type PatientCareStackParamList = {
|
||||
// Patients Screen - Main patient list with search and filtering
|
||||
PatientsScreen: PatientsScreenParams;
|
||||
|
||||
// Patient Details Screen - Comprehensive patient information and DICOM images
|
||||
PatientDetails: PatientDetailsScreenParams;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// SCREEN PARAMETER INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* PatientsScreenParams
|
||||
*
|
||||
* Purpose: Parameters for the patients list screen
|
||||
*
|
||||
* Parameters:
|
||||
* - None required - this is the main entry point
|
||||
*/
|
||||
export interface PatientsScreenParams {
|
||||
// No parameters required for main patients screen
|
||||
}
|
||||
|
||||
/**
|
||||
* PatientDetailsScreenParams
|
||||
*
|
||||
* Purpose: Parameters for the patient details screen
|
||||
*
|
||||
* Parameters:
|
||||
* - patientId: Required patient ID to display details
|
||||
* - patientName: Required patient name for display
|
||||
* - medicalCase: Required medical case data with full patient information
|
||||
*/
|
||||
export interface PatientDetailsScreenParams {
|
||||
patientId: string;
|
||||
patientName: string;
|
||||
medicalCase: MedicalCase;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NAVIGATION PROP TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* PatientCareNavigationProp - Navigation prop type for PatientCare screens
|
||||
*
|
||||
* Purpose: Provides type-safe navigation methods for PatientCare module screens
|
||||
*/
|
||||
export type PatientCareNavigationProp = StackNavigationProp<PatientCareStackParamList>;
|
||||
|
||||
// ============================================================================
|
||||
// SCREEN PROP TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* PatientsScreenProps - Props for PatientsScreen component
|
||||
*/
|
||||
export interface PatientsScreenProps {
|
||||
navigation: PatientCareNavigationProp;
|
||||
route: {
|
||||
params: PatientsScreenParams;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* PatientDetailsScreenProps - Props for PatientDetailsScreen component
|
||||
*/
|
||||
export interface PatientDetailsScreenProps {
|
||||
navigation: PatientCareNavigationProp;
|
||||
route: {
|
||||
params: PatientDetailsScreenParams;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NAVIGATION UTILITY TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* NavigationHelper - Helper type for navigation functions
|
||||
*
|
||||
* Purpose: Provides type-safe navigation helper functions
|
||||
*/
|
||||
export type NavigationHelper = {
|
||||
navigateToPatientDetails: (
|
||||
patientId: string,
|
||||
patientName: string,
|
||||
medicalCase: MedicalCase
|
||||
) => void;
|
||||
navigateBack: () => void;
|
||||
};
|
||||
|
||||
/*
|
||||
* End of File: navigationTypes.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -26,7 +26,6 @@ export const fetchPatients = createAsyncThunk(
|
||||
try {
|
||||
// Make actual API call to fetch medical cases
|
||||
const response :any = await patientAPI.getPatients(token);
|
||||
console.log('patients response',response)
|
||||
if (response.ok && response.data&&response.data.success) {
|
||||
// Add random case types to each patient record
|
||||
const caseTypes: Array<'Critical' | 'Emergency' | 'Routine'> = ['Critical', 'Emergency', 'Routine'];
|
||||
@ -36,7 +35,6 @@ export const fetchPatients = createAsyncThunk(
|
||||
type: caseTypes[Math.floor(Math.random() * caseTypes.length)]
|
||||
}));
|
||||
|
||||
console.log('patients with random types', patientsWithTypes);
|
||||
return patientsWithTypes as MedicalCase[];
|
||||
} else {
|
||||
// Fallback to mock data for development
|
||||
@ -125,7 +123,7 @@ export const fetchPatientDetails = createAsyncThunk(
|
||||
async (patientId: string, { rejectWithValue }) => {
|
||||
try {
|
||||
// TODO: Replace with actual API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve as any, 1000));
|
||||
|
||||
// Mock patient details for specific patient
|
||||
const mockPatient: MedicalCase = {
|
||||
@ -179,7 +177,7 @@ export const updatePatient = createAsyncThunk(
|
||||
async (patientData: Partial<MedicalCase> & { id: number }, { rejectWithValue }) => {
|
||||
try {
|
||||
// TODO: Replace with actual API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
await new Promise((resolve) => setTimeout(resolve as any, 800));
|
||||
return patientData;
|
||||
} catch (error) {
|
||||
return rejectWithValue('Failed to update patient.');
|
||||
@ -388,8 +386,8 @@ const patientCareSlice = createSlice({
|
||||
state.isLoading = false;
|
||||
state.patients = action.payload;
|
||||
state.totalItems = action.payload.length;
|
||||
state.lastUpdated = new Date();
|
||||
state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes
|
||||
state.lastUpdated = new Date().toLocaleDateString();
|
||||
state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000).toLocaleDateString(); // 5 minutes
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchPatients.rejected, (state, action) => {
|
||||
|
||||
1042
app/modules/PatientCare/screens/PatientDetailsScreen.tsx
Normal file
1042
app/modules/PatientCare/screens/PatientDetailsScreen.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -56,6 +56,7 @@ import LoadingState from '../components/LoadingState';
|
||||
|
||||
// Import types
|
||||
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
|
||||
import { PatientsScreenProps } from '../navigation/navigationTypes';
|
||||
|
||||
// Get screen dimensions
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
@ -64,10 +65,6 @@ const { width: screenWidth } = Dimensions.get('window');
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface PatientsScreenProps {
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PATIENTS SCREEN COMPONENT
|
||||
// ============================================================================
|
||||
@ -241,7 +238,7 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
|
||||
navigation.navigate('PatientDetails', {
|
||||
patientId: patient.id,
|
||||
patientId:'1',
|
||||
patientName: patientData.Name || 'Unknown Patient',
|
||||
medicalCase: patient,
|
||||
});
|
||||
@ -397,7 +394,7 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
style={styles.headerButton}
|
||||
onPress={() => {
|
||||
// TODO: Implement notifications screen
|
||||
navigation.navigate('Notifications');
|
||||
Alert.alert('Notifications', 'Notifications feature coming soon');
|
||||
}}
|
||||
>
|
||||
<Icon name="bell" size={20} color={theme.colors.textSecondary} />
|
||||
@ -473,15 +470,15 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
/>
|
||||
}
|
||||
// Performance optimizations
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={10}
|
||||
initialNumToRender={8}
|
||||
getItemLayout={(data, index) => ({
|
||||
length: 120, // Approximate height of PatientCard
|
||||
offset: 120 * index,
|
||||
index,
|
||||
})}
|
||||
// removeClippedSubviews={true}
|
||||
// maxToRenderPerBatch={10}
|
||||
// windowSize={10}
|
||||
// initialNumToRender={8}
|
||||
// getItemLayout={(data, index) => ({
|
||||
// length: 120, // Approximate height of PatientCard
|
||||
// offset: 120 * index,
|
||||
// index,
|
||||
// })}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
export { default as PatientsScreen } from './PatientsScreen';
|
||||
export { default as PatientDetailsScreen } from './PatientDetailsScreen';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
|
||||
@ -34,7 +34,7 @@ const Stack = createStackNavigator<SettingsStackParamList>();
|
||||
const SettingsStackNavigator: React.FC = () => {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName="Settings"
|
||||
initialRouteName="SettingScreen"
|
||||
screenOptions={{
|
||||
// Header styling for settings screens
|
||||
headerStyle: {
|
||||
@ -71,7 +71,7 @@ const SettingsStackNavigator: React.FC = () => {
|
||||
>
|
||||
{/* Settings Screen - Main settings entry point */}
|
||||
<Stack.Screen
|
||||
name="Settings"
|
||||
name="SettingScreen"
|
||||
component={SettingsScreen}
|
||||
options={{
|
||||
title: 'Settings',
|
||||
|
||||
@ -16,7 +16,7 @@ import { UserProfile, UserPreferences } from '../../../shared/types';
|
||||
*/
|
||||
export type SettingsStackParamList = {
|
||||
// Settings screen - Main settings with profile and preferences
|
||||
Settings: SettingsScreenParams;
|
||||
SettingScreen: SettingsScreenParams;
|
||||
|
||||
// Profile Edit screen - Edit user profile information
|
||||
ProfileEdit: ProfileEditScreenParams;
|
||||
|
||||
@ -108,7 +108,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
const userProfilePhoto = useAppSelector(selectUserProfilePhoto);
|
||||
const notificationPreferences = useAppSelector(selectNotificationPreferences);
|
||||
const dashboardSettings = useAppSelector(selectDashboardSettings);
|
||||
console.log('user details i got', user);
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// SETTINGS SECTIONS GENERATION
|
||||
|
||||
@ -14,7 +14,7 @@ import { AIPredictionStackNavigator } from '../modules/AIPrediction/navigation';
|
||||
import { MainTabParamList } from './navigationTypes';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { ComingSoonScreen } from '../shared/components';
|
||||
import { PatientsScreen } from '../modules/PatientCare';
|
||||
import { PatientCareStackNavigator } from '../modules/PatientCare/navigation';
|
||||
|
||||
// Create the bottom tab navigator
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||
@ -86,7 +86,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
{/* Patients Tab - Patient list and management */}
|
||||
<Tab.Screen
|
||||
name="Patients"
|
||||
component={PatientsScreen} // TODO: Replace with actual PatientsScreen
|
||||
component={PatientCareStackNavigator}
|
||||
options={{
|
||||
title: 'Patient List',
|
||||
tabBarLabel: 'Patients',
|
||||
|
||||
207
app/shared/components/DICOM_VIEWER_README.md
Normal file
207
app/shared/components/DICOM_VIEWER_README.md
Normal file
@ -0,0 +1,207 @@
|
||||
# DICOM Viewer Component
|
||||
|
||||
## Overview
|
||||
The DICOM Viewer component is a React Native component that uses WebView to display DICOM medical imaging files. It integrates with Cornerstone.js and Cornerstone WADO Image Loader for robust DICOM file handling.
|
||||
|
||||
## Features
|
||||
- ✅ WebView-based DICOM rendering
|
||||
- ✅ Cornerstone.js integration for medical imaging
|
||||
- ✅ Support for remote DICOM URLs
|
||||
- ✅ Loading states and error handling
|
||||
- ✅ Real-time communication with React Native
|
||||
- ✅ Responsive design for mobile devices
|
||||
|
||||
## Components
|
||||
|
||||
### 1. DicomViewer
|
||||
The main DICOM viewer component.
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface DicomViewerProps {
|
||||
dicomUrl: string; // URL to the DICOM file
|
||||
onError?: (error: string) => void; // Error callback
|
||||
onLoad?: () => void; // Load success callback
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { DicomViewer } from '../shared/components';
|
||||
|
||||
<DicomViewer
|
||||
dicomUrl="https://example.com/sample.dcm"
|
||||
onError={(error) => console.error('DICOM Error:', error)}
|
||||
onLoad={() => console.log('DICOM loaded successfully')}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2. DicomViewerTest
|
||||
A test component for testing different DICOM URLs and debugging issues.
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { DicomViewerTest } from '../shared/components';
|
||||
|
||||
<DicomViewerTest />
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. WebView Setup
|
||||
- Loads a local HTML file (`dicom-viewer.html`)
|
||||
- Enables JavaScript and DOM storage
|
||||
- Allows file access and universal access from file URLs
|
||||
|
||||
### 2. Library Loading
|
||||
- Dynamically loads Cornerstone.js from CDN
|
||||
- Loads Cornerstone WADO Image Loader
|
||||
- Initializes the viewer when libraries are ready
|
||||
|
||||
### 3. DICOM Processing
|
||||
- Receives DICOM URL from React Native via postMessage
|
||||
- Uses Cornerstone to load and display the DICOM image
|
||||
- Handles errors and success states
|
||||
|
||||
### 4. Communication
|
||||
- Sends status messages back to React Native
|
||||
- Reports loading, success, and error states
|
||||
- Enables debugging and user feedback
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Black Screen Issues
|
||||
|
||||
#### 1. Check Console Logs
|
||||
Open the React Native debugger and check for:
|
||||
- WebView loading errors
|
||||
- JavaScript execution errors
|
||||
- Network request failures
|
||||
|
||||
#### 2. Verify DICOM URL
|
||||
- Ensure the URL is accessible from the device
|
||||
- Check if the URL returns a valid DICOM file
|
||||
- Verify CORS settings if loading from a different domain
|
||||
|
||||
#### 3. Library Loading Issues
|
||||
- Check internet connectivity (libraries load from CDN)
|
||||
- Verify the HTML file path is correct
|
||||
- Check WebView permissions and settings
|
||||
|
||||
#### 4. Platform-Specific Issues
|
||||
|
||||
**Android:**
|
||||
- Ensure `allowFileAccess` is enabled
|
||||
- Check if the HTML file is in the correct assets folder
|
||||
- Verify WebView permissions in AndroidManifest.xml
|
||||
|
||||
**iOS:**
|
||||
- Check WebView configuration in Info.plist
|
||||
- Ensure JavaScript is enabled
|
||||
- Verify file access permissions
|
||||
|
||||
### Common Error Messages
|
||||
|
||||
#### "Failed to load DICOM viewer libraries"
|
||||
- Check internet connectivity
|
||||
- Verify CDN URLs are accessible
|
||||
- Check WebView JavaScript settings
|
||||
|
||||
#### "Failed to load DICOM image"
|
||||
- Verify DICOM URL is accessible
|
||||
- Check if the file is a valid DICOM format
|
||||
- Ensure the server supports CORS
|
||||
|
||||
#### "Invalid message received from app"
|
||||
- Check the message format being sent
|
||||
- Verify the postMessage implementation
|
||||
- Check WebView message handling
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Use Sample URLs
|
||||
The test component includes sample DICOM URLs that are known to work:
|
||||
- Sample DICOM 1-3 from OHIF examples
|
||||
|
||||
### 2. Test Custom URLs
|
||||
- Enter your own DICOM URLs
|
||||
- Test with different file formats
|
||||
- Verify error handling
|
||||
|
||||
### 3. Debug Mode
|
||||
- Check console logs in React Native debugger
|
||||
- Monitor WebView messages
|
||||
- Use the test component for isolated testing
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### 1. Image Optimization
|
||||
- Use compressed DICOM files when possible
|
||||
- Consider implementing progressive loading
|
||||
- Cache frequently accessed images
|
||||
|
||||
### 2. Memory Management
|
||||
- Dispose of WebView when not in use
|
||||
- Monitor memory usage with large DICOM files
|
||||
- Implement proper cleanup in useEffect
|
||||
|
||||
### 3. Network Optimization
|
||||
- Use CDN for DICOM files when possible
|
||||
- Implement retry logic for failed requests
|
||||
- Consider offline caching for critical images
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. URL Validation
|
||||
- Validate DICOM URLs before loading
|
||||
- Implement URL whitelisting if needed
|
||||
- Sanitize user input for custom URLs
|
||||
|
||||
### 2. WebView Security
|
||||
- Limit WebView permissions to minimum required
|
||||
- Implement proper origin whitelisting
|
||||
- Monitor for malicious content
|
||||
|
||||
### 3. Data Privacy
|
||||
- Ensure DICOM files don't contain PHI
|
||||
- Implement proper data handling protocols
|
||||
- Follow HIPAA compliance guidelines
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### 1. Offline Support
|
||||
- Bundle Cornerstone libraries locally
|
||||
- Implement offline DICOM caching
|
||||
- Support for local DICOM files
|
||||
|
||||
### 2. Advanced Features
|
||||
- Multi-planar reconstruction (MPR)
|
||||
- Measurement tools
|
||||
- Annotation capabilities
|
||||
- 3D rendering support
|
||||
|
||||
### 3. Performance Improvements
|
||||
- WebAssembly integration
|
||||
- GPU acceleration
|
||||
- Progressive image loading
|
||||
- Background processing
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
1. Check this README for common solutions
|
||||
2. Review console logs and error messages
|
||||
3. Test with sample URLs first
|
||||
4. Verify WebView configuration
|
||||
5. Check platform-specific requirements
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `react-native-webview`: WebView component
|
||||
- `cornerstone-core`: Medical imaging library
|
||||
- `cornerstone-wado-image-loader`: DICOM file loader
|
||||
|
||||
## License
|
||||
|
||||
Design & Developed by Tech4Biz Solutions
|
||||
Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
306
app/shared/components/DicomViewer.tsx
Normal file
306
app/shared/components/DicomViewer.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
/*
|
||||
* File: DicomViewer.tsx
|
||||
* Description: DICOM viewer component using WebView for medical imaging
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||
import { Platform, View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
|
||||
// Interface for component props
|
||||
interface DicomViewerProps {
|
||||
dicomUrl: string;
|
||||
onError?: (error: string) => void;
|
||||
onLoad?: () => void;
|
||||
debugMode?: boolean;
|
||||
}
|
||||
|
||||
// Interface for WebView reference
|
||||
interface WebViewRef {
|
||||
postMessage: (message: string) => void;
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = false }: DicomViewerProps): React.ReactElement {
|
||||
const webViewRef = useRef<WebViewRef>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [debugInfo, setDebugInfo] = useState<string[]>([]);
|
||||
const [webViewReady, setWebViewReady] = useState(false);
|
||||
|
||||
// Debug logging function
|
||||
const debugLog = (message: string) => {
|
||||
if (debugMode) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logMessage = `[${timestamp}] ${message}`;
|
||||
console.log(logMessage);
|
||||
setDebugInfo(prev => [...prev.slice(-9), logMessage]); // Keep last 10 messages
|
||||
}
|
||||
};
|
||||
|
||||
// Handle WebView load events
|
||||
const handleLoadStart = () => {
|
||||
debugLog('WebView load started');
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
};
|
||||
|
||||
const handleLoadEnd = () => {
|
||||
debugLog('WebView load ended');
|
||||
setIsLoading(false);
|
||||
setWebViewReady(true);
|
||||
onLoad?.();
|
||||
};
|
||||
|
||||
const handleError = (error: any) => {
|
||||
debugLog(`WebView error: ${JSON.stringify(error)}`);
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
onError?.(error?.nativeEvent?.description || 'Failed to load DICOM viewer');
|
||||
};
|
||||
|
||||
const handleMessage = (event: WebViewMessageEvent) => {
|
||||
try {
|
||||
const message = event.nativeEvent.data;
|
||||
debugLog(`Message from WebView: ${message}`);
|
||||
|
||||
// Try to parse JSON message
|
||||
if (typeof message === 'string') {
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
debugLog(`Parsed message: ${JSON.stringify(parsedMessage)}`);
|
||||
|
||||
if (parsedMessage.type === 'error') {
|
||||
setHasError(true);
|
||||
onError?.(parsedMessage.message);
|
||||
} else if (parsedMessage.type === 'success') {
|
||||
setHasError(false);
|
||||
}
|
||||
} catch (parseError) {
|
||||
debugLog(`Failed to parse message as JSON: ${parseError}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog(`Error handling WebView message: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Send DICOM URL to WebView when component mounts or URL changes
|
||||
useEffect(() => {
|
||||
if (webViewRef.current && dicomUrl && webViewReady) {
|
||||
debugLog(`Sending DICOM URL to WebView: ${dicomUrl}`);
|
||||
|
||||
// Wait a bit for WebView to be ready
|
||||
const timer = setTimeout(() => {
|
||||
if (webViewRef.current) {
|
||||
try {
|
||||
webViewRef.current.postMessage(dicomUrl);
|
||||
debugLog('DICOM URL sent successfully');
|
||||
} catch (error) {
|
||||
debugLog(`Failed to send DICOM URL: ${error}`);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [dicomUrl, webViewReady]);
|
||||
|
||||
// Reload WebView if there's an error
|
||||
const handleRetry = () => {
|
||||
debugLog('Retrying WebView load');
|
||||
if (webViewRef.current) {
|
||||
setHasError(false);
|
||||
setIsLoading(true);
|
||||
setWebViewReady(false);
|
||||
webViewRef.current.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Clear debug info
|
||||
const clearDebugInfo = () => {
|
||||
setDebugInfo([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<WebView
|
||||
ref={webViewRef as any}
|
||||
source={require('../../assets/dicom/dicom-viewer.html')}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
domStorageEnabled
|
||||
allowFileAccess
|
||||
allowUniversalAccessFromFileURLs
|
||||
allowFileAccessFromFileURLs
|
||||
onLoadStart={handleLoadStart}
|
||||
onLoadEnd={handleLoadEnd}
|
||||
onError={handleError}
|
||||
onMessage={handleMessage}
|
||||
style={styles.webview}
|
||||
startInLoadingState
|
||||
renderLoading={() => (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#2196F3" />
|
||||
<Text style={styles.loadingText}>Loading DICOM Viewer...</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
{hasError && (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Failed to load DICOM viewer</Text>
|
||||
<Text style={styles.errorDetails}>
|
||||
URL: {dicomUrl}
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={handleRetry}>
|
||||
<Text style={styles.retryButtonText}>Tap to retry</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{debugMode && (
|
||||
<View style={styles.debugContainer}>
|
||||
<View style={styles.debugHeader}>
|
||||
<Text style={styles.debugTitle}>Debug Info</Text>
|
||||
<TouchableOpacity onPress={clearDebugInfo}>
|
||||
<Text style={styles.clearButton}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.debugContent}>
|
||||
{debugInfo.map((info, index) => (
|
||||
<Text key={index} style={styles.debugText}>{info}</Text>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.debugStatus}>
|
||||
<Text style={styles.debugStatusText}>
|
||||
WebView Ready: {webViewReady ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
<Text style={styles.debugStatusText}>
|
||||
Loading: {isLoading ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
<Text style={styles.debugStatusText}>
|
||||
Error: {hasError ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
webview: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
loadingText: {
|
||||
color: '#FFF',
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
padding: 20,
|
||||
},
|
||||
errorText: {
|
||||
color: '#F44336',
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorDetails: {
|
||||
color: '#FF9800',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
retryButton: {
|
||||
backgroundColor: '#2196F3',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
debugContainer: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
backgroundColor: 'rgba(0,0,0,0.9)',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
maxWidth: 300,
|
||||
maxHeight: 400,
|
||||
},
|
||||
debugHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
debugTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
clearButton: {
|
||||
color: '#2196F3',
|
||||
fontSize: 12,
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
debugContent: {
|
||||
maxHeight: 200,
|
||||
},
|
||||
debugText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: 2,
|
||||
},
|
||||
debugStatus: {
|
||||
marginTop: 8,
|
||||
paddingTop: 8,
|
||||
borderTopColor: '#333',
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
debugStatusText: {
|
||||
color: '#CCC',
|
||||
fontSize: 10,
|
||||
marginBottom: 2,
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: DicomViewer.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
252
app/shared/components/DicomViewerTest.tsx
Normal file
252
app/shared/components/DicomViewerTest.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* File: DicomViewerTest.tsx
|
||||
* Description: Test component for DICOM viewer with sample URLs
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView, Alert } from 'react-native';
|
||||
import DicomViewer from './DicomViewer';
|
||||
|
||||
// Sample DICOM URLs for testing
|
||||
const SAMPLE_DICOM_URLS = [
|
||||
{
|
||||
name: 'Sample DICOM 1',
|
||||
url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'
|
||||
},
|
||||
{
|
||||
name: 'Sample DICOM 2',
|
||||
url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm'
|
||||
},
|
||||
{
|
||||
name: 'Sample DICOM 3',
|
||||
url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-003.dcm'
|
||||
}
|
||||
];
|
||||
|
||||
export default function DicomViewerTest(): React.ReactElement {
|
||||
const [dicomUrl, setDicomUrl] = useState(SAMPLE_DICOM_URLS[0].url);
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
|
||||
const handleUrlSelect = (url: string) => {
|
||||
setDicomUrl(url);
|
||||
setCustomUrl(url);
|
||||
};
|
||||
|
||||
const handleCustomUrlSubmit = () => {
|
||||
if (customUrl.trim()) {
|
||||
setDicomUrl(customUrl.trim());
|
||||
} else {
|
||||
Alert.alert('Error', 'Please enter a valid URL');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewerError = (error: string) => {
|
||||
console.error('DICOM Viewer Error:', error);
|
||||
Alert.alert('DICOM Viewer Error', error);
|
||||
};
|
||||
|
||||
const handleViewerLoad = () => {
|
||||
console.log('DICOM Viewer loaded successfully');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>DICOM Viewer Test</Text>
|
||||
<Text style={styles.subtitle}>Test different DICOM URLs</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.urlSection}>
|
||||
<Text style={styles.sectionTitle}>Sample DICOM URLs:</Text>
|
||||
{SAMPLE_DICOM_URLS.map((sample, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[
|
||||
styles.urlButton,
|
||||
dicomUrl === sample.url && styles.selectedUrlButton
|
||||
]}
|
||||
onPress={() => handleUrlSelect(sample.url)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.urlButtonText,
|
||||
dicomUrl === sample.url && styles.selectedUrlButtonText
|
||||
]}>
|
||||
{sample.name}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.urlText,
|
||||
dicomUrl === sample.url && styles.selectedUrlText
|
||||
]}>
|
||||
{sample.url}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
<View style={styles.customUrlSection}>
|
||||
<Text style={styles.sectionTitle}>Custom DICOM URL:</Text>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
value={customUrl}
|
||||
onChangeText={setCustomUrl}
|
||||
placeholder="Enter DICOM URL..."
|
||||
placeholderTextColor="#999"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.submitButton}
|
||||
onPress={handleCustomUrlSubmit}
|
||||
>
|
||||
<Text style={styles.submitButtonText}>Load</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.currentUrlSection}>
|
||||
<Text style={styles.sectionTitle}>Current URL:</Text>
|
||||
<Text style={styles.currentUrl}>{dicomUrl}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.viewerContainer}>
|
||||
<Text style={styles.viewerTitle}>DICOM Viewer:</Text>
|
||||
<DicomViewer
|
||||
dicomUrl={dicomUrl}
|
||||
onError={handleViewerError}
|
||||
onLoad={handleViewerLoad}
|
||||
debugMode={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
header: {
|
||||
backgroundColor: '#2196F3',
|
||||
padding: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#E3F2FD',
|
||||
},
|
||||
urlSection: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#212121',
|
||||
marginBottom: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
urlButton: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E0E0E0',
|
||||
},
|
||||
selectedUrlButton: {
|
||||
backgroundColor: '#E3F2FD',
|
||||
borderColor: '#2196F3',
|
||||
},
|
||||
urlButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#212121',
|
||||
marginBottom: 4,
|
||||
},
|
||||
selectedUrlButtonText: {
|
||||
color: '#1976D2',
|
||||
},
|
||||
urlText: {
|
||||
fontSize: 12,
|
||||
color: '#757575',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
selectedUrlText: {
|
||||
color: '#1976D2',
|
||||
},
|
||||
customUrlSection: {
|
||||
marginTop: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E0E0E0',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 14,
|
||||
color: '#212121',
|
||||
marginRight: 8,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#4CAF50',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
submitButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
currentUrlSection: {
|
||||
marginTop: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E0E0E0',
|
||||
},
|
||||
currentUrl: {
|
||||
fontSize: 12,
|
||||
color: '#757575',
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: '#F5F5F5',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
viewerContainer: {
|
||||
flex: 2,
|
||||
backgroundColor: '#000000',
|
||||
margin: 16,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
viewerTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
padding: 12,
|
||||
backgroundColor: '#333333',
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: DicomViewerTest.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -15,6 +15,12 @@ export { CustomModal } from './CustomModal';
|
||||
// Coming Soon Screen Component
|
||||
export { ComingSoonScreen } from './ComingSoonScreen';
|
||||
|
||||
// DICOM Viewer Component
|
||||
export { default as DicomViewer } from './DicomViewer';
|
||||
|
||||
// DICOM Viewer Test Component
|
||||
export { default as DicomViewerTest } from './DicomViewerTest';
|
||||
|
||||
// ============================================================================
|
||||
// TYPE EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
@ -150,8 +150,8 @@ export interface PatientCareState {
|
||||
totalItems: number;
|
||||
|
||||
// Cache
|
||||
lastUpdated: Date | null;
|
||||
cacheExpiry: Date | null;
|
||||
lastUpdated: string | null;
|
||||
cacheExpiry: string | null;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -9,6 +9,7 @@ import Config from 'react-native-config';
|
||||
// API Configuration
|
||||
export const API_CONFIG = {
|
||||
BASE_URL:Config.BASE_URL,
|
||||
DICOM_BASE_URL:'https://demo.medpacsystems.com',
|
||||
TIMEOUT: 30000,
|
||||
RETRY_ATTEMPTS: 3,
|
||||
RETRY_DELAY: 1000,
|
||||
|
||||
@ -8,12 +8,12 @@
|
||||
export const typography = {
|
||||
// Font Families
|
||||
fontFamily: {
|
||||
bold: 'Roboto-Bold',
|
||||
medium: 'Roboto-Medium',
|
||||
regular: 'Roboto-Regular',
|
||||
light: 'Roboto-Light',
|
||||
semibold: 'Roboto-SemiBold',
|
||||
extrabold: 'Roboto-ExtraBold',
|
||||
bold: 'WorkSans-Bold',
|
||||
medium: 'WorkSans-Medium',
|
||||
regular: 'WorkSans-Regular',
|
||||
light: 'WorkSans-Light',
|
||||
semibold: 'WorkSans-SemiBold',
|
||||
extrabold: 'WorkSans-ExtraBold',
|
||||
},
|
||||
|
||||
// Font Weights
|
||||
|
||||
@ -11,14 +11,14 @@
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
9BFF7E8F967043379EA34EF5 /* Roboto-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C93B020BF2D44311BFAF317D /* Roboto-Black.ttf */; };
|
||||
64ADC498A16B4DD2979F9EC6 /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E8DE31AC3A3B46AFBAB8E623 /* Roboto-Bold.ttf */; };
|
||||
9A05BCBBB57F42CF891B4E76 /* Roboto-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6D158A6D27784CB3AAFC4572 /* Roboto-ExtraBold.ttf */; };
|
||||
E9AB985237534D1A92E3A5C8 /* Roboto-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6787284EF58F40B4A29BE810 /* Roboto-ExtraLight.ttf */; };
|
||||
A345282A09764F4B8935E1CE /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F3B7B24AB39048E28AA013C6 /* Roboto-Light.ttf */; };
|
||||
6C3A045CF24641D79616809F /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 57615910A7564380B1D23731 /* Roboto-Medium.ttf */; };
|
||||
A79B0AFEC3DA42AAAA56DC75 /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B9D41D27862846A8A6563185 /* Roboto-Regular.ttf */; };
|
||||
264B0BD7896E430EB2761212 /* Roboto-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7539BBD08F0743178F5517A3 /* Roboto-SemiBold.ttf */; };
|
||||
90EC1A731F2F480594C0F9D1 /* WorkSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6606F41B1382422DA695F61C /* WorkSans-Bold.ttf */; };
|
||||
7E1975A765074059A27FA6F1 /* WorkSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1569DC7537534ED39A79EE9E /* WorkSans-ExtraBold.ttf */; };
|
||||
E09CC0AFCD63425FABA5714F /* WorkSans-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FF48E43749B74116A2C4083C /* WorkSans-ExtraLight.ttf */; };
|
||||
6A06861DDC314E49B482B4EB /* WorkSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8C4C96FCA144E859DC2AFDA /* WorkSans-Light.ttf */; };
|
||||
3BEFE3BEC3334662878A37D5 /* WorkSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55E647ABF16B47F6A83403C0 /* WorkSans-Medium.ttf */; };
|
||||
6E9607F0F9DE4649802D618B /* WorkSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 589149FBA9F64E9F94AA50AF /* WorkSans-Regular.ttf */; };
|
||||
DCDB35B731E1422DBD1481D1 /* WorkSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A58733FB00504503BFBF4295 /* WorkSans-SemiBold.ttf */; };
|
||||
25B52D1C5AB64F039BEF062F /* WorkSans-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C3614D19A4004E3E858E4CEC /* WorkSans-Thin.ttf */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -43,14 +43,14 @@
|
||||
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = NeoScan_Physician/AppDelegate.swift; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = NeoScan_Physician/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
C93B020BF2D44311BFAF317D /* Roboto-Black.ttf */ = {isa = PBXFileReference; name = "Roboto-Black.ttf"; path = "../app/assets/fonts/Roboto-Black.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
E8DE31AC3A3B46AFBAB8E623 /* Roboto-Bold.ttf */ = {isa = PBXFileReference; name = "Roboto-Bold.ttf"; path = "../app/assets/fonts/Roboto-Bold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
6D158A6D27784CB3AAFC4572 /* Roboto-ExtraBold.ttf */ = {isa = PBXFileReference; name = "Roboto-ExtraBold.ttf"; path = "../app/assets/fonts/Roboto-ExtraBold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
6787284EF58F40B4A29BE810 /* Roboto-ExtraLight.ttf */ = {isa = PBXFileReference; name = "Roboto-ExtraLight.ttf"; path = "../app/assets/fonts/Roboto-ExtraLight.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
F3B7B24AB39048E28AA013C6 /* Roboto-Light.ttf */ = {isa = PBXFileReference; name = "Roboto-Light.ttf"; path = "../app/assets/fonts/Roboto-Light.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
57615910A7564380B1D23731 /* Roboto-Medium.ttf */ = {isa = PBXFileReference; name = "Roboto-Medium.ttf"; path = "../app/assets/fonts/Roboto-Medium.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
B9D41D27862846A8A6563185 /* Roboto-Regular.ttf */ = {isa = PBXFileReference; name = "Roboto-Regular.ttf"; path = "../app/assets/fonts/Roboto-Regular.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
7539BBD08F0743178F5517A3 /* Roboto-SemiBold.ttf */ = {isa = PBXFileReference; name = "Roboto-SemiBold.ttf"; path = "../app/assets/fonts/Roboto-SemiBold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
6606F41B1382422DA695F61C /* WorkSans-Bold.ttf */ = {isa = PBXFileReference; name = "WorkSans-Bold.ttf"; path = "../app/assets/fonts/WorkSans-Bold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
1569DC7537534ED39A79EE9E /* WorkSans-ExtraBold.ttf */ = {isa = PBXFileReference; name = "WorkSans-ExtraBold.ttf"; path = "../app/assets/fonts/WorkSans-ExtraBold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
FF48E43749B74116A2C4083C /* WorkSans-ExtraLight.ttf */ = {isa = PBXFileReference; name = "WorkSans-ExtraLight.ttf"; path = "../app/assets/fonts/WorkSans-ExtraLight.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
B8C4C96FCA144E859DC2AFDA /* WorkSans-Light.ttf */ = {isa = PBXFileReference; name = "WorkSans-Light.ttf"; path = "../app/assets/fonts/WorkSans-Light.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
55E647ABF16B47F6A83403C0 /* WorkSans-Medium.ttf */ = {isa = PBXFileReference; name = "WorkSans-Medium.ttf"; path = "../app/assets/fonts/WorkSans-Medium.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
589149FBA9F64E9F94AA50AF /* WorkSans-Regular.ttf */ = {isa = PBXFileReference; name = "WorkSans-Regular.ttf"; path = "../app/assets/fonts/WorkSans-Regular.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
A58733FB00504503BFBF4295 /* WorkSans-SemiBold.ttf */ = {isa = PBXFileReference; name = "WorkSans-SemiBold.ttf"; path = "../app/assets/fonts/WorkSans-SemiBold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
C3614D19A4004E3E858E4CEC /* WorkSans-Thin.ttf */ = {isa = PBXFileReference; name = "WorkSans-Thin.ttf"; path = "../app/assets/fonts/WorkSans-Thin.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -136,14 +136,14 @@
|
||||
C38CA987921A4CA4AEDBB3E5 /* Resources */ = {
|
||||
isa = "PBXGroup";
|
||||
children = (
|
||||
C93B020BF2D44311BFAF317D /* Roboto-Black.ttf */,
|
||||
E8DE31AC3A3B46AFBAB8E623 /* Roboto-Bold.ttf */,
|
||||
6D158A6D27784CB3AAFC4572 /* Roboto-ExtraBold.ttf */,
|
||||
6787284EF58F40B4A29BE810 /* Roboto-ExtraLight.ttf */,
|
||||
F3B7B24AB39048E28AA013C6 /* Roboto-Light.ttf */,
|
||||
57615910A7564380B1D23731 /* Roboto-Medium.ttf */,
|
||||
B9D41D27862846A8A6563185 /* Roboto-Regular.ttf */,
|
||||
7539BBD08F0743178F5517A3 /* Roboto-SemiBold.ttf */,
|
||||
6606F41B1382422DA695F61C /* WorkSans-Bold.ttf */,
|
||||
1569DC7537534ED39A79EE9E /* WorkSans-ExtraBold.ttf */,
|
||||
FF48E43749B74116A2C4083C /* WorkSans-ExtraLight.ttf */,
|
||||
B8C4C96FCA144E859DC2AFDA /* WorkSans-Light.ttf */,
|
||||
55E647ABF16B47F6A83403C0 /* WorkSans-Medium.ttf */,
|
||||
589149FBA9F64E9F94AA50AF /* WorkSans-Regular.ttf */,
|
||||
A58733FB00504503BFBF4295 /* WorkSans-SemiBold.ttf */,
|
||||
C3614D19A4004E3E858E4CEC /* WorkSans-Thin.ttf */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@ -218,14 +218,14 @@
|
||||
files = (
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
9BFF7E8F967043379EA34EF5 /* Roboto-Black.ttf in Resources */,
|
||||
64ADC498A16B4DD2979F9EC6 /* Roboto-Bold.ttf in Resources */,
|
||||
9A05BCBBB57F42CF891B4E76 /* Roboto-ExtraBold.ttf in Resources */,
|
||||
E9AB985237534D1A92E3A5C8 /* Roboto-ExtraLight.ttf in Resources */,
|
||||
A345282A09764F4B8935E1CE /* Roboto-Light.ttf in Resources */,
|
||||
6C3A045CF24641D79616809F /* Roboto-Medium.ttf in Resources */,
|
||||
A79B0AFEC3DA42AAAA56DC75 /* Roboto-Regular.ttf in Resources */,
|
||||
264B0BD7896E430EB2761212 /* Roboto-SemiBold.ttf in Resources */,
|
||||
90EC1A731F2F480594C0F9D1 /* WorkSans-Bold.ttf in Resources */,
|
||||
7E1975A765074059A27FA6F1 /* WorkSans-ExtraBold.ttf in Resources */,
|
||||
E09CC0AFCD63425FABA5714F /* WorkSans-ExtraLight.ttf in Resources */,
|
||||
6A06861DDC314E49B482B4EB /* WorkSans-Light.ttf in Resources */,
|
||||
3BEFE3BEC3334662878A37D5 /* WorkSans-Medium.ttf in Resources */,
|
||||
6E9607F0F9DE4649802D618B /* WorkSans-Regular.ttf in Resources */,
|
||||
DCDB35B731E1422DBD1481D1 /* WorkSans-SemiBold.ttf in Resources */,
|
||||
25B52D1C5AB64F039BEF062F /* WorkSans-Thin.ttf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@ -49,17 +49,16 @@
|
||||
<false/>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Roboto-Black.ttf</string>
|
||||
<string>Roboto-Bold.ttf</string>
|
||||
<string>Roboto-ExtraBold.ttf</string>
|
||||
<string>Roboto-ExtraLight.ttf</string>
|
||||
<string>Roboto-Light.ttf</string>
|
||||
<string>Roboto-Medium.ttf</string>
|
||||
<string>Roboto-Regular.ttf</string>
|
||||
<string>Roboto-SemiBold.ttf</string>
|
||||
<string>WorkSans-Bold.ttf</string>
|
||||
<string>WorkSans-ExtraBold.ttf</string>
|
||||
<string>WorkSans-ExtraLight.ttf</string>
|
||||
<string>WorkSans-Light.ttf</string>
|
||||
<string>WorkSans-Medium.ttf</string>
|
||||
<string>WorkSans-Regular.ttf</string>
|
||||
<string>WorkSans-SemiBold.ttf</string>
|
||||
<string>WorkSans-Thin.ttf</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs access to the camera to take photos and videos.</string>
|
||||
|
||||
<string>This app needs access to the camera to take photos and videos.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -2,36 +2,36 @@
|
||||
"migIndex": 1,
|
||||
"data": [
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Black.ttf",
|
||||
"sha1": "d1678489a8d5645f16486ec52d77b651ff0bf327"
|
||||
"path": "app/assets/fonts/WorkSans-Bold.ttf",
|
||||
"sha1": "ec84061651ead3c3c5cbb61c2d338aca0bacdc1e"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Bold.ttf",
|
||||
"sha1": "508c35dee818addce6cc6d1fb6e42f039da5a7cf"
|
||||
"path": "app/assets/fonts/WorkSans-ExtraBold.ttf",
|
||||
"sha1": "0b371d1dbfbdd15db880bbd129b239530c71accb"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-ExtraBold.ttf",
|
||||
"sha1": "3dbfd71b6fbcfbd8e7ee8a8dd033dc5aaad63249"
|
||||
"path": "app/assets/fonts/WorkSans-ExtraLight.ttf",
|
||||
"sha1": "74596e55487e2961b6c43993698d658e2ceee77b"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-ExtraLight.ttf",
|
||||
"sha1": "df556e64732e5c272349e13cb5f87591a1ae779b"
|
||||
"path": "app/assets/fonts/WorkSans-Light.ttf",
|
||||
"sha1": "293e11dae7e8b930bf5eea0b06ca979531f22189"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Light.ttf",
|
||||
"sha1": "318b44c0a32848f78bf11d4fbf3355d00647a796"
|
||||
"path": "app/assets/fonts/WorkSans-Medium.ttf",
|
||||
"sha1": "c281f8454dd193c2260e43ae2de171c5dd4086e4"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Medium.ttf",
|
||||
"sha1": "fa5192203f85ddb667579e1bdf26f12098bb873b"
|
||||
"path": "app/assets/fonts/WorkSans-Regular.ttf",
|
||||
"sha1": "5e0183b29b57c54595c62ac6bc223b21f1434226"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-Regular.ttf",
|
||||
"sha1": "3bff51436aa7eb995d84cfc592cc63e1316bb400"
|
||||
"path": "app/assets/fonts/WorkSans-SemiBold.ttf",
|
||||
"sha1": "64b8fe156fafce221a0f66504255257053fc6062"
|
||||
},
|
||||
{
|
||||
"path": "app/assets/fonts/Roboto-SemiBold.ttf",
|
||||
"sha1": "9ca139684fe902c8310dd82991648376ac9838db"
|
||||
"path": "app/assets/fonts/WorkSans-Thin.ttf",
|
||||
"sha1": "a62251331038fdd079c47bc413a350efbf702db8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@ -44,6 +44,7 @@
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-tts": "^4.1.1",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"react-native-webview": "^13.15.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0"
|
||||
},
|
||||
@ -12077,6 +12078,20 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webview": {
|
||||
"version": "13.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
|
||||
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"invariant": "2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
|
||||
"version": "0.79.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.0.tgz",
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-tts": "^4.1.1",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"react-native-webview": "^13.15.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0"
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user