@cinecrew/cinecrew-player 0.1.1 โ†’ 0.1.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.
@@ -0,0 +1,30 @@
1
+ import dataset from './emoji-data.json';
2
+
3
+ export const EMOJI_GROUPS = [
4
+ { name: 'Smileys & Emotion', icon: '๐Ÿ˜€' },
5
+ { name: 'People & Body', icon: '๐Ÿ‘‹' },
6
+ { name: 'Animals & Nature', icon: '๐Ÿป' },
7
+ { name: 'Food & Drink', icon: '๐Ÿ”' },
8
+ { name: 'Travel & Places', icon: '๐Ÿš—' },
9
+ { name: 'Activities', icon: 'โšฝ' },
10
+ { name: 'Objects', icon: '๐Ÿ’ก' },
11
+ { name: 'Symbols', icon: 'โค๏ธ' },
12
+ { name: 'Flags', icon: '๐Ÿณ๏ธ' },
13
+ ].map((group) => ({
14
+ ...group,
15
+ items: dataset.filter((item) => item.status === 'fully-qualified' && item.group === group.name),
16
+ }));
17
+
18
+ export const EMOJI_ITEMS = EMOJI_GROUPS.flatMap((group) => group.items);
19
+
20
+ export function searchEmojis(query) {
21
+ const normalizedQuery = String(query || '').trim().toLocaleLowerCase();
22
+ if (!normalizedQuery) return EMOJI_ITEMS;
23
+
24
+ return EMOJI_ITEMS.filter((item) => [
25
+ item.short_name,
26
+ item.description,
27
+ ...(item.aliases || []),
28
+ ...(item.keywords || []),
29
+ ].some((value) => String(value || '').toLocaleLowerCase().includes(normalizedQuery)));
30
+ }
@@ -476,7 +476,9 @@ function InlinePreviewFrame({
476
476
  onPress={onPromotePreview}
477
477
  />
478
478
  <InlinePreviewTopActions controls={controls} muted={muted} videoOnlyMode={videoOnlyMode} handleMuteAction={handleMuteAction} />
479
- <InlinePreviewCenterAction controls={controls} colors={colors} isPlaying={isPlaying} handlePlayPauseAction={handlePlayPauseAction} />
479
+ {!isLoading ? (
480
+ <InlinePreviewCenterAction controls={controls} colors={colors} isPlaying={isPlaying} handlePlayPauseAction={handlePlayPauseAction} />
481
+ ) : null}
480
482
  <InlinePreviewBottomActions
481
483
  controls={controls}
482
484
  title={title}
@@ -570,10 +572,9 @@ function FullscreenControlsPanel(props) {
570
572
  onRestart={props.handleRestartAction}
571
573
  onToggleMute={props.handleMuteAction}
572
574
  onToggleLock={props.handleLockAction}
573
- onMinimize={props.handleMinimizeAction}
574
575
  />
575
576
  <CenterControls
576
- visible={!props.isAudioOnly && props.controls.playPause !== false}
577
+ visible={!props.isAudioOnly && !props.isLoading && props.controls.playPause !== false}
577
578
  isLive={props.isLive}
578
579
  isPlaying={props.isPlaying}
579
580
  onSeekBy={props.handleSeekByAction}
@@ -687,6 +688,8 @@ function FullscreenChatLayer(props) {
687
688
  streamId={props.mediaId}
688
689
  integrations={props.integrations}
689
690
  colors={props.colors}
691
+ messagePageSize={props.messagePageSize}
692
+ drawerStyle={props.drawerStyle}
690
693
  />
691
694
  );
692
695
  }
@@ -773,7 +776,6 @@ export const MediaPlayerView = ({
773
776
  inlinePreview = false,
774
777
  inlinePreviewRect = null,
775
778
  onPromotePreview,
776
- onMinimize,
777
779
  mediaId,
778
780
  posterUrl,
779
781
  episodeLabel,
@@ -786,6 +788,8 @@ export const MediaPlayerView = ({
786
788
  shuffle,
787
789
  onNextEpisode,
788
790
  liveChatNonce = 0,
791
+ messagePageSize = 50,
792
+ drawerStyle,
789
793
  genre,
790
794
  categoryName,
791
795
  controls = {},
@@ -1904,9 +1908,6 @@ export const MediaPlayerView = ({
1904
1908
  const handleFullscreenAction = useCallback(() => invokeAction(
1905
1909
  'onFullscreen', toggleFullscreen, { isFullscreen: !isFullscreen },
1906
1910
  ), [invokeAction, toggleFullscreen, isFullscreen]);
1907
- const handleMinimizeAction = useCallback(() => invokeAction(
1908
- 'onMinimize', onMinimize, { title, mediaId },
1909
- ), [invokeAction, onMinimize, title, mediaId]);
1910
1911
  const handleRecordingAction = useCallback((name, fallback, event) => {
1911
1912
  event?.stopPropagation?.();
1912
1913
  return invokeAction(name, fallback, { title, streamUrl, mediaId });
@@ -1937,7 +1938,6 @@ export const MediaPlayerView = ({
1937
1938
  seekBy: handleSeekBy,
1938
1939
  setPlaybackRate: (rate) => setPlaybackRate(clampNumber(rate, 0.25, 4, 1)),
1939
1940
  back: handleClose,
1940
- minimize: handleMinimizeAction,
1941
1941
  getVideoElement: () => vlcRef.current?.getVideoElement?.() || null,
1942
1942
  getAudioTracks: () => audioTracks,
1943
1943
  };
@@ -1945,7 +1945,7 @@ export const MediaPlayerView = ({
1945
1945
  return () => {
1946
1946
  for (const key of Object.keys(api)) delete playerApiRef.current[key];
1947
1947
  };
1948
- }, [playerApiRef, togglePlayPause, handleRestart, toggleMute, handleSelectAspectRatio, handleAudioSelect, handleSeekTo, handleSeekBy, handleClose, handleMinimizeAction, audioTracks]);
1948
+ }, [playerApiRef, togglePlayPause, handleRestart, toggleMute, handleSelectAspectRatio, handleAudioSelect, handleSeekTo, handleSeekBy, handleClose, audioTracks]);
1949
1949
 
1950
1950
  const handleSpeedSelect = useCallback((speed) => invokeAction(
1951
1951
  'onPlaybackRateChange', () => {
@@ -2004,6 +2004,8 @@ export const MediaPlayerView = ({
2004
2004
  windowWidthRef.current = windowWidth;
2005
2005
  const windowHeightRef = useRef(windowHeight);
2006
2006
  windowHeightRef.current = windowHeight;
2007
+ const showControlsRef = useRef(showControls);
2008
+ showControlsRef.current = showControls;
2007
2009
  const handleSeekByRef = useRef(handleSeekByAction);
2008
2010
  handleSeekByRef.current = handleSeekByAction;
2009
2011
  const triggerSeekRippleRef = useRef(triggerSeekRipple);
@@ -2019,10 +2021,21 @@ export const MediaPlayerView = ({
2019
2021
  const panResponder = useMemo(() => {
2020
2022
  if (isWeb()) return null;
2021
2023
 
2024
+ const isSeekScrubGesture = (evt) => {
2025
+ if (!showControlsRef.current) return false;
2026
+ const height = windowHeightRef.current || 0;
2027
+ const y = Number(evt?.nativeEvent?.locationY);
2028
+ if (height <= 0 || !Number.isFinite(y)) return false;
2029
+ // Keep the bottom timeline region available to the native Slider instead
2030
+ // of letting the full-screen tap/gesture layer claim horizontal scrubs.
2031
+ return y >= height - Math.max(180, height * 0.14);
2032
+ };
2033
+
2022
2034
  return PanResponder.create({
2023
- onStartShouldSetPanResponder: () => true,
2035
+ onStartShouldSetPanResponder: (evt) => !isSeekScrubGesture(evt),
2024
2036
  onStartShouldSetPanResponderCapture: () => false,
2025
- onMoveShouldSetPanResponder: (_, gestureState) => {
2037
+ onMoveShouldSetPanResponder: (evt, gestureState) => {
2038
+ if (isSeekScrubGesture(evt)) return false;
2026
2039
  return Math.abs(gestureState.dx) > 3 || Math.abs(gestureState.dy) > 3;
2027
2040
  },
2028
2041
  onMoveShouldSetPanResponderCapture: () => false,
@@ -2506,6 +2519,8 @@ export const MediaPlayerView = ({
2506
2519
  popupMode
2507
2520
  integrations={integrations}
2508
2521
  colors={colors}
2522
+ messagePageSize={messagePageSize}
2523
+ drawerStyle={drawerStyle}
2509
2524
  />
2510
2525
  ) : null;
2511
2526
  return (
@@ -2564,7 +2579,6 @@ export const MediaPlayerView = ({
2564
2579
  handleRestartAction,
2565
2580
  handleMuteAction,
2566
2581
  handleLockAction,
2567
- handleMinimizeAction,
2568
2582
  isAudioOnly,
2569
2583
  isPlaying,
2570
2584
  handleSeekByAction,
@@ -2686,6 +2700,8 @@ export const MediaPlayerView = ({
2686
2700
  diagnosticsEnabled={diagnosticsOverlayEnabled}
2687
2701
  integrations={integrations}
2688
2702
  colors={colors}
2703
+ messagePageSize={messagePageSize}
2704
+ drawerStyle={drawerStyle}
2689
2705
  />
2690
2706
  <FullscreenRecordingLayer
2691
2707
  isAudioOnly={isAudioOnly}
@@ -1,30 +1,226 @@
1
- import React from 'react';
2
- import { useColorScheme } from 'react-native';
1
+ import React, { useMemo, useState } from 'react';
3
2
  import {
4
- EmojiPickerModal as HirakuEmojiPickerModal,
5
- emojiData,
6
- } from '@hiraku-ai/react-native-emoji-picker';
3
+ FlatList,
4
+ Modal,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ TextInput,
10
+ useColorScheme,
11
+ View,
12
+ useWindowDimensions,
13
+ } from 'react-native';
14
+ import { EMOJI_GROUPS, searchEmojis } from '../../data/emoji';
7
15
 
8
16
  export const EmojiPickerModal = ({ visible, onClose, onSelectEmoji, colors }) => {
9
17
  const systemScheme = useColorScheme();
10
- const darkMode = colors?.mode ? colors.mode === 'dark' : systemScheme !== 'light';
18
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
19
+ const [activeGroup, setActiveGroup] = useState(EMOJI_GROUPS[0].name);
20
+ const [query, setQuery] = useState('');
21
+ const isDark = colors?.mode ? colors.mode === 'dark' : systemScheme !== 'light';
22
+ const sheetWidth = Math.min(windowWidth - 32, 420);
23
+ const numColumns = Math.max(5, Math.min(9, Math.floor((sheetWidth - 32) / 42)));
24
+ const activeGroupData = EMOJI_GROUPS.find((group) => group.name === activeGroup) || EMOJI_GROUPS[0];
25
+ const emojis = useMemo(
26
+ () => query.trim() ? searchEmojis(query) : activeGroupData.items,
27
+ [activeGroupData, query],
28
+ );
29
+ const palette = {
30
+ surface: colors?.surfaceColor || (isDark ? '#17212B' : '#FFFFFF'),
31
+ text: colors?.controlColor || (isDark ? '#F5F7FA' : '#202124'),
32
+ muted: colors?.mutedColor || (isDark ? '#AAB4C0' : '#5F6368'),
33
+ accent: colors?.accentColor || '#00B8D4',
34
+ outline: colors?.borderColor || (isDark ? 'rgba(255,255,255,0.16)' : 'rgba(32,33,36,0.18)'),
35
+ backdrop: 'rgba(0,0,0,0.58)',
36
+ };
37
+ const close = () => {
38
+ onClose?.();
39
+ };
11
40
 
12
41
  return (
13
- <HirakuEmojiPickerModal
42
+ <Modal
14
43
  visible={visible}
15
- onClose={onClose}
16
- onEmojiSelect={(emoji) => {
17
- onSelectEmoji(emoji);
18
- }}
19
- emojis={emojiData}
20
- darkMode={darkMode}
21
- modalTitle="Emojis"
22
- showSearchBar={true}
23
- showTabs={true}
24
- showHistoryTab={true}
25
- showSkinToneSelector={true}
26
- modalMaxWidth={420}
27
- modalMaxHeight={380}
28
- />
44
+ transparent
45
+ animationType="fade"
46
+ onRequestClose={close}
47
+ statusBarTranslucent
48
+ >
49
+ <Pressable style={[styles.backdrop, { backgroundColor: palette.backdrop }]} onPress={close}>
50
+ <Pressable
51
+ style={[styles.sheet, {
52
+ width: sheetWidth,
53
+ height: Math.min(Math.max(300, windowHeight * 0.72), 560),
54
+ backgroundColor: palette.surface,
55
+ borderColor: palette.outline,
56
+ }]}
57
+ onPress={(event) => event.stopPropagation()}
58
+ >
59
+ <View style={styles.header}>
60
+ <Text style={[styles.title, { color: palette.text }]}>Choose an emoji</Text>
61
+ <Pressable
62
+ accessibilityRole="button"
63
+ accessibilityLabel="Close emoji picker"
64
+ hitSlop={10}
65
+ onPress={close}
66
+ style={[styles.closeButton, { borderColor: palette.outline }]}
67
+ >
68
+ <Text style={[styles.closeLabel, { color: palette.muted }]}>ร—</Text>
69
+ </Pressable>
70
+ </View>
71
+ <TextInput
72
+ accessibilityLabel="Search emojis"
73
+ value={query}
74
+ onChangeText={setQuery}
75
+ placeholder="Search all emojis"
76
+ placeholderTextColor={palette.muted}
77
+ returnKeyType="search"
78
+ style={[styles.search, { color: palette.text, borderColor: palette.outline }]}
79
+ />
80
+ <ScrollView
81
+ horizontal
82
+ showsHorizontalScrollIndicator={false}
83
+ keyboardShouldPersistTaps="handled"
84
+ contentContainerStyle={styles.categoryList}
85
+ accessibilityLabel="Emoji categories"
86
+ >
87
+ {EMOJI_GROUPS.map((group) => {
88
+ const selected = !query.trim() && group.name === activeGroup;
89
+ return (
90
+ <Pressable
91
+ key={group.name}
92
+ accessibilityRole="button"
93
+ accessibilityLabel={`${group.name} emojis`}
94
+ accessibilityState={{ selected }}
95
+ onPress={() => setActiveGroup(group.name)}
96
+ style={[styles.categoryButton, { borderColor: selected ? palette.accent : 'transparent' }]}
97
+ >
98
+ <Text style={styles.categoryIcon}>{group.icon}</Text>
99
+ </Pressable>
100
+ );
101
+ })}
102
+ </ScrollView>
103
+ <Text style={[styles.groupTitle, { color: palette.muted }]}>
104
+ {query.trim() ? `Search results ยท ${emojis.length}` : activeGroupData.name}
105
+ </Text>
106
+ <FlatList
107
+ key={`${numColumns}-${query.trim() ? 'search' : activeGroup}`}
108
+ data={emojis}
109
+ keyExtractor={(item) => item.codepoints}
110
+ numColumns={numColumns}
111
+ keyboardShouldPersistTaps="handled"
112
+ contentContainerStyle={styles.grid}
113
+ initialNumToRender={numColumns * 6}
114
+ maxToRenderPerBatch={numColumns * 8}
115
+ windowSize={7}
116
+ renderItem={({ item }) => (
117
+ <Pressable
118
+ accessibilityRole="button"
119
+ accessibilityLabel={`Insert ${item.short_name}`}
120
+ onPress={() => {
121
+ onSelectEmoji?.(item.emoji);
122
+ close();
123
+ }}
124
+ style={({ pressed }) => [styles.emojiButton, pressed && { backgroundColor: `${palette.accent}24` }]}
125
+ >
126
+ <Text style={styles.emoji}>{item.emoji}</Text>
127
+ </Pressable>
128
+ )}
129
+ ListEmptyComponent={<Text style={[styles.empty, { color: palette.muted }]}>No emojis found.</Text>}
130
+ />
131
+ </Pressable>
132
+ </Pressable>
133
+ </Modal>
29
134
  );
30
135
  };
136
+
137
+ const styles = StyleSheet.create({
138
+ backdrop: {
139
+ flex: 1,
140
+ justifyContent: 'center',
141
+ alignItems: 'center',
142
+ padding: 16,
143
+ },
144
+ sheet: {
145
+ maxHeight: '82%',
146
+ borderWidth: StyleSheet.hairlineWidth,
147
+ borderRadius: 20,
148
+ padding: 16,
149
+ overflow: 'hidden',
150
+ elevation: 12,
151
+ shadowColor: '#000',
152
+ shadowOffset: { width: 0, height: 8 },
153
+ shadowOpacity: 0.28,
154
+ shadowRadius: 20,
155
+ },
156
+ header: {
157
+ minHeight: 44,
158
+ flexDirection: 'row',
159
+ alignItems: 'center',
160
+ justifyContent: 'space-between',
161
+ marginBottom: 10,
162
+ },
163
+ title: {
164
+ fontSize: 17,
165
+ fontWeight: '700',
166
+ },
167
+ closeButton: {
168
+ width: 34,
169
+ height: 34,
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ borderWidth: StyleSheet.hairlineWidth,
173
+ borderRadius: 17,
174
+ },
175
+ closeLabel: {
176
+ fontSize: 25,
177
+ lineHeight: 29,
178
+ },
179
+ search: {
180
+ minHeight: 42,
181
+ borderWidth: StyleSheet.hairlineWidth,
182
+ borderRadius: 12,
183
+ paddingHorizontal: 12,
184
+ marginBottom: 10,
185
+ fontSize: 15,
186
+ },
187
+ categoryList: {
188
+ alignItems: 'center',
189
+ gap: 4,
190
+ paddingBottom: 8,
191
+ },
192
+ categoryButton: {
193
+ width: 38,
194
+ height: 38,
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ borderBottomWidth: 2,
198
+ borderRadius: 8,
199
+ },
200
+ categoryIcon: {
201
+ fontSize: 20,
202
+ },
203
+ groupTitle: {
204
+ fontSize: 13,
205
+ fontWeight: '600',
206
+ marginBottom: 6,
207
+ },
208
+ grid: {
209
+ alignItems: 'center',
210
+ paddingBottom: 8,
211
+ },
212
+ emojiButton: {
213
+ width: 42,
214
+ height: 42,
215
+ alignItems: 'center',
216
+ justifyContent: 'center',
217
+ borderRadius: 12,
218
+ },
219
+ emoji: {
220
+ fontSize: 24,
221
+ },
222
+ empty: {
223
+ paddingVertical: 20,
224
+ textAlign: 'center',
225
+ },
226
+ });