xtralab 0.15.1 → 0.15.2
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/README.md +58 -33
- package/lib/fileBrowser/commands.d.ts +1 -0
- package/lib/fileBrowser/commands.js +46 -1
- package/lib/fileBrowser/fileBrowser.js +4 -8
- package/lib/fileBrowser/gitStatus.d.ts +16 -5
- package/lib/fileBrowser/gitStatus.js +28 -17
- package/lib/fileBrowser/widget.d.ts +15 -0
- package/lib/fileBrowser/widget.js +13 -0
- package/lib/git/commands.d.ts +5 -0
- package/lib/git/commands.js +4 -3
- package/lib/git/diffPreferences.d.ts +90 -0
- package/lib/git/diffPreferences.js +107 -0
- package/lib/git/diffProvider.js +10 -2
- package/lib/git/diffSurface.d.ts +34 -19
- package/lib/git/diffSurface.js +49 -115
- package/lib/git/diffTheme.d.ts +7 -0
- package/lib/git/diffTheme.js +23 -0
- package/lib/git/diffWidget.d.ts +7 -30
- package/lib/git/diffWidget.js +56 -100
- package/lib/git/imageDiff.d.ts +9 -0
- package/lib/git/imageDiff.js +2 -28
- package/lib/git/index.js +41 -2
- package/lib/git/notebookDiff.js +3 -2
- package/package.json +5 -3
- package/schema/git-diff-preferences.json +42 -0
- package/src/fileBrowser/commands.ts +64 -0
- package/src/fileBrowser/fileBrowser.tsx +4 -9
- package/src/fileBrowser/gitStatus.ts +33 -20
- package/src/fileBrowser/widget.tsx +22 -0
- package/src/git/commands.ts +13 -1
- package/src/git/diffPreferences.ts +192 -0
- package/src/git/diffProvider.tsx +11 -2
- package/src/git/diffSurface.tsx +113 -146
- package/src/git/diffTheme.ts +24 -0
- package/src/git/diffWidget.tsx +108 -138
- package/src/git/imageDiff.tsx +12 -34
- package/src/git/index.ts +51 -2
- package/src/git/notebookDiff.tsx +3 -2
- package/style/git.css +4 -2
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { GitStatus, GitStatusEntry } from '@pierre/trees';
|
|
2
2
|
|
|
3
3
|
import { expandStatusFiles, status } from '../git/api';
|
|
4
|
-
import type { FileChangeStatus } from '../git/tokens';
|
|
4
|
+
import type { FileChangeStatus, IFileChange } from '../git/tokens';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Repo path for `/git/*` calls; empty means the server root, letting git
|
|
8
|
+
* resolve the enclosing repo (same convention as the git panel).
|
|
9
|
+
*/
|
|
10
|
+
export const GIT_REPO_PATH = '';
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* Translate a `jupyterlab_git` porcelain status into a `@pierre/trees`
|
|
@@ -25,32 +31,39 @@ function toTreeStatus(value: FileChangeStatus): GitStatus {
|
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* Fetch the porcelain status for `repoPath
|
|
29
|
-
* Any failure yields an empty array — the file browser
|
|
30
|
-
*
|
|
31
|
-
* winning (mirrors what is on disk, matching VS Code).
|
|
34
|
+
* Fetch the porcelain status for `repoPath`, one entry per staged or
|
|
35
|
+
* unstaged change. Any failure yields an empty array — the file browser
|
|
36
|
+
* must work without git.
|
|
32
37
|
*/
|
|
33
|
-
export async function
|
|
34
|
-
repoPath: string
|
|
35
|
-
): Promise<GitStatusEntry[]> {
|
|
38
|
+
export async function loadGitChanges(repoPath: string): Promise<IFileChange[]> {
|
|
36
39
|
try {
|
|
37
40
|
const result = await status(repoPath);
|
|
38
41
|
if (result.code !== 0) {
|
|
39
42
|
return [];
|
|
40
43
|
}
|
|
41
|
-
|
|
42
|
-
const byPath = new Map<string, FileChangeStatus>();
|
|
43
|
-
for (const change of changes) {
|
|
44
|
-
if (change.group === 'unstaged' || !byPath.has(change.path)) {
|
|
45
|
-
byPath.set(change.path, change.status);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const entries: GitStatusEntry[] = [];
|
|
49
|
-
byPath.forEach((value, path) => {
|
|
50
|
-
entries.push({ path, status: toTreeStatus(value) });
|
|
51
|
-
});
|
|
52
|
-
return entries;
|
|
44
|
+
return expandStatusFiles(result.files);
|
|
53
45
|
} catch {
|
|
54
46
|
return [];
|
|
55
47
|
}
|
|
56
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Collapse changes into the tree's `GitStatusEntry` values. A file both
|
|
52
|
+
* staged and unstaged gets one entry, the unstaged status winning (mirrors
|
|
53
|
+
* what is on disk, matching VS Code).
|
|
54
|
+
*/
|
|
55
|
+
export function toGitStatusEntries(
|
|
56
|
+
changes: readonly IFileChange[]
|
|
57
|
+
): GitStatusEntry[] {
|
|
58
|
+
const byPath = new Map<string, FileChangeStatus>();
|
|
59
|
+
for (const change of changes) {
|
|
60
|
+
if (change.group === 'unstaged' || !byPath.has(change.path)) {
|
|
61
|
+
byPath.set(change.path, change.status);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const entries: GitStatusEntry[] = [];
|
|
65
|
+
byPath.forEach((value, path) => {
|
|
66
|
+
entries.push({ path, status: toTreeStatus(value) });
|
|
67
|
+
});
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { Signal, ISignal } from '@lumino/signaling';
|
|
18
18
|
import { AccordionPanel, PanelLayout, Widget } from '@lumino/widgets';
|
|
19
19
|
|
|
20
|
+
import type { IFileChange } from '../git/tokens';
|
|
20
21
|
import { FileBrowserComponent } from './fileBrowser';
|
|
21
22
|
import { xtralabFileBrowserIcon } from './icons';
|
|
22
23
|
|
|
@@ -92,6 +93,12 @@ export interface IXtralabFileBrowser {
|
|
|
92
93
|
*/
|
|
93
94
|
readonly selectionChanged: ISignal<IXtralabFileBrowser, readonly string[]>;
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Git changes from the latest status poll, one per staged or unstaged
|
|
98
|
+
* change; paths are repo-relative.
|
|
99
|
+
*/
|
|
100
|
+
readonly gitChanges: readonly IFileChange[];
|
|
101
|
+
|
|
95
102
|
/**
|
|
96
103
|
* Emits when {@link refresh} is called.
|
|
97
104
|
*/
|
|
@@ -251,6 +258,13 @@ export class XtralabFileBrowser
|
|
|
251
258
|
return this._selectionChanged;
|
|
252
259
|
}
|
|
253
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Git changes from the latest status poll.
|
|
263
|
+
*/
|
|
264
|
+
get gitChanges(): readonly IFileChange[] {
|
|
265
|
+
return this._gitChanges;
|
|
266
|
+
}
|
|
267
|
+
|
|
254
268
|
/**
|
|
255
269
|
* A signal emitted when {@link refresh} is called.
|
|
256
270
|
*/
|
|
@@ -308,6 +322,13 @@ export class XtralabFileBrowser
|
|
|
308
322
|
this._selectionChanged.emit(paths);
|
|
309
323
|
}
|
|
310
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Update the cached git changes from the React tree's status poll.
|
|
327
|
+
*/
|
|
328
|
+
updateGitChanges(changes: readonly IFileChange[]): void {
|
|
329
|
+
this._gitChanges = changes;
|
|
330
|
+
}
|
|
331
|
+
|
|
311
332
|
/**
|
|
312
333
|
* Trigger a refresh of every loaded directory in the tree.
|
|
313
334
|
*/
|
|
@@ -537,6 +558,7 @@ export class XtralabFileBrowser
|
|
|
537
558
|
private _translator: ITranslator | undefined;
|
|
538
559
|
private _selectedPaths: readonly string[] = [];
|
|
539
560
|
private _selectionChanged = new Signal<this, readonly string[]>(this);
|
|
561
|
+
private _gitChanges: readonly IFileChange[] = [];
|
|
540
562
|
private _refreshRequested = new Signal<this, void>(this);
|
|
541
563
|
private _pathAdded = new Signal<this, string>(this);
|
|
542
564
|
private _revealRequested = new Signal<this, string>(this);
|
package/src/git/commands.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { ISignal, Signal } from '@lumino/signaling';
|
|
|
10
10
|
import type { IAskAgent } from '../askAgent/tokens';
|
|
11
11
|
import { getTreeIcon } from '../fileBrowser/icons';
|
|
12
12
|
import { fileChangeToDiffModel } from './diffModel';
|
|
13
|
+
import type { IDiffPreferences } from './diffPreferences';
|
|
13
14
|
import {
|
|
14
15
|
DIFF_WIDGET_CSS_CLASS,
|
|
15
16
|
XtralabDiffWidget,
|
|
@@ -156,6 +157,10 @@ interface ICreateDiffWidgetOptions {
|
|
|
156
157
|
* Ask-agent popup for prompting an agent about diff lines; may be `null`.
|
|
157
158
|
*/
|
|
158
159
|
askAgent: IAskAgent | null;
|
|
160
|
+
/**
|
|
161
|
+
* The display choices shared by all diff views.
|
|
162
|
+
*/
|
|
163
|
+
preferences: IDiffPreferences;
|
|
159
164
|
/**
|
|
160
165
|
* Translation bundle for user-facing strings.
|
|
161
166
|
*/
|
|
@@ -180,12 +185,13 @@ function createDiffWidget(
|
|
|
180
185
|
contentsManager,
|
|
181
186
|
rendermime,
|
|
182
187
|
askAgent,
|
|
188
|
+
preferences,
|
|
183
189
|
trans
|
|
184
190
|
} = options;
|
|
185
191
|
const pinned = options.pinned === true;
|
|
186
192
|
const content = new XtralabDiffWidget(
|
|
187
193
|
fileChangeToDiffModel(repoPath, change),
|
|
188
|
-
{ contentsManager, rendermime, themeManager, askAgent, trans }
|
|
194
|
+
{ contentsManager, rendermime, themeManager, askAgent, preferences, trans }
|
|
189
195
|
);
|
|
190
196
|
const widget = new DiffMainAreaWidget({ content }, change, pinned, trans);
|
|
191
197
|
widget.id = pinned ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
|
|
@@ -279,6 +285,10 @@ interface IRegisterGitCommandsOptions {
|
|
|
279
285
|
* Ask-agent popup for prompting an agent about diff lines; may be `null`.
|
|
280
286
|
*/
|
|
281
287
|
askAgent: IAskAgent | null;
|
|
288
|
+
/**
|
|
289
|
+
* The display choices shared by all diff views.
|
|
290
|
+
*/
|
|
291
|
+
preferences: IDiffPreferences;
|
|
282
292
|
/**
|
|
283
293
|
* Translation bundle for user-facing strings.
|
|
284
294
|
*/
|
|
@@ -309,6 +319,7 @@ export function registerGitCommands(
|
|
|
309
319
|
contentsManager,
|
|
310
320
|
rendermime,
|
|
311
321
|
askAgent,
|
|
322
|
+
preferences,
|
|
312
323
|
trans,
|
|
313
324
|
trackDiff,
|
|
314
325
|
findDiff,
|
|
@@ -340,6 +351,7 @@ export function registerGitCommands(
|
|
|
340
351
|
contentsManager,
|
|
341
352
|
rendermime,
|
|
342
353
|
askAgent,
|
|
354
|
+
preferences,
|
|
343
355
|
trans,
|
|
344
356
|
pinned: pin,
|
|
345
357
|
onPinned
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
4
|
+
import type { IStateDB } from '@jupyterlab/statedb';
|
|
5
|
+
import { Token } from '@lumino/coreutils';
|
|
6
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
7
|
+
|
|
8
|
+
export type DiffStyle = 'split' | 'unified';
|
|
9
|
+
|
|
10
|
+
export type DiffStyleControlMode = 'buttons' | 'toggle';
|
|
11
|
+
|
|
12
|
+
export type NotebookDiffViewMode = 'notebook' | 'json';
|
|
13
|
+
|
|
14
|
+
export type ImageDiffViewMode = '2-up' | 'swipe' | 'onion';
|
|
15
|
+
|
|
16
|
+
export const MIN_SPLIT_RATIO = 0.1;
|
|
17
|
+
export const MAX_SPLIT_RATIO = 0.9;
|
|
18
|
+
export const DEFAULT_SPLIT_RATIO = 0.5;
|
|
19
|
+
|
|
20
|
+
const SPLIT_RATIO_STATE_KEY = 'xtralab:git-diff:split-ratio';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Display choices shared by every diff view. The toggles live in the
|
|
24
|
+
* plugin settings; the split width lives in the state database.
|
|
25
|
+
*/
|
|
26
|
+
export interface IDiffPreferences {
|
|
27
|
+
/**
|
|
28
|
+
* The current values; a new object after each change.
|
|
29
|
+
*/
|
|
30
|
+
readonly values: IDiffPreferences.IValues;
|
|
31
|
+
/**
|
|
32
|
+
* A signal emitted when any value changes.
|
|
33
|
+
*/
|
|
34
|
+
readonly changed: ISignal<IDiffPreferences, void>;
|
|
35
|
+
/**
|
|
36
|
+
* Change some values and persist them.
|
|
37
|
+
*/
|
|
38
|
+
update(values: Partial<IDiffPreferences.IValues>): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export namespace IDiffPreferences {
|
|
42
|
+
/**
|
|
43
|
+
* The diff display values.
|
|
44
|
+
*/
|
|
45
|
+
export interface IValues {
|
|
46
|
+
/**
|
|
47
|
+
* Split or unified layout for textual file diffs.
|
|
48
|
+
*/
|
|
49
|
+
diffStyle: DiffStyle;
|
|
50
|
+
/**
|
|
51
|
+
* Two toolbar buttons for the layout, or one button that toggles it.
|
|
52
|
+
*/
|
|
53
|
+
diffStyleControl: DiffStyleControlMode;
|
|
54
|
+
/**
|
|
55
|
+
* Whether long lines wrap instead of scrolling horizontally.
|
|
56
|
+
*/
|
|
57
|
+
lineWrap: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Rendered or JSON view for notebook diffs.
|
|
60
|
+
*/
|
|
61
|
+
notebookViewMode: NotebookDiffViewMode;
|
|
62
|
+
/**
|
|
63
|
+
* Comparison mode for image diffs.
|
|
64
|
+
*/
|
|
65
|
+
imageViewMode: ImageDiffViewMode;
|
|
66
|
+
/**
|
|
67
|
+
* Width of the left pane in split view, as a fraction of the diff width.
|
|
68
|
+
*/
|
|
69
|
+
splitRatio: number;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const IDiffPreferences = new Token<IDiffPreferences>(
|
|
74
|
+
'xtralab:IDiffPreferences',
|
|
75
|
+
'Display choices shared by the git diff views.'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
type SettingKey = Exclude<keyof IDiffPreferences.IValues, 'splitRatio'>;
|
|
79
|
+
|
|
80
|
+
const SETTING_KEYS: SettingKey[] = [
|
|
81
|
+
'diffStyle',
|
|
82
|
+
'diffStyleControl',
|
|
83
|
+
'lineWrap',
|
|
84
|
+
'notebookViewMode',
|
|
85
|
+
'imageViewMode'
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* {@link IDiffPreferences} backed by the settings registry and the state
|
|
90
|
+
* database. Without them it keeps the values in memory only.
|
|
91
|
+
*/
|
|
92
|
+
export class DiffPreferences implements IDiffPreferences {
|
|
93
|
+
get values(): IDiffPreferences.IValues {
|
|
94
|
+
return this._values;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get changed(): ISignal<this, void> {
|
|
98
|
+
return this._changed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
update(values: Partial<IDiffPreferences.IValues>): void {
|
|
102
|
+
if (!this._apply(values)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const key of SETTING_KEYS) {
|
|
106
|
+
if (key in values) {
|
|
107
|
+
this._settings?.set(key, this._values[key]).catch(reason => {
|
|
108
|
+
console.error(`xtralab: failed to save the diff ${key}`, reason);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if ('splitRatio' in values) {
|
|
113
|
+
this._state
|
|
114
|
+
?.save(SPLIT_RATIO_STATE_KEY, this._values.splitRatio)
|
|
115
|
+
.catch(reason => {
|
|
116
|
+
console.error('xtralab: failed to save the diff split', reason);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read the toggles from the plugin settings and follow their changes.
|
|
123
|
+
*/
|
|
124
|
+
connectSettings(settings: ISettingRegistry.ISettings): void {
|
|
125
|
+
this._settings = settings;
|
|
126
|
+
const sync = (): void => {
|
|
127
|
+
this._apply(settings.composite as Partial<IDiffPreferences.IValues>);
|
|
128
|
+
};
|
|
129
|
+
sync();
|
|
130
|
+
settings.changed.connect(sync);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Restore the split width from the state database and save it there.
|
|
135
|
+
*/
|
|
136
|
+
async connectState(state: IStateDB): Promise<void> {
|
|
137
|
+
this._state = state;
|
|
138
|
+
const ratio = await state.fetch(SPLIT_RATIO_STATE_KEY);
|
|
139
|
+
if (
|
|
140
|
+
typeof ratio === 'number' &&
|
|
141
|
+
ratio >= MIN_SPLIT_RATIO &&
|
|
142
|
+
ratio <= MAX_SPLIT_RATIO
|
|
143
|
+
) {
|
|
144
|
+
this._apply({ splitRatio: ratio });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Merge values without persisting them; return whether one changed.
|
|
150
|
+
*/
|
|
151
|
+
private _apply(values: Partial<IDiffPreferences.IValues>): boolean {
|
|
152
|
+
const next = { ...this._values, ...values };
|
|
153
|
+
const keys = Object.keys(next) as (keyof IDiffPreferences.IValues)[];
|
|
154
|
+
if (keys.every(key => next[key] === this._values[key])) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
this._values = next;
|
|
158
|
+
this._changed.emit();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private _values: IDiffPreferences.IValues = {
|
|
163
|
+
diffStyle: 'split',
|
|
164
|
+
diffStyleControl: 'buttons',
|
|
165
|
+
lineWrap: false,
|
|
166
|
+
notebookViewMode: 'notebook',
|
|
167
|
+
imageViewMode: '2-up',
|
|
168
|
+
splitRatio: DEFAULT_SPLIT_RATIO
|
|
169
|
+
};
|
|
170
|
+
private _changed = new Signal<this, void>(this);
|
|
171
|
+
private _settings: ISettingRegistry.ISettings | null = null;
|
|
172
|
+
private _state: IStateDB | null = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Re-render on every preference change and return the current values.
|
|
177
|
+
*/
|
|
178
|
+
export function useDiffPreferences(
|
|
179
|
+
preferences: IDiffPreferences
|
|
180
|
+
): IDiffPreferences.IValues {
|
|
181
|
+
const subscribe = React.useCallback(
|
|
182
|
+
(onChange: () => void) => {
|
|
183
|
+
const slot = (): void => onChange();
|
|
184
|
+
preferences.changed.connect(slot);
|
|
185
|
+
return () => {
|
|
186
|
+
preferences.changed.disconnect(slot);
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
[preferences]
|
|
190
|
+
);
|
|
191
|
+
return React.useSyncExternalStore(subscribe, () => preferences.values);
|
|
192
|
+
}
|
package/src/git/diffProvider.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { Git, IGitExtension } from '@jupyterlab/git';
|
|
|
9
9
|
|
|
10
10
|
import { IAskAgent } from '../askAgent/tokens';
|
|
11
11
|
|
|
12
|
+
import { DiffPreferences, IDiffPreferences } from './diffPreferences';
|
|
12
13
|
import {
|
|
13
14
|
IXtralabDiffContext,
|
|
14
15
|
XtralabDiffWidget,
|
|
@@ -37,14 +38,21 @@ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
37
38
|
"Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
|
|
38
39
|
autoStart: true,
|
|
39
40
|
requires: [IGitExtension],
|
|
40
|
-
optional: [
|
|
41
|
+
optional: [
|
|
42
|
+
IRenderMimeRegistry,
|
|
43
|
+
IThemeManager,
|
|
44
|
+
ITranslator,
|
|
45
|
+
IAskAgent,
|
|
46
|
+
IDiffPreferences
|
|
47
|
+
],
|
|
41
48
|
activate: (
|
|
42
49
|
app: JupyterFrontEnd,
|
|
43
50
|
gitExtension: IGitExtension,
|
|
44
51
|
rendermime: IRenderMimeRegistry | null,
|
|
45
52
|
themeManager: IThemeManager | null,
|
|
46
53
|
translator: ITranslator | null,
|
|
47
|
-
askAgent: IAskAgent | null
|
|
54
|
+
askAgent: IAskAgent | null,
|
|
55
|
+
preferences: IDiffPreferences | null
|
|
48
56
|
): void => {
|
|
49
57
|
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
50
58
|
const factory = makeXtralabDiffFactory({
|
|
@@ -52,6 +60,7 @@ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
52
60
|
rendermime,
|
|
53
61
|
themeManager,
|
|
54
62
|
askAgent,
|
|
63
|
+
preferences: preferences ?? new DiffPreferences(),
|
|
55
64
|
trans
|
|
56
65
|
});
|
|
57
66
|
|