Compare commits
3 Commits
70d4ec6690
...
d0fe2eaa34
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0fe2eaa34 | ||
|
|
579d11c9a5 | ||
|
|
8fe78a6d7c |
@ -7,7 +7,7 @@ alwaysApply: true
|
|||||||
|
|
||||||
### 1. Root Level Organization
|
### 1. Root Level Organization
|
||||||
```
|
```
|
||||||
NeoScan_Physician/
|
NeoScan_Radiologist/
|
||||||
├── app/ # Main application code
|
├── app/ # Main application code
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
├── android/ # Android native code
|
├── android/ # Android native code
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
## 📁 Complete Directory Structure
|
## 📁 Complete Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
NeoScan_Physician/
|
NeoScan_Radiologist/
|
||||||
├── app/ # Main application code
|
├── app/ # Main application code
|
||||||
│ ├── modules/ # Feature-based modules
|
│ ├── modules/ # Feature-based modules
|
||||||
│ │ ├── Auth/ # Authentication module
|
│ │ ├── Auth/ # Authentication module
|
||||||
@ -150,7 +150,7 @@ NeoScan_Physician/
|
|||||||
│ │ ├── AndroidManifest.xml # Main manifest
|
│ │ ├── AndroidManifest.xml # Main manifest
|
||||||
│ │ ├── java/ # Java source
|
│ │ ├── java/ # Java source
|
||||||
│ │ │ └── com/ # Package structure
|
│ │ │ └── com/ # Package structure
|
||||||
│ │ │ └── neoscan_physician/
|
│ │ │ └── neoscan_radiologist/
|
||||||
│ │ │ ├── MainActivity.kt # Main activity
|
│ │ │ ├── MainActivity.kt # Main activity
|
||||||
│ │ │ └── MainApplication.kt # Application class
|
│ │ │ └── MainApplication.kt # Application class
|
||||||
│ │ └── res/ # Resources
|
│ │ └── res/ # Resources
|
||||||
@ -166,7 +166,7 @@ NeoScan_Physician/
|
|||||||
│ ├── gradlew.bat # Windows gradle wrapper
|
│ ├── gradlew.bat # Windows gradle wrapper
|
||||||
│ └── settings.gradle # Gradle settings
|
│ └── settings.gradle # Gradle settings
|
||||||
├── ios/ # iOS native code
|
├── ios/ # iOS native code
|
||||||
│ ├── NeoScan_Physician/ # iOS app
|
│ ├── NeoScan_Radiologist/ # iOS app
|
||||||
│ │ ├── AppDelegate.swift # App delegate
|
│ │ ├── AppDelegate.swift # App delegate
|
||||||
│ │ ├── Images.xcassets/ # Image assets
|
│ │ ├── Images.xcassets/ # Image assets
|
||||||
│ │ │ ├── AppIcon.appiconset/ # App icons
|
│ │ │ ├── AppIcon.appiconset/ # App icons
|
||||||
@ -174,11 +174,11 @@ NeoScan_Physician/
|
|||||||
│ │ ├── Info.plist # App info
|
│ │ ├── Info.plist # App info
|
||||||
│ │ ├── LaunchScreen.storyboard # Launch screen
|
│ │ ├── LaunchScreen.storyboard # Launch screen
|
||||||
│ │ └── PrivacyInfo.xcprivacy # Privacy info
|
│ │ └── PrivacyInfo.xcprivacy # Privacy info
|
||||||
│ ├── NeoScan_Physician.xcodeproj/ # Xcode project
|
│ ├── NeoScan_Radiologist.xcodeproj/ # Xcode project
|
||||||
│ │ ├── project.pbxproj # Project file
|
│ │ ├── project.pbxproj # Project file
|
||||||
│ │ └── xcshareddata/ # Shared data
|
│ │ └── xcshareddata/ # Shared data
|
||||||
│ │ └── xcschemes/ # Build schemes
|
│ │ └── xcschemes/ # Build schemes
|
||||||
│ │ └── NeoScan_Physician.xcscheme
|
│ │ └── NeoScan_Radiologist.xcscheme
|
||||||
│ └── Podfile # CocoaPods configuration
|
│ └── Podfile # CocoaPods configuration
|
||||||
├── __tests__/ # Test files
|
├── __tests__/ # Test files
|
||||||
│ ├── App.test.tsx # App component tests
|
│ ├── App.test.tsx # App component tests
|
||||||
|
|||||||
@ -42,7 +42,7 @@ A comprehensive React Native application designed for emergency department physi
|
|||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
```
|
```
|
||||||
NeoScan_Physician/
|
NeoScan_Radiologist/
|
||||||
├── app/ # Main application code
|
├── app/ # Main application code
|
||||||
│ ├── modules/ # Feature-based modules
|
│ ├── modules/ # Feature-based modules
|
||||||
│ │ ├── Auth/ # Authentication module
|
│ │ ├── Auth/ # Authentication module
|
||||||
@ -120,7 +120,7 @@ NeoScan_Physician/
|
|||||||
1. **Clone the repository**
|
1. **Clone the repository**
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd NeoScan_Physician
|
cd NeoScan_Radiologist
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies**
|
2. **Install dependencies**
|
||||||
|
|||||||
@ -79,7 +79,7 @@ android {
|
|||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
namespace "com.neoscan_physician"
|
namespace "com.neoscan_radiologist"
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
enable true
|
enable true
|
||||||
@ -88,7 +88,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.neoscan_physician"
|
applicationId "com.neoscan_radiologist"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|||||||
733
android/app/src/main/assets/dicom-viewer.html
Normal file
@ -0,0 +1,733 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>DICOM Viewer - Fullscreen Mobile Friendly</title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0; padding: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: #111; color: white; font-family: sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#dicomContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
#dicomImage {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
background: black;
|
||||||
|
touch-action: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dicomImage canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure cornerstone canvas shows original rectangular view */
|
||||||
|
.cornerstone-element {
|
||||||
|
position: relative !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cornerstone-canvas {
|
||||||
|
position: relative !important;
|
||||||
|
top: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
clip-path: none !important;
|
||||||
|
mask: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove any circular clipping */
|
||||||
|
.cornerstone-element canvas,
|
||||||
|
#dicomImage canvas,
|
||||||
|
canvas {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
clip-path: none !important;
|
||||||
|
mask: none !important;
|
||||||
|
shape-outside: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure full rectangular display */
|
||||||
|
div[class*="cornerstone"] {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
clip-path: none !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
#controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 15px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#controls {
|
||||||
|
bottom: 20px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dicomImage {
|
||||||
|
height: calc(100% - 110px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#controls {
|
||||||
|
bottom: 25px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
transform: none;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dicomImage {
|
||||||
|
height: calc(100% - 130px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#frameControls, #actionControls {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frameControls {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 240px;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#frameControls, #actionControls {
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frameControls {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 255px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#frameControls, #actionControls {
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frameControls {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 270px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 34px;
|
||||||
|
min-height: 34px;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 35px;
|
||||||
|
min-height: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 18px;
|
||||||
|
min-width: 37px;
|
||||||
|
min-height: 37px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type=range] {
|
||||||
|
width: 135px;
|
||||||
|
height: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #2196F3;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #2196F3;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]:disabled::-webkit-slider-thumb {
|
||||||
|
background: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]:disabled::-moz-range-thumb {
|
||||||
|
background: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
input[type=range] {
|
||||||
|
width: 155px;
|
||||||
|
height: 6px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
input[type=range] {
|
||||||
|
width: 170px;
|
||||||
|
height: 6px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type=file] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frameInfo {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: rgba(255,255,255,0.9);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#frameInfo {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#frameInfo {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Overlay Styles */
|
||||||
|
#loadingOverlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10000;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loadingOverlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 4px solid #2196F3;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-subtext {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 300px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.loader {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-subtext {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.loader {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-subtext {
|
||||||
|
font-size: 18px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="dicomContainer">
|
||||||
|
<div id="dicomImage"></div>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div id="loadingOverlay" class="hidden">
|
||||||
|
<div class="loader"></div>
|
||||||
|
<div class="loading-text">Loading DICOM Image...</div>
|
||||||
|
<div class="loading-subtext">Please wait while we process your medical image</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
<div id="actionControls">
|
||||||
|
<button onclick="resetView()">⟳</button>
|
||||||
|
<button onclick="zoomIn()">+</button>
|
||||||
|
<button onclick="zoomOut()">-</button>
|
||||||
|
<button onclick="document.getElementById('fileInput').click()">📂</button>
|
||||||
|
</div>
|
||||||
|
<div id="frameControls">
|
||||||
|
<button onclick="prevFrame()">◀</button>
|
||||||
|
<input type="range" id="frameSlider" min="1" max="2" value="1" disabled>
|
||||||
|
<button onclick="nextFrame()">▶</button>
|
||||||
|
</div>
|
||||||
|
<div id="frameInfo"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden file input -->
|
||||||
|
<input type="file" id="fileInput" accept=".dcm" multiple>
|
||||||
|
|
||||||
|
<!-- Load from URL (Hidden for WebView usage) -->
|
||||||
|
<input type="text" id="dicomUrl" placeholder="Enter DICOM URL" size="50" style="display: none;">
|
||||||
|
<button id="loadUrlBtn" style="display: none;">Load URL</button>
|
||||||
|
|
||||||
|
<!-- Libraries -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/cornerstone-core@2.6.1/dist/cornerstone.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/dicom-parser/dist/dicomParser.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.13.2/dist/cornerstoneWADOImageLoader.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/cornerstone-tools@4.22.1/dist/cornerstoneTools.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const element = document.getElementById('dicomImage');
|
||||||
|
const frameInfo = document.getElementById('frameInfo');
|
||||||
|
const frameSlider = document.getElementById('frameSlider');
|
||||||
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||||
|
|
||||||
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||||
|
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||||
|
|
||||||
|
cornerstone.enable(element);
|
||||||
|
cornerstoneTools.init({ showSVGCursors: true });
|
||||||
|
|
||||||
|
// Touch tools
|
||||||
|
cornerstoneTools.addTool(cornerstoneTools.ZoomTouchPinchTool);
|
||||||
|
cornerstoneTools.addTool(cornerstoneTools.StackScrollMultiTouchTool);
|
||||||
|
cornerstoneTools.addTool(cornerstoneTools.WwwcRegionTool);
|
||||||
|
|
||||||
|
cornerstoneTools.setToolActive('ZoomTouchPinch', {});
|
||||||
|
cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
|
||||||
|
cornerstoneTools.setToolActive('WwwcRegion', {});
|
||||||
|
|
||||||
|
// 🔑 Listen for messages from React Native WebView
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
try {
|
||||||
|
const message = event.data;
|
||||||
|
|
||||||
|
// Handle direct URL string
|
||||||
|
if (typeof message === 'string' && message.startsWith('http')) {
|
||||||
|
loadDicom("wadouri:" + message);
|
||||||
|
document.getElementById('dicomUrl').value = message;
|
||||||
|
|
||||||
|
// Send success message back to React Native
|
||||||
|
if (window.ReactNativeWebView) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
type: 'success',
|
||||||
|
message: 'DICOM URL received and loading started'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle structured JSON message
|
||||||
|
try {
|
||||||
|
const parsedMessage = JSON.parse(message);
|
||||||
|
if (parsedMessage.type === 'loadDicom' && parsedMessage.data) {
|
||||||
|
loadDicom("wadouri:" + parsedMessage.data);
|
||||||
|
document.getElementById('dicomUrl').value = parsedMessage.data;
|
||||||
|
|
||||||
|
// Send success message back to React Native
|
||||||
|
if (window.ReactNativeWebView) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Structured DICOM message received and loading started'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// Message is not JSON, treating as string
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
// Send error message back to React Native
|
||||||
|
if (window.ReactNativeWebView) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to process message: ' + error.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Android WebView compatibility
|
||||||
|
document.addEventListener('message', (event) => {
|
||||||
|
window.dispatchEvent(new MessageEvent('message', { data: event.data }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify React Native that WebView is ready
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Initialize slider appearance
|
||||||
|
frameSlider.style.opacity = '0.6';
|
||||||
|
|
||||||
|
if (window.ReactNativeWebView) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
type: 'ready',
|
||||||
|
message: 'DICOM Viewer is ready to receive URLs'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔑 Resize observer for responsiveness
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
cornerstone.resize(element, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Loading overlay control functions
|
||||||
|
function showLoading(message = 'Loading DICOM Image...', subtext = 'Please wait while we process your medical image') {
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.querySelector('.loading-text').textContent = message;
|
||||||
|
loadingOverlay.querySelector('.loading-subtext').textContent = subtext;
|
||||||
|
loadingOverlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading() {
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stack = null;
|
||||||
|
|
||||||
|
// File input
|
||||||
|
document.getElementById('fileInput').addEventListener('change', e => {
|
||||||
|
const files = Array.from(e.target.files);
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
// Show loading while processing files
|
||||||
|
if (files.length === 1) {
|
||||||
|
showLoading('Processing DICOM File...', 'Loading selected medical image...');
|
||||||
|
setTimeout(() => {
|
||||||
|
loadDicom(cornerstoneWADOImageLoader.wadouri.fileManager.add(files[0]));
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
showLoading('Processing DICOM Files...', `Loading ${files.length} selected medical images...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
loadSeries(files.map(file => cornerstoneWADOImageLoader.wadouri.fileManager.add(file)));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// URL load
|
||||||
|
document.getElementById('loadUrlBtn').addEventListener('click', () => {
|
||||||
|
const url = document.getElementById('dicomUrl').value.trim();
|
||||||
|
if (!url) return;
|
||||||
|
showLoading('Loading from URL...', 'Fetching DICOM image from provided URL...');
|
||||||
|
setTimeout(() => {
|
||||||
|
loadDicom("wadouri:" + url);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load dicom
|
||||||
|
function loadDicom(imageId) {
|
||||||
|
// Show loading overlay
|
||||||
|
showLoading('Loading DICOM Image...', 'Processing medical image data, please wait...');
|
||||||
|
|
||||||
|
cornerstone.loadImage(imageId).then(image => {
|
||||||
|
// Hide loading overlay on successful load
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
|
cornerstone.displayImage(element, image);
|
||||||
|
cornerstone.resize(element, true); // ensure fit on load
|
||||||
|
|
||||||
|
const numFrames = parseInt(image.data.string('x00280008') || '1', 10);
|
||||||
|
|
||||||
|
// Always setup frame controls, even for single frames
|
||||||
|
if (numFrames > 1) {
|
||||||
|
stack = { currentImageIdIndex: 0, imageIds: [] };
|
||||||
|
for (let i = 0; i < numFrames; i++) {
|
||||||
|
stack.imageIds.push(imageId + `&frame=${i}`);
|
||||||
|
}
|
||||||
|
setupStack(stack);
|
||||||
|
} else {
|
||||||
|
// Single frame - setup basic stack for consistency
|
||||||
|
stack = { currentImageIdIndex: 0, imageIds: [imageId] };
|
||||||
|
setupStack(stack, true); // Pass true to indicate single frame
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Notify React Native of successful load
|
||||||
|
if (window.ReactNativeWebView) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
type: 'success',
|
||||||
|
message: 'DICOM image loaded and displayed successfully',
|
||||||
|
data: {
|
||||||
|
frames: numFrames,
|
||||||
|
imageId: imageId
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch(err => {
|
||||||
|
// Hide loading overlay on error
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
|
// Notify React Native of error
|
||||||
|
if (window.ReactNativeWebView) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to load DICOM image: ' + err.message || err,
|
||||||
|
data: {
|
||||||
|
imageId: imageId,
|
||||||
|
error: err.message || err.toString()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSeries(imageIds) {
|
||||||
|
// Show loading overlay for series
|
||||||
|
showLoading('Loading DICOM Series...', `Processing ${imageIds.length} images, please wait...`);
|
||||||
|
|
||||||
|
stack = { currentImageIdIndex: 0, imageIds };
|
||||||
|
cornerstone.loadImage(imageIds[0]).then(image => {
|
||||||
|
// Hide loading overlay on successful load
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
|
cornerstone.displayImage(element, image);
|
||||||
|
cornerstone.resize(element, true);
|
||||||
|
setupStack(stack, imageIds.length === 1);
|
||||||
|
}).catch(err => {
|
||||||
|
// Hide loading overlay on error
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
|
// Handle error silently or send to React Native
|
||||||
|
if (window.ReactNativeWebView) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to load DICOM series: ' + err.message || err
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupStack(s, isSingleFrame = false) {
|
||||||
|
cornerstoneTools.addStackStateManager(element, ['stack']);
|
||||||
|
cornerstoneTools.addToolState(element, 'stack', s);
|
||||||
|
|
||||||
|
// Handle single frame slider display
|
||||||
|
if (isSingleFrame || s.imageIds.length === 1) {
|
||||||
|
frameSlider.min = 1;
|
||||||
|
frameSlider.max = 2; // Set max to 2 to show proper indicator
|
||||||
|
frameSlider.value = 1;
|
||||||
|
frameSlider.disabled = true; // Disable interaction for single frame
|
||||||
|
frameSlider.style.opacity = '0.6'; // Visual indicator it's disabled
|
||||||
|
} else {
|
||||||
|
frameSlider.min = 1;
|
||||||
|
frameSlider.max = s.imageIds.length;
|
||||||
|
frameSlider.value = 1;
|
||||||
|
frameSlider.disabled = false; // Enable interaction for multiple frames
|
||||||
|
frameSlider.style.opacity = '1'; // Full opacity for enabled state
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFrameInfo(s);
|
||||||
|
|
||||||
|
// Remove existing event listeners to avoid duplicates
|
||||||
|
element.removeEventListener('cornerstonetoolsstackscroll', updateFrameInfo);
|
||||||
|
frameSlider.removeEventListener('input', handleSliderInput);
|
||||||
|
|
||||||
|
// Add event listeners only if not single frame
|
||||||
|
if (!isSingleFrame && s.imageIds.length > 1) {
|
||||||
|
element.addEventListener('cornerstonetoolsstackscroll', () => updateFrameInfo(s));
|
||||||
|
frameSlider.addEventListener('input', handleSliderInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSliderInput() {
|
||||||
|
const index = parseInt(frameSlider.value) - 1;
|
||||||
|
changeFrame(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFrameInfo(s) {
|
||||||
|
frameInfo.textContent = `Image ${s.currentImageIdIndex + 1} of ${s.imageIds.length}`;
|
||||||
|
frameSlider.value = s.currentImageIdIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function changeFrame(index) {
|
||||||
|
if (!stack) return;
|
||||||
|
if (index < 0 || index >= stack.imageIds.length) return;
|
||||||
|
stack.currentImageIdIndex = index;
|
||||||
|
cornerstone.loadImage(stack.imageIds[index]).then(img => {
|
||||||
|
cornerstone.displayImage(element, img);
|
||||||
|
cornerstone.resize(element, true);
|
||||||
|
updateFrameInfo(stack);
|
||||||
|
}).catch(err => {
|
||||||
|
// Handle frame change error silently
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevFrame() { if (stack) changeFrame(stack.currentImageIdIndex - 1); }
|
||||||
|
function nextFrame() { if (stack) changeFrame(stack.currentImageIdIndex + 1); }
|
||||||
|
|
||||||
|
function resetView() { cornerstone.reset(element); }
|
||||||
|
function zoomIn() {
|
||||||
|
const viewport = cornerstone.getViewport(element);
|
||||||
|
viewport.scale *= 1.2;
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
}
|
||||||
|
function zoomOut() {
|
||||||
|
const viewport = cornerstone.getViewport(element);
|
||||||
|
viewport.scale /= 1.2;
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.neoscan_physician
|
package com.neoscan_radiologist
|
||||||
|
|
||||||
import com.facebook.react.ReactActivity
|
import com.facebook.react.ReactActivity
|
||||||
import com.facebook.react.ReactActivityDelegate
|
import com.facebook.react.ReactActivityDelegate
|
||||||
@ -11,7 +11,7 @@ class MainActivity : ReactActivity() {
|
|||||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||||
* rendering of the component.
|
* rendering of the component.
|
||||||
*/
|
*/
|
||||||
override fun getMainComponentName(): String = "NeoScan_Physician"
|
override fun getMainComponentName(): String = "NeoScan_Radiologist"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.neoscan_physician
|
package com.neoscan_radiologist
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.facebook.react.PackageList
|
import com.facebook.react.PackageList
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 227 KiB |
@ -4,6 +4,7 @@
|
|||||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||||
|
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
9
android/app/src/main/res/xml/network_security_config.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
<certificates src="user" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
</network-security-config>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||||
plugins { id("com.facebook.react.settings") }
|
plugins { id("com.facebook.react.settings") }
|
||||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||||
rootProject.name = 'NeoScan_Physician'
|
rootProject.name = 'NeoScan_Radiologist'
|
||||||
include ':app'
|
include ':app'
|
||||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||||
|
|||||||
4
app.json
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "NeoScan_Physician",
|
"name": "NeoScan_Radiologist",
|
||||||
"displayName": "NeoScan_Physician"
|
"displayName": "NeoScan_Radiologist"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { StatusBar } from 'react-native';
|
import { StatusBar } from 'react-native';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native';
|
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native';
|
||||||
import { theme } from './theme/theme';
|
import { theme } from './theme/theme';
|
||||||
import { RootStackNavigator, setNavigationRef } from './navigation';
|
import { RootStackNavigator, setNavigationRef } from './navigation';
|
||||||
@ -184,6 +184,7 @@ function AppContent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
|
<SafeAreaView style={{flex:1}} edges={['top', 'left', 'right']} >
|
||||||
<NavigationContainer ref={navigationRef}>
|
<NavigationContainer ref={navigationRef}>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
barStyle="dark-content" // Dark text on light background
|
barStyle="dark-content" // Dark text on light background
|
||||||
@ -195,6 +196,7 @@ function AppContent() {
|
|||||||
bottomOffset={20}
|
bottomOffset={20}
|
||||||
/>
|
/>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
</SafeAreaView>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,443 +1,96 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DICOM Viewer Test</title>
|
<title>Dummy Page</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
.container {
|
header {
|
||||||
max-width: 800px;
|
background: #0073e6;
|
||||||
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;
|
color: white;
|
||||||
border: none;
|
padding: 1rem;
|
||||||
padding: 10px 20px;
|
text-align: center;
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
.load-button:hover {
|
nav {
|
||||||
background: #1976D2;
|
background: #005bb5;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.viewer-container {
|
nav a {
|
||||||
margin-top: 20px;
|
color: white;
|
||||||
border: 2px solid #ddd;
|
margin: 0 10px;
|
||||||
border-radius: 8px;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
padding: 10px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
#dicomImage {
|
main {
|
||||||
width: 100%;
|
padding: 2rem;
|
||||||
height: 400px;
|
}
|
||||||
background: #000;
|
section {
|
||||||
display: flex;
|
margin-bottom: 2rem;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
footer {
|
||||||
|
background: #333;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
text-align: center;
|
||||||
.error {
|
padding: 1rem;
|
||||||
color: #F44336;
|
margin-top: 2rem;
|
||||||
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<header>
|
||||||
<h1>DICOM Viewer Test</h1>
|
<h1>Welcome to the Dummy Page</h1>
|
||||||
<p>Test the DICOM viewer functionality in your browser before using it in React Native.</p>
|
<p>This is just a placeholder website.</p>
|
||||||
|
</header>
|
||||||
<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">
|
<nav>
|
||||||
<h3>Custom DICOM URL</h3>
|
<a href="#">Home</a>
|
||||||
<input type="text" id="customUrl" class="url-input" placeholder="Enter DICOM URL here..." />
|
<a href="#">About</a>
|
||||||
<button onclick="loadCustomUrl()" class="load-button">Load DICOM Image</button>
|
<a href="#">Services</a>
|
||||||
</div>
|
<a href="#">Contact</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="viewer-container">
|
<main>
|
||||||
<div class="status" id="status">Ready to load DICOM image</div>
|
<section>
|
||||||
<div id="dicomImage">
|
<h2>About Us</h2>
|
||||||
<div>Click a sample URL above or enter a custom URL to load a DICOM image</div>
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac orci vel nisi gravida feugiat.</p>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dicomInfo" class="dicom-info" style="display: none;">
|
<section>
|
||||||
<strong>DICOM Information:</strong><br>
|
<h2>Our Services</h2>
|
||||||
<div id="dicomInfoContent"></div>
|
<ul>
|
||||||
</div>
|
<li>Service One</li>
|
||||||
|
<li>Service Two</li>
|
||||||
|
<li>Service Three</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div id="messages"></div>
|
<section>
|
||||||
</div>
|
<h2>Contact</h2>
|
||||||
|
<form>
|
||||||
|
<label for="name">Name:</label><br>
|
||||||
|
<input type="text" id="name" name="name"><br><br>
|
||||||
|
|
||||||
|
<label for="email">Email:</label><br>
|
||||||
|
<input type="email" id="email" name="email"><br><br>
|
||||||
|
|
||||||
|
<label for="msg">Message:</label><br>
|
||||||
|
<textarea id="msg" name="msg" rows="4"></textarea><br><br>
|
||||||
|
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
<script>
|
<footer>
|
||||||
let cornerstone = null;
|
<p>© 2025 Dummy Company. All rights reserved.</p>
|
||||||
let cornerstoneWADOImageLoader = null;
|
</footer>
|
||||||
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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Alert,
|
Alert,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@ -541,7 +540,6 @@ const AIPredictionDetailScreen: React.FC<AIPredictionDetailsScreenProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
|
|
||||||
<TouchableWithoutFeedback onPress={closeAllDropdowns} disabled={!showSuggestionTypeDropdown && !showPriorityDropdown}>
|
<TouchableWithoutFeedback onPress={closeAllDropdowns} disabled={!showSuggestionTypeDropdown && !showPriorityDropdown}>
|
||||||
<View style={{flex:1}}>
|
<View style={{flex:1}}>
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import {
|
|||||||
RefreshControl,
|
RefreshControl,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
Alert,
|
||||||
StatusBar,
|
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Icon from 'react-native-vector-icons/Feather';
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
@ -552,8 +551,7 @@ const AIPredictionsScreen: React.FC<AIPredictionsScreenProps> = ({ navigation })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.headerTitle}>AI Predictions</Text>
|
<Text style={styles.headerTitle}>AI Predictions</Text>
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export const login = createAsyncThunk(
|
|||||||
async (credentials: { email: string; password: string }, { rejectWithValue }) => {
|
async (credentials: { email: string; password: string }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response:any = await authAPI.login(credentials.email, credentials.password,'web');
|
const response:any = await authAPI.login(credentials.email, credentials.password,'web');
|
||||||
console.log('user response',response)
|
|
||||||
|
|
||||||
if(response.data.message && !response.data.success){
|
if(response.data.message && !response.data.success){
|
||||||
showError(response.data.message)
|
showError(response.data.message)
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
StatusBar,
|
|
||||||
Alert,
|
Alert,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@ -409,10 +408,6 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ navigation }) => {
|
|||||||
style={styles.container}
|
style={styles.container}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
>
|
>
|
||||||
<StatusBar
|
|
||||||
barStyle="dark-content"
|
|
||||||
backgroundColor={theme.colors.background}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Conditional Content Rendering */}
|
{/* Conditional Content Rendering */}
|
||||||
{currentStep === 'hospital' ? (
|
{currentStep === 'hospital' ? (
|
||||||
|
|||||||
@ -40,6 +40,14 @@ export const authAPI = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
//upload profile photo for onboarding
|
||||||
|
uploadProfilePhoto: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-profile-photo', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
...(token && { 'Authorization': `Bearer ${token}` }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
// Update user profile
|
// Update user profile
|
||||||
updateUserProfile: (userId: string, profileData: {
|
updateUserProfile: (userId: string, profileData: {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import { DashboardHeader } from '../components/DashboardHeader';
|
|||||||
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
|
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
|
||||||
import { FeedbackAnalysisPieChart } from '../components/FeedbackAnalysisPieChart';
|
import { FeedbackAnalysisPieChart } from '../components/FeedbackAnalysisPieChart';
|
||||||
import { useAIDashboard } from '../hooks/useAIDashboard';
|
import { useAIDashboard } from '../hooks/useAIDashboard';
|
||||||
|
import { selectUserDisplayName, selectUserFirstName } from '../../Auth/redux/authSelectors';
|
||||||
|
import { useAppSelector } from '../../../store/hooks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DashboardScreenProps Interface
|
* DashboardScreenProps Interface
|
||||||
@ -123,7 +125,7 @@ export const DashboardScreen: React.FC<DashboardScreenProps> = ({
|
|||||||
navigation,
|
navigation,
|
||||||
}) => {
|
}) => {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CUSTOM HOOKS
|
// CUSTOM HOOKS & SELECTORS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Use custom hook for AI dashboard functionality
|
// Use custom hook for AI dashboard functionality
|
||||||
@ -136,6 +138,40 @@ export const DashboardScreen: React.FC<DashboardScreenProps> = ({
|
|||||||
refreshDashboardStatistics
|
refreshDashboardStatistics
|
||||||
} = useAIDashboard();
|
} = useAIDashboard();
|
||||||
|
|
||||||
|
// Get user display name from auth state
|
||||||
|
const userDisplayName = useAppSelector(selectUserFirstName);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getPersonalizedGreeting Function
|
||||||
|
*
|
||||||
|
* Purpose: Generate a personalized greeting based on time of day and user's display name
|
||||||
|
*
|
||||||
|
* @returns Personalized greeting string
|
||||||
|
*/
|
||||||
|
const getPersonalizedGreeting = (): string => {
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
let timeGreeting = '';
|
||||||
|
|
||||||
|
// Determine time-based greeting
|
||||||
|
if (currentHour >= 5 && currentHour < 12) {
|
||||||
|
timeGreeting = 'Good Morning';
|
||||||
|
} else if (currentHour >= 12 && currentHour < 17) {
|
||||||
|
timeGreeting = 'Good Afternoon';
|
||||||
|
} else if (currentHour >= 17 && currentHour < 21) {
|
||||||
|
timeGreeting = 'Good Evening';
|
||||||
|
} else {
|
||||||
|
timeGreeting = 'Good Evening';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create personalized greeting with fallback
|
||||||
|
const displayName = userDisplayName || 'Doctor';
|
||||||
|
return `${timeGreeting}, Dr. ${displayName}`;
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// EVENT HANDLERS
|
// EVENT HANDLERS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -611,7 +647,7 @@ export const DashboardScreen: React.FC<DashboardScreenProps> = ({
|
|||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
{/* Dashboard header with title and refresh button */}
|
{/* Dashboard header with title and refresh button */}
|
||||||
<View style={styles.headerTop}>
|
<View style={styles.headerTop}>
|
||||||
<Text style={styles.dashboardTitle}>AI Analysis Dashboard</Text>
|
<Text style={styles.dashboardTitle}>{getPersonalizedGreeting()}</Text>
|
||||||
<Text style={styles.dashboardSubtitle}>
|
<Text style={styles.dashboardSubtitle}>
|
||||||
{dashboardMessage}
|
{dashboardMessage}
|
||||||
</Text>
|
</Text>
|
||||||
@ -761,10 +797,11 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Dashboard title styling
|
// Dashboard title styling
|
||||||
dashboardTitle: {
|
dashboardTitle: {
|
||||||
fontSize: theme.typography.fontSize.displayLarge,
|
fontSize: theme.typography.fontSize.displayMedium,
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
color: theme.colors.textPrimary,
|
color: theme.colors.textPrimary,
|
||||||
marginBottom: theme.spacing.xs,
|
marginBottom: theme.spacing.xs,
|
||||||
|
marginTop: theme.spacing.sm,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Dashboard subtitle styling
|
// Dashboard subtitle styling
|
||||||
|
|||||||
@ -200,7 +200,6 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MAIN RENDER
|
// MAIN RENDER
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StatusBar,
|
|
||||||
Alert,
|
Alert,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Image,
|
Image,
|
||||||
@ -32,6 +31,7 @@ import { theme } from '../../../theme/theme';
|
|||||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||||
import Icon from 'react-native-vector-icons/Feather';
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { DicomViewerModal } from '../../../shared/components';
|
||||||
|
|
||||||
// Import types and API
|
// Import types and API
|
||||||
import { patientAPI } from '../services/patientAPI';
|
import { patientAPI } from '../services/patientAPI';
|
||||||
@ -142,6 +142,14 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
const [showFullImage, setShowFullImage] = useState(false);
|
const [showFullImage, setShowFullImage] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<'overview' | 'aiAnalysis' | 'history'>('overview');
|
const [activeTab, setActiveTab] = useState<'overview' | 'aiAnalysis' | 'history'>('overview');
|
||||||
|
|
||||||
|
// DICOM Modal state
|
||||||
|
const [dicomModalVisible, setDicomModalVisible] = useState(false);
|
||||||
|
const [selectedDicomData, setSelectedDicomData] = useState<{
|
||||||
|
dicomUrl: string;
|
||||||
|
seriesData: SeriesSummary;
|
||||||
|
prediction?: Prediction;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Navigation state
|
// Navigation state
|
||||||
const [selectedSeriesForDetail, setSelectedSeriesForDetail] = useState<SeriesSummary | null>(null);
|
const [selectedSeriesForDetail, setSelectedSeriesForDetail] = useState<SeriesSummary | null>(null);
|
||||||
|
|
||||||
@ -222,24 +230,77 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
}, [fetchPatientData]);
|
}, [fetchPatientData]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Image Press
|
* Handle DICOM Image Press
|
||||||
*
|
*
|
||||||
* Purpose: Open full-screen image viewer for selected series
|
* Purpose: Open DICOM viewer modal for selected series
|
||||||
*
|
*
|
||||||
* @param seriesIndex - Index of the series
|
* @param seriesIndex - Index of the series
|
||||||
*/
|
*/
|
||||||
const handleImagePress = useCallback((seriesIndex: number) => {
|
const handleImagePress = useCallback((seriesIndex: number) => {
|
||||||
setSelectedImageIndex(seriesIndex);
|
if (!patientData || !patientData.series_summary[seriesIndex]) return;
|
||||||
setShowFullImage(true);
|
|
||||||
}, []);
|
const series = patientData.series_summary[seriesIndex];
|
||||||
|
const seriesPredictions = patientData.predictions_by_series[series.series_num] || [];
|
||||||
|
const firstPrediction = seriesPredictions[0];
|
||||||
|
|
||||||
|
if (firstPrediction?.preview) {
|
||||||
|
const dicomUrl = API_CONFIG.BASE_URL +'/api/dicom'+ firstPrediction.file_path;
|
||||||
|
console.log('dicomUrl', dicomUrl);
|
||||||
|
setSelectedDicomData({
|
||||||
|
dicomUrl,
|
||||||
|
seriesData: series,
|
||||||
|
prediction: firstPrediction,
|
||||||
|
});
|
||||||
|
setDicomModalVisible(true);
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
'No DICOM Available',
|
||||||
|
'No DICOM image is available for this series.',
|
||||||
|
[{ text: 'OK' }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [patientData]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Close Image Viewer
|
* Handle Open DICOM Modal
|
||||||
*
|
*
|
||||||
* Purpose: Close full-screen image viewer
|
* Purpose: Open DICOM modal with specific series and prediction data
|
||||||
|
*
|
||||||
|
* @param series - Series data
|
||||||
|
* @param prediction - Optional prediction data
|
||||||
*/
|
*/
|
||||||
const handleCloseImageViewer = useCallback(() => {
|
const handleOpenDicomModal = useCallback((series: SeriesSummary, prediction?: Prediction) => {
|
||||||
setShowFullImage(false);
|
if (!patientData) return;
|
||||||
|
|
||||||
|
const seriesPredictions = patientData.predictions_by_series[series.series_num] || [];
|
||||||
|
const targetPrediction = prediction || seriesPredictions[0];
|
||||||
|
|
||||||
|
if (targetPrediction?.preview) {
|
||||||
|
const dicomUrl = API_CONFIG.DICOM_BASE_URL + targetPrediction.preview;
|
||||||
|
|
||||||
|
setSelectedDicomData({
|
||||||
|
dicomUrl,
|
||||||
|
seriesData: series,
|
||||||
|
prediction: targetPrediction,
|
||||||
|
});
|
||||||
|
setDicomModalVisible(true);
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
'No DICOM Available',
|
||||||
|
'No DICOM image is available for this series.',
|
||||||
|
[{ text: 'OK' }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [patientData]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Close DICOM Modal
|
||||||
|
*
|
||||||
|
* Purpose: Close DICOM viewer modal and reset state
|
||||||
|
*/
|
||||||
|
const handleCloseDicomModal = useCallback(() => {
|
||||||
|
setDicomModalVisible(false);
|
||||||
|
setSelectedDicomData(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -696,7 +757,7 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
const hasPredictions = seriesPredictions.length > 0;
|
const hasPredictions = seriesPredictions.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={seriesIndex} style={styles.seriesContainer}>
|
<View style={styles.seriesContainer}>
|
||||||
{/* Series Header */}
|
{/* Series Header */}
|
||||||
<View style={styles.seriesHeader}>
|
<View style={styles.seriesHeader}>
|
||||||
<View style={styles.seriesHeaderTop}>
|
<View style={styles.seriesHeaderTop}>
|
||||||
@ -784,7 +845,7 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
|
|
||||||
{hasPredictions ? (
|
{hasPredictions ? (
|
||||||
seriesPredictions.map((prediction) => (
|
seriesPredictions.map((prediction) => (
|
||||||
<View key={prediction.id} style={styles.predictionCard}>
|
<View style={styles.predictionCard}>
|
||||||
<View style={styles.predictionHeader}>
|
<View style={styles.predictionHeader}>
|
||||||
<Text style={styles.predictionLabel}>{prediction.prediction.label}</Text>
|
<Text style={styles.predictionLabel}>{prediction.prediction.label}</Text>
|
||||||
<View style={[
|
<View style={[
|
||||||
@ -906,7 +967,6 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
{renderLoadingState()}
|
{renderLoadingState()}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@ -915,7 +975,6 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
{renderErrorState()}
|
{renderErrorState()}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@ -924,7 +983,6 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
if (!patientData) {
|
if (!patientData) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Icon name="user-x" size={48} color={theme.colors.textMuted} />
|
<Icon name="user-x" size={48} color={theme.colors.textMuted} />
|
||||||
<Text style={styles.errorTitle}>Patient Not Found</Text>
|
<Text style={styles.errorTitle}>Patient Not Found</Text>
|
||||||
@ -936,7 +994,6 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@ -992,7 +1049,17 @@ const PatientDetailsScreen: React.FC<PatientDetailsScreenProps> = ({ navigation,
|
|||||||
{activeTab === 'history' && renderHistoryTab()}
|
{activeTab === 'history' && renderHistoryTab()}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* DICOM Viewer Modal */}
|
||||||
|
{selectedDicomData && (
|
||||||
|
<DicomViewerModal
|
||||||
|
visible={dicomModalVisible}
|
||||||
|
dicomUrl={selectedDicomData.dicomUrl}
|
||||||
|
onClose={handleCloseDicomModal}
|
||||||
|
title={`${selectedDicomData.seriesData.series_description}`}
|
||||||
|
patientName={patientData?.patient_info.name}
|
||||||
|
studyDescription={`Series ${selectedDicomData.seriesData.series_num} - ${selectedDicomData.seriesData.modality}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Alert,
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
@ -262,7 +261,6 @@ const PatientsScreen: React.FC = () => {
|
|||||||
if (error && !isLoading) {
|
if (error && !isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={styles.errorTitle}>Error Loading Patients</Text>
|
<Text style={styles.errorTitle}>Error Loading Patients</Text>
|
||||||
<Text style={styles.errorMessage}>{error}</Text>
|
<Text style={styles.errorMessage}>{error}</Text>
|
||||||
@ -280,7 +278,6 @@ const PatientsScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StatusBar,
|
|
||||||
Alert,
|
Alert,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Image,
|
Image,
|
||||||
@ -33,6 +32,7 @@ import { theme } from '../../../theme/theme';
|
|||||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||||
import Icon from 'react-native-vector-icons/Feather';
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { DicomViewerModal } from '../../../shared/components';
|
||||||
|
|
||||||
// Import types and API
|
// Import types and API
|
||||||
import { patientAPI } from '../services/patientAPI';
|
import { patientAPI } from '../services/patientAPI';
|
||||||
@ -151,6 +151,14 @@ const SeriesDetailScreen: React.FC<SeriesDetailScreenProps> = ({ navigation, rou
|
|||||||
|
|
||||||
// Track newly added feedback for visual indication
|
// Track newly added feedback for visual indication
|
||||||
const [newFeedbackIds, setNewFeedbackIds] = useState<Set<string>>(new Set());
|
const [newFeedbackIds, setNewFeedbackIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// DICOM Modal state
|
||||||
|
const [dicomModalVisible, setDicomModalVisible] = useState(false);
|
||||||
|
const [selectedDicomData, setSelectedDicomData] = useState<{
|
||||||
|
dicomUrl: string;
|
||||||
|
prediction: Prediction;
|
||||||
|
imageIndex: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// EFFECTS
|
// EFFECTS
|
||||||
@ -218,6 +226,44 @@ const SeriesDetailScreen: React.FC<SeriesDetailScreenProps> = ({ navigation, rou
|
|||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle DICOM Image Press
|
||||||
|
*
|
||||||
|
* Purpose: Open DICOM viewer modal for selected prediction image
|
||||||
|
*
|
||||||
|
* @param prediction - Prediction data containing DICOM file path
|
||||||
|
* @param imageIndex - Index of the image in the series
|
||||||
|
*/
|
||||||
|
const handleDicomImagePress = useCallback((prediction: Prediction, imageIndex: number) => {
|
||||||
|
if (prediction?.file_path) {
|
||||||
|
const dicomUrl = API_CONFIG.BASE_URL + '/api/dicom' + prediction.file_path;
|
||||||
|
console.log('DICOM URL:', dicomUrl);
|
||||||
|
|
||||||
|
setSelectedDicomData({
|
||||||
|
dicomUrl,
|
||||||
|
prediction,
|
||||||
|
imageIndex,
|
||||||
|
});
|
||||||
|
setDicomModalVisible(true);
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
'No DICOM Available',
|
||||||
|
'No DICOM file path is available for this image.',
|
||||||
|
[{ text: 'OK' }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Close DICOM Modal
|
||||||
|
*
|
||||||
|
* Purpose: Close DICOM viewer modal and reset state
|
||||||
|
*/
|
||||||
|
const handleCloseDicomModal = useCallback(() => {
|
||||||
|
setDicomModalVisible(false);
|
||||||
|
setSelectedDicomData(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Refresh
|
* Handle Refresh
|
||||||
*
|
*
|
||||||
@ -1337,11 +1383,22 @@ const SeriesDetailScreen: React.FC<SeriesDetailScreenProps> = ({ navigation, rou
|
|||||||
{predictions.map((prediction: Prediction, index: number) => (
|
{predictions.map((prediction: Prediction, index: number) => (
|
||||||
<View key={prediction.id} style={styles.imageContainer}>
|
<View key={prediction.id} style={styles.imageContainer}>
|
||||||
{prediction.preview ? (
|
{prediction.preview ? (
|
||||||
<Image
|
<TouchableOpacity
|
||||||
source={{ uri: API_CONFIG.DICOM_BASE_URL + prediction.preview }}
|
style={styles.imageClickable}
|
||||||
style={styles.seriesImage}
|
onPress={() => handleDicomImagePress(prediction, index)}
|
||||||
resizeMode="cover"
|
activeOpacity={0.7}
|
||||||
/>
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: API_CONFIG.DICOM_BASE_URL + prediction.preview }}
|
||||||
|
style={styles.seriesImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
{/* Overlay to indicate clickable */}
|
||||||
|
<View style={styles.imageOverlay}>
|
||||||
|
<Icon name="maximize-2" size={20} color={theme.colors.background} />
|
||||||
|
<Text style={styles.imageOverlayText}>View DICOM</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.noImagePlaceholder}>
|
<View style={styles.noImagePlaceholder}>
|
||||||
<Icon name="image" size={32} color={theme.colors.textMuted} />
|
<Icon name="image" size={32} color={theme.colors.textMuted} />
|
||||||
@ -1477,7 +1534,6 @@ const SeriesDetailScreen: React.FC<SeriesDetailScreenProps> = ({ navigation, rou
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@ -1704,6 +1760,19 @@ const SeriesDetailScreen: React.FC<SeriesDetailScreenProps> = ({ navigation, rou
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* DICOM Viewer Modal */}
|
||||||
|
{selectedDicomData && (
|
||||||
|
<DicomViewerModal
|
||||||
|
visible={dicomModalVisible}
|
||||||
|
dicomUrl={selectedDicomData.dicomUrl}
|
||||||
|
onClose={handleCloseDicomModal}
|
||||||
|
title={`${selectedDicomData.prediction.prediction.label}`}
|
||||||
|
patientName={patientName}
|
||||||
|
studyDescription={`Series ${seriesNumber} - Image ${selectedDicomData.imageIndex + 1} - ${selectedDicomData.prediction.prediction.finding_type}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1934,11 +2003,34 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: theme.spacing.md,
|
marginRight: theme.spacing.md,
|
||||||
},
|
},
|
||||||
|
imageClickable: {
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
seriesImage: {
|
seriesImage: {
|
||||||
width: 120,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: theme.spacing.xs,
|
},
|
||||||
|
imageOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
imageOverlayText: {
|
||||||
|
color: theme.colors.background,
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
marginTop: 4,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
noImagePlaceholder: {
|
noImagePlaceholder: {
|
||||||
width: 120,
|
width: 120,
|
||||||
@ -2340,6 +2432,8 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: theme.colors.textSecondary,
|
color: theme.colors.textSecondary,
|
||||||
fontFamily: theme.typography.fontFamily.regular,
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
paddingHorizontal:theme.spacing.sm,
|
||||||
|
flex:1
|
||||||
},
|
},
|
||||||
detailsGrid: {
|
detailsGrid: {
|
||||||
marginBottom: theme.spacing.md,
|
marginBottom: theme.spacing.md,
|
||||||
|
|||||||
@ -14,6 +14,11 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
Image,
|
Image,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
ActionSheetIOS,
|
||||||
|
Platform,
|
||||||
|
PermissionsAndroid,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { theme } from '../../../theme/theme';
|
import { theme } from '../../../theme/theme';
|
||||||
import {
|
import {
|
||||||
@ -27,6 +32,7 @@ import { ProfileCard } from '../components/ProfileCard';
|
|||||||
import { CustomModal } from '../../../shared/components';
|
import { CustomModal } from '../../../shared/components';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||||
import { logoutUser } from '../../Auth/redux/authActions';
|
import { logoutUser } from '../../Auth/redux/authActions';
|
||||||
|
import { updateUserProfile } from '../../Auth/redux/authSlice';
|
||||||
import {
|
import {
|
||||||
selectUser,
|
selectUser,
|
||||||
selectUserDisplayName,
|
selectUserDisplayName,
|
||||||
@ -37,6 +43,14 @@ import {
|
|||||||
selectDashboardSettings
|
selectDashboardSettings
|
||||||
} from '../../Auth/redux/authSelectors';
|
} from '../../Auth/redux/authSelectors';
|
||||||
import { API_CONFIG } from '../../../shared/utils';
|
import { API_CONFIG } from '../../../shared/utils';
|
||||||
|
import { authAPI } from '../../Auth/services/authAPI';
|
||||||
|
import {
|
||||||
|
launchImageLibrary,
|
||||||
|
launchCamera,
|
||||||
|
ImagePickerResponse,
|
||||||
|
MediaType
|
||||||
|
} from 'react-native-image-picker';
|
||||||
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SettingsScreenProps Interface
|
* SettingsScreenProps Interface
|
||||||
@ -84,6 +98,19 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
// UI state
|
// UI state
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
// Profile photo state
|
||||||
|
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||||
|
const [tempProfilePhoto, setTempProfilePhoto] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Upload response interface
|
||||||
|
interface UploadPhotoResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
data?: {
|
||||||
|
profile_photo_url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Modal state
|
// Modal state
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [modalConfig, setModalConfig] = useState({
|
const [modalConfig, setModalConfig] = useState({
|
||||||
@ -197,10 +224,345 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
setSettingsSections(generateSettingsSections());
|
setSettingsSections(generateSettingsSections());
|
||||||
}, [user, dashboardSettings]);
|
}, [user, dashboardSettings]);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PERMISSION HANDLERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* requestCameraPermission Function
|
||||||
|
*
|
||||||
|
* Purpose: Request camera permission for Android devices
|
||||||
|
*
|
||||||
|
* @returns Promise<boolean> - Whether permission was granted
|
||||||
|
*/
|
||||||
|
const requestCameraPermission = async (): Promise<boolean> => {
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
try {
|
||||||
|
const granted = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.CAMERA,
|
||||||
|
{
|
||||||
|
title: 'Camera Permission',
|
||||||
|
message: 'This app needs camera permission to capture profile photos.',
|
||||||
|
buttonNeutral: 'Ask Me Later',
|
||||||
|
buttonNegative: 'Cancel',
|
||||||
|
buttonPositive: 'OK',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Camera permission error:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // iOS permissions are handled via Info.plist
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// EVENT HANDLERS
|
// EVENT HANDLERS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handleProfilePhotoUpdate Function
|
||||||
|
*
|
||||||
|
* Purpose: Show action sheet with camera and gallery options for profile photo update
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. Show action sheet with camera and gallery options
|
||||||
|
* 2. Handle user selection
|
||||||
|
* 3. Launch appropriate image picker
|
||||||
|
*/
|
||||||
|
const handleProfilePhotoUpdate = () => {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
ActionSheetIOS.showActionSheetWithOptions(
|
||||||
|
{
|
||||||
|
options: ['Cancel', 'Take Photo', 'Choose from Gallery'],
|
||||||
|
cancelButtonIndex: 0,
|
||||||
|
userInterfaceStyle: 'light',
|
||||||
|
},
|
||||||
|
(buttonIndex) => {
|
||||||
|
if (buttonIndex === 1) {
|
||||||
|
handleCameraCapture();
|
||||||
|
} else if (buttonIndex === 2) {
|
||||||
|
handleGallerySelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// For Android, show custom action sheet or Alert
|
||||||
|
Alert.alert(
|
||||||
|
'Update Profile Photo',
|
||||||
|
'Choose an option',
|
||||||
|
[
|
||||||
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
|
{ text: 'Take Photo', onPress: handleCameraCapture },
|
||||||
|
{ text: 'Choose from Gallery', onPress: handleGallerySelection },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handleCameraCapture Function
|
||||||
|
*
|
||||||
|
* Purpose: Launch camera to capture new profile photo
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. Check camera permissions
|
||||||
|
* 2. Launch camera with callback
|
||||||
|
* 3. Validate captured image
|
||||||
|
* 4. Upload to server
|
||||||
|
* 5. Update local state
|
||||||
|
*/
|
||||||
|
const handleCameraCapture = async () => {
|
||||||
|
try {
|
||||||
|
// Check camera permission first
|
||||||
|
const hasPermission = await requestCameraPermission();
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
setModalConfig({
|
||||||
|
title: 'Permission Required',
|
||||||
|
message: 'Camera permission is required to capture profile photos.',
|
||||||
|
type: 'error',
|
||||||
|
onConfirm: () => {},
|
||||||
|
showCancel: false,
|
||||||
|
icon: 'camera',
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch camera with callback
|
||||||
|
const options = {
|
||||||
|
mediaType: 'photo' as MediaType,
|
||||||
|
quality: 0.8 as const,
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 800,
|
||||||
|
saveToPhotos: false,
|
||||||
|
includeBase64: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
launchCamera(options, (response: ImagePickerResponse) => {
|
||||||
|
try {
|
||||||
|
// Handle user cancellation
|
||||||
|
if (response.didCancel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
if (response.errorMessage) {
|
||||||
|
throw new Error(response.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response and assets
|
||||||
|
if (!response.assets || response.assets.length === 0) {
|
||||||
|
throw new Error('No image captured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = response.assets[0];
|
||||||
|
if (!asset.uri) {
|
||||||
|
throw new Error('Invalid image data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (max 5MB)
|
||||||
|
if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) {
|
||||||
|
throw new Error('Image size must be less than 5MB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set temporary photo for preview
|
||||||
|
setTempProfilePhoto(asset.uri);
|
||||||
|
|
||||||
|
// Upload the captured photo
|
||||||
|
uploadProfilePhoto(asset.uri);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Camera capture processing error:', error);
|
||||||
|
setModalConfig({
|
||||||
|
title: 'Camera Error',
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to capture photo',
|
||||||
|
type: 'error',
|
||||||
|
onConfirm: () => {},
|
||||||
|
showCancel: false,
|
||||||
|
icon: 'alert-circle',
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Camera launch error:', error);
|
||||||
|
setModalConfig({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to launch camera. Please try again.',
|
||||||
|
type: 'error',
|
||||||
|
onConfirm: () => {},
|
||||||
|
showCancel: false,
|
||||||
|
icon: 'alert-circle',
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handleGallerySelection Function
|
||||||
|
*
|
||||||
|
* Purpose: Launch gallery to select existing profile photo
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. Launch image picker with callback
|
||||||
|
* 2. Validate selected image
|
||||||
|
* 3. Upload to server
|
||||||
|
* 4. Update local state
|
||||||
|
*/
|
||||||
|
const handleGallerySelection = () => {
|
||||||
|
try {
|
||||||
|
// Launch image picker with callback
|
||||||
|
const options = {
|
||||||
|
mediaType: 'photo' as MediaType,
|
||||||
|
quality: 0.8 as const,
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 800,
|
||||||
|
includeBase64: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
launchImageLibrary(options, (response: ImagePickerResponse) => {
|
||||||
|
try {
|
||||||
|
// Handle user cancellation
|
||||||
|
if (response.didCancel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
if (response.errorMessage) {
|
||||||
|
throw new Error(response.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response and assets
|
||||||
|
if (!response.assets || response.assets.length === 0) {
|
||||||
|
throw new Error('No image selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = response.assets[0];
|
||||||
|
if (!asset.uri) {
|
||||||
|
throw new Error('Invalid image data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (max 5MB)
|
||||||
|
if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) {
|
||||||
|
throw new Error('Image size must be less than 5MB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set temporary photo for preview
|
||||||
|
setTempProfilePhoto(asset.uri);
|
||||||
|
|
||||||
|
// Upload the selected photo
|
||||||
|
uploadProfilePhoto(asset.uri);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gallery selection processing error:', error);
|
||||||
|
setModalConfig({
|
||||||
|
title: 'Gallery Error',
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to select photo',
|
||||||
|
type: 'error',
|
||||||
|
onConfirm: () => {},
|
||||||
|
showCancel: false,
|
||||||
|
icon: 'alert-circle',
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gallery launch error:', error);
|
||||||
|
setModalConfig({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to open gallery. Please try again.',
|
||||||
|
type: 'error',
|
||||||
|
onConfirm: () => {},
|
||||||
|
showCancel: false,
|
||||||
|
icon: 'alert-circle',
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uploadProfilePhoto Function
|
||||||
|
*
|
||||||
|
* Purpose: Upload selected profile photo to server
|
||||||
|
*
|
||||||
|
* @param imageUri - URI of the selected image
|
||||||
|
*/
|
||||||
|
const uploadProfilePhoto = async (imageUri: string) => {
|
||||||
|
try {
|
||||||
|
setUploadingPhoto(true);
|
||||||
|
|
||||||
|
// Create form data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('profile_photo', {
|
||||||
|
uri: imageUri,
|
||||||
|
type: 'image/jpeg',
|
||||||
|
name: 'profile_photo.jpg',
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
// Get user token from Redux
|
||||||
|
const token = user?.access_token;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Authentication token not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload using authAPI
|
||||||
|
const response = await authAPI.uploadProfilePhoto(formData, token);
|
||||||
|
|
||||||
|
// Type the response properly
|
||||||
|
const responseData = response.data as UploadPhotoResponse;
|
||||||
|
|
||||||
|
if (responseData.success) {
|
||||||
|
// Update local state with new photo
|
||||||
|
setTempProfilePhoto(null);
|
||||||
|
|
||||||
|
// Update Redux state with new profile photo URL
|
||||||
|
if (responseData.data?.profile_photo_url) {
|
||||||
|
console.log('Updating user profile with new photo URL:', responseData.data.profile_photo_url);
|
||||||
|
dispatch(updateUserProfile({
|
||||||
|
self_url: responseData.data.profile_photo_url
|
||||||
|
}));
|
||||||
|
console.log('Redux state updated successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
setModalConfig({
|
||||||
|
title: 'Success',
|
||||||
|
message: responseData.message || 'Profile photo updated successfully!',
|
||||||
|
type: 'success',
|
||||||
|
icon: 'check-circle',
|
||||||
|
onConfirm: () => {
|
||||||
|
// Optional: Refresh if needed, but Redux update should be enough
|
||||||
|
// handleRefresh();
|
||||||
|
},
|
||||||
|
showCancel: false,
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
} else {
|
||||||
|
throw new Error(responseData.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading photo:', error);
|
||||||
|
setModalConfig({
|
||||||
|
title: 'Upload Failed',
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to upload profile photo',
|
||||||
|
type: 'error',
|
||||||
|
icon: 'alert-circle',
|
||||||
|
onConfirm: () => {},
|
||||||
|
showCancel: false,
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
} finally {
|
||||||
|
setUploadingPhoto(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handleRefresh Function
|
* handleRefresh Function
|
||||||
*
|
*
|
||||||
@ -333,7 +695,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
console.log('user', user)
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MAIN RENDER
|
// MAIN RENDER
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -361,10 +723,20 @@ console.log('user', user)
|
|||||||
{user && (
|
{user && (
|
||||||
<View style={styles.profileCard}>
|
<View style={styles.profileCard}>
|
||||||
<View style={styles.profileHeader}>
|
<View style={styles.profileHeader}>
|
||||||
<View style={styles.profileImageContainer}>
|
<TouchableOpacity
|
||||||
{user.profile_photo_url ? (
|
style={styles.profileImageContainer}
|
||||||
|
onPress={handleProfilePhotoUpdate}
|
||||||
|
disabled={uploadingPhoto}
|
||||||
|
>
|
||||||
|
{tempProfilePhoto ? (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: API_CONFIG.BASE_URL + '/api/auth' + user.profile_photo_url }}
|
source={{ uri: tempProfilePhoto }}
|
||||||
|
style={styles.profileImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
) : user.self_url ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: API_CONFIG.BASE_URL + '/api/auth' + user.self_url }}
|
||||||
style={styles.profileImage}
|
style={styles.profileImage}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
@ -375,7 +747,22 @@ console.log('user', user)
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
|
||||||
|
{/* Edit icon overlay */}
|
||||||
|
<View style={styles.editIconOverlay}>
|
||||||
|
<Icon name="edit-3" size={16} color={theme.colors.background} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Loading indicator */}
|
||||||
|
{uploadingPhoto && (
|
||||||
|
<View style={styles.uploadingOverlay}>
|
||||||
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.profileInfo}>
|
<View style={styles.profileInfo}>
|
||||||
<Text style={styles.profileName}>
|
<Text style={styles.profileName}>
|
||||||
@ -389,12 +776,12 @@ console.log('user', user)
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Settings sections */}
|
{/* Settings sections */}
|
||||||
{settingsSections.map((section) => (
|
{settingsSections.map((section, index) =>
|
||||||
<SettingsSectionComponent
|
React.createElement(SettingsSectionComponent, {
|
||||||
key={section.id}
|
key: `${section.id}-${index}`,
|
||||||
section={section}
|
section: section
|
||||||
/>
|
})
|
||||||
))}
|
)}
|
||||||
|
|
||||||
{/* Bottom spacing for tab bar */}
|
{/* Bottom spacing for tab bar */}
|
||||||
<View style={styles.bottomSpacing} />
|
<View style={styles.bottomSpacing} />
|
||||||
@ -522,6 +909,33 @@ const styles = StyleSheet.create({
|
|||||||
color: theme.colors.primary,
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Edit icon overlay for profile photo update
|
||||||
|
editIconOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
borderRadius: 12,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: theme.colors.background,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Uploading overlay with loading indicator
|
||||||
|
uploadingOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
borderRadius: 30,
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ interface DicomViewerProps {
|
|||||||
dicomUrl: string;
|
dicomUrl: string;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
onLoad?: () => void;
|
onLoad?: () => void;
|
||||||
debugMode?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for WebView reference
|
// Interface for WebView reference
|
||||||
@ -23,40 +22,24 @@ interface WebViewRef {
|
|||||||
reload: () => void;
|
reload: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = false }: DicomViewerProps): React.ReactElement {
|
export default function DicomViewer({ dicomUrl, onError, onLoad }: DicomViewerProps): React.ReactElement {
|
||||||
const webViewRef = useRef<WebViewRef>(null);
|
const webViewRef = useRef<WebViewRef>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
const [debugInfo, setDebugInfo] = useState<string[]>([]);
|
|
||||||
const [webViewReady, setWebViewReady] = useState(false);
|
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
|
// Handle WebView load events
|
||||||
const handleLoadStart = () => {
|
const handleLoadStart = () => {
|
||||||
debugLog('WebView load started');
|
|
||||||
setIsLoading(true);
|
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadEnd = () => {
|
const handleLoadEnd = () => {
|
||||||
debugLog('WebView load ended');
|
|
||||||
setIsLoading(false);
|
|
||||||
setWebViewReady(true);
|
setWebViewReady(true);
|
||||||
onLoad?.();
|
onLoad?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (error: any) => {
|
const handleError = (error: any) => {
|
||||||
debugLog(`WebView error: ${JSON.stringify(error)}`);
|
|
||||||
setIsLoading(false);
|
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
onError?.(error?.nativeEvent?.description || 'Failed to load DICOM viewer');
|
onError?.(error?.nativeEvent?.description || 'Failed to load DICOM viewer');
|
||||||
};
|
};
|
||||||
@ -64,13 +47,11 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
|||||||
const handleMessage = (event: WebViewMessageEvent) => {
|
const handleMessage = (event: WebViewMessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const message = event.nativeEvent.data;
|
const message = event.nativeEvent.data;
|
||||||
debugLog(`Message from WebView: ${message}`);
|
|
||||||
|
|
||||||
// Try to parse JSON message
|
// Try to parse JSON message
|
||||||
if (typeof message === 'string') {
|
if (typeof message === 'string') {
|
||||||
try {
|
try {
|
||||||
const parsedMessage = JSON.parse(message);
|
const parsedMessage = JSON.parse(message);
|
||||||
debugLog(`Parsed message: ${JSON.stringify(parsedMessage)}`);
|
|
||||||
|
|
||||||
if (parsedMessage.type === 'error') {
|
if (parsedMessage.type === 'error') {
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
@ -79,26 +60,23 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
|||||||
setHasError(false);
|
setHasError(false);
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
debugLog(`Failed to parse message as JSON: ${parseError}`);
|
// Failed to parse message as JSON
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog(`Error handling WebView message: ${error}`);
|
// Error handling WebView message
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send DICOM URL to WebView when component mounts or URL changes
|
// Send DICOM URL to WebView when component mounts or URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (webViewRef.current && dicomUrl && webViewReady) {
|
if (webViewRef.current && dicomUrl && webViewReady) {
|
||||||
debugLog(`Sending DICOM URL to WebView: ${dicomUrl}`);
|
|
||||||
|
|
||||||
// Wait a bit for WebView to be ready
|
// Wait a bit for WebView to be ready
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (webViewRef.current) {
|
if (webViewRef.current) {
|
||||||
try {
|
try {
|
||||||
// Send the URL directly as a string message
|
// Send the URL directly as a string message
|
||||||
webViewRef.current.postMessage(dicomUrl);
|
webViewRef.current.postMessage(dicomUrl);
|
||||||
debugLog('DICOM URL sent successfully');
|
|
||||||
|
|
||||||
// Also try sending as a structured message
|
// Also try sending as a structured message
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -108,12 +86,11 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
|||||||
data: dicomUrl
|
data: dicomUrl
|
||||||
});
|
});
|
||||||
webViewRef.current.postMessage(structuredMessage);
|
webViewRef.current.postMessage(structuredMessage);
|
||||||
debugLog('Structured DICOM message sent');
|
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog(`Failed to send DICOM URL: ${error}`);
|
// Failed to send DICOM URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@ -124,25 +101,20 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
|||||||
|
|
||||||
// Reload WebView if there's an error
|
// Reload WebView if there's an error
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
debugLog('Retrying WebView load');
|
|
||||||
if (webViewRef.current) {
|
if (webViewRef.current) {
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
setIsLoading(true);
|
|
||||||
setWebViewReady(false);
|
setWebViewReady(false);
|
||||||
webViewRef.current.reload();
|
webViewRef.current.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear debug info
|
|
||||||
const clearDebugInfo = () => {
|
|
||||||
setDebugInfo([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef as any}
|
ref={webViewRef as any}
|
||||||
source={require('../../assets/dicom/dicom-viewer.html')}
|
source={{ uri: 'file:///android_asset/dicom-viewer.html' }}
|
||||||
originWhitelist={['*']}
|
originWhitelist={['*']}
|
||||||
javaScriptEnabled
|
javaScriptEnabled
|
||||||
domStorageEnabled
|
domStorageEnabled
|
||||||
@ -154,13 +126,7 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
|||||||
onError={handleError}
|
onError={handleError}
|
||||||
onMessage={handleMessage}
|
onMessage={handleMessage}
|
||||||
style={styles.webview}
|
style={styles.webview}
|
||||||
startInLoadingState
|
mixedContentMode="always"
|
||||||
renderLoading={() => (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<ActivityIndicator size="large" color="#2196F3" />
|
|
||||||
<Text style={styles.loadingText}>Loading DICOM Viewer...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasError && (
|
{hasError && (
|
||||||
@ -175,32 +141,7 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
|||||||
</View>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -214,21 +155,7 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#000',
|
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: {
|
errorContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -265,52 +192,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
241
app/shared/components/DicomViewerModal.example.tsx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* File: DicomViewerModal.example.tsx
|
||||||
|
* Description: Example usage of DicomViewerModal component
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
|
||||||
|
import { DicomViewerModal } from './index';
|
||||||
|
import { theme } from '../../theme/theme';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// EXAMPLE COMPONENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DicomViewerModalExample Component
|
||||||
|
*
|
||||||
|
* Purpose: Demonstrates how to use the DicomViewerModal component
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Shows how to pass dicomUrl to modal
|
||||||
|
* - Demonstrates modal state management
|
||||||
|
* - Example with patient information
|
||||||
|
* - Error handling examples
|
||||||
|
*/
|
||||||
|
export const DicomViewerModalExample: React.FC = () => {
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MANAGEMENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// Example DICOM URLs (replace with your actual URLs)
|
||||||
|
const exampleDicomUrl = 'https://example-dicom-server.com/studies/123/series/456/instances/789';
|
||||||
|
|
||||||
|
// Example patient data
|
||||||
|
const patientData = {
|
||||||
|
name: 'John Doe',
|
||||||
|
studyDescription: 'CT Brain with Contrast',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// EVENT HANDLERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open DICOM viewer modal
|
||||||
|
*/
|
||||||
|
const openDicomViewer = () => {
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close DICOM viewer modal
|
||||||
|
*/
|
||||||
|
const closeDicomViewer = () => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RENDER
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Trigger Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={openDicomViewer}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>View DICOM Image</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* DICOM Viewer Modal */}
|
||||||
|
<DicomViewerModal
|
||||||
|
visible={isModalVisible}
|
||||||
|
dicomUrl={exampleDicomUrl}
|
||||||
|
onClose={closeDicomViewer}
|
||||||
|
title="CT Brain Scan"
|
||||||
|
patientName={patientData.name}
|
||||||
|
studyDescription={patientData.studyDescription}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STYLES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
padding: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
|
||||||
|
button: {
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
paddingHorizontal: theme.spacing.xl,
|
||||||
|
paddingVertical: theme.spacing.md,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
...theme.shadows.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.background,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// USAGE EXAMPLES IN OTHER COMPONENTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
// Example 1: Basic Usage in Patient Details Screen
|
||||||
|
import { DicomViewerModal } from '../../../shared/components';
|
||||||
|
|
||||||
|
const PatientDetailsExample = () => {
|
||||||
|
const [showDicom, setShowDicom] = useState(false);
|
||||||
|
const dicomUrl = patient.scanResults?.dicomUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity onPress={() => setShowDicom(true)}>
|
||||||
|
<Text>View Scan Results</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<DicomViewerModal
|
||||||
|
visible={showDicom}
|
||||||
|
dicomUrl={dicomUrl}
|
||||||
|
onClose={() => setShowDicom(false)}
|
||||||
|
patientName={patient.name}
|
||||||
|
studyDescription={patient.scanResults?.description}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 2: Usage with Series Selection
|
||||||
|
import { DicomViewerModal } from '../../../shared/components';
|
||||||
|
|
||||||
|
const SeriesListExample = () => {
|
||||||
|
const [selectedDicom, setSelectedDicom] = useState<string | null>(null);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const openDicom = (dicomUrl: string) => {
|
||||||
|
setSelectedDicom(dicomUrl);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDicom = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setSelectedDicom(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{seriesList.map((series) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={series.id}
|
||||||
|
onPress={() => openDicom(series.dicomUrl)}
|
||||||
|
>
|
||||||
|
<Text>{series.description}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<DicomViewerModal
|
||||||
|
visible={modalVisible}
|
||||||
|
dicomUrl={selectedDicom || ''}
|
||||||
|
onClose={closeDicom}
|
||||||
|
title="Series Viewer"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 3: Usage with Error Handling
|
||||||
|
import { DicomViewerModal } from '../../../shared/components';
|
||||||
|
|
||||||
|
const ErrorHandlingExample = () => {
|
||||||
|
const [dicomModalState, setDicomModalState] = useState({
|
||||||
|
visible: false,
|
||||||
|
url: '',
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDicomWithValidation = (url: string, title: string) => {
|
||||||
|
if (!url) {
|
||||||
|
Alert.alert('Error', 'No DICOM URL available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDicomModalState({
|
||||||
|
visible: true,
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDicomModal = () => {
|
||||||
|
setDicomModalState({
|
||||||
|
visible: false,
|
||||||
|
url: '',
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => openDicomWithValidation(scan.url, scan.title)}
|
||||||
|
>
|
||||||
|
<Text>View Scan</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<DicomViewerModal
|
||||||
|
visible={dicomModalState.visible}
|
||||||
|
dicomUrl={dicomModalState.url}
|
||||||
|
onClose={closeDicomModal}
|
||||||
|
title={dicomModalState.title}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* End of File: DicomViewerModal.example.tsx
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
344
app/shared/components/DicomViewerModal.tsx
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
/*
|
||||||
|
* File: DicomViewerModal.tsx
|
||||||
|
* Description: Reusable modal component for DICOM image viewing
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
Modal,
|
||||||
|
SafeAreaView,
|
||||||
|
StatusBar,
|
||||||
|
Dimensions,
|
||||||
|
Alert,
|
||||||
|
} from 'react-native';
|
||||||
|
import { theme } from '../../theme/theme';
|
||||||
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
|
import DicomViewer from './DicomViewer';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DicomViewerModalProps Interface
|
||||||
|
*
|
||||||
|
* Purpose: Defines the props required by the DicomViewerModal component
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - visible: Whether the modal is visible
|
||||||
|
* - dicomUrl: URL of the DICOM file to display
|
||||||
|
* - onClose: Callback function when modal is closed
|
||||||
|
* - title: Optional title for the modal header
|
||||||
|
* - patientName: Optional patient name for context
|
||||||
|
* - studyDescription: Optional study description
|
||||||
|
*/
|
||||||
|
interface DicomViewerModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
dicomUrl: string;
|
||||||
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
|
patientName?: string;
|
||||||
|
studyDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DICOM VIEWER MODAL COMPONENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DicomViewerModal Component
|
||||||
|
*
|
||||||
|
* Purpose: Provides a full-screen modal for viewing DICOM medical images
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Full-screen DICOM image viewing
|
||||||
|
* - Modal overlay with close functionality
|
||||||
|
* - Error handling and display
|
||||||
|
* - Loading states
|
||||||
|
* - Header with patient/study information
|
||||||
|
* - Responsive design for different screen sizes
|
||||||
|
* - Proper medical image viewing environment (dark background)
|
||||||
|
*/
|
||||||
|
export const DicomViewerModal: React.FC<DicomViewerModalProps> = ({
|
||||||
|
visible,
|
||||||
|
dicomUrl,
|
||||||
|
onClose,
|
||||||
|
title = 'DICOM Viewer',
|
||||||
|
patientName,
|
||||||
|
studyDescription,
|
||||||
|
}) => {
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MANAGEMENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// EVENT HANDLERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle DICOM viewer load completion
|
||||||
|
*/
|
||||||
|
const handleDicomLoad = () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setHasError(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle DICOM viewer errors
|
||||||
|
* @param error - Error message from DICOM viewer
|
||||||
|
*/
|
||||||
|
const handleDicomError = (error: string) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setHasError(true);
|
||||||
|
|
||||||
|
// Show error alert to user
|
||||||
|
Alert.alert(
|
||||||
|
'DICOM Loading Error',
|
||||||
|
`Failed to load DICOM file: ${error}`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Retry',
|
||||||
|
onPress: () => {
|
||||||
|
setHasError(false);
|
||||||
|
setIsLoading(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Close',
|
||||||
|
onPress: onClose,
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle modal close request
|
||||||
|
*/
|
||||||
|
const handleClose = () => {
|
||||||
|
// Reset states when closing
|
||||||
|
setIsLoading(false);
|
||||||
|
setHasError(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle back button press (Android)
|
||||||
|
*/
|
||||||
|
const handleRequestClose = () => {
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RENDER HEADER
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render modal header with title and close button
|
||||||
|
*/
|
||||||
|
const renderHeader = () => (
|
||||||
|
<View style={styles.header}>
|
||||||
|
{/* Title and Patient Info */}
|
||||||
|
<View style={styles.headerContent}>
|
||||||
|
<Text style={styles.headerTitle}>{title}</Text>
|
||||||
|
{patientName && (
|
||||||
|
<Text style={styles.headerSubtitle}>Patient: {patientName}</Text>
|
||||||
|
)}
|
||||||
|
{studyDescription && (
|
||||||
|
<Text style={styles.headerSubtitle}>Study: {studyDescription}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Close Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.closeButton}
|
||||||
|
onPress={handleClose}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
>
|
||||||
|
<Icon name="x" size={24} color={theme.colors.background} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RENDER CONTENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render DICOM viewer content
|
||||||
|
*/
|
||||||
|
const renderContent = () => {
|
||||||
|
if (!dicomUrl) {
|
||||||
|
return (
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<Icon name="file-minus" size={48} color={theme.colors.textMuted} />
|
||||||
|
<Text style={styles.emptyText}>No DICOM URL provided</Text>
|
||||||
|
<TouchableOpacity style={styles.closeButtonSecondary} onPress={handleClose}>
|
||||||
|
<Text style={styles.closeButtonText}>Close</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.viewerContainer}>
|
||||||
|
<DicomViewer
|
||||||
|
dicomUrl={dicomUrl}
|
||||||
|
onLoad={handleDicomLoad}
|
||||||
|
onError={handleDicomError}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MAIN RENDER
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
presentationStyle="fullScreen"
|
||||||
|
onRequestClose={handleRequestClose}
|
||||||
|
>
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
{/* Set status bar to light content for dark background */}
|
||||||
|
<StatusBar
|
||||||
|
barStyle="light-content"
|
||||||
|
backgroundColor="#000000"
|
||||||
|
translucent={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
{renderHeader()}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{renderContent()}
|
||||||
|
</SafeAreaView>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STYLES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
// Main container
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Header section
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: theme.spacing.lg,
|
||||||
|
paddingVertical: theme.spacing.md,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Header content
|
||||||
|
headerContent: {
|
||||||
|
flex: 1,
|
||||||
|
marginRight: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Header title
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: theme.typography.fontSize.displaySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.background,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Header subtitle
|
||||||
|
headerSubtitle: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
lineHeight: theme.typography.fontSize.bodyMedium * 1.2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
closeButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// DICOM viewer container
|
||||||
|
viewerContainer: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Empty state container
|
||||||
|
emptyContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
paddingHorizontal: theme.spacing.xl,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Empty state text
|
||||||
|
emptyText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
color: theme.colors.textMuted,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: theme.spacing.md,
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Secondary close button
|
||||||
|
closeButtonSecondary: {
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
paddingHorizontal: theme.spacing.xl,
|
||||||
|
paddingVertical: theme.spacing.md,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
...theme.shadows.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Close button text
|
||||||
|
closeButtonText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.background,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// EXPORT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export default DicomViewerModal;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* End of File: DicomViewerModal.tsx
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
@ -18,6 +18,9 @@ export { ComingSoonScreen } from './ComingSoonScreen';
|
|||||||
// DICOM Viewer Component
|
// DICOM Viewer Component
|
||||||
export { default as DicomViewer } from './DicomViewer';
|
export { default as DicomViewer } from './DicomViewer';
|
||||||
|
|
||||||
|
// DICOM Viewer Modal Component
|
||||||
|
export { default as DicomViewerModal } from './DicomViewerModal';
|
||||||
|
|
||||||
// DICOM Viewer Test Component
|
// DICOM Viewer Test Component
|
||||||
export { default as DicomViewerTest } from './DicomViewerTest';
|
export { default as DicomViewerTest } from './DicomViewerTest';
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export interface User {
|
|||||||
onboarding_message: string;
|
onboarding_message: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
platform: 'app'|'web';
|
platform: 'app'|'web';
|
||||||
|
self_url:string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserRole =
|
export type UserRole =
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0C80B921A6F3F58F76C31292 /* libPods-NeoScan_Physician.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Physician.a */; };
|
0C80B921A6F3F58F76C31292 /* libPods-NeoScan_Radiologist.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Radiologist.a */; };
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
||||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||||
@ -27,21 +27,21 @@
|
|||||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||||
proxyType = 1;
|
proxyType = 1;
|
||||||
remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
|
remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
|
||||||
remoteInfo = NeoScan_Physician;
|
remoteInfo = NeoScan_Radiologist;
|
||||||
};
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
13B07F961A680F5B00A75B9A /* NeoScan_Physician.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NeoScan_Physician.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
13B07F961A680F5B00A75B9A /* NeoScan_Radiologist.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NeoScan_Radiologist.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = NeoScan_Physician/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = NeoScan_Radiologist/Images.xcassets; sourceTree = "<group>"; };
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = NeoScan_Physician/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = NeoScan_Radiologist/Info.plist; sourceTree = "<group>"; };
|
||||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = NeoScan_Physician/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = NeoScan_Radiologist/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
3B4392A12AC88292D35C810B /* Pods-NeoScan_Physician.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NeoScan_Physician.debug.xcconfig"; path = "Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician.debug.xcconfig"; sourceTree = "<group>"; };
|
3B4392A12AC88292D35C810B /* Pods-NeoScan_Radiologist.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NeoScan_Radiologist.debug.xcconfig"; path = "Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
5709B34CF0A7D63546082F79 /* Pods-NeoScan_Physician.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NeoScan_Physician.release.xcconfig"; path = "Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician.release.xcconfig"; sourceTree = "<group>"; };
|
5709B34CF0A7D63546082F79 /* Pods-NeoScan_Radiologist.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NeoScan_Radiologist.release.xcconfig"; path = "Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Physician.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NeoScan_Physician.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Radiologist.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NeoScan_Radiologist.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = NeoScan_Physician/AppDelegate.swift; sourceTree = "<group>"; };
|
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = NeoScan_Radiologist/AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = NeoScan_Physician/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = NeoScan_Radiologist/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
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; };
|
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; };
|
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; };
|
||||||
@ -58,7 +58,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
0C80B921A6F3F58F76C31292 /* libPods-NeoScan_Physician.a in Frameworks */,
|
0C80B921A6F3F58F76C31292 /* libPods-NeoScan_Radiologist.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -73,7 +73,7 @@
|
|||||||
name = "Supporting Files";
|
name = "Supporting Files";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
13B07FAE1A68108700A75B9A /* NeoScan_Physician */ = {
|
13B07FAE1A68108700A75B9A /* NeoScan_Radiologist */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||||
@ -82,14 +82,14 @@
|
|||||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
||||||
);
|
);
|
||||||
name = NeoScan_Physician;
|
name = NeoScan_Radiologist;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||||
5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Physician.a */,
|
5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Radiologist.a */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -104,7 +104,7 @@
|
|||||||
83CBB9F61A601CBA00E9B192 = {
|
83CBB9F61A601CBA00E9B192 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
13B07FAE1A68108700A75B9A /* NeoScan_Physician */,
|
13B07FAE1A68108700A75B9A /* NeoScan_Radiologist */,
|
||||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||||
83CBBA001A601CBA00E9B192 /* Products */,
|
83CBBA001A601CBA00E9B192 /* Products */,
|
||||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||||
@ -119,7 +119,7 @@
|
|||||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
13B07F961A680F5B00A75B9A /* NeoScan_Physician.app */,
|
13B07F961A680F5B00A75B9A /* NeoScan_Radiologist.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -127,8 +127,8 @@
|
|||||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3B4392A12AC88292D35C810B /* Pods-NeoScan_Physician.debug.xcconfig */,
|
3B4392A12AC88292D35C810B /* Pods-NeoScan_Radiologist.debug.xcconfig */,
|
||||||
5709B34CF0A7D63546082F79 /* Pods-NeoScan_Physician.release.xcconfig */,
|
5709B34CF0A7D63546082F79 /* Pods-NeoScan_Radiologist.release.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -152,9 +152,9 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
13B07F861A680F5B00A75B9A /* NeoScan_Physician */ = {
|
13B07F861A680F5B00A75B9A /* NeoScan_Radiologist */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NeoScan_Physician" */;
|
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NeoScan_Radiologist" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
|
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
|
||||||
13B07F871A680F5B00A75B9A /* Sources */,
|
13B07F871A680F5B00A75B9A /* Sources */,
|
||||||
@ -168,9 +168,9 @@
|
|||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = NeoScan_Physician;
|
name = NeoScan_Radiologist;
|
||||||
productName = NeoScan_Physician;
|
productName = NeoScan_Radiologist;
|
||||||
productReference = 13B07F961A680F5B00A75B9A /* NeoScan_Physician.app */;
|
productReference = 13B07F961A680F5B00A75B9A /* NeoScan_Radiologist.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
@ -186,7 +186,7 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NeoScan_Physician" */;
|
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NeoScan_Radiologist" */;
|
||||||
compatibilityVersion = "Xcode 12.0";
|
compatibilityVersion = "Xcode 12.0";
|
||||||
developmentRegion = en;
|
developmentRegion = en;
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
@ -199,7 +199,7 @@
|
|||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
13B07F861A680F5B00A75B9A /* NeoScan_Physician */,
|
13B07F861A680F5B00A75B9A /* NeoScan_Radiologist */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@ -254,15 +254,15 @@
|
|||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist-frameworks.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
|
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
@ -280,7 +280,7 @@
|
|||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"$(DERIVED_FILE_DIR)/Pods-NeoScan_Physician-checkManifestLockResult.txt",
|
"$(DERIVED_FILE_DIR)/Pods-NeoScan_Radiologist-checkManifestLockResult.txt",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
@ -293,15 +293,15 @@
|
|||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Radiologist/Pods-NeoScan_Radiologist-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
@ -320,7 +320,7 @@
|
|||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
|
00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = 13B07F861A680F5B00A75B9A /* NeoScan_Physician */;
|
target = 13B07F861A680F5B00A75B9A /* NeoScan_Radiologist */;
|
||||||
targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
|
targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
@ -328,13 +328,13 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-NeoScan_Physician.debug.xcconfig */;
|
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-NeoScan_Radiologist.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = NeoScan_Physician/Info.plist;
|
INFOPLIST_FILE = NeoScan_Radiologist/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -347,7 +347,7 @@
|
|||||||
"-lc++",
|
"-lc++",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||||
PRODUCT_NAME = NeoScan_Physician;
|
PRODUCT_NAME = NeoScan_Radiologist;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
@ -356,12 +356,12 @@
|
|||||||
};
|
};
|
||||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-NeoScan_Physician.release.xcconfig */;
|
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-NeoScan_Radiologist.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
INFOPLIST_FILE = NeoScan_Physician/Info.plist;
|
INFOPLIST_FILE = NeoScan_Radiologist/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -374,7 +374,7 @@
|
|||||||
"-lc++",
|
"-lc++",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||||
PRODUCT_NAME = NeoScan_Physician;
|
PRODUCT_NAME = NeoScan_Radiologist;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
@ -522,7 +522,7 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NeoScan_Physician" */ = {
|
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NeoScan_Radiologist" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
13B07F941A680F5B00A75B9A /* Debug */,
|
13B07F941A680F5B00A75B9A /* Debug */,
|
||||||
@ -531,7 +531,7 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NeoScan_Physician" */ = {
|
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NeoScan_Radiologist" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "NeoScan_Physician.app"
|
BuildableName = "NeoScan_Radiologist.app"
|
||||||
BlueprintName = "NeoScan_Physician"
|
BlueprintName = "NeoScan_Radiologist"
|
||||||
ReferencedContainer = "container:NeoScan_Physician.xcodeproj">
|
ReferencedContainer = "container:NeoScan_Radiologist.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||||
BuildableName = "NeoScan_PhysicianTests.xctest"
|
BuildableName = "NeoScan_PhysicianTests.xctest"
|
||||||
BlueprintName = "NeoScan_PhysicianTests"
|
BlueprintName = "NeoScan_PhysicianTests"
|
||||||
ReferencedContainer = "container:NeoScan_Physician.xcodeproj">
|
ReferencedContainer = "container:NeoScan_Radiologist.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
@ -55,9 +55,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "NeoScan_Physician.app"
|
BuildableName = "NeoScan_Radiologist.app"
|
||||||
BlueprintName = "NeoScan_Physician"
|
BlueprintName = "NeoScan_Radiologist"
|
||||||
ReferencedContainer = "container:NeoScan_Physician.xcodeproj">
|
ReferencedContainer = "container:NeoScan_Radiologist.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
@ -72,9 +72,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "NeoScan_Physician.app"
|
BuildableName = "NeoScan_Radiologist.app"
|
||||||
BlueprintName = "NeoScan_Physician"
|
BlueprintName = "NeoScan_Radiologist"
|
||||||
ReferencedContainer = "container:NeoScan_Physician.xcodeproj">
|
ReferencedContainer = "container:NeoScan_Radiologist.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
|
|||||||
@ -24,7 +24,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
window = UIWindow(frame: UIScreen.main.bounds)
|
window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
|
|
||||||
factory.startReactNative(
|
factory.startReactNative(
|
||||||
withModuleName: "NeoScan_Physician",
|
withModuleName: "NeoScan_Radiologist",
|
||||||
in: window,
|
in: window,
|
||||||
launchOptions: launchOptions
|
launchOptions: launchOptions
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>NeoScan_Physician</string>
|
<string>NeoScan_Radiologist</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="NeoScan_Physician" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="NeoScan_Radiologist" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||||
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
|
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ if linkage != nil
|
|||||||
use_frameworks! :linkage => linkage.to_sym
|
use_frameworks! :linkage => linkage.to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'NeoScan_Physician' do
|
target 'NeoScan_Radiologist' do
|
||||||
config = use_native_modules!
|
config = use_native_modules!
|
||||||
|
|
||||||
use_react_native!(
|
use_react_native!(
|
||||||
|
|||||||
4
package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "NeoScan_Physician",
|
"name": "NeoScan_Radiologist",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "NeoScan_Physician",
|
"name": "NeoScan_Radiologist",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native-async-storage/async-storage": "^2.1.0",
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "NeoScan_Physician",
|
"name": "NeoScan_Radiologist",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||