jbrowse-plugin-mafviewer 1.0.1
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.
- package/LICENSE +201 -0
- package/README.md +8 -0
- package/dist/jbrowse-plugin-mafviewer.umd.development.js +1310 -0
- package/dist/jbrowse-plugin-mafviewer.umd.development.js.map +1 -0
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +2 -0
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +1 -0
- package/package.json +90 -0
- package/src/BigMafAdapter/BigMafAdapter.ts +120 -0
- package/src/BigMafAdapter/configSchema.ts +33 -0
- package/src/BigMafAdapter/index.ts +15 -0
- package/src/LinearMafDisplay/components/ColorLegend.tsx +59 -0
- package/src/LinearMafDisplay/components/ReactComponent.tsx +23 -0
- package/src/LinearMafDisplay/components/RectBg.tsx +14 -0
- package/src/LinearMafDisplay/components/YScaleBars.tsx +63 -0
- package/src/LinearMafDisplay/configSchema.ts +23 -0
- package/src/LinearMafDisplay/index.ts +21 -0
- package/src/LinearMafDisplay/renderSvg.tsx +26 -0
- package/src/LinearMafDisplay/stateModel.ts +111 -0
- package/src/LinearMafRenderer/LinearMafRenderer.ts +172 -0
- package/src/LinearMafRenderer/components/ReactComponent.tsx +9 -0
- package/src/LinearMafRenderer/configSchema.ts +19 -0
- package/src/LinearMafRenderer/index.ts +16 -0
- package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +166 -0
- package/src/MafAddTrackWorkflow/index.ts +17 -0
- package/src/MafTabixAdapter/MafTabixAdapter.ts +104 -0
- package/src/MafTabixAdapter/configSchema.ts +45 -0
- package/src/MafTabixAdapter/index.ts +15 -0
- package/src/MafTrack/configSchema.ts +20 -0
- package/src/MafTrack/index.ts +18 -0
- package/src/declare.d.ts +1 -0
- package/src/index.ts +26 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #config LinearMafRenderer
|
|
5
|
+
*/
|
|
6
|
+
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
7
|
+
|
|
8
|
+
const configSchema = ConfigurationSchema(
|
|
9
|
+
'LinearMafRenderer',
|
|
10
|
+
{},
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* #baseConfiguration
|
|
14
|
+
*/
|
|
15
|
+
explicitlyTyped: true,
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export default configSchema
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import configSchema from './configSchema'
|
|
3
|
+
import LinearMafRenderer from './LinearMafRenderer'
|
|
4
|
+
import ReactComponent from './components/ReactComponent'
|
|
5
|
+
|
|
6
|
+
export default function LinearMafRendererF(pluginManager: PluginManager) {
|
|
7
|
+
pluginManager.addRendererType(
|
|
8
|
+
() =>
|
|
9
|
+
new LinearMafRenderer({
|
|
10
|
+
name: 'LinearMafRenderer',
|
|
11
|
+
ReactComponent,
|
|
12
|
+
configSchema,
|
|
13
|
+
pluginManager,
|
|
14
|
+
}),
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
FormControl,
|
|
5
|
+
FormControlLabel,
|
|
6
|
+
FormLabel,
|
|
7
|
+
Paper,
|
|
8
|
+
Radio,
|
|
9
|
+
RadioGroup,
|
|
10
|
+
TextField,
|
|
11
|
+
} from '@mui/material'
|
|
12
|
+
import { makeStyles } from 'tss-react/mui'
|
|
13
|
+
import {
|
|
14
|
+
FileLocation,
|
|
15
|
+
getSession,
|
|
16
|
+
isSessionModelWithWidgets,
|
|
17
|
+
isSessionWithAddTracks,
|
|
18
|
+
} from '@jbrowse/core/util'
|
|
19
|
+
import { AddTrackModel } from '@jbrowse/plugin-data-management'
|
|
20
|
+
import { ErrorMessage, FileSelector } from '@jbrowse/core/ui'
|
|
21
|
+
import { getRoot } from 'mobx-state-tree'
|
|
22
|
+
|
|
23
|
+
const useStyles = makeStyles()(theme => ({
|
|
24
|
+
textbox: {
|
|
25
|
+
width: '100%',
|
|
26
|
+
},
|
|
27
|
+
paper: {
|
|
28
|
+
margin: theme.spacing(),
|
|
29
|
+
padding: theme.spacing(),
|
|
30
|
+
},
|
|
31
|
+
submit: {
|
|
32
|
+
marginTop: 25,
|
|
33
|
+
marginBottom: 100,
|
|
34
|
+
display: 'block',
|
|
35
|
+
},
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
export default function MultiMAFWidget({ model }: { model: AddTrackModel }) {
|
|
39
|
+
const { classes } = useStyles()
|
|
40
|
+
const [samples, setSamples] = useState('')
|
|
41
|
+
const [loc, setLoc] = useState<FileLocation>()
|
|
42
|
+
const [indexLoc, setIndexLoc] = useState<FileLocation>()
|
|
43
|
+
const [error, setError] = useState<unknown>()
|
|
44
|
+
const [trackName, setTrackName] = useState('MAF track')
|
|
45
|
+
const [choice, setChoice] = useState('BigMafAdapter')
|
|
46
|
+
const rootModel = getRoot<any>(model)
|
|
47
|
+
return (
|
|
48
|
+
<Paper className={classes.paper}>
|
|
49
|
+
<Paper>
|
|
50
|
+
{error ? <ErrorMessage error={error} /> : null}
|
|
51
|
+
<FormControl>
|
|
52
|
+
<FormLabel>File type</FormLabel>
|
|
53
|
+
<RadioGroup
|
|
54
|
+
value={choice}
|
|
55
|
+
onChange={event => setChoice(event.target.value)}
|
|
56
|
+
>
|
|
57
|
+
<FormControlLabel
|
|
58
|
+
value="BigMafAdapter"
|
|
59
|
+
control={<Radio />}
|
|
60
|
+
checked={choice === 'BigMafAdapter'}
|
|
61
|
+
label="bigMaf"
|
|
62
|
+
/>
|
|
63
|
+
<FormControlLabel
|
|
64
|
+
value="MafTabixAdapter"
|
|
65
|
+
control={<Radio />}
|
|
66
|
+
checked={choice === 'MafTabixAdapter'}
|
|
67
|
+
label="mafTabix"
|
|
68
|
+
/>
|
|
69
|
+
</RadioGroup>
|
|
70
|
+
</FormControl>
|
|
71
|
+
{choice === 'BigMafAdapter' ? (
|
|
72
|
+
<FileSelector
|
|
73
|
+
location={loc}
|
|
74
|
+
name="Path to bigMaf"
|
|
75
|
+
setLocation={arg => setLoc(arg)}
|
|
76
|
+
rootModel={rootModel}
|
|
77
|
+
/>
|
|
78
|
+
) : (
|
|
79
|
+
<>
|
|
80
|
+
<FileSelector
|
|
81
|
+
location={loc}
|
|
82
|
+
name="Path to MAF tabix"
|
|
83
|
+
setLocation={arg => setLoc(arg)}
|
|
84
|
+
rootModel={rootModel}
|
|
85
|
+
/>
|
|
86
|
+
<FileSelector
|
|
87
|
+
location={indexLoc}
|
|
88
|
+
name="Path to MAF tabix index"
|
|
89
|
+
setLocation={arg => setIndexLoc(arg)}
|
|
90
|
+
rootModel={rootModel}
|
|
91
|
+
/>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</Paper>
|
|
95
|
+
<TextField
|
|
96
|
+
multiline
|
|
97
|
+
rows={10}
|
|
98
|
+
value={samples}
|
|
99
|
+
onChange={event => setSamples(event.target.value)}
|
|
100
|
+
placeholder={
|
|
101
|
+
'Enter sample names from the MAF file, one per line, or JSON formatted array of samples'
|
|
102
|
+
}
|
|
103
|
+
variant="outlined"
|
|
104
|
+
fullWidth
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<TextField
|
|
108
|
+
value={trackName}
|
|
109
|
+
onChange={event => setTrackName(event.target.value)}
|
|
110
|
+
helperText="Track name"
|
|
111
|
+
/>
|
|
112
|
+
<Button
|
|
113
|
+
variant="contained"
|
|
114
|
+
className={classes.submit}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
try {
|
|
117
|
+
const session = getSession(model)
|
|
118
|
+
let sampleNames = [] as string[]
|
|
119
|
+
try {
|
|
120
|
+
sampleNames = JSON.parse(samples)
|
|
121
|
+
} catch (e) {
|
|
122
|
+
sampleNames = samples.split(/\n|\r\n|\r/)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const trackId = [
|
|
126
|
+
`${trackName.toLowerCase().replaceAll(' ', '_')}-${Date.now()}`,
|
|
127
|
+
`${session.adminMode ? '' : '-sessionTrack'}`,
|
|
128
|
+
].join('')
|
|
129
|
+
|
|
130
|
+
if (isSessionWithAddTracks(session)) {
|
|
131
|
+
session.addTrackConf({
|
|
132
|
+
trackId,
|
|
133
|
+
type: 'MafTrack',
|
|
134
|
+
name: trackName,
|
|
135
|
+
assemblyNames: [model.assembly],
|
|
136
|
+
adapter:
|
|
137
|
+
choice === 'BigMafAdapter'
|
|
138
|
+
? {
|
|
139
|
+
type: choice,
|
|
140
|
+
bigBedLocation: loc,
|
|
141
|
+
samples: sampleNames,
|
|
142
|
+
}
|
|
143
|
+
: {
|
|
144
|
+
type: choice,
|
|
145
|
+
bedGzLocation: loc,
|
|
146
|
+
index: { location: indexLoc },
|
|
147
|
+
samples: sampleNames,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
model.view?.showTrack(trackId)
|
|
152
|
+
}
|
|
153
|
+
model.clearData()
|
|
154
|
+
if (isSessionModelWithWidgets(session)) {
|
|
155
|
+
session.hideWidget(model)
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
setError(e)
|
|
159
|
+
}
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
Submit
|
|
163
|
+
</Button>
|
|
164
|
+
</Paper>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import { AddTrackWorkflowType } from '@jbrowse/core/pluggableElementTypes'
|
|
3
|
+
import { types } from 'mobx-state-tree'
|
|
4
|
+
|
|
5
|
+
// locals
|
|
6
|
+
import MultiMAFWidget from './AddTrackWorkflow'
|
|
7
|
+
|
|
8
|
+
export default function MafAddTrackWorkflowF(pluginManager: PluginManager) {
|
|
9
|
+
pluginManager.addAddTrackWorkflowType(
|
|
10
|
+
() =>
|
|
11
|
+
new AddTrackWorkflowType({
|
|
12
|
+
name: 'MAF track',
|
|
13
|
+
ReactComponent: MultiMAFWidget,
|
|
14
|
+
stateModel: types.model({}),
|
|
15
|
+
}),
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
|
|
2
|
+
import { getSnapshot } from 'mobx-state-tree'
|
|
3
|
+
import { Feature, Region, SimpleFeature } from '@jbrowse/core/util'
|
|
4
|
+
import { ObservableCreate } from '@jbrowse/core/util/rxjs'
|
|
5
|
+
import { firstValueFrom, toArray } from 'rxjs'
|
|
6
|
+
|
|
7
|
+
interface OrganismRecord {
|
|
8
|
+
chr: string
|
|
9
|
+
start: number
|
|
10
|
+
srcSize: number
|
|
11
|
+
strand: number
|
|
12
|
+
unknown: number
|
|
13
|
+
data: string
|
|
14
|
+
}
|
|
15
|
+
export default class BigMafAdapter extends BaseFeatureDataAdapter {
|
|
16
|
+
public setupP?: Promise<{ adapter: BaseFeatureDataAdapter }>
|
|
17
|
+
|
|
18
|
+
async setup() {
|
|
19
|
+
const config = this.config
|
|
20
|
+
if (!this.getSubAdapter) {
|
|
21
|
+
throw new Error('no getSubAdapter available')
|
|
22
|
+
}
|
|
23
|
+
const adapter = await this.getSubAdapter({
|
|
24
|
+
...getSnapshot(config),
|
|
25
|
+
type: 'BedTabixAdapter',
|
|
26
|
+
})
|
|
27
|
+
return {
|
|
28
|
+
adapter: adapter.dataAdapter as BaseFeatureDataAdapter,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async setupPre() {
|
|
32
|
+
if (!this.setupP) {
|
|
33
|
+
this.setupP = this.setup().catch(e => {
|
|
34
|
+
this.setupP = undefined
|
|
35
|
+
throw e
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
return this.setupP
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getRefNames() {
|
|
42
|
+
const { adapter } = await this.setup()
|
|
43
|
+
return adapter.getRefNames()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getFeatures(query: Region) {
|
|
47
|
+
return ObservableCreate<Feature>(async observer => {
|
|
48
|
+
const { adapter } = await this.setup()
|
|
49
|
+
const features = await firstValueFrom(
|
|
50
|
+
adapter.getFeatures(query).pipe(toArray()),
|
|
51
|
+
)
|
|
52
|
+
for (const feature of features) {
|
|
53
|
+
const data = (feature.get('field5') as string).split(',')
|
|
54
|
+
const alignments = {} as Record<string, OrganismRecord>
|
|
55
|
+
const main = data[0]
|
|
56
|
+
const aln = main.split(':')[5]
|
|
57
|
+
const alns = data.map(elt => elt.split(':')[5])
|
|
58
|
+
const alns2 = data.map(() => '')
|
|
59
|
+
// remove extraneous data in other alignments
|
|
60
|
+
// reason being: cannot represent missing data in main species that are in others)
|
|
61
|
+
for (let i = 0, o = 0; i < aln.length; i++, o++) {
|
|
62
|
+
if (aln[i] !== '-') {
|
|
63
|
+
for (let j = 0; j < data.length; j++) {
|
|
64
|
+
alns2[j] += alns[j][i]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (let j = 0; j < data.length; j++) {
|
|
69
|
+
const elt = data[j]
|
|
70
|
+
|
|
71
|
+
const ad = elt.split(':')
|
|
72
|
+
const org = ad[0].split('.')[0]
|
|
73
|
+
const chr = ad[0].split('.')[1]
|
|
74
|
+
|
|
75
|
+
alignments[org] = {
|
|
76
|
+
chr: chr,
|
|
77
|
+
start: +ad[1],
|
|
78
|
+
srcSize: +ad[2],
|
|
79
|
+
strand: ad[3] === '-' ? -1 : 1,
|
|
80
|
+
unknown: +ad[4],
|
|
81
|
+
data: alns2[j],
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
observer.next(
|
|
86
|
+
new SimpleFeature({
|
|
87
|
+
id: feature.id(),
|
|
88
|
+
data: {
|
|
89
|
+
start: feature.get('start'),
|
|
90
|
+
end: feature.get('end'),
|
|
91
|
+
refName: feature.get('refName'),
|
|
92
|
+
name: feature.get('name'),
|
|
93
|
+
score: feature.get('score'),
|
|
94
|
+
alignments: alignments,
|
|
95
|
+
seq: alns2[0],
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
observer.complete()
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
freeResources(): void {}
|
|
104
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #config MafTabixAdapter
|
|
5
|
+
* used to configure MafTabix adapter
|
|
6
|
+
*/
|
|
7
|
+
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
8
|
+
|
|
9
|
+
const configSchema = ConfigurationSchema(
|
|
10
|
+
'MafTabixAdapter',
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* #slot
|
|
14
|
+
*/
|
|
15
|
+
samples: {
|
|
16
|
+
type: 'stringArray',
|
|
17
|
+
defaultValue: [],
|
|
18
|
+
},
|
|
19
|
+
/**
|
|
20
|
+
* #slot
|
|
21
|
+
*/
|
|
22
|
+
bedGzLocation: {
|
|
23
|
+
type: 'fileLocation',
|
|
24
|
+
defaultValue: {
|
|
25
|
+
uri: '/path/to/my.bed.gz.tbi',
|
|
26
|
+
locationType: 'UriLocation',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
index: ConfigurationSchema('Index', {
|
|
30
|
+
location: {
|
|
31
|
+
type: 'fileLocation',
|
|
32
|
+
defaultValue: {
|
|
33
|
+
uri: '/path/to/my.bed.gz.tbi',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
indexType: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
defaultValue: 'TBI',
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
{ explicitlyTyped: true },
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
export default configSchema
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import { AdapterType } from '@jbrowse/core/pluggableElementTypes'
|
|
3
|
+
import configSchema from './configSchema'
|
|
4
|
+
import MafTabixAdapter from './MafTabixAdapter'
|
|
5
|
+
|
|
6
|
+
export default function MafTabixAdapterF(pluginManager: PluginManager) {
|
|
7
|
+
return pluginManager.addAdapterType(
|
|
8
|
+
() =>
|
|
9
|
+
new AdapterType({
|
|
10
|
+
name: 'MafTabixAdapter',
|
|
11
|
+
AdapterClass: MafTabixAdapter,
|
|
12
|
+
configSchema,
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
3
|
+
import { createBaseTrackConfig } from '@jbrowse/core/pluggableElementTypes'
|
|
4
|
+
|
|
5
|
+
export default function configSchemaF(pluginManager: PluginManager) {
|
|
6
|
+
return ConfigurationSchema(
|
|
7
|
+
'MafTrack',
|
|
8
|
+
{},
|
|
9
|
+
{
|
|
10
|
+
/**
|
|
11
|
+
* #baseConfiguration
|
|
12
|
+
*/
|
|
13
|
+
baseConfiguration: createBaseTrackConfig(pluginManager),
|
|
14
|
+
/**
|
|
15
|
+
* #identifier
|
|
16
|
+
*/
|
|
17
|
+
explicitIdentifier: 'trackId',
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import {
|
|
3
|
+
TrackType,
|
|
4
|
+
createBaseTrackModel,
|
|
5
|
+
} from '@jbrowse/core/pluggableElementTypes'
|
|
6
|
+
import configSchemaF from './configSchema'
|
|
7
|
+
|
|
8
|
+
export default function MafTrackF(pluginManager: PluginManager) {
|
|
9
|
+
return pluginManager.addTrackType(() => {
|
|
10
|
+
const configSchema = configSchemaF(pluginManager)
|
|
11
|
+
return new TrackType({
|
|
12
|
+
name: 'MafTrack',
|
|
13
|
+
configSchema,
|
|
14
|
+
displayName: 'MAF track',
|
|
15
|
+
stateModel: createBaseTrackModel(pluginManager, 'MafTrack', configSchema),
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
}
|
package/src/declare.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.json'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Plugin from '@jbrowse/core/Plugin'
|
|
2
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
3
|
+
|
|
4
|
+
import { version } from '../package.json'
|
|
5
|
+
import BigMafAdapterF from './BigMafAdapter'
|
|
6
|
+
import MafTrackF from './MafTrack'
|
|
7
|
+
import LinearMafDisplayF from './LinearMafDisplay'
|
|
8
|
+
import LinearMafRendererF from './LinearMafRenderer'
|
|
9
|
+
import MafTabixAdapterF from './MafTabixAdapter'
|
|
10
|
+
import MafAddTrackWorkflowF from './MafAddTrackWorkflow'
|
|
11
|
+
|
|
12
|
+
export default class MafViewerPlugin extends Plugin {
|
|
13
|
+
name = 'MafViewerPlugin'
|
|
14
|
+
version = version
|
|
15
|
+
|
|
16
|
+
install(pluginManager: PluginManager) {
|
|
17
|
+
BigMafAdapterF(pluginManager)
|
|
18
|
+
MafTrackF(pluginManager)
|
|
19
|
+
LinearMafDisplayF(pluginManager)
|
|
20
|
+
LinearMafRendererF(pluginManager)
|
|
21
|
+
MafTabixAdapterF(pluginManager)
|
|
22
|
+
MafAddTrackWorkflowF(pluginManager)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
configure(pluginManager: PluginManager) {}
|
|
26
|
+
}
|