@ldelia/react-media 0.2.0
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/.bitmap +48 -0
- package/.eslintcache +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +7 -0
- package/.storybook/main.js +15 -0
- package/.storybook/main.ts +18 -0
- package/.storybook/preview.js +4 -0
- package/.storybook/preview.ts +14 -0
- package/README.md +26 -0
- package/package.json +87 -0
- package/src/components/__tests__/timeline/timeline.test.tsx +26 -0
- package/src/components/index.ts +1 -0
- package/src/components/timeline/README.md +111 -0
- package/src/components/timeline/RangeSelectorCanvas.tsx +168 -0
- package/src/components/timeline/TickTime.tsx +45 -0
- package/src/components/timeline/TickTimeCollectionDisplay.tsx +42 -0
- package/src/components/timeline/VaLueLineCanvas.tsx +101 -0
- package/src/components/timeline/constants.ts +13 -0
- package/src/components/timeline/index.tsx +125 -0
- package/src/components/timeline/utils/utils.ts +16 -0
- package/src/index.ts +1 -0
- package/src/modules.d.ts +1 -0
- package/src/react-app-env.d.ts +1 -0
- package/src/reportWebVitals.ts +15 -0
- package/src/setupTests.ts +5 -0
- package/src/stories/Button.stories.ts +52 -0
- package/src/stories/Button.tsx +52 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/button.css +30 -0
- package/src/stories/timeline.stories.tsx +38 -0
- package/tsconfig.json +26 -0
package/.bitmap
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* THIS IS A BIT-AUTO-GENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */
|
|
2
|
+
|
|
3
|
+
{
|
|
4
|
+
"ldelia.react-media/timeline@1.0.8": {
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"relativePath": "src/components/timeline/README.md",
|
|
8
|
+
"test": false,
|
|
9
|
+
"name": "README.md"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"relativePath": "src/components/timeline/RangeSelectorCanvas.tsx",
|
|
13
|
+
"test": false,
|
|
14
|
+
"name": "RangeSelectorCanvas.tsx"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"relativePath": "src/components/timeline/TickTime.tsx",
|
|
18
|
+
"test": false,
|
|
19
|
+
"name": "TickTime.tsx"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"relativePath": "src/components/timeline/TickTimeCollectionDisplay.tsx",
|
|
23
|
+
"test": false,
|
|
24
|
+
"name": "TickTimeCollectionDisplay.tsx"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"relativePath": "src/components/timeline/VaLueLineCanvas.tsx",
|
|
28
|
+
"test": false,
|
|
29
|
+
"name": "VaLueLineCanvas.tsx"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"relativePath": "src/components/timeline/index.tsx",
|
|
33
|
+
"test": false,
|
|
34
|
+
"name": "index.tsx"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"relativePath": "src/components/timeline/utils/utils.ts",
|
|
38
|
+
"test": false,
|
|
39
|
+
"name": "utils.ts"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"mainFile": "src/components/timeline/index.tsx",
|
|
43
|
+
"trackDir": "src/components/timeline",
|
|
44
|
+
"origin": "AUTHORED",
|
|
45
|
+
"exported": true
|
|
46
|
+
},
|
|
47
|
+
"version": "14.8.8"
|
|
48
|
+
}
|
package/.eslintcache
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"/home/ldelia/Proyectos/react-media/src/components/timeline/index.tsx":"1","/home/ldelia/Proyectos/react-media/src/components/timeline/VaLueLineCanvas.tsx":"2","/home/ldelia/Proyectos/react-media/src/components/timeline/TickTimeCollectionDisplay.tsx":"3","/home/ldelia/Proyectos/react-media/src/components/timeline/RangeSelectorCanvas.tsx":"4","/home/ldelia/Proyectos/react-media/src/components/timeline/utils/utils.ts":"5","/home/ldelia/Proyectos/react-media/src/components/timeline/TickTime.tsx":"6","/home/ldelia/Proyectos/react-media/src/stories/timeline.stories.tsx":"7","/home/ldelia/Proyectos/react-media/src/components/timeline/constants.ts":"8"},{"size":4115,"mtime":1612815036821,"results":"9","hashOfConfig":"10"},{"size":3108,"mtime":1611772905837,"results":"11","hashOfConfig":"10"},{"size":1159,"mtime":1611772905837,"results":"12","hashOfConfig":"10"},{"size":6266,"mtime":1611772905837,"results":"13","hashOfConfig":"10"},{"size":404,"mtime":1611772905841,"results":"14","hashOfConfig":"10"},{"size":988,"mtime":1611772905837,"results":"15","hashOfConfig":"10"},{"size":860,"mtime":1611772905841,"results":"16","hashOfConfig":"10"},{"size":319,"mtime":1612814974971,"results":"17","hashOfConfig":"10"},{"filePath":"18","messages":"19","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"v8ab83",{"filePath":"20","messages":"21","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"22","messages":"23","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/ldelia/Proyectos/react-media/src/components/timeline/index.tsx",[],"/home/ldelia/Proyectos/react-media/src/components/timeline/VaLueLineCanvas.tsx",[],"/home/ldelia/Proyectos/react-media/src/components/timeline/TickTimeCollectionDisplay.tsx",[],"/home/ldelia/Proyectos/react-media/src/components/timeline/RangeSelectorCanvas.tsx",[],"/home/ldelia/Proyectos/react-media/src/components/timeline/utils/utils.ts",[],"/home/ldelia/Proyectos/react-media/src/components/timeline/TickTime.tsx",[],"/home/ldelia/Proyectos/react-media/src/stories/timeline.stories.tsx",[],"/home/ldelia/Proyectos/react-media/src/components/timeline/constants.ts",[]]
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
"stories": [
|
|
3
|
+
"../src/**/*.stories.mdx",
|
|
4
|
+
"../src/**/*.stories.@(js|jsx|ts|tsx)"
|
|
5
|
+
],
|
|
6
|
+
"addons": [
|
|
7
|
+
"@storybook/addon-links",
|
|
8
|
+
"@storybook/addon-essentials",
|
|
9
|
+
"@storybook/preset-create-react-app"
|
|
10
|
+
],
|
|
11
|
+
"framework": {
|
|
12
|
+
name: "@storybook/react-webpack5",
|
|
13
|
+
options: {},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StorybookConfig } from "@storybook/react-webpack5";
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
5
|
+
addons: [
|
|
6
|
+
"@storybook/preset-create-react-app",
|
|
7
|
+
"@storybook/addon-onboarding",
|
|
8
|
+
"@storybook/addon-links",
|
|
9
|
+
"@storybook/addon-essentials",
|
|
10
|
+
"@chromatic-com/storybook",
|
|
11
|
+
"@storybook/addon-interactions",
|
|
12
|
+
],
|
|
13
|
+
framework: {
|
|
14
|
+
name: "@storybook/react-webpack5",
|
|
15
|
+
options: {},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export default config;
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# React Media
|
|
2
|
+
|
|
3
|
+
A React components collection for media-related features.
|
|
4
|
+
|
|
5
|
+
## Available Components
|
|
6
|
+
|
|
7
|
+
- Timeline ([Docs](./src/components/timeline/README.md))
|
|
8
|
+
|
|
9
|
+
## In Progress
|
|
10
|
+
|
|
11
|
+
- Audio/Video player
|
|
12
|
+
|
|
13
|
+
## Storybook
|
|
14
|
+
|
|
15
|
+
Launch the Storybook playground:
|
|
16
|
+
|
|
17
|
+
##### `npm run storybook`
|
|
18
|
+
|
|
19
|
+
Make sure storybook is already installed by running npx storybook@latest init
|
|
20
|
+
|
|
21
|
+
## Test suite collection
|
|
22
|
+
|
|
23
|
+
Launch the test suite collection:
|
|
24
|
+
|
|
25
|
+
##### `npm test`
|
|
26
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ldelia/react-media",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A React components collection for media-related features.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"keywords": [
|
|
7
|
+
"timeline",
|
|
8
|
+
"react"
|
|
9
|
+
],
|
|
10
|
+
"author": "Lisandro Delia",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"bugs": "https://github.com/ldelia/react-media/issues",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"react": "^18.3.1",
|
|
15
|
+
"react-dom": "^18.3.1",
|
|
16
|
+
"react-scripts": "^5.0.1",
|
|
17
|
+
"styled-components": "^6.1.11",
|
|
18
|
+
"typescript": "^4.9.5",
|
|
19
|
+
"web-vitals": "^0.2.4"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "react-scripts start",
|
|
23
|
+
"build": "react-scripts build",
|
|
24
|
+
"test": "react-scripts test",
|
|
25
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\"",
|
|
26
|
+
"eject": "react-scripts eject",
|
|
27
|
+
"storybook": "storybook dev -p 6006",
|
|
28
|
+
"build-storybook": "storybook build"
|
|
29
|
+
},
|
|
30
|
+
"eslintConfig": {
|
|
31
|
+
"extends": [
|
|
32
|
+
"react-app",
|
|
33
|
+
"react-app/jest",
|
|
34
|
+
"plugin:storybook/recommended",
|
|
35
|
+
"prettier"
|
|
36
|
+
],
|
|
37
|
+
"plugins": ["prettier"],
|
|
38
|
+
"rules": {
|
|
39
|
+
"prettier/prettier": "error"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"browserslist": {
|
|
43
|
+
"production": [
|
|
44
|
+
">0.2%",
|
|
45
|
+
"not dead",
|
|
46
|
+
"not op_mini all"
|
|
47
|
+
],
|
|
48
|
+
"development": [
|
|
49
|
+
"last 1 chrome version",
|
|
50
|
+
"last 1 firefox version",
|
|
51
|
+
"last 1 safari version"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@chromatic-com/storybook": "^1.5.0",
|
|
56
|
+
"@storybook/addon-actions": "^8.1.10",
|
|
57
|
+
"@storybook/addon-essentials": "^8.1.10",
|
|
58
|
+
"@storybook/addon-interactions": "^8.1.10",
|
|
59
|
+
"@storybook/addon-knobs": "^8.0.1",
|
|
60
|
+
"@storybook/addon-links": "^8.1.10",
|
|
61
|
+
"@storybook/addon-onboarding": "^8.1.10",
|
|
62
|
+
"@storybook/blocks": "^8.1.10",
|
|
63
|
+
"@storybook/node-logger": "^8.1.10",
|
|
64
|
+
"@storybook/preset-create-react-app": "^8.1.10",
|
|
65
|
+
"@storybook/react": "^8.1.10",
|
|
66
|
+
"@storybook/react-webpack5": "^8.1.10",
|
|
67
|
+
"@storybook/test": "^8.1.10",
|
|
68
|
+
"@testing-library/jest-dom": "^6.4.6",
|
|
69
|
+
"@testing-library/react": "^16.0.0",
|
|
70
|
+
"@testing-library/user-event": "^14.5.2",
|
|
71
|
+
"@types/jest": "^29.5.12",
|
|
72
|
+
"@types/node": "^12.19.9",
|
|
73
|
+
"@types/react": "^18.3.3",
|
|
74
|
+
"@types/react-dom": "^18.3.0",
|
|
75
|
+
"@types/styled-components": "^5.1.34",
|
|
76
|
+
"eslint-config-prettier": "^9.1.0",
|
|
77
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
78
|
+
"eslint-plugin-storybook": "^0.8.0",
|
|
79
|
+
"postcss-flexbugs-fixes": "^5.0.2",
|
|
80
|
+
"postcss-normalize": "^10.0.1",
|
|
81
|
+
"postcss-preset-env": "^9.5.14",
|
|
82
|
+
"prettier": "^3.3.2",
|
|
83
|
+
"prop-types": "^15.8.1",
|
|
84
|
+
"storybook": "^8.1.10",
|
|
85
|
+
"webpack": "^5.92.0"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import Timeline, { TimelineProps } from '../../timeline';
|
|
4
|
+
|
|
5
|
+
describe('Timeline', () => {
|
|
6
|
+
let props: TimelineProps;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
props = {
|
|
10
|
+
duration: 300,
|
|
11
|
+
value: 15,
|
|
12
|
+
onChange: jest.fn(),
|
|
13
|
+
onRangeChange: jest.fn(),
|
|
14
|
+
zoomLevel: 0,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('render()', () => {
|
|
19
|
+
it('renders a timeline', () => {
|
|
20
|
+
const rootElement = document.createElement('div');
|
|
21
|
+
|
|
22
|
+
const root = createRoot(rootElement);
|
|
23
|
+
root.render(<Timeline {...props} />);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './timeline';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Timeline
|
|
2
|
+
|
|
3
|
+
<!-- STORY -->
|
|
4
|
+
|
|
5
|
+
<hr>
|
|
6
|
+
|
|
7
|
+
A component for displaying the duration of a media file (mp3, mp4, etc.) and the current reproduction position. It also allows selecting a range for reproduction looping purposes.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### NPM
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npm i @bit/ldelia.react-media.timeline
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### YARN
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
yarn add @bit/ldelia.react-media.timeline
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### BIT
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
bit import ldelia.react-media/timeline
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Import
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import Timeline from '@bit/ldelia.react-media.timeline';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
<Timeline
|
|
39
|
+
duration={301}
|
|
40
|
+
value={15}
|
|
41
|
+
onChange={() => 'Do something'}
|
|
42
|
+
onRangeChange={() => 'Do something'}
|
|
43
|
+
/>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
##### Required props
|
|
47
|
+
|
|
48
|
+
| Name | Type | Description |
|
|
49
|
+
| ---------- | -------- | --------------------------------- |
|
|
50
|
+
| `duration` | `number` | The media file duration |
|
|
51
|
+
| `value` | `number` | The current reproduction position |
|
|
52
|
+
|
|
53
|
+
##### Optional props
|
|
54
|
+
|
|
55
|
+
| Name | Type | Default | Description |
|
|
56
|
+
| --------------- | ---------- | ---------- | -------------------------------------------------- |
|
|
57
|
+
| `onChange` | `function` | `() => {}` | Fired when the user double-clicks on the timeline |
|
|
58
|
+
| `onRangeChange` | `function` | `() => {}` | Fired when the user select a range in the timeline |
|
|
59
|
+
| `selectedRange` | `array` | `[]` | The current selected range |
|
|
60
|
+
| `zoomLevel` | `number` | `0` | `The current timeline zoom level` |
|
|
61
|
+
|
|
62
|
+
## Customization
|
|
63
|
+
|
|
64
|
+
#### Timeline widget
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
const StyledTimeline = styled(Timeline)`
|
|
68
|
+
background-color: green;
|
|
69
|
+
`;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Value bar
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
const StyledTimeline = styled(Timeline)`
|
|
76
|
+
.media-timeline-value-line {
|
|
77
|
+
background-color: red;
|
|
78
|
+
width: 5px;
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Tick-time labels
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
const StyledTimeline = styled(Timeline)`
|
|
87
|
+
.media-timeline-tick-time {
|
|
88
|
+
color: red;
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Tick-time/Value bar container
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
const StyledTimeline = styled(Timeline)`
|
|
97
|
+
.media-timeline-value-line-canvas {
|
|
98
|
+
color: red;
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Range selector
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
const StyledTimeline = styled(Timeline)`
|
|
107
|
+
.media-timeline-range-selector-canvas {
|
|
108
|
+
color: red;
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
```
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import { ZoomContext, ZoomContextType } from './index';
|
|
5
|
+
import { useContext, useEffect, useMemo, useRef } from 'react';
|
|
6
|
+
import { pixelToSeconds, secondsToPixel } from './utils/utils';
|
|
7
|
+
|
|
8
|
+
export interface RangeSelectorCanvasProps {
|
|
9
|
+
selectedRange: number[];
|
|
10
|
+
onChange: (value: number) => void;
|
|
11
|
+
onRangeChange: (value: number[]) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const OverlayCanvas = styled.canvas`
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
color: cadetblue;
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const RangeSelectorCanvas: React.FC<RangeSelectorCanvasProps> = ({
|
|
24
|
+
selectedRange,
|
|
25
|
+
onChange,
|
|
26
|
+
onRangeChange,
|
|
27
|
+
}) => {
|
|
28
|
+
const canvasRef = useRef(null);
|
|
29
|
+
const zoomContextValue: ZoomContextType = useContext(ZoomContext);
|
|
30
|
+
|
|
31
|
+
let isSelectingRange: boolean = false;
|
|
32
|
+
let selectedRangeInPixels: number[] = useMemo(() => [], []);
|
|
33
|
+
if (selectedRange.length === 2) {
|
|
34
|
+
selectedRangeInPixels = [
|
|
35
|
+
secondsToPixel(zoomContextValue, selectedRange[0]),
|
|
36
|
+
secondsToPixel(zoomContextValue, selectedRange[1]),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
let lastValidSelectedRangeInPixels: number[] = [];
|
|
40
|
+
|
|
41
|
+
const getMousePointerPixelPosition = (e: { clientX: number }) => {
|
|
42
|
+
const canvas: HTMLCanvasElement = canvasRef.current!;
|
|
43
|
+
let rect = canvas.getBoundingClientRect();
|
|
44
|
+
return e.clientX - rect.left;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const drawRect = (pixelX0: number, pixelX1: number) => {
|
|
48
|
+
const canvas: HTMLCanvasElement = canvasRef.current!;
|
|
49
|
+
const context: CanvasRenderingContext2D = canvas.getContext('2d')!;
|
|
50
|
+
|
|
51
|
+
context.globalAlpha = 0.3;
|
|
52
|
+
context.fillStyle = window
|
|
53
|
+
.getComputedStyle(canvas)
|
|
54
|
+
.getPropertyValue('color');
|
|
55
|
+
context.fillRect(pixelX0, 0, pixelX1 - pixelX0, context.canvas.height);
|
|
56
|
+
context.globalAlpha = 1.0;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const onCanvasDoubleClick = (e: { clientX: number }) => {
|
|
60
|
+
const x0 = getMousePointerPixelPosition(e);
|
|
61
|
+
onChange(pixelToSeconds(zoomContextValue, x0));
|
|
62
|
+
selectedRangeInPixels = lastValidSelectedRangeInPixels;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isPixelNearSelectedRange = (x0: number) => {
|
|
66
|
+
if (selectedRangeInPixels.length === 2) {
|
|
67
|
+
const diff = Math.min(
|
|
68
|
+
Math.abs(selectedRangeInPixels[0] - x0),
|
|
69
|
+
Math.abs(selectedRangeInPixels[1] - x0),
|
|
70
|
+
);
|
|
71
|
+
return diff <= zoomContextValue.pixelsInSecond / 2;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onCanvasMouseDown = (e: { clientX: number }) => {
|
|
77
|
+
const mouseCurrentPosition = getMousePointerPixelPosition(e);
|
|
78
|
+
|
|
79
|
+
if (!isPixelNearSelectedRange(mouseCurrentPosition)) {
|
|
80
|
+
// Keep track of the first position of the new range.
|
|
81
|
+
selectedRangeInPixels = [mouseCurrentPosition, mouseCurrentPosition];
|
|
82
|
+
}
|
|
83
|
+
isSelectingRange = true;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onCanvasMouseMove = (e: { clientX: number }) => {
|
|
87
|
+
const canvas: HTMLCanvasElement = canvasRef.current!;
|
|
88
|
+
const context = canvas.getContext('2d')!;
|
|
89
|
+
const mouseCurrentPosition = getMousePointerPixelPosition(e);
|
|
90
|
+
|
|
91
|
+
if (selectedRangeInPixels.length === 2) {
|
|
92
|
+
const diff = Math.min(
|
|
93
|
+
Math.abs(selectedRangeInPixels[0] - mouseCurrentPosition),
|
|
94
|
+
Math.abs(selectedRangeInPixels[1] - mouseCurrentPosition),
|
|
95
|
+
);
|
|
96
|
+
// Change the mouse's cursor if it's near a selected range.
|
|
97
|
+
canvas.style.cursor =
|
|
98
|
+
diff <= zoomContextValue.pixelsInSecond / 2 ? 'col-resize' : 'default';
|
|
99
|
+
|
|
100
|
+
if (!isSelectingRange) return;
|
|
101
|
+
|
|
102
|
+
if (mouseCurrentPosition < selectedRangeInPixels[0]) {
|
|
103
|
+
// The left side must be enlarged.
|
|
104
|
+
selectedRangeInPixels[0] = mouseCurrentPosition;
|
|
105
|
+
} else if (mouseCurrentPosition > selectedRangeInPixels[1]) {
|
|
106
|
+
// The right side must be enlarged.
|
|
107
|
+
selectedRangeInPixels[1] = mouseCurrentPosition;
|
|
108
|
+
} else {
|
|
109
|
+
const diffX0 = mouseCurrentPosition - selectedRangeInPixels[0];
|
|
110
|
+
const diffX1 = selectedRangeInPixels[1] - mouseCurrentPosition;
|
|
111
|
+
if (diffX0 < diffX1) {
|
|
112
|
+
// The left side must be shrunk.
|
|
113
|
+
selectedRangeInPixels[0] = mouseCurrentPosition;
|
|
114
|
+
} else {
|
|
115
|
+
// The right side must be shrunk.
|
|
116
|
+
selectedRangeInPixels[1] = mouseCurrentPosition;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
120
|
+
drawRect(selectedRangeInPixels[0], selectedRangeInPixels[1]);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const onCanvasMouseUp = (e: { clientX: number }) => {
|
|
125
|
+
if (selectedRangeInPixels.length !== 2) return;
|
|
126
|
+
|
|
127
|
+
isSelectingRange = false;
|
|
128
|
+
const mouseCurrentPosition = getMousePointerPixelPosition(e);
|
|
129
|
+
const pixelRange = [
|
|
130
|
+
Math.min(selectedRangeInPixels[0], mouseCurrentPosition),
|
|
131
|
+
Math.max(selectedRangeInPixels[1], mouseCurrentPosition),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
if (pixelRange[1] - pixelRange[0] <= 1) {
|
|
135
|
+
// It was just a click
|
|
136
|
+
selectedRangeInPixels = lastValidSelectedRangeInPixels;
|
|
137
|
+
} else {
|
|
138
|
+
lastValidSelectedRangeInPixels = pixelRange;
|
|
139
|
+
onRangeChange([
|
|
140
|
+
pixelToSeconds(zoomContextValue, pixelRange[0]),
|
|
141
|
+
pixelToSeconds(zoomContextValue, pixelRange[1]),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const canvas: HTMLCanvasElement = canvasRef.current!;
|
|
148
|
+
|
|
149
|
+
// https://stackoverflow.com/questions/8696631/canvas-drawings-like-lines-are-blurry
|
|
150
|
+
canvas.width = canvas.offsetWidth;
|
|
151
|
+
canvas.height = canvas.offsetHeight;
|
|
152
|
+
|
|
153
|
+
if (selectedRangeInPixels.length === 0) return;
|
|
154
|
+
drawRect(selectedRangeInPixels[0], selectedRangeInPixels[1]);
|
|
155
|
+
}, [selectedRangeInPixels]);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<OverlayCanvas
|
|
159
|
+
ref={canvasRef}
|
|
160
|
+
onDoubleClick={onCanvasDoubleClick}
|
|
161
|
+
onMouseDown={onCanvasMouseDown}
|
|
162
|
+
onMouseMove={onCanvasMouseMove}
|
|
163
|
+
onMouseUp={onCanvasMouseUp}
|
|
164
|
+
className={'media-timeline-range-selector-canvas'}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
export default React.memo(RangeSelectorCanvas);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
export type Props = {
|
|
5
|
+
start: number;
|
|
6
|
+
leftPosition: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
interface TickTimeContainerProps {
|
|
10
|
+
left: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const TickTimeContainer = styled.span.attrs<TickTimeContainerProps>(
|
|
14
|
+
(props) => ({
|
|
15
|
+
style: {
|
|
16
|
+
left: props.left,
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
)<TickTimeContainerProps>`
|
|
20
|
+
background: transparent;
|
|
21
|
+
position: absolute;
|
|
22
|
+
padding-left: 8px;
|
|
23
|
+
color: #646464;
|
|
24
|
+
user-select: none;
|
|
25
|
+
min-width: 20px;
|
|
26
|
+
max-width: 20px;
|
|
27
|
+
align-self: flex-end;
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const TickTime: React.FC<Props> = ({ start, leftPosition }) => {
|
|
31
|
+
const minutes = Math.floor(start / 60);
|
|
32
|
+
let seconds = start - minutes * 60;
|
|
33
|
+
let secondsFormatted: string =
|
|
34
|
+
seconds < 10 ? '0' + seconds : seconds.toString();
|
|
35
|
+
return (
|
|
36
|
+
<TickTimeContainer
|
|
37
|
+
left={leftPosition}
|
|
38
|
+
className={'media-timeline-tick-time'}
|
|
39
|
+
>
|
|
40
|
+
{minutes}:{secondsFormatted}
|
|
41
|
+
</TickTimeContainer>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default TickTime;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import TickTime from './TickTime';
|
|
4
|
+
import { ZoomContext } from './index';
|
|
5
|
+
|
|
6
|
+
export interface TickTimeCollectionDisplayProps {
|
|
7
|
+
tickTimes: number[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TickTimeCollectionDisplayContainer = styled.div`
|
|
11
|
+
position: absolute;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-self: flex-end;
|
|
14
|
+
height: 100%;
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const TickTimeCollectionDisplay: React.FC<TickTimeCollectionDisplayProps> = ({
|
|
18
|
+
tickTimes = [],
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<ZoomContext.Consumer>
|
|
22
|
+
{(value) => {
|
|
23
|
+
return (
|
|
24
|
+
<TickTimeCollectionDisplayContainer>
|
|
25
|
+
{tickTimes.map((tickTimeValue, index) => {
|
|
26
|
+
const leftPosition: string =
|
|
27
|
+
tickTimeValue * value.pixelsInSecond + 'px';
|
|
28
|
+
return (
|
|
29
|
+
<TickTime
|
|
30
|
+
key={index}
|
|
31
|
+
start={tickTimeValue}
|
|
32
|
+
leftPosition={leftPosition}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
})}
|
|
36
|
+
</TickTimeCollectionDisplayContainer>
|
|
37
|
+
);
|
|
38
|
+
}}
|
|
39
|
+
</ZoomContext.Consumer>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
export default TickTimeCollectionDisplay;
|