@searpent/react-image-annotate 2.0.1 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,11 @@ export type ToolEnum =
23
23
  | "create-expanding-line"
24
24
  | "create-keypoints"
25
25
 
26
+ export type Metadata = {
27
+ key: string,
28
+ value: string
29
+ }
30
+
26
31
  export type Image = {
27
32
  src: string,
28
33
  thumbnailSrc?: string,
@@ -31,6 +36,7 @@ export type Image = {
31
36
  pixelSize?: { w: number, h: number },
32
37
  realSize?: { w: number, h: number, unitName: string },
33
38
  frameTime?: number,
39
+ metadata?: Array<Metadata>
34
40
  }
35
41
 
36
42
  export type Mode =
@@ -38,79 +44,79 @@ export type Mode =
38
44
  | {| mode: "DRAW_POLYGON", regionId: string |}
39
45
  | {| mode: "MOVE_POLYGON_POINT", regionId: string, pointIndex: number |}
40
46
  | {|
41
- mode: "RESIZE_BOX",
42
- editLabelEditorAfter?: boolean,
43
- regionId: string,
47
+ mode: "RESIZE_BOX",
48
+ editLabelEditorAfter ?: boolean,
49
+ regionId: string,
44
50
  freedom: [number, number],
45
- original: { x: number, y: number, w: number, h: number },
46
- isNew?: boolean,
51
+ original: { x: number, y: number, w: number, h: number },
52
+ isNew ?: boolean,
47
53
  |}
48
54
  | {| mode: "MOVE_REGION" |}
49
55
  | {| mode: "MOVE_KEYPOINT", regionId: string, keypointId: string |}
50
56
  | {|
51
- mode: "RESIZE_KEYPOINTS",
52
- landmarks: {
53
- [string]: KeypointDefinition,
57
+ mode: "RESIZE_KEYPOINTS",
58
+ landmarks: {
59
+ [string]: KeypointDefinition,
54
60
  },
55
- centerX: number,
56
- centerY: number,
57
- regionId: string,
61
+ centerX: number,
62
+ centerY: number,
63
+ regionId: string,
58
64
  isNew: boolean,
59
65
  |}
60
66
 
61
67
  export type MainLayoutStateBase = {|
62
68
  annotationType: "video" | "image",
63
- mouseDownAt?: ?{ x: number, y: number },
64
- fullScreen?: boolean,
65
- settingsOpen?: boolean,
66
- minRegionSize?: number,
67
- showTags: boolean,
68
- showMask: boolean,
69
- showPointDistances?: boolean,
70
- pointDistancePrecision?: number,
71
- selectedTool: ToolEnum,
72
- selectedCls?: string,
73
- mode: Mode,
74
- taskDescription: string,
75
- allowedArea?: { x: number, y: number, w: number, h: number },
76
- regionClsList?: Array<string>,
77
- regionTagList?: Array<string>,
78
- imageClsList?: Array<string>,
79
- imageTagList?: Array<string>,
80
- enabledTools: Array<string>,
81
- history: Array<{ time: Date, state: MainLayoutState, name: string }>,
82
- keypointDefinitions: KeypointsDefinition,
69
+ mouseDownAt ?: ? { x: number, y: number },
70
+ fullScreen ?: boolean,
71
+ settingsOpen ?: boolean,
72
+ minRegionSize ?: number,
73
+ showTags: boolean,
74
+ showMask: boolean,
75
+ showPointDistances ?: boolean,
76
+ pointDistancePrecision ?: number,
77
+ selectedTool: ToolEnum,
78
+ selectedCls ?: string,
79
+ mode: Mode,
80
+ taskDescription: string,
81
+ allowedArea ?: { x: number, y: number, w: number, h: number },
82
+ regionClsList ?: Array < string >,
83
+ regionTagList ?: Array < string >,
84
+ imageClsList ?: Array < string >,
85
+ imageTagList ?: Array < string >,
86
+ enabledTools: Array < string >,
87
+ history: Array < { time: Date, state: MainLayoutState, name: string } >,
88
+ keypointDefinitions: KeypointsDefinition,
83
89
  |}
84
90
 
85
91
  export type MainLayoutImageAnnotationState = {|
86
92
  ...MainLayoutStateBase,
87
93
  annotationType: "image",
88
94
 
89
- selectedImage?: string,
90
- images: Array<Image>,
91
- labelImages?: boolean,
95
+ selectedImage ?: string,
96
+ images: Array < Image >,
97
+ labelImages ?: boolean,
92
98
 
93
- // If the selectedImage corresponds to a frame of a video
94
- selectedImageFrameTime?: number,
99
+ // If the selectedImage corresponds to a frame of a video
100
+ selectedImageFrameTime ?: number,
95
101
  |}
96
102
 
97
103
  export type MainLayoutVideoAnnotationState = {|
98
104
  ...MainLayoutStateBase,
99
105
  annotationType: "video",
100
106
 
101
- videoSrc: string,
102
- currentVideoTime: number,
103
- videoName?: string,
104
- videoPlaying: boolean,
105
- videoDuration?: number,
106
- keyframes: {
107
- [time: number]: {|
108
- time: number,
109
- regions: Array<Region>,
107
+ videoSrc: string,
108
+ currentVideoTime: number,
109
+ videoName ?: string,
110
+ videoPlaying: boolean,
111
+ videoDuration ?: number,
112
+ keyframes: {
113
+ [time: number]: {|
114
+ time: number,
115
+ regions: Array < Region >,
110
116
  |},
111
- },
112
- pixelSize?: { w: number, h: number },
113
- realSize?: { w: number, h: number, unitName: string },
117
+ },
118
+ pixelSize ?: { w: number, h: number },
119
+ realSize ?: { w: number, h: number, unitName: string },
114
120
  |}
115
121
 
116
122
  export type MainLayoutState =
@@ -121,11 +127,11 @@ export type Action =
121
127
  | {| type: "@@INIT" |}
122
128
  | {| type: "SELECT_IMAGE", image: Image, imageIndex: number |}
123
129
  | {|
124
- type: "IMAGE_OR_VIDEO_LOADED",
125
- metadata: {
126
- naturalWidth: number,
127
- naturalHeight: number,
128
- duration?: number,
130
+ type: "IMAGE_OR_VIDEO_LOADED",
131
+ metadata: {
132
+ naturalWidth: number,
133
+ naturalHeight: number,
134
+ duration ?: number,
129
135
  },
130
136
  |}
131
137
  | {| type: "CHANGE_REGION", region: Region |}
@@ -137,10 +143,10 @@ export type Action =
137
143
  | {| type: "BEGIN_MOVE_POLYGON_POINT", polygon: Polygon, pointIndex: number |}
138
144
  | {| type: "BEGIN_MOVE_KEYPOINT", region: Keypoints, keypointId: string |}
139
145
  | {|
140
- type: "ADD_POLYGON_POINT",
141
- polygon: Polygon,
146
+ type: "ADD_POLYGON_POINT",
147
+ polygon: Polygon,
142
148
  point: { x: number, y: number },
143
- pointIndex: number,
149
+ pointIndex: number,
144
150
  |}
145
151
  | {| type: "MOUSE_MOVE", x: number, y: number |}
146
152
  | {| type: "MOUSE_DOWN", x: number, y: number |}
@@ -154,3 +160,5 @@ export type Action =
154
160
  | {| type: "SELECT_TOOL", selectedTool: ToolEnum |}
155
161
  | {| type: "CANCEL" |}
156
162
  | {| type: "SELECT_CLASSIFICATION", cls: string |}
163
+ | {| type: "UPDATE_REGIONS", imageId: string, regions: Arrat < Region >}
164
+ | {| type: "IMAGES_CHANGED", updatedAt: Date |}
@@ -0,0 +1,98 @@
1
+ // @flow
2
+
3
+ import React, { memo } from "react"
4
+ import SidebarBoxContainer from "../SidebarBoxContainer"
5
+ import DescriptionIcon from "@mui/icons-material/Description"
6
+ import { styled } from "@mui/material/styles"
7
+ import { createTheme, ThemeProvider } from "@mui/material/styles"
8
+ import { grey } from "@mui/material/colors"
9
+
10
+ type MetadataItemProps = {
11
+ name: string,
12
+ value: string,
13
+ imageIndex: number,
14
+ onChange: ({ name: string, value: String, imageIndex: number }) => void
15
+ }
16
+
17
+ const MetadataItemDiv = styled("div")(({ theme }) => ({
18
+ display: "flex",
19
+ flexDirection: "column",
20
+ marginBottom: "1rem",
21
+ "& > label": {
22
+ fontSize: "1rem",
23
+ marginBottom: ".5rem",
24
+ textTransform: "capitalize"
25
+ }
26
+ }))
27
+
28
+ const MetadataItem = ({ name, value, imageIndex, onChange }: MetadataItemProps) => {
29
+ const handleChange = e => {
30
+ e.preventDefault()
31
+ const { name, value } = e.target
32
+ onChange({
33
+ name,
34
+ value,
35
+ imageIndex
36
+ })
37
+ }
38
+
39
+ return (
40
+ <MetadataItemDiv>
41
+ <label for={name}>{name}</label>
42
+ <input type="text" value={value} name={name} onChange={handleChange} />
43
+ </MetadataItemDiv>
44
+ )
45
+ }
46
+
47
+ const MetadataList = ({ title, metadata, imageIndex, onMetadataChange }) => (
48
+ <div>
49
+ <h2>{title}</h2>
50
+ {
51
+ metadata.map(({ key, value }) => (
52
+ <MetadataItem name={key} value={value} imageIndex={imageIndex} onChange={onMetadataChange} />
53
+ ))
54
+ }
55
+ </div>
56
+ )
57
+
58
+ const theme = createTheme()
59
+ const DivContainer = styled("div")(({ theme }) => ({
60
+ paddingLeft: 16,
61
+ paddingRight: 16,
62
+ fontSize: 12,
63
+ "& h1": { fontSize: 18 },
64
+ "& h2": { fontSize: 14 },
65
+ "& h3": { fontSize: 12 },
66
+ "& h4": { fontSize: 12 },
67
+ "& h5": { fontSize: 12 },
68
+ "& h6": { fontSize: 12 },
69
+ "& p": { fontSize: 12 },
70
+ "& a": {},
71
+ "& img": { width: "100%" },
72
+ }))
73
+
74
+ export const MetadataEditorSidebarBox = ({ state, onMetadataChange }) => {
75
+
76
+
77
+ return (
78
+ <ThemeProvider theme={theme}>
79
+ <SidebarBoxContainer
80
+ title="Metadata"
81
+ icon={<DescriptionIcon style={{ color: grey[700] }} />}
82
+ expandedByDefault={true}
83
+ >
84
+ <DivContainer>
85
+ <MetadataList title="Global" metadata={state.metadata} onMetadataChange={onMetadataChange} />
86
+ {
87
+ state?.images[state.selectedImage]?.metadata && (
88
+ <MetadataList title="Local" metadata={state.images[state.selectedImage].metadata} imageIndex={state.selectedImage} onMetadataChange={onMetadataChange} />
89
+ )
90
+ }
91
+
92
+ </DivContainer>
93
+ </SidebarBoxContainer>
94
+ </ThemeProvider>
95
+ )
96
+ }
97
+
98
+ export default memo(MetadataEditorSidebarBox)
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import classnames from "classnames"
4
+ require('./page-selector.css').toString();
5
+
6
+ function PageThumbnail({ src, isActive, onClick, pageNumber }) {
7
+ return (
8
+ <div
9
+ role="button"
10
+ tabIndex={0}
11
+ className={classnames('page-thumbnail', {
12
+ 'page-thumbnail-is-active': isActive
13
+ })}
14
+ onClick={onClick}
15
+ >
16
+ <img src={src} alt="" />
17
+ {
18
+ (pageNumber !== undefined) && (
19
+ <div className="page-number-wrapper">
20
+ <span className="page-number">{pageNumber}</span>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ </div >
26
+ );
27
+ }
28
+
29
+ function PagesSelector({ pages, onPageClick, onRecalc, onSave, recalcActive, saveActive }) {
30
+ return (
31
+ <div className="page-selector">
32
+ <div className="bottom-buttons">
33
+ <button onClick={onRecalc} disabled={!recalcActive}>Recalc</button>
34
+ <button onClick={onSave} disabled={!saveActive}>Save</button>
35
+ </div>
36
+ <div className="pages">
37
+ {pages.map((page, idx) => (
38
+ <PageThumbnail
39
+ key={page.id}
40
+ src={page.src}
41
+ isActive={page.isActive}
42
+ onClick={() => onPageClick(idx)}
43
+ />
44
+ ))}
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ PagesSelector.propTypes = {
51
+ pages: PropTypes.arrayOf(
52
+ PropTypes.shape({
53
+ id: PropTypes.string.isRequired,
54
+ src: PropTypes.string.isRequired,
55
+ isActive: PropTypes.string.isRequired,
56
+ pageNumber: PropTypes.string
57
+ })
58
+ ).isRequired,
59
+ onPageClick: PropTypes.func,
60
+ onRecalc: PropTypes.func,
61
+ onSave: PropTypes.func,
62
+ recalcActive: PropTypes.bool,
63
+ saveActive: PropTypes.bool,
64
+ pageNumber: PropTypes.string
65
+ };
66
+
67
+ PagesSelector.defaultProps = {
68
+ onPageClick: () => { },
69
+ onRecalc: () => { },
70
+ onSave: () => { },
71
+ recalcActive: false,
72
+ saveActive: false,
73
+ pageNumber: undefined
74
+ };
75
+
76
+ export default PagesSelector;
@@ -0,0 +1,69 @@
1
+ .page-selector {
2
+ height: 100vh;
3
+ overflow-y: scroll;
4
+ width: 10%;
5
+ }
6
+
7
+ .pages {
8
+ list-style: none;
9
+ padding: 1rem;
10
+ }
11
+
12
+ .page-thumbnail {
13
+ margin-bottom: 1rem;
14
+ border-radius: .25rem !important;
15
+ overflow: hidden;
16
+ filter: grayscale(1);
17
+ transition: transform .2s;
18
+ opacity: .5;
19
+ }
20
+
21
+ .page-thumbnail:hover {
22
+ box-shadow: 0 0 2rem 0 #8898aa !important;
23
+ filter: grayscale(0);
24
+ cursor: pointer;
25
+ opacity: 1;
26
+ }
27
+
28
+ .page-thumbnail-is-active {
29
+ filter: grayscale(0);
30
+ opacity: 1;
31
+ }
32
+
33
+ .page-thumbnail img {
34
+ width: 100%;
35
+ }
36
+
37
+ .bottom-buttons {
38
+ background: linear-gradient(#8898aa, rgba(255, 255, 255, 0));
39
+ position: sticky;
40
+ top: 0;
41
+ display: flex;
42
+ flex-direction: column;
43
+ padding: 1rem;
44
+ margin-bottom: 1rem;
45
+ z-index: 100;
46
+ }
47
+
48
+ .bottom-buttons button {
49
+ margin-bottom: 1rem;
50
+ width: 100%;
51
+ }
52
+
53
+ .page-number-wrapper {
54
+ position: absolute;
55
+ bottom: 0;
56
+ width: 100%;
57
+ height: 10%;
58
+ z-index: 100;
59
+ display: flex;
60
+ justify-content: center;
61
+ align-items: center;
62
+ background: linear-gradient(rgba(255, 255, 255, 0), #8898aa);
63
+ padding: .5rem 0;
64
+ }
65
+
66
+ .page-number {
67
+ font-size: 1.5rem;
68
+ font-weight: 800;
69
+ }
@@ -25,6 +25,7 @@ type Props = {
25
25
  editing?: boolean,
26
26
  allowedClasses?: Array<string>,
27
27
  allowedTags?: Array<string>,
28
+ allowedGroups?: Array<{ value: String, label: String }>,
28
29
  cls?: string,
29
30
  tags?: Array<string>,
30
31
  onDelete: (Region) => null,
@@ -33,6 +34,7 @@ type Props = {
33
34
  onOpen: (Region) => null,
34
35
  onRegionClassAdded: () => {},
35
36
  allowComments?: boolean,
37
+ hideNotEditingLabel?: boolean,
36
38
  }
37
39
 
38
40
  export const RegionLabel = ({
@@ -46,6 +48,8 @@ export const RegionLabel = ({
46
48
  onOpen,
47
49
  onRegionClassAdded,
48
50
  allowComments,
51
+ hideNotEditingLabel,
52
+ allowedGroups,
49
53
  }: Props) => {
50
54
  const classes = useStyles()
51
55
  const commentInputRef = useRef(null)
@@ -56,6 +60,8 @@ export const RegionLabel = ({
56
60
  if (commentInput) return commentInput.focus()
57
61
  }
58
62
 
63
+ if (hideNotEditingLabel && !editing) return null
64
+
59
65
  return (
60
66
  <ThemeProvider theme={theme}>
61
67
  <Paper
@@ -136,6 +142,21 @@ export const RegionLabel = ({
136
142
  />
137
143
  </div>
138
144
  )}
145
+ {(allowedGroups || []).length > 0 && (
146
+ <Select
147
+ onChange={(newGroup) => onChange({
148
+ ...(region: any),
149
+ groupId: newGroup.value,
150
+ })
151
+
152
+ }
153
+ placeholder="Group"
154
+ value={
155
+ allowedGroups.filter(g => g.value === region.groupId)
156
+ }
157
+ options={allowedGroups}
158
+ />
159
+ )}
139
160
  {(allowedTags || []).length > 0 && (
140
161
  <div style={{ marginTop: 4 }}>
141
162
  <Select
@@ -2,6 +2,8 @@
2
2
 
3
3
  import React, { memo } from "react"
4
4
  import colorAlpha from "color-alpha"
5
+ import useColors from '../hooks/use-colors';
6
+
5
7
 
6
8
  function clamp(num, min, max) {
7
9
  return num <= min ? min : num >= max ? max : num
@@ -31,19 +33,36 @@ const RegionComponents = {
31
33
  />
32
34
  </g>
33
35
  )),
34
- box: memo(({ region, iw, ih }) => (
35
- <g transform={`translate(${region.x * iw} ${region.y * ih})`}>
36
- <rect
37
- strokeWidth={2}
38
- x={0}
39
- y={0}
40
- width={Math.max(region.w * iw, 0)}
41
- height={Math.max(region.h * ih, 0)}
42
- stroke={colorAlpha(region.color, 0.75)}
43
- fill={colorAlpha(region.color, 0.25)}
44
- />
45
- </g>
46
- )),
36
+ box: memo(({ region, iw, ih }) => {
37
+ const { clsColor, groupColor } = useColors();
38
+
39
+ if (region.groupId !== undefined) {
40
+ return <g transform={`translate(${region.x * iw} ${region.y * ih})`}>
41
+ <rect
42
+ strokeWidth={(region.groupHighlighted) ? 3 : 0}
43
+ x={0}
44
+ y={0}
45
+ width={Math.max(region.w * iw, 0)}
46
+ height={Math.max(region.h * ih, 0)}
47
+ stroke={colorAlpha(clsColor(region.cls), 0.85)}
48
+ fill={(region.groupHighlighted) ? colorAlpha(groupColor(region.groupId), 0.5) : colorAlpha(groupColor(region.groupId), 0.25)}
49
+ />
50
+ </g>
51
+ } else {
52
+ return <g transform={`translate(${region.x * iw} ${region.y * ih})`}>
53
+ <rect
54
+ strokeWidth={2}
55
+ x={0}
56
+ y={0}
57
+ width={Math.max(region.w * iw, 0)}
58
+ height={Math.max(region.h * ih, 0)}
59
+ stroke={colorAlpha(clsColor(region.cls), 0.85)}
60
+ fill={colorAlpha(clsColor(region.cls), 0.25)}
61
+ />
62
+ </g>
63
+ }
64
+ }
65
+ ),
47
66
  polygon: memo(({ region, iw, ih, fullSegmentationMode }) => {
48
67
  const Component = region.open ? "polyline" : "polygon"
49
68
  const alphaBase = fullSegmentationMode ? 0.5 : 1
@@ -144,9 +163,8 @@ const RegionComponents = {
144
163
  {points.map(({ x, y, angle }, i) => (
145
164
  <g
146
165
  key={i}
147
- transform={`translate(${x * iw} ${y * ih}) rotate(${
148
- (-(angle || 0) * 180) / Math.PI
149
- })`}
166
+ transform={`translate(${x * iw} ${y * ih}) rotate(${(-(angle || 0) * 180) / Math.PI
167
+ })`}
150
168
  >
151
169
  <g>
152
170
  <rect
@@ -28,6 +28,8 @@ export const RegionTags = ({
28
28
  RegionEditLabel,
29
29
  onRegionClassAdded,
30
30
  allowComments,
31
+ hideNotEditingLabel,
32
+ allowedGroups
31
33
  }) => {
32
34
  const RegionLabel =
33
35
  RegionEditLabel != null ? RegionEditLabel : DefaultRegionLabel
@@ -44,9 +46,9 @@ export const RegionTags = ({
44
46
 
45
47
  const coords = displayOnTop
46
48
  ? {
47
- left: pbox.x,
48
- top: pbox.y - margin / 2,
49
- }
49
+ left: pbox.x,
50
+ top: pbox.y - margin / 2,
51
+ }
50
52
  : { left: pbox.x, top: pbox.y + pbox.h + margin / 2 }
51
53
  if (region.locked) {
52
54
  return (
@@ -120,6 +122,8 @@ export const RegionTags = ({
120
122
  imageSrc={imageSrc}
121
123
  onRegionClassAdded={onRegionClassAdded}
122
124
  allowComments={allowComments}
125
+ hideNotEditingLabel={hideNotEditingLabel}
126
+ allowedGroups={allowedGroups}
123
127
  />
124
128
  </div>
125
129
  </div>
@@ -20,7 +20,7 @@ const pullSettingsFromLocalStorage = () => {
20
20
  settings[key.replace("settings_", "")] = JSON.parse(
21
21
  window.localStorage.getItem(key)
22
22
  )
23
- } catch (e) {}
23
+ } catch (e) { }
24
24
  }
25
25
  }
26
26
  return settings
@@ -28,8 +28,12 @@ const pullSettingsFromLocalStorage = () => {
28
28
 
29
29
  export const useSettings = () => useContext(SettingsContext)
30
30
 
31
- export const SettingsProvider = ({ children }) => {
32
- const [state, changeState] = useState(() => pullSettingsFromLocalStorage())
31
+ export const SettingsProvider = ({ children, clsColors, groupColors }) => {
32
+ const [state, changeState] = useState({
33
+ ...() => pullSettingsFromLocalStorage(),
34
+ clsColors,
35
+ groupColors,
36
+ })
33
37
  const changeSetting = (setting: string, value: any) => {
34
38
  changeState({ ...state, [setting]: value })
35
39
  window.localStorage.setItem(`settings_${setting}`, JSON.stringify(value))
@@ -0,0 +1,74 @@
1
+ import { useSettings } from "../SettingsProvider"
2
+
3
+ function defaultClsColor(cls) {
4
+ switch (cls) {
5
+ case 'title':
6
+ return '#f70202'
7
+ case 'subtitle':
8
+ return "#ffb405"
9
+ case 'text':
10
+ return "#14deef"
11
+ case 'author':
12
+ return "#f8d51e"
13
+ case 'appendix':
14
+ return "#bfede2"
15
+ case 'photo_author':
16
+ return "#9a17bb"
17
+ case 'photo_caption':
18
+ return "#ff84f6"
19
+ case 'advertisement':
20
+ return "#ffb201"
21
+ case 'other_graphics':
22
+ return "#ff5400"
23
+ case 'unknown':
24
+ return "#bfede2"
25
+ case 'about_author':
26
+ return "#9a17bb"
27
+ case 'image':
28
+ return "#14deef"
29
+ case 'interview':
30
+ return "#23b20f"
31
+ case 'table':
32
+ return "#02b4ba"
33
+ default:
34
+ return "#02b4ba"
35
+ }
36
+ }
37
+
38
+ function defaultGroupColor(groupId) {
39
+ switch (groupId) {
40
+ case "0":
41
+ return "#FDDFDF"
42
+ case "1":
43
+ return "#FCF7DE"
44
+ case "2":
45
+ return "#DEFDE0"
46
+ case "3":
47
+ return "#DEF3FD"
48
+ case "4":
49
+ return "#F0DEFD"
50
+ default:
51
+ return "#F0DEFD"
52
+ }
53
+ }
54
+
55
+ const useColors = () => {
56
+ const { clsColors, groupColors } = useSettings()
57
+
58
+ const clsColor = (cls) => {
59
+ if (clsColors[cls]) { return clsColors[cls] }
60
+ return defaultClsColor(cls)
61
+ }
62
+
63
+ const groupColor = (groupId) => {
64
+ if (groupColors[groupId]) { return groupColors[groupId] }
65
+ return defaultGroupColor(groupId)
66
+ }
67
+
68
+ return {
69
+ clsColor,
70
+ groupColor
71
+ }
72
+ }
73
+
74
+ export default useColors;
@@ -0,0 +1,5 @@
1
+ function onlyUnique(value, index, self) {
2
+ return self.indexOf(value) === index;
3
+ }
4
+
5
+ export default onlyUnique;