fix(bugs): Update dependencies and enhance UI components (#4478)
This commit is contained in:
parent
61e3a9cb52
commit
05d41c5206
@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.1.17",
|
||||
"@cornerstonejs/core": "^2.1.17",
|
||||
"@cornerstonejs/adapters": "^2.2.1",
|
||||
"@cornerstonejs/core": "^2.2.1",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.1.17",
|
||||
"@cornerstonejs/core": "^2.1.17",
|
||||
"@cornerstonejs/adapters": "^2.2.1",
|
||||
"@cornerstonejs/core": "^2.2.1",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } f
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
import { useSegmentationPresentationStore } from '@ohif/extension-cornerstone';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
@ -251,6 +250,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
if (toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// keep the already stored segmentationPresentation for this viewport in memory
|
||||
// so that we can restore it after hydrating the SEG
|
||||
commandsManager.runCommand('updateStoredSegmentationPresentation', {
|
||||
displaySet: segDisplaySet,
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
});
|
||||
|
||||
// always start fresh for this viewport since it is special type of viewport
|
||||
// that should only show one segmentation at a time.
|
||||
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.1.17",
|
||||
"@cornerstonejs/core": "^2.1.17",
|
||||
"@cornerstonejs/tools": "^2.1.17",
|
||||
"@cornerstonejs/adapters": "^2.2.1",
|
||||
"@cornerstonejs/core": "^2.2.1",
|
||||
"@cornerstonejs/tools": "^2.2.1",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^2.1.17",
|
||||
"@cornerstonejs/tools": "^2.1.17",
|
||||
"@cornerstonejs/core": "^2.2.1",
|
||||
"@cornerstonejs/tools": "^2.2.1",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.1.17",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.2.1",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@ohif/core": "3.9.0-beta.109",
|
||||
"@ohif/ui": "3.9.0-beta.109",
|
||||
@ -55,9 +55,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.1.17",
|
||||
"@cornerstonejs/core": "^2.1.17",
|
||||
"@cornerstonejs/tools": "^2.1.17",
|
||||
"@cornerstonejs/adapters": "^2.2.1",
|
||||
"@cornerstonejs/core": "^2.2.1",
|
||||
"@cornerstonejs/tools": "^2.2.1",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@ -111,16 +111,14 @@ function commandsModule({
|
||||
commandsManager.run(options, optionsToUse);
|
||||
},
|
||||
updateStoredSegmentationPresentation: ({ displaySet, type }) => {
|
||||
const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
|
||||
const { addSegmentationPresentationItem } = useSegmentationPresentationStore.getState();
|
||||
|
||||
const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID;
|
||||
setSegmentationPresentation(referencedDisplaySetInstanceUID, [
|
||||
{
|
||||
segmentationId: displaySet.displaySetInstanceUID,
|
||||
hydrated: true,
|
||||
type,
|
||||
},
|
||||
]);
|
||||
addSegmentationPresentationItem(referencedDisplaySetInstanceUID, {
|
||||
segmentationId: displaySet.displaySetInstanceUID,
|
||||
hydrated: true,
|
||||
type,
|
||||
});
|
||||
},
|
||||
updateStoredPositionPresentation: ({ viewportId, displaySetInstanceUID }) => {
|
||||
const presentations = cornerstoneViewportService.getPresentations(viewportId);
|
||||
@ -1043,7 +1041,7 @@ function commandsModule({
|
||||
* @param props.key - The style key to set
|
||||
* @param props.value - The style value
|
||||
*/
|
||||
setSegmentationStyleCommand: ({ segmentationId, type, key, value }) => {
|
||||
setSegmentationStyleCommand: ({ type, key, value }) => {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
segmentationService.setStyle({ type }, { [key]: value });
|
||||
},
|
||||
@ -1150,12 +1148,17 @@ function commandsModule({
|
||||
const segment = segmentation.segments[segmentIndex];
|
||||
const { label } = segment;
|
||||
|
||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||
const callback = (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
||||
};
|
||||
|
||||
callInputDialog(uiDialogService, label, callback, false, {
|
||||
dialogTitle: 'Edit Segment Label',
|
||||
inputLabel: 'Enter new label',
|
||||
});
|
||||
},
|
||||
|
||||
@ -1169,12 +1172,17 @@ function commandsModule({
|
||||
|
||||
const { label } = segmentation;
|
||||
|
||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||
const callback = (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.addOrUpdateSegmentation({ segmentationId, label });
|
||||
};
|
||||
|
||||
callInputDialog(uiDialogService, label, callback, false, {
|
||||
dialogTitle: 'Edit Segmentation Label',
|
||||
inputLabel: 'Enter new label',
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -6,8 +6,6 @@ import { metaData } from '@cornerstonejs/core';
|
||||
export default function PanelSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
children,
|
||||
}: withAppTypes) {
|
||||
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
|
||||
@ -91,23 +89,23 @@ export default function PanelSegmentation({
|
||||
commandsManager.run('deleteSegmentation', { segmentationId });
|
||||
},
|
||||
|
||||
setFillAlpha: (type, value) => {
|
||||
setFillAlpha: ({ type }, value) => {
|
||||
commandsManager.run('setFillAlpha', { type, value });
|
||||
},
|
||||
|
||||
setOutlineWidth: (type, value) => {
|
||||
setOutlineWidth: ({ type }, value) => {
|
||||
commandsManager.run('setOutlineWidth', { type, value });
|
||||
},
|
||||
|
||||
setRenderFill: (type, value) => {
|
||||
setRenderFill: ({ type }, value) => {
|
||||
commandsManager.run('setRenderFill', { type, value });
|
||||
},
|
||||
|
||||
setRenderOutline: (type, value) => {
|
||||
setRenderOutline: ({ type }, value) => {
|
||||
commandsManager.run('setRenderOutline', { type, value });
|
||||
},
|
||||
|
||||
setFillAlphaInactive: (type, value) => {
|
||||
setFillAlphaInactive: ({ type }, value) => {
|
||||
commandsManager.run('setFillAlphaInactive', { type, value });
|
||||
},
|
||||
|
||||
@ -152,6 +150,14 @@ export default function PanelSegmentation({
|
||||
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
|
||||
const { representationData, segmentationId } = segmentation;
|
||||
const { Labelmap } = representationData;
|
||||
|
||||
if (!Labelmap) {
|
||||
return {
|
||||
segmentationId,
|
||||
isExportable: true,
|
||||
};
|
||||
}
|
||||
|
||||
const referencedImageIds = Labelmap.referencedImageIds;
|
||||
const firstImageId = referencedImageIds[0];
|
||||
|
||||
|
||||
@ -100,10 +100,16 @@ class SegmentationService extends PubSubService {
|
||||
this._segmentationIdToColorLUTIndexMap = new Map();
|
||||
|
||||
this.servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
public onModeEnter(): void {
|
||||
this._initSegmentationService();
|
||||
}
|
||||
|
||||
public onModeExit(): void {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a segmentation by its ID.
|
||||
*
|
||||
@ -1386,7 +1392,11 @@ class SegmentationService extends PubSubService {
|
||||
const frameOfReferenceUID = viewport.getFrameOfReferenceUID();
|
||||
const segImage = cache.getImage(imageIds[0]);
|
||||
|
||||
if (segImage?.FrameOfReferenceUID === frameOfReferenceUID) {
|
||||
if (
|
||||
segImage?.FrameOfReferenceUID &&
|
||||
frameOfReferenceUID &&
|
||||
segImage.FrameOfReferenceUID === frameOfReferenceUID
|
||||
) {
|
||||
const isConverted = await this.convertStackToVolumeViewport(viewport);
|
||||
triggerSegmentationRepresentationModified(
|
||||
viewportId,
|
||||
|
||||
@ -763,6 +763,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
});
|
||||
}
|
||||
|
||||
if (!volumesNotLoaded.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This returns the async continuation only
|
||||
return this.setVolumesForViewport(viewport, volumeInputArray, presentations);
|
||||
}
|
||||
@ -1114,8 +1118,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
|
||||
const { segmentationService } = this.servicesManager.services;
|
||||
|
||||
segmentationPresentation.forEach((presentation: SegmentationPresentationItem) => {
|
||||
const { segmentationId, type, hydrated } = presentation;
|
||||
segmentationPresentation.forEach((presentationItem: SegmentationPresentationItem) => {
|
||||
const { segmentationId, type, hydrated } = presentationItem;
|
||||
|
||||
if (hydrated) {
|
||||
segmentationService.addSegmentationRepresentation(viewport.id, {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { SegmentationPresentation } from '../types/Presentation';
|
||||
import { SegmentationPresentation, SegmentationPresentationItem } from '../types/Presentation';
|
||||
import { JOIN_STR } from './presentationUtils';
|
||||
import { getViewportOrientationFromImageOrientationPatient } from '../utils/getViewportOrientationFromImageOrientationPatient';
|
||||
|
||||
@ -62,10 +62,9 @@ type SegmentationPresentationStore = {
|
||||
* @param segmentationPresentation - The `SegmentationPresentation` to add.
|
||||
* @param servicesManager - The services manager instance.
|
||||
*/
|
||||
addSegmentationPresentation: (
|
||||
addSegmentationPresentationItem: (
|
||||
presentationId: string,
|
||||
segmentationPresentation: SegmentationPresentation,
|
||||
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
|
||||
segmentationPresentationItem: SegmentationPresentationItem
|
||||
) => void;
|
||||
|
||||
/**
|
||||
@ -183,33 +182,52 @@ const createSegmentationPresentationStore = set => ({
|
||||
set({ segmentationPresentationStore: {} }, false, 'clearSegmentationPresentationStore'),
|
||||
|
||||
/**
|
||||
* Adds a new segmentation presentation to the store.
|
||||
* Adds a new segmentation presentation item to the store.
|
||||
*
|
||||
* segmentationPresentationItem: {
|
||||
* segmentationId: string;
|
||||
* type: SegmentationRepresentations;
|
||||
* hydrated: boolean | null;
|
||||
* config?: unknown;
|
||||
* }
|
||||
*/
|
||||
addSegmentationPresentation: (
|
||||
addSegmentationPresentationItem: (
|
||||
presentationId: string,
|
||||
segmentationPresentation: SegmentationPresentation,
|
||||
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
|
||||
segmentationPresentationItem: SegmentationPresentationItem
|
||||
) =>
|
||||
set(
|
||||
state => ({
|
||||
segmentationPresentationStore: {
|
||||
...state.segmentationPresentationStore,
|
||||
[presentationId]: segmentationPresentation,
|
||||
[presentationId]: [
|
||||
...(state.segmentationPresentationStore[presentationId] || []),
|
||||
segmentationPresentationItem,
|
||||
],
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'addSegmentationPresentation'
|
||||
'addSegmentationPresentationItem'
|
||||
),
|
||||
|
||||
/**
|
||||
* Sets the segmentation presentation for a given presentation ID.
|
||||
* Sets the segmentation presentation for a given presentation ID. A segmentation
|
||||
* presentation is an array of SegmentationPresentationItem.
|
||||
*
|
||||
* segmentationPresentationItem: {
|
||||
* segmentationId: string;
|
||||
* type: SegmentationRepresentations;
|
||||
* hydrated: boolean | null;
|
||||
* config?: unknown;
|
||||
* }
|
||||
*
|
||||
* segmentationPresentation: SegmentationPresentationItem[]
|
||||
*/
|
||||
setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) =>
|
||||
setSegmentationPresentation: (presentationId: string, values: SegmentationPresentation) =>
|
||||
set(
|
||||
state => ({
|
||||
segmentationPresentationStore: {
|
||||
...state.segmentationPresentationStore,
|
||||
[presentationId]: value,
|
||||
[presentationId]: values,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
|
||||
@ -4,9 +4,9 @@ import getDisplayUnit from './getDisplayUnit';
|
||||
export const getStatisticDisplayString = (numbers, unit, key) => {
|
||||
if (Array.isArray(numbers) && numbers.length > 0) {
|
||||
const results = numbers.map(number => utils.roundNumber(number, 2));
|
||||
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${results.join(', ')} <small>${getDisplayUnit(unit)}</small>`;
|
||||
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${results.join(', ')} ${getDisplayUnit(unit)}`;
|
||||
}
|
||||
|
||||
const result = utils.roundNumber(numbers, 2);
|
||||
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${result} <small>${getDisplayUnit(unit)}</small>`;
|
||||
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${result} ${getDisplayUnit(unit)}`;
|
||||
};
|
||||
|
||||
@ -23,16 +23,26 @@ const SidePanelWithServices = ({
|
||||
|
||||
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
|
||||
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
||||
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
||||
const [sidePanelOpen, setSidePanelOpen] = useState(activeTabIndexProp !== null);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
|
||||
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
|
||||
|
||||
const handleSidePanelOpen = useCallback(() => {
|
||||
setHasBeenOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
setSidePanelOpen(activeTabIndex !== null);
|
||||
}, []);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setSidePanelOpen(true);
|
||||
// If panel is being opened but no tab is active, set first tab as active
|
||||
if (activeTabIndex === null && tabs.length > 0) {
|
||||
setActiveTabIndex(0);
|
||||
}
|
||||
}, [activeTabIndex, tabs]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSidePanelOpen(false);
|
||||
setActiveTabIndex(null);
|
||||
}, []);
|
||||
|
||||
/** update the active tab index from outside */
|
||||
@ -61,7 +71,7 @@ const SidePanelWithServices = ({
|
||||
const activatePanelSubscription = panelService.subscribe(
|
||||
panelService.EVENTS.ACTIVATE_PANEL,
|
||||
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
||||
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
||||
if (sidePanelOpen || activatePanelEvent.forceActive) {
|
||||
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
|
||||
if (tabIndex !== -1) {
|
||||
setActiveTabIndex(tabIndex);
|
||||
@ -73,7 +83,7 @@ const SidePanelWithServices = ({
|
||||
return () => {
|
||||
activatePanelSubscription.unsubscribe();
|
||||
};
|
||||
}, [tabs, hasBeenOpened, panelService]);
|
||||
}, [tabs, sidePanelOpen, panelService]);
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
@ -81,10 +91,11 @@ const SidePanelWithServices = ({
|
||||
side={side}
|
||||
tabs={tabs}
|
||||
activeTabIndex={activeTabIndex}
|
||||
onOpen={handleSidePanelOpen}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
onActiveTabIndexChange={handleActiveTabIndexChange}
|
||||
expandedWidth={expandedWidth}
|
||||
></SidePanel>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "^2.1.17",
|
||||
"@cornerstonejs/tools": "^2.1.17",
|
||||
"@cornerstonejs/core": "^2.2.1",
|
||||
"@cornerstonejs/tools": "^2.2.1",
|
||||
"@ohif/core": "3.9.0-beta.109",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.9.0-beta.109",
|
||||
"@ohif/extension-default": "3.9.0-beta.109",
|
||||
|
||||
@ -98,6 +98,9 @@ function TrackedCornerstoneViewport(
|
||||
useEffect(() => {
|
||||
if (isTracked) {
|
||||
annotation.config.style.setViewportToolStyles(viewportId, {
|
||||
ReferenceLines: {
|
||||
lineDash: '4,4',
|
||||
},
|
||||
global: {
|
||||
lineDash: '',
|
||||
},
|
||||
|
||||
@ -140,7 +140,13 @@ const toolbarButtons: Button[] = [
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
evaluate: [
|
||||
'evaluate.action',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['video', 'wholeSlide'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -213,7 +213,13 @@ const toolbarButtons: Button[] = [
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
commands: 'showDownloadViewportModal',
|
||||
evaluate: 'evaluate.action',
|
||||
evaluate: [
|
||||
'evaluate.action',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['video', 'wholeSlide'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -85,8 +85,14 @@ function modeFactory({ modeConfiguration }) {
|
||||
* Lifecycle hooks
|
||||
*/
|
||||
onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) {
|
||||
const { measurementService, toolbarService, toolGroupService, customizationService } =
|
||||
servicesManager.services;
|
||||
const {
|
||||
measurementService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
panelService,
|
||||
segmentationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
|
||||
@ -110,25 +116,23 @@ function modeFactory({ modeConfiguration }) {
|
||||
|
||||
// // ActivatePanel event trigger for when a segmentation or measurement is added.
|
||||
// // Do not force activation so as to respect the state the user may have left the UI in.
|
||||
// _activatePanelTriggersSubscriptions = [
|
||||
// ...panelService.addActivatePanelTriggers(dicomSeg.panel, [
|
||||
// {
|
||||
// sourcePubSubService: segmentationService,
|
||||
// sourceEvents: [
|
||||
// segmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED,
|
||||
// ],
|
||||
// },
|
||||
// ]),
|
||||
// ...panelService.addActivatePanelTriggers(tracked.measurements, [
|
||||
// {
|
||||
// sourcePubSubService: measurementService,
|
||||
// sourceEvents: [
|
||||
// measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||
// measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||
// ],
|
||||
// },
|
||||
// ]),
|
||||
// ];
|
||||
_activatePanelTriggersSubscriptions = [
|
||||
...panelService.addActivatePanelTriggers(cornerstone.segmentation, [
|
||||
{
|
||||
sourcePubSubService: segmentationService,
|
||||
sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
|
||||
},
|
||||
]),
|
||||
...panelService.addActivatePanelTriggers(tracked.measurements, [
|
||||
{
|
||||
sourcePubSubService: measurementService,
|
||||
sourceEvents: [
|
||||
measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||
],
|
||||
},
|
||||
]),
|
||||
];
|
||||
},
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
const {
|
||||
|
||||
@ -172,7 +172,7 @@ const toolbarButtons: Button[] = [
|
||||
'evaluate.action',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['video'],
|
||||
unsupportedViewportTypes: ['video', 'wholeSlide'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -114,8 +114,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
},
|
||||
],
|
||||
extensions: extensionDependencies,
|
||||
hangingProtocol: ['default'],
|
||||
|
||||
hangingProtocol: 'default',
|
||||
sopClassHandlers: [
|
||||
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler',
|
||||
|
||||
@ -114,7 +114,13 @@ const toolbarButtons = [
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
commands: 'showDownloadViewportModal',
|
||||
evaluate: 'evaluate.action',
|
||||
evaluate: [
|
||||
'evaluate.action',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['video', 'wholeSlide'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -133,7 +133,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
props: {
|
||||
leftPanels: [ohif.leftPanel],
|
||||
rightPanels: [cornerstone.panelTool],
|
||||
leftPanelClosed: true,
|
||||
// leftPanelClosed: true,
|
||||
viewports: [
|
||||
{
|
||||
namespace: cornerstone.viewport,
|
||||
|
||||
@ -69,7 +69,13 @@ const toolbarButtons: Button[] = [
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
commands: 'showDownloadViewportModal',
|
||||
evaluate: 'evaluate.action',
|
||||
evaluate: [
|
||||
'evaluate.action',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['video', 'wholeSlide'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.1.17",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.2.1",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.9.0-beta.109",
|
||||
"@ohif/extension-cornerstone": "3.9.0-beta.109",
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.1.17",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.2.1",
|
||||
"@ohif/ui": "3.9.0-beta.109",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@ -165,6 +165,10 @@ export default class CustomizationService extends PubSubService {
|
||||
this.modeCustomizations.clear();
|
||||
}
|
||||
|
||||
public onModeExit(): void {
|
||||
this.onModeEnter();
|
||||
}
|
||||
|
||||
public getModeCustomizations(): Map<string, Customization> {
|
||||
return this.modeCustomizations;
|
||||
}
|
||||
|
||||
@ -9,15 +9,20 @@ const generateAcceptHeader = (
|
||||
}
|
||||
|
||||
let acceptHeader = ['multipart/related'];
|
||||
let hasTransferSyntax = false;
|
||||
if (requestTransferSyntaxUID && typeForTS[requestTransferSyntaxUID]) {
|
||||
const type = typeForTS[requestTransferSyntaxUID];
|
||||
acceptHeader.push('type=' + type);
|
||||
acceptHeader.push('transfer-syntax=' + requestTransferSyntaxUID);
|
||||
hasTransferSyntax = true;
|
||||
} else {
|
||||
acceptHeader.push('type=application/octet-stream');
|
||||
}
|
||||
|
||||
acceptHeader.push('transfer-syntax=*');
|
||||
if (!hasTransferSyntax) {
|
||||
acceptHeader.push('transfer-syntax=*');
|
||||
}
|
||||
|
||||
if (!omitQuotationForMultipartRequest) {
|
||||
//need to add quotation for each mime type of each accept entry
|
||||
acceptHeader = acceptHeader.map(mime => {
|
||||
|
||||
@ -178,6 +178,31 @@ Now:
|
||||
|
||||
---
|
||||
|
||||
## SidePanel auto switch if open
|
||||
|
||||
In `basic viewer` mode, if the side panel is open and the segmentation panel is active, adding a measurement will automatically switch to the measurement panel. This switch won't occur if the side panel is closed. To enable or disable this feature, adjust your mode configuration accordingly.
|
||||
|
||||
```js
|
||||
panelService.addActivatePanelTriggers('your.panel.id', [
|
||||
{
|
||||
sourcePubSubService: segmentationService,
|
||||
sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
|
||||
},
|
||||
])
|
||||
|
||||
panelService.addActivatePanelTriggers('your.panel.id', [
|
||||
{
|
||||
sourcePubSubService: measurementService,
|
||||
sourceEvents: [
|
||||
measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||
],
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DicomUpload
|
||||
|
||||
The DICOM upload functionality in OHIF has been refactored to use the standard customization service pattern. Now you don't need to put
|
||||
|
||||
@ -53,7 +53,6 @@
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-shepherd": "6.1.1",
|
||||
"sanitize-html": "^2.13.1",
|
||||
"shepherd.js": "13.0.3",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
} from '../../components/DropdownMenu';
|
||||
import { Icons } from '../../components/Icons/Icons';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/Tooltip/Tooltip';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
/**
|
||||
* DataRow is a complex UI component that displays a selectable, interactive row with hierarchical data.
|
||||
@ -131,7 +130,7 @@ const DataRow: React.FC<DataRowProps> = ({
|
||||
key={`empty-${indent}`}
|
||||
className="h-2"
|
||||
></div>
|
||||
); // Empty row
|
||||
);
|
||||
}
|
||||
const cleanText = decodeHTML(text);
|
||||
return (
|
||||
@ -142,11 +141,11 @@ const DataRow: React.FC<DataRowProps> = ({
|
||||
{indentation}
|
||||
{cleanText.includes(':') ? (
|
||||
<>
|
||||
<span className="font-medium">{sanitizeHtml(cleanText.split(':')[0])}:</span>
|
||||
{sanitizeHtml(cleanText.split(':')[1])}
|
||||
<span className="font-medium">{cleanText.split(':')[0]}:</span>
|
||||
{cleanText.split(':')[1]}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium">{sanitizeHtml(cleanText)}</span>
|
||||
<span className="font-medium">{cleanText}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -51,17 +51,19 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
|
||||
|
||||
return (
|
||||
<div className="bg-primary-dark my-px flex h-7 w-full items-center justify-between rounded pl-0.5 pr-7">
|
||||
{allowAddSegment ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pr pl-0.5"
|
||||
onClick={() => onSegmentAdd(segmentationIdToUse)}
|
||||
>
|
||||
<Icons.Add />
|
||||
Add Segment
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="flex-1">
|
||||
{allowAddSegment ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pr pl-0.5"
|
||||
onClick={() => onSegmentAdd(segmentationIdToUse)}
|
||||
>
|
||||
<Icons.Add />
|
||||
Add Segment
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
||||
@ -61,7 +61,7 @@ export const SegmentationSelectorHeader: React.FC<{ children?: React.ReactNode }
|
||||
info: seg.segmentation.cachedStats?.info,
|
||||
}));
|
||||
|
||||
const allowExport = exportOptions.find(
|
||||
const allowExport = exportOptions?.find(
|
||||
({ segmentationId }) => segmentationId === activeSegmentation.id
|
||||
)?.isExportable;
|
||||
|
||||
|
||||
@ -149,6 +149,7 @@ const SidePanel = ({
|
||||
activeTabIndex: activeTabIndexProp = null,
|
||||
tabs,
|
||||
onOpen,
|
||||
onClose,
|
||||
expandedWidth = 280,
|
||||
onActiveTabIndexChange,
|
||||
}) => {
|
||||
@ -167,9 +168,11 @@ const SidePanel = ({
|
||||
setPanelOpen(panelOpen);
|
||||
if (panelOpen && onOpen) {
|
||||
onOpen();
|
||||
} else if (onClose && !panelOpen) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onOpen]
|
||||
[onOpen, onClose]
|
||||
);
|
||||
|
||||
const updateActiveTabIndex = useCallback(
|
||||
@ -407,6 +410,7 @@ SidePanel.propTypes = {
|
||||
),
|
||||
]),
|
||||
onOpen: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
onActiveTabIndexChange: PropTypes.func,
|
||||
expandedWidth: PropTypes.number,
|
||||
};
|
||||
|
||||
66
yarn.lock
66
yarn.lock
@ -2580,10 +2580,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@cornerstonejs/adapters@^2.1.17":
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-2.1.17.tgz#509e62d500c733ab8babde7a3918548d59d6fa08"
|
||||
integrity sha512-+eptnnP+C+qKfRh0t0ZoZqQvxtDtwXZzMWk5IzNs/APRPh4XTt/VGh4hWt9UPUwxldG67k/bndjxilQkFUVNkA==
|
||||
"@cornerstonejs/adapters@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-2.2.1.tgz#9bed051a7fd9ca5ad5af3ccb97fbb79862feadc1"
|
||||
integrity sha512-7eIZTN3PO4s95vaLrAAD018bNKsD1w7l0FL+bLv+eqebNHrhBMQQbAfEWuQCueE+tYd1mVDCpNWVDMKMMabHmQ==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs2" "^7.17.8"
|
||||
buffer "^6.0.3"
|
||||
@ -2616,32 +2616,33 @@
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0"
|
||||
integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg==
|
||||
|
||||
"@cornerstonejs/core@^2.1.17":
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-2.1.17.tgz#3cf4e7b586fccc0cd430947933f69bb08aaa6839"
|
||||
integrity sha512-rtjRzRlEWk95Q4xlWkUaLitcX9g6bPsGXLASDm2nds8Dd5aEXCnPyEdBo6ow/rhvanohd7I1eC2fK2Wz7WWbJw==
|
||||
"@cornerstonejs/core@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-2.2.1.tgz#37cc052d40fffcc093f0f773051787c610d07386"
|
||||
integrity sha512-5ZZcpa7hU+E45jM4MvEJrIFA+WHFaLWIVh9FO1HksJXtkIeBurdYr6R95aOfk7ujkkft2rsgQA29a5Jl8E6aGg==
|
||||
dependencies:
|
||||
"@kitware/vtk.js" "32.1.0"
|
||||
comlink "^4.4.1"
|
||||
gl-matrix "^3.4.3"
|
||||
|
||||
"@cornerstonejs/dicom-image-loader@^2.1.17":
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-2.1.17.tgz#5429a10b921ed5586f093f51a363524e45cf6ecb"
|
||||
integrity sha512-UIH4th4+F+a2s/myACpEmMjkPWFljAFbXeoj2Qo0JpL02FYYYGQuO7Ov+gjK/gv90twAh7q8Qg2+yNbHkC9pzA==
|
||||
"@cornerstonejs/dicom-image-loader@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-2.2.1.tgz#01f38061cd128c56b00fb5a3c47488880fb0f6bf"
|
||||
integrity sha512-yFO6K4vFZqG1dkUBpVSsQhOxS6nkb7uKEETwobhroAs8lIIfQ8141R8u2DfxBTIY7o9uJuYpabYzlhbyUYr4eA==
|
||||
dependencies:
|
||||
"@cornerstonejs/codec-charls" "^1.2.3"
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
|
||||
"@cornerstonejs/codec-openjpeg" "^1.2.2"
|
||||
"@cornerstonejs/codec-openjph" "^2.4.5"
|
||||
comlink "^4.4.1"
|
||||
jpeg-lossless-decoder-js "^2.1.0"
|
||||
pako "^2.0.4"
|
||||
uuid "^9.0.0"
|
||||
|
||||
"@cornerstonejs/tools@^2.1.17":
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-2.1.17.tgz#74e7a18845da779f5bc513902de6dbfe1880add0"
|
||||
integrity sha512-vHdCVKV0kxYgvbS2ff9lDjd6AOKy6hP2Ohp96ac0enbZBNakUwDhqxWp94SezKqK7vn19fxRdSnPRLIchrqvJA==
|
||||
"@cornerstonejs/tools@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-2.2.1.tgz#a6d34c39c330927c485e4af8b4c99026226249e4"
|
||||
integrity sha512-cxTZAr+zA1atAJjspA4u4yD+mMZsV+H/fqf2Oa8yn3EZU5ONmFF37QbwLiXd3Zo47GYfEgTPDKQwFWG7wIAsxg==
|
||||
dependencies:
|
||||
"@types/offscreencanvas" "2019.7.3"
|
||||
comlink "^4.4.1"
|
||||
@ -5490,6 +5491,11 @@
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778"
|
||||
integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==
|
||||
|
||||
"@rspack/binding-darwin-arm64@1.0.14":
|
||||
version "1.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.0.14.tgz#b9d99fb71e047f5300a851614f89cb9d7168db3e"
|
||||
@ -13955,7 +13961,7 @@ htmlparser2@^6.1.0:
|
||||
domutils "^2.5.2"
|
||||
entities "^2.0.0"
|
||||
|
||||
htmlparser2@^8.0.0, htmlparser2@^8.0.1:
|
||||
htmlparser2@^8.0.1:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||
@ -15488,6 +15494,13 @@ joi@^17.9.2:
|
||||
"@sideway/formula" "^3.0.1"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
jpeg-lossless-decoder-js@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jpeg-lossless-decoder-js/-/jpeg-lossless-decoder-js-2.1.2.tgz#9ff0ecfcafa6acaee6097c532720bd526278aa3d"
|
||||
integrity sha512-fYf/plymnuKwVw+s8gJ8O/BPw8y8OLaN11MBvEcT05kuy3m45o7BEC4J0JbxMNUYyO+MdK/I/jq0q1gkVYZm2Q==
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-linux-x64-gnu" "4.13.0"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -18990,11 +19003,6 @@ parse-path@^7.0.0:
|
||||
dependencies:
|
||||
protocols "^2.0.0"
|
||||
|
||||
parse-srcset@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
|
||||
|
||||
parse-url@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-8.1.0.tgz#972e0827ed4b57fc85f0ea6b0d839f0d8a57a57d"
|
||||
@ -20145,7 +20153,7 @@ postcss@^8.2.1, postcss@^8.3.5, postcss@^8.4.18, postcss@^8.4.23, postcss@^8.4.3
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
postcss@^8.3.11, postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38, postcss@^8.4.47:
|
||||
postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38, postcss@^8.4.47:
|
||||
version "8.4.47"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
|
||||
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
|
||||
@ -21860,18 +21868,6 @@ safe-regex-test@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sanitize-html@^2.13.1:
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.1.tgz#b4639b0a09574ab62b1b353cb99b1b87af742834"
|
||||
integrity sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==
|
||||
dependencies:
|
||||
deepmerge "^4.2.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
htmlparser2 "^8.0.0"
|
||||
is-plain-object "^5.0.0"
|
||||
parse-srcset "^1.0.2"
|
||||
postcss "^8.3.11"
|
||||
|
||||
sax@^1.2.4:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user