@adriansteffan/reactive 0.0.26 → 0.0.28

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,158 @@
1
+ import { BaseComponentProps } from '../mod';
2
+ import { useEffect, useRef, useState } from 'react';
3
+
4
+ interface DeviceInfo {
5
+ windowWidth: number;
6
+ windowHeight: number;
7
+ screenWidth: number;
8
+ screenHeight: number;
9
+ browser: string;
10
+ browserVersion: string;
11
+ isMobile: boolean;
12
+ operatingSystem: string;
13
+ hasWebAudio: boolean;
14
+ hasFullscreen: boolean;
15
+ hasWebcam: boolean;
16
+ hasMicrophone: boolean;
17
+ }
18
+
19
+ function CheckDevice({
20
+ check,
21
+ content = <div>Your device doesn't meet the requirements for this experiment.</div>,
22
+ data,
23
+ store,
24
+ updateStore,
25
+ next,
26
+ }: {
27
+ check: (deviceInfo: DeviceInfo, data: any, store: any) => boolean;
28
+ content?: React.ReactNode;
29
+ } & BaseComponentProps) {
30
+ const [showContent, setShowContent] = useState<boolean>(false);
31
+
32
+ const hasNavigatedRef = useRef(false);
33
+
34
+ useEffect(() => {
35
+ if (hasNavigatedRef.current) {
36
+ return;
37
+ }
38
+
39
+ const gatherDeviceInfo = async () => {
40
+ const windowWidth = window.innerWidth;
41
+ const windowHeight = window.innerHeight;
42
+
43
+ const screenWidth = window.screen.width;
44
+ const screenHeight = window.screen.height;
45
+
46
+ const userAgent = navigator.userAgent;
47
+ let browser = 'Unknown';
48
+ let browserVersion = 'Unknown';
49
+
50
+ if (userAgent.indexOf('Chrome') > -1) {
51
+ browser = 'Chrome';
52
+ const match = userAgent.match(/Chrome\/(\d+\.\d+)/);
53
+ browserVersion = match ? match[1] : 'Unknown';
54
+ } else if (userAgent.indexOf('Firefox') > -1) {
55
+ browser = 'Firefox';
56
+ const match = userAgent.match(/Firefox\/(\d+\.\d+)/);
57
+ browserVersion = match ? match[1] : 'Unknown';
58
+ } else if (userAgent.indexOf('Safari') > -1) {
59
+ browser = 'Safari';
60
+ const match = userAgent.match(/Version\/(\d+\.\d+)/);
61
+ browserVersion = match ? match[1] : 'Unknown';
62
+ } else if (userAgent.indexOf('Edge') > -1) {
63
+ browser = 'Edge';
64
+ const match = userAgent.match(/Edge\/(\d+\.\d+)/);
65
+ browserVersion = match ? match[1] : 'Unknown';
66
+ } else if (userAgent.indexOf('MSIE') > -1 || userAgent.indexOf('Trident/') > -1) {
67
+ browser = 'Internet Explorer';
68
+ const match = userAgent.match(/(?:MSIE |rv:)(\d+\.\d+)/);
69
+ browserVersion = match ? match[1] : 'Unknown';
70
+ }
71
+
72
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
73
+ userAgent,
74
+ );
75
+
76
+ let operatingSystem = 'Unknown';
77
+ if (/Windows/.test(userAgent)) {
78
+ operatingSystem = 'Windows';
79
+ } else if (/Macintosh|Mac OS X/.test(userAgent)) {
80
+ operatingSystem = 'MacOS';
81
+ } else if (/Linux/.test(userAgent)) {
82
+ operatingSystem = 'Linux';
83
+ } else if (/Android/.test(userAgent)) {
84
+ operatingSystem = 'Android';
85
+ } else if (/iOS|iPhone|iPad|iPod/.test(userAgent)) {
86
+ operatingSystem = 'iOS';
87
+ }
88
+
89
+ const hasWebAudio = !!(
90
+ window.AudioContext ||
91
+ (window as any).webkitAudioContext ||
92
+ (window as any).mozAudioContext ||
93
+ (window as any).oAudioContext ||
94
+ (window as any).msAudioContext
95
+ );
96
+
97
+ const hasFullscreen = !!(
98
+ document.exitFullscreen ||
99
+ (document as any).webkitExitFullscreen ||
100
+ (document as any).mozCancelFullScreen ||
101
+ (document as any).msExitFullscreen ||
102
+ document.fullscreenEnabled ||
103
+ (document as any).webkitFullscreenEnabled ||
104
+ (document as any).mozFullScreenEnabled ||
105
+ (document as any).msFullscreenEnabled
106
+ );
107
+
108
+ let hasWebcam = false;
109
+ let hasMicrophone = false;
110
+
111
+ try {
112
+ const devices = await navigator.mediaDevices.enumerateDevices();
113
+ hasWebcam = devices.some((device) => device.kind === 'videoinput');
114
+ hasMicrophone = devices.some((device) => device.kind === 'audioinput');
115
+ } catch (error) {
116
+ console.error('Error checking media devices:', error);
117
+ }
118
+
119
+ const deviceInfo: DeviceInfo = {
120
+ windowWidth,
121
+ windowHeight,
122
+ screenWidth,
123
+ screenHeight,
124
+ browser,
125
+ browserVersion,
126
+ isMobile,
127
+ operatingSystem,
128
+ hasWebAudio,
129
+ hasFullscreen,
130
+ hasWebcam,
131
+ hasMicrophone,
132
+ };
133
+
134
+ updateStore({ _reactiveDeviceInfo: deviceInfo });
135
+
136
+ const checkResult = check ? check(deviceInfo, data, store) : true;
137
+
138
+ if (checkResult && !hasNavigatedRef.current) {
139
+ hasNavigatedRef.current = true;
140
+ next(deviceInfo);
141
+ } else {
142
+ setShowContent(true);
143
+ }
144
+ };
145
+
146
+ gatherDeviceInfo();
147
+ }, [check, data, updateStore, next]);
148
+
149
+ return showContent ? (
150
+ <div className='max-w-prose mx-auto mt-20 mb-20 px-4'>
151
+ <article className='prose prose-2xl prose-slate text-xl prose-a:text-blue-600 prose-a:underline prose-h1:text-4xl prose-h1:mb-10 prose-h1:font-bold prose-p:mb-4 prose-strong:font-bold text-black leading-relaxed'>
152
+ {content}
153
+ </article>
154
+ </div>
155
+ ) : null;
156
+ }
157
+
158
+ export default CheckDevice;
@@ -1,52 +1,135 @@
1
- import { BaseComponentProps } from '../utils/common';
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react'; // Import useState
2
+ import { BaseComponentProps, getPlatform, isFullscreen } from '../utils/common';
2
3
  import Text from '../components/text';
3
4
 
4
5
  import { StatusBar } from '@capacitor/status-bar';
5
6
  import { ImmersiveMode } from '@adriansteffan/immersive-mode';
6
-
7
-
8
- function enterFullscreen(element: any) {
9
-
10
- // Android
11
- StatusBar.hide();
12
- ImmersiveMode.enable();
13
-
14
- if (element.requestFullscreen) {
15
- element.requestFullscreen();
16
- } else if (element.mozRequestFullScreen) {
17
- // Firefox
18
- element.mozRequestFullScreen();
19
- } else if (element.webkitRequestFullscreen) {
20
- // Chrome, Safari and Opera
21
- element.webkitRequestFullscreen();
22
- } else if (element.msRequestFullscreen) {
23
- // IE/Edge
24
- element.msRequestFullscreen();
25
- }
26
- }
7
+ import { Capacitor } from '@capacitor/core';
27
8
 
28
9
  export default function EnterFullscreen({
29
10
  content,
30
11
  buttonText,
31
12
  next,
32
- }: { prolificCode?: string; content?: React.ReactNode; buttonText?: string } & BaseComponentProps) {
13
+ data,
14
+ updateStore,
15
+ delayMs = 0,
16
+ }: {
17
+ prolificCode?: string;
18
+ content?: React.ReactNode;
19
+ buttonText?: string;
20
+ delayMs?: number;
21
+ } & BaseComponentProps) {
33
22
  const contentWrap = (
34
23
  <div className='flex flex-col items-center'>
35
24
  {!!content && content}
36
- {!content && (
37
- <p className=''>Please click the Button below to enter Fullscreen mode.</p>
38
- )}
25
+ {!content && <p className=''>Please click the Button below to enter Fullscreen mode.</p>}
39
26
  </div>
40
27
  );
41
28
 
29
+ const [isWaiting, setIsWaiting] = useState(false);
30
+ const listenerFallbackActive = useRef(false);
31
+ const timeoutId = useRef<NodeJS.Timeout | null>(null);
32
+
33
+ const cancelPendingTimeout = useCallback(() => {
34
+ if (timeoutId.current) {
35
+ clearTimeout(timeoutId.current);
36
+ timeoutId.current = null;
37
+ }
38
+ }, []);
39
+
40
+ const removeListenersAndTimeout = useCallback(() => {
41
+ document.removeEventListener('fullscreenchange', handleFullscreenChangeForFallback);
42
+ document.removeEventListener('webkitfullscreenchange', handleFullscreenChangeForFallback);
43
+ document.removeEventListener('mozfullscreenchange', handleFullscreenChangeForFallback);
44
+ document.removeEventListener('MSFullscreenChange', handleFullscreenChangeForFallback);
45
+ cancelPendingTimeout();
46
+ listenerFallbackActive.current = false;
47
+ }, [cancelPendingTimeout]);
48
+
49
+ const proceedWithDelay = useCallback(() => {
50
+ cancelPendingTimeout();
51
+ setIsWaiting(true);
52
+ timeoutId.current = setTimeout(() => {
53
+ timeoutId.current = null;
54
+
55
+ next({});
56
+ }, delayMs);
57
+ }, [next, delayMs, cancelPendingTimeout]);
58
+
59
+ const handleFullscreenChangeForFallback = useCallback(() => {
60
+ if (!listenerFallbackActive.current) return;
61
+ const currentlyFullscreen = isFullscreen();
62
+ removeListenersAndTimeout();
63
+ if (currentlyFullscreen) {
64
+ proceedWithDelay();
65
+ } else {
66
+ setIsWaiting(false);
67
+ }
68
+ }, [proceedWithDelay, removeListenersAndTimeout]);
69
+
70
+ const addListenersForFallback = useCallback(() => {
71
+ removeListenersAndTimeout();
72
+ listenerFallbackActive.current = true;
73
+ document.addEventListener('fullscreenchange', handleFullscreenChangeForFallback);
74
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChangeForFallback);
75
+ document.addEventListener('mozfullscreenchange', handleFullscreenChangeForFallback);
76
+ document.addEventListener('MSFullscreenChange', handleFullscreenChangeForFallback);
77
+ }, [handleFullscreenChangeForFallback, removeListenersAndTimeout]);
78
+
79
+ const handleEnterFullscreenClick = useCallback(async () => {
80
+ setIsWaiting(true);
81
+ removeListenersAndTimeout();
82
+
83
+ const element = document.documentElement;
84
+
85
+ if (getPlatform() === 'mobile' && Capacitor.getPlatform() === 'android') {
86
+ StatusBar.hide();
87
+ ImmersiveMode.enable();
88
+ }
89
+
90
+ if (element.requestFullscreen) {
91
+ try {
92
+ await element.requestFullscreen();
93
+ proceedWithDelay();
94
+ return;
95
+ } catch (err) {
96
+ console.error("Fullscreen request failed:", err);
97
+ return;
98
+ }
99
+ }
100
+
101
+ if ((element as any).mozRequestFullScreen) {
102
+ (element as any).mozRequestFullScreen();
103
+ addListenersForFallback();
104
+ } else if ((element as any).webkitRequestFullscreen) {
105
+ (element as any).webkitRequestFullscreen();
106
+ addListenersForFallback();
107
+ } else if ((element as any).msRequestFullscreen) {
108
+ (element as any).msRequestFullscreen();
109
+ addListenersForFallback();
110
+ } else {
111
+ console.warn('Fullscreen API is not supported by this browser.');
112
+ }
113
+ }, [proceedWithDelay, addListenersForFallback, removeListenersAndTimeout]);
114
+
115
+ useEffect(() => {
116
+ return () => {
117
+ removeListenersAndTimeout();
118
+ };
119
+ }, [removeListenersAndTimeout]);
120
+
121
+
122
+ if (isWaiting) {
123
+ return null;
124
+ }
125
+
42
126
  return (
43
127
  <Text
128
+ data={data}
129
+ updateStore={updateStore}
44
130
  content={contentWrap}
45
131
  buttonText={buttonText ?? 'Enter Fullscreen Mode'}
46
- next={() => {
47
- enterFullscreen(document.documentElement);
48
- next({});
49
- }}
132
+ next={handleEnterFullscreenClick}
50
133
  />
51
134
  );
52
- }
135
+ }
@@ -1,29 +1,106 @@
1
- import { BaseComponentProps } from '../utils/common';
2
- import { useEffect } from 'react';
1
+ import { useCallback, useRef, useEffect } from 'react';
2
+ import { BaseComponentProps, getPlatform, isFullscreen } from '../utils/common';
3
3
  import { StatusBar } from '@capacitor/status-bar';
4
4
  import { ImmersiveMode } from '@adriansteffan/immersive-mode';
5
+ import { Capacitor } from '@capacitor/core';
5
6
 
6
- function exitFullscreen() {
7
+ export default function ExitFullscreen({
8
+ next,
9
+ delayMs = 0,
10
+ }: {
11
+ delayMs?: number;
12
+ } & BaseComponentProps) {
13
+
14
+ const listenerFallbackActive = useRef(false);
15
+ const timeoutId = useRef<NodeJS.Timeout | null>(null);
7
16
 
8
- StatusBar.show();
9
- ImmersiveMode.disable();
17
+ const cancelPendingTimeout = useCallback(() => {
18
+ if (timeoutId.current) {
19
+ clearTimeout(timeoutId.current);
20
+ timeoutId.current = null;
21
+ }
22
+ }, []);
23
+
24
+ const removeListenersAndTimeout = useCallback(() => {
25
+ document.removeEventListener('fullscreenchange', handleFullscreenChangeForFallback);
26
+ document.removeEventListener('webkitfullscreenchange', handleFullscreenChangeForFallback);
27
+ document.removeEventListener('mozfullscreenchange', handleFullscreenChangeForFallback);
28
+ document.removeEventListener('MSFullscreenChange', handleFullscreenChangeForFallback);
29
+ cancelPendingTimeout();
30
+ listenerFallbackActive.current = false;
31
+ }, [cancelPendingTimeout]);
32
+
33
+ const proceedWithDelay = useCallback(() => {
34
+ cancelPendingTimeout();
35
+ timeoutId.current = setTimeout(() => {
36
+ timeoutId.current = null;
37
+ next({});
38
+ }, delayMs);
39
+ }, [next, delayMs, cancelPendingTimeout]);
40
+
41
+ const handleFullscreenChangeForFallback = useCallback(() => {
42
+ if (!listenerFallbackActive.current) return;
43
+ const currentlyFullscreen = isFullscreen();
44
+ removeListenersAndTimeout();
45
+ if (!currentlyFullscreen) {
46
+ proceedWithDelay();
47
+ }
48
+ }, [proceedWithDelay, removeListenersAndTimeout]);
10
49
 
11
- if (document.exitFullscreen) {
12
- document.exitFullscreen();
13
- } else if ((document as any).mozCancelFullScreen) {
14
- (document as any).mozCancelFullScreen();
15
- } else if ((document as any).webkitExitFullscreen) {
16
- (document as any).webkitExitFullscreen();
17
- } else if ((document as any).msExitFullscreen) {
18
- (document as any).msExitFullscreen();
19
- }
20
- }
50
+ const addListenersForFallback = useCallback(() => {
51
+ removeListenersAndTimeout();
52
+ listenerFallbackActive.current = true;
53
+ document.addEventListener('fullscreenchange', handleFullscreenChangeForFallback);
54
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChangeForFallback);
55
+ document.addEventListener('mozfullscreenchange', handleFullscreenChangeForFallback);
56
+ document.addEventListener('MSFullscreenChange', handleFullscreenChangeForFallback);
57
+ }, [handleFullscreenChangeForFallback, removeListenersAndTimeout]);
21
58
 
22
- export default function ExitFullscreen({ next }: BaseComponentProps) {
23
59
  useEffect(() => {
24
- exitFullscreen();
25
- next({});
26
- }, []);
60
+ const performExitFullscreen = async () => {
61
+ if (!isFullscreen()) {
62
+ next({});
63
+ return;
64
+ }
65
+
66
+ if (getPlatform() === 'mobile' && Capacitor.getPlatform() === 'android') {
67
+ StatusBar.show();
68
+ ImmersiveMode.disable();
69
+ }
70
+
71
+ if (document.exitFullscreen) {
72
+ try {
73
+ await document.exitFullscreen();
74
+ proceedWithDelay();
75
+ return;
76
+ } catch (err) {
77
+ console.error("Exiting fullscreen failed:", err);
78
+ next({});
79
+ return;
80
+ }
81
+ }
82
+
83
+ if ((document as any).mozCancelFullScreen) {
84
+ (document as any).mozCancelFullScreen();
85
+ addListenersForFallback();
86
+ } else if ((document as any).webkitExitFullscreen) {
87
+ (document as any).webkitExitFullscreen();
88
+ addListenersForFallback();
89
+ } else if ((document as any).msExitFullscreen) {
90
+ (document as any).msExitFullscreen();
91
+ addListenersForFallback();
92
+ } else {
93
+ console.warn('Fullscreen API is not supported by this browser.');
94
+ next({});
95
+ }
96
+ };
97
+
98
+ performExitFullscreen();
99
+
100
+ return () => {
101
+ removeListenersAndTimeout();
102
+ };
103
+ }, [proceedWithDelay, addListenersForFallback, removeListenersAndTimeout]);
27
104
 
28
- return <></>;
29
- }
105
+ return null;
106
+ }
@@ -4,25 +4,39 @@ import { ToastContainer } from 'react-toastify';
4
4
  const queryClient = new QueryClient();
5
5
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6
6
  import { ReactNode } from 'react';
7
+ import { SettingsScreen } from './settingsscreen';
8
+ import { Param } from '../utils/common';
7
9
 
8
- export default function ExperimentProvider({children}: {children: ReactNode}){
9
- return(<QueryClientProvider client={queryClient}>
10
- {children}
11
- <ToastContainer
12
- position='top-center'
13
- autoClose={3000}
14
- hideProgressBar
15
- newestOnTop={false}
16
- closeOnClick
17
- rtl={false}
18
- pauseOnFocusLoss
19
- draggable={false}
20
- pauseOnHover
21
- theme='light'
22
- toastClassName={() =>
23
- 'relative flex p-4 min-h-10 rounded-none justify-between overflow-hidden cursor-pointer border-2 border-black shadow-[2px_2px_0px_rgba(0,0,0,1)] bg-white'
24
- }
25
- bodyClassName="text-black font-sans"
26
- />
27
- </QueryClientProvider>)
10
+ export default function ExperimentProvider({ children, disableSettings }: { children: ReactNode, disableSettings?: boolean }) {
11
+
12
+ if (window.location.pathname.endsWith('/settings') && !disableSettings) {
13
+ return (
14
+ <SettingsScreen
15
+ paramRegistry={Param.getRegistry()}
16
+ timelineRepresentation={Param.getTimelineRepresentation()}
17
+ />
18
+ );
19
+ }
20
+
21
+ return (
22
+ <QueryClientProvider client={queryClient}>
23
+ {children}
24
+ <ToastContainer
25
+ position='top-center'
26
+ autoClose={3000}
27
+ hideProgressBar
28
+ newestOnTop={false}
29
+ closeOnClick
30
+ rtl={false}
31
+ pauseOnFocusLoss
32
+ draggable={false}
33
+ pauseOnHover
34
+ theme='light'
35
+ toastClassName={() =>
36
+ 'relative flex p-4 min-h-10 rounded-none justify-between overflow-hidden cursor-pointer border-2 border-black shadow-[2px_2px_0px_rgba(0,0,0,1)] bg-white'
37
+ }
38
+ bodyClassName='text-black font-sans'
39
+ />
40
+ </QueryClientProvider>
41
+ );
28
42
  }