@adriansteffan/reactive 0.0.18 → 0.0.20
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/bin/setup.js +13 -1
- package/dist/mod-BlYvqdtq.js +71352 -0
- package/dist/mod.d.ts +13 -1
- package/dist/reactive.es.js +14 -70864
- package/dist/reactive.umd.js +32 -32
- package/dist/style.css +1 -1
- package/dist/web-EvjUSKrQ.js +435 -0
- package/package.json +4 -1
- package/src/components/enterfullscreen.tsx +43 -0
- package/src/components/exitfullscreen.tsx +23 -0
- package/src/components/experiment.tsx +6 -2
- package/src/components/microphonecheck.tsx +1 -1
- package/src/components/quest.tsx +1 -1
- package/src/components/text.tsx +1 -1
- package/src/components/upload.tsx +224 -13
- package/src/components/voicerecorder.tsx +1 -1
- package/src/mod.tsx +4 -2
- package/src/utils/common.ts +4 -0
- package/template/.dockerignore +3 -0
- package/template/README.md +159 -30
- package/template/capacitor.config.ts +15 -0
- package/template/electron/main.js +131 -0
- package/template/electron/preload.js +9 -0
- package/template/electron.vite.config.js +45 -0
- package/template/package-lock.json +8577 -2968
- package/template/package.json +55 -1
- package/template/{vite.config.ts → shared.vite.config.js} +4 -7
- package/template/src/App.tsx +4 -0
- package/template/tsconfig.json +1 -1
- package/template/tsconfig.node.json +3 -2
- package/template/vite.config.js +11 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useMutation } from '@tanstack/react-query';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
import { post } from '../utils/request';
|
|
5
5
|
import { FileUpload, getParam, StudyEvent } from '../utils/common';
|
|
6
6
|
import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js';
|
|
7
7
|
|
|
8
|
+
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
|
9
|
+
import { Capacitor } from '@capacitor/core';
|
|
10
|
+
|
|
8
11
|
interface UploadPayload {
|
|
9
12
|
sessionId: string;
|
|
10
13
|
files: FileUpload[];
|
|
@@ -15,22 +18,175 @@ interface UploadResponse {
|
|
|
15
18
|
message?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
|
|
22
|
+
interface FileBackend {
|
|
23
|
+
directoryExists(path: string): Promise<boolean>;
|
|
24
|
+
createDirectory(path: string): Promise<void>;
|
|
25
|
+
saveFile(filename: string, content: string, directory: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createElectronFileBackend = (): FileBackend => {
|
|
29
|
+
const electronAPI = (window as any).electronAPI;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
directoryExists: async (path: string): Promise<boolean> => {
|
|
33
|
+
const result = await electronAPI.directoryExists(path);
|
|
34
|
+
return result.success && result.exists;
|
|
35
|
+
},
|
|
36
|
+
createDirectory: async (path: string): Promise<void> => {
|
|
37
|
+
await electronAPI.createDirectory(path);
|
|
38
|
+
},
|
|
39
|
+
saveFile: async (filename: string, content: string, directory: string): Promise<void> => {
|
|
40
|
+
await electronAPI.saveFile(filename, content, directory);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const createCapacitorFileBackend = (parentFolder?: string): FileBackend => {
|
|
47
|
+
|
|
48
|
+
const getPath = (path: string): string => {
|
|
49
|
+
if (!parentFolder) {
|
|
50
|
+
return path;
|
|
51
|
+
}
|
|
52
|
+
return path ? `${parentFolder}/${path}` : parentFolder;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
directoryExists: async (path: string): Promise<boolean> => {
|
|
57
|
+
try {
|
|
58
|
+
|
|
59
|
+
if (parentFolder) {
|
|
60
|
+
try {
|
|
61
|
+
await Filesystem.readdir({
|
|
62
|
+
path: parentFolder,
|
|
63
|
+
directory: Directory.Documents
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
// Parent folder doesn't exist, so subpath doesn't exist either
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const fullPath = getPath(path);
|
|
72
|
+
const result = await Filesystem.readdir({
|
|
73
|
+
path: fullPath,
|
|
74
|
+
directory: Directory.Documents
|
|
75
|
+
});
|
|
76
|
+
return result.files.length >= 0;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
createDirectory: async (path: string): Promise<void> => {
|
|
83
|
+
try {
|
|
84
|
+
|
|
85
|
+
if (parentFolder) {
|
|
86
|
+
try {
|
|
87
|
+
await Filesystem.mkdir({
|
|
88
|
+
path: parentFolder,
|
|
89
|
+
directory: Directory.Documents,
|
|
90
|
+
recursive: false
|
|
91
|
+
});
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Parent directory might already exist, that's fine
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fullPath = getPath(path);
|
|
98
|
+
await Filesystem.mkdir({
|
|
99
|
+
path: fullPath,
|
|
100
|
+
directory: Directory.Documents,
|
|
101
|
+
recursive: true
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error('Error creating directory:', e);
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
saveFile: async (filename: string, content: string, directory: string): Promise<void> => {
|
|
110
|
+
|
|
111
|
+
const fullDirectory = getPath(directory);
|
|
112
|
+
|
|
113
|
+
await Filesystem.writeFile({
|
|
114
|
+
path: `${fullDirectory}/${filename}`,
|
|
115
|
+
data: content,
|
|
116
|
+
directory: Directory.Documents,
|
|
117
|
+
encoding: 'utf8' as Encoding,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type Platform = 'electron' | 'capacitor' | 'web';
|
|
124
|
+
|
|
125
|
+
export const getPlatform = (): Platform => {
|
|
126
|
+
if ((window as any).electronAPI) {
|
|
127
|
+
return 'electron';
|
|
128
|
+
} else if (Capacitor.isNativePlatform()) {
|
|
129
|
+
return 'capacitor';
|
|
130
|
+
} else {
|
|
131
|
+
return 'web';
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const getFileBackend = (parentDir?: string): { backend: FileBackend | null, type: Platform } => {
|
|
136
|
+
const platform = getPlatform();
|
|
137
|
+
|
|
138
|
+
switch (platform) {
|
|
139
|
+
case 'electron':
|
|
140
|
+
return { backend: createElectronFileBackend(), type: platform };
|
|
141
|
+
case 'capacitor':
|
|
142
|
+
return { backend: createCapacitorFileBackend(parentDir), type: platform };
|
|
143
|
+
case 'web':
|
|
144
|
+
return { backend: null, type: platform };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Function to generate a unique directory name
|
|
149
|
+
const getUniqueDirectoryName = async (
|
|
150
|
+
backend: FileBackend,
|
|
151
|
+
baseSessionId: string
|
|
152
|
+
): Promise<string> => {
|
|
153
|
+
let uniqueSessionID = baseSessionId;
|
|
154
|
+
|
|
155
|
+
if (await backend.directoryExists(uniqueSessionID)) {
|
|
156
|
+
let counter = 1;
|
|
157
|
+
uniqueSessionID = `${baseSessionId}_${counter}`;
|
|
158
|
+
|
|
159
|
+
while (await backend.directoryExists(uniqueSessionID)) {
|
|
160
|
+
counter++;
|
|
161
|
+
uniqueSessionID = `${baseSessionId}_${counter}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return uniqueSessionID;
|
|
166
|
+
};
|
|
167
|
+
|
|
18
168
|
export default function Upload({
|
|
19
169
|
data,
|
|
20
170
|
next,
|
|
21
171
|
sessionID,
|
|
22
172
|
generateFiles,
|
|
23
173
|
uploadRaw = true,
|
|
174
|
+
autoUpload = false,
|
|
175
|
+
androidFolderName,
|
|
24
176
|
}: {
|
|
25
177
|
data: StudyEvent[];
|
|
26
178
|
next: () => void;
|
|
27
179
|
sessionID?: string | null;
|
|
28
180
|
generateFiles: (sessionID: string, data: StudyEvent[]) => FileUpload[];
|
|
29
181
|
uploadRaw: boolean;
|
|
182
|
+
autoUpload: boolean;
|
|
183
|
+
androidFolderName?: string;
|
|
30
184
|
}) {
|
|
31
185
|
const [uploadState, setUploadState] = useState<'initial' | 'uploading' | 'success' | 'error'>(
|
|
32
186
|
'initial',
|
|
33
187
|
);
|
|
188
|
+
const uploadInitiatedRef = useRef(false);
|
|
189
|
+
|
|
34
190
|
const shouldUpload = getParam('upload', true, 'boolean');
|
|
35
191
|
const shouldDownload = getParam('download', false, 'boolean');
|
|
36
192
|
|
|
@@ -70,12 +226,19 @@ export default function Upload({
|
|
|
70
226
|
document.body.removeChild(a);
|
|
71
227
|
URL.revokeObjectURL(url);
|
|
72
228
|
}, []);
|
|
73
|
-
|
|
74
|
-
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
const handleUpload = useCallback(async () => {
|
|
75
232
|
setUploadState('uploading');
|
|
76
|
-
|
|
233
|
+
|
|
234
|
+
if (uploadInitiatedRef.current) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
uploadInitiatedRef.current = true;
|
|
239
|
+
|
|
77
240
|
const sessionIDUpload = sessionID ?? uuidv4();
|
|
78
|
-
|
|
241
|
+
|
|
79
242
|
const files: FileUpload[] = generateFiles ? generateFiles(sessionIDUpload, data) : [];
|
|
80
243
|
if (uploadRaw) {
|
|
81
244
|
files.push({
|
|
@@ -84,32 +247,80 @@ export default function Upload({
|
|
|
84
247
|
encoding: 'utf8',
|
|
85
248
|
});
|
|
86
249
|
}
|
|
87
|
-
|
|
250
|
+
|
|
88
251
|
try {
|
|
89
252
|
const payload: UploadPayload = {
|
|
90
253
|
sessionId: sessionIDUpload,
|
|
91
254
|
files,
|
|
92
255
|
};
|
|
93
|
-
|
|
256
|
+
|
|
94
257
|
if (shouldDownload) {
|
|
95
258
|
await downloadFiles(files);
|
|
96
259
|
}
|
|
97
|
-
|
|
260
|
+
|
|
98
261
|
if (!shouldUpload) {
|
|
99
262
|
next();
|
|
100
263
|
return;
|
|
101
264
|
}
|
|
102
|
-
|
|
103
|
-
|
|
265
|
+
|
|
266
|
+
// Get the current platform and appropriate file backend
|
|
267
|
+
const { backend, type } = getFileBackend(androidFolderName);
|
|
268
|
+
|
|
269
|
+
if (type === 'web') {
|
|
270
|
+
// Web API case
|
|
271
|
+
uploadData.mutate(payload);
|
|
272
|
+
} else if (backend) {
|
|
273
|
+
try {
|
|
274
|
+
// Get a unique directory name
|
|
275
|
+
const uniqueSessionID = await getUniqueDirectoryName(backend, sessionIDUpload);
|
|
276
|
+
|
|
277
|
+
// Create the directory
|
|
278
|
+
await backend.createDirectory(uniqueSessionID);
|
|
279
|
+
|
|
280
|
+
// Save all files
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
await backend.saveFile(file.filename, file.content, uniqueSessionID);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
setUploadState('success');
|
|
286
|
+
next();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error(`Error saving files with ${type}:`, error);
|
|
289
|
+
setUploadState('error');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
104
292
|
} catch (error) {
|
|
105
293
|
console.error('Error uploading:', error);
|
|
106
294
|
setUploadState('error');
|
|
107
295
|
}
|
|
108
|
-
}
|
|
296
|
+
}, [
|
|
297
|
+
sessionID,
|
|
298
|
+
generateFiles,
|
|
299
|
+
data,
|
|
300
|
+
uploadRaw,
|
|
301
|
+
shouldDownload,
|
|
302
|
+
shouldUpload,
|
|
303
|
+
downloadFiles,
|
|
304
|
+
next,
|
|
305
|
+
uploadData,
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (autoUpload && !uploadInitiatedRef.current && handleUpload) {
|
|
310
|
+
handleUpload();
|
|
311
|
+
}
|
|
312
|
+
}, [autoUpload, handleUpload]);
|
|
313
|
+
|
|
314
|
+
// reset the duplicate prevention if there was an error uploading
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (uploadState === 'error') {
|
|
317
|
+
uploadInitiatedRef.current = false;
|
|
318
|
+
}
|
|
319
|
+
}, [uploadState]);
|
|
109
320
|
|
|
110
321
|
return (
|
|
111
|
-
<div className='flex flex-col items-center justify-center gap-4 p-6 text-xl mt-16'>
|
|
112
|
-
{uploadState == 'initial' && (
|
|
322
|
+
<div className='flex flex-col items-center justify-center gap-4 p-6 text-xl mt-16 px-10'>
|
|
323
|
+
{uploadState == 'initial' && !autoUpload && (
|
|
113
324
|
<>
|
|
114
325
|
<p className=''>
|
|
115
326
|
Thank you for participating! Please click the button below to submit your data.
|
|
@@ -271,7 +271,7 @@ export const VoiceRecorder = ({
|
|
|
271
271
|
};
|
|
272
272
|
|
|
273
273
|
return (
|
|
274
|
-
<div className='flex flex-col items-center space-y-4 p-4 bg-white'>
|
|
274
|
+
<div className='flex flex-col items-center space-y-4 p-4 bg-white px-8 '>
|
|
275
275
|
{/* Recording button */}
|
|
276
276
|
{!audioUrl && (
|
|
277
277
|
<button
|
package/src/mod.tsx
CHANGED
|
@@ -4,11 +4,13 @@ import ProlificEnding from './components/prolificending';
|
|
|
4
4
|
import MicCheck from './components/microphonecheck';
|
|
5
5
|
import Quest from './components/quest';
|
|
6
6
|
import Upload from './components/upload';
|
|
7
|
+
import EnterFullscreen from './components/enterfullscreen';
|
|
8
|
+
import ExitFullscreen from './components/exitfullscreen';
|
|
7
9
|
import ExperimentProvider from './components/experimentprovider';
|
|
8
10
|
import Experiment from './components/experiment';
|
|
9
|
-
import {
|
|
11
|
+
import { BaseComponentProps, ExperimentConfig } from './utils/common';
|
|
10
12
|
|
|
11
|
-
export { Text, ProlificEnding, MicCheck, Quest, Upload,
|
|
13
|
+
export { Text, ProlificEnding, MicCheck, Quest, Upload, EnterFullscreen, ExitFullscreen, Experiment, ExperimentProvider};
|
|
12
14
|
export type { BaseComponentProps, ExperimentConfig };
|
|
13
15
|
export * from './utils/common';
|
|
14
16
|
|
package/src/utils/common.ts
CHANGED
package/template/.dockerignore
CHANGED
package/template/README.md
CHANGED
|
@@ -2,11 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
Built with [reactive](https://github.com/adriansteffan/reactive)
|
|
4
4
|
|
|
5
|
-
## Setup
|
|
5
|
+
## Prerequisites and Setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Regardless of what platform you target with your experiment, you will need a current version of [node.js](https://nodejs.org/en/download/) installed on your system for development.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
To install the needed dependencies, run the following in the root directory:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm i && npm i --prefix backend
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You can target multiple platforms at once, just follow the setup processes for all platforms you want to run you study on.
|
|
16
|
+
|
|
17
|
+
## Target: Web (Online Experiment)
|
|
18
|
+
|
|
19
|
+
This is the version of the experiment you should use if your testing devices have internet access and don't need any device-specific functionalities (e.g. sensors). The web version will work on all platforms! The specific targets serve as replacements in situations where a stable internet connection might not be given, or privacy directives prevent you from uploading data to a server (and/or you need device-specific functionality).
|
|
20
|
+
|
|
21
|
+
### Development
|
|
22
|
+
|
|
23
|
+
The web version of the experiment needs no additional setup for development.
|
|
24
|
+
|
|
25
|
+
Run the app in development mode with
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm run dev:all
|
|
29
|
+
```
|
|
30
|
+
in the root directory.
|
|
31
|
+
|
|
32
|
+
By default, open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
|
33
|
+
The page will reload if you make edits.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#### Buidling the frontend locally (to test)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
From the `frontend` directory, run
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
npm run build
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
the resulting output can be found in `frontend/dist/`
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Deployment
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
For deployment, you will need a server with an installation of [docker](https://docs.docker.com/engine/install/)
|
|
10
52
|
|
|
11
53
|
|
|
12
54
|
To build the docker images, run
|
|
@@ -17,7 +59,7 @@ docker compose build
|
|
|
17
59
|
|
|
18
60
|
in the root directory. This might take a while.
|
|
19
61
|
|
|
20
|
-
|
|
62
|
+
#### Running the app
|
|
21
63
|
|
|
22
64
|
After completing the setup, start the webapp with
|
|
23
65
|
|
|
@@ -34,69 +76,156 @@ docker compose down
|
|
|
34
76
|
The server will be attached to the ports you specified in the .env files.
|
|
35
77
|
Use Virtualhosts (Apache) or Server Blocks (Nginx) with reverse proxy to expose these to the outside. [This guide](https://gist.github.com/adriansteffan/48c9bda7237a8a7fcc5bb6987c8e1790) explains how to do this for our setup.
|
|
36
78
|
|
|
37
|
-
|
|
79
|
+
#### Updating
|
|
38
80
|
|
|
39
|
-
To update the app, simply stop the running containers, run a `git pull` and build the docker containers once more.
|
|
81
|
+
To update the app, simply stop the running containers, run a `git pull` and build the docker containers once more, and start the containers again.
|
|
40
82
|
|
|
41
|
-
|
|
83
|
+
#### Where is my data stored?
|
|
42
84
|
|
|
43
|
-
|
|
85
|
+
The server will create a "data" directory in the root directory of the cloned repo,
|
|
44
86
|
|
|
45
|
-
You will need a current version of [node.js](https://nodejs.org/en/download/) installed on your system.
|
|
46
87
|
|
|
47
|
-
|
|
88
|
+
## Target: Windows or MacOS
|
|
48
89
|
|
|
49
|
-
|
|
90
|
+
### Development
|
|
50
91
|
|
|
51
|
-
|
|
92
|
+
The desktop version of the experiment needs no additional setup for development.
|
|
93
|
+
|
|
94
|
+
Run the app in the development mode with
|
|
52
95
|
|
|
53
96
|
```
|
|
54
|
-
npm
|
|
97
|
+
npm run electron:dev
|
|
55
98
|
```
|
|
99
|
+
in the root directory.
|
|
56
100
|
|
|
57
|
-
|
|
101
|
+
### Packaging
|
|
58
102
|
|
|
59
|
-
|
|
103
|
+
To build platform specific executables, run either
|
|
60
104
|
|
|
61
105
|
```
|
|
62
|
-
npm run
|
|
106
|
+
npm run package --mac
|
|
63
107
|
```
|
|
64
|
-
in the root directory.
|
|
65
108
|
|
|
66
|
-
|
|
67
|
-
The page will reload if you make edits.
|
|
109
|
+
or
|
|
68
110
|
|
|
69
|
-
|
|
111
|
+
```
|
|
112
|
+
npm run package --win
|
|
113
|
+
```
|
|
70
114
|
|
|
115
|
+
There will be a directory called "release" that will contain your executables.
|
|
71
116
|
|
|
72
|
-
|
|
117
|
+
#### Where is my data stored?
|
|
118
|
+
|
|
119
|
+
* Windows:
|
|
120
|
+
* On windows, a folder called "data" is created next the executable as soon as the first participant has completed the study
|
|
121
|
+
|
|
122
|
+
* MacOS:
|
|
123
|
+
* Right click on the application and go to TODO > TODO > TODO, where a folder called "data" is created as soon as the first participant has completed the study
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## Target: Android
|
|
128
|
+
|
|
129
|
+
For ease of use, it makes sense to run the web version for most of the development and only switch to the specific device emulators for implementing device-specific features and testing compatibility before packaging.
|
|
130
|
+
|
|
131
|
+
### Setup
|
|
132
|
+
|
|
133
|
+
To build and test an Adroid app containing your experiment, you will need an installation of
|
|
134
|
+
[Android Studio](https://developer.android.com/studio) and [Java 21 or later](https://www.oracle.com/de/java/technologies/downloads/)
|
|
135
|
+
|
|
136
|
+
To set up the android project, run this command in the root directory:
|
|
137
|
+
```
|
|
138
|
+
npx cap add android
|
|
139
|
+
````
|
|
140
|
+
|
|
141
|
+
Next, find the [path to your local android sdk](https://stackoverflow.com/questions/25176594/android-sdk-location) and create a `local.properties` file in the `android` folder with the following content:
|
|
73
142
|
|
|
143
|
+
```
|
|
144
|
+
sdk.dir=SDK_PATH_HERE
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
For more information, refer to the [Capacitor documentation on Android](https://capacitorjs.com/docs/android)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
### Running
|
|
151
|
+
|
|
152
|
+
Whenever you have made changes to the code and want to run it on the Android emulator, run:
|
|
74
153
|
```
|
|
75
154
|
npm run build
|
|
155
|
+
npx cap sync
|
|
156
|
+
npx cap run android
|
|
76
157
|
```
|
|
77
158
|
|
|
78
|
-
|
|
159
|
+
### Packaging
|
|
160
|
+
|
|
161
|
+
From the root directory, run
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
npx cap open android
|
|
165
|
+
```
|
|
79
166
|
|
|
167
|
+
. Wait for Android Studio to install the project dependencies and then run Build > Generate APKs and Bundles > Generate APKs. Once your build is finished, there will be a popup guiding you to the location of the bundle's app that you can transfer to your Android device.
|
|
80
168
|
|
|
81
|
-
|
|
169
|
+
#### Where is my data stored?
|
|
82
170
|
|
|
83
|
-
|
|
171
|
+
In the "Internal Storage" section of the file browser, you will find a "Documents" folder that will contain subfolders with your data as soon as the first participant has finished the experiment.
|
|
84
172
|
|
|
85
|
-
|
|
173
|
+
## Target: iOS
|
|
174
|
+
|
|
175
|
+
For ease of use, it makes sense to run the web version for most of the development and only switch to the specific device emulators for implementing device-specific features and testing compatibility before packaging.
|
|
176
|
+
|
|
177
|
+
### Setup
|
|
178
|
+
|
|
179
|
+
To build an iOS app containing your experiment, you will need a machine running MacOS and an installation of [XCode](https://apps.apple.com/us/app/xcode/id497799835). Furthermore, you will need to install [CocoaPods](https://cocoapods.org/), which is most easily installed with [Homebrew](https://brew.sh/).
|
|
180
|
+
|
|
181
|
+
To set up the iOS project, run
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
npx cap add android
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Then, go to `iso > App > App > Info.plist`
|
|
188
|
+
|
|
189
|
+
And add these lines next to the other keys:
|
|
86
190
|
|
|
87
191
|
```
|
|
88
|
-
|
|
192
|
+
<key>UIFileSharingEnabled</key>
|
|
193
|
+
<string>YES</string>
|
|
194
|
+
<key>LSSupportsOpeningDocumentsInPlace</key>
|
|
195
|
+
<string>YES</string>
|
|
89
196
|
```
|
|
90
197
|
|
|
91
|
-
|
|
198
|
+
For more information, refer to the [Capacitor documentation on iOS](https://capacitorjs.com/docs/ios)
|
|
92
199
|
|
|
200
|
+
### Running
|
|
93
201
|
|
|
94
|
-
|
|
202
|
+
Whenever you have made changes to the code and want to run it on the iOS emulator, run:
|
|
95
203
|
|
|
204
|
+
```
|
|
205
|
+
npm run build
|
|
206
|
+
npx cap sync
|
|
207
|
+
npx cap run ios
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Packaging
|
|
211
|
+
|
|
212
|
+
Packaging iOS apps is quite an undertaking, which is why the process is not overly streamlined/documented yet. The best way to get your experiment on an iOS device right now is to run
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
npx cap run ios
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
which will open the finished project in XCode. From there, follow a guide on how to sign, bundle and install your app/experiment.
|
|
219
|
+
|
|
220
|
+
#### Where is my data stored?
|
|
221
|
+
|
|
222
|
+
After the first participant is run, a folder with the name of your app will appear under "My iPhone/iPad" in the files app, which contains your data.
|
|
96
223
|
|
|
97
|
-
TODO
|
|
98
224
|
|
|
99
225
|
|
|
100
226
|
## Authors
|
|
101
227
|
|
|
102
|
-
* **Adrian Steffan** - [adriansteffan](https://github.com/adriansteffan)
|
|
228
|
+
* **Adrian Steffan** - [adriansteffan](https://github.com/adriansteffan)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CapacitorConfig } from '@capacitor/cli';
|
|
2
|
+
|
|
3
|
+
const config: CapacitorConfig = {
|
|
4
|
+
appId: 'com.PROJECT_NAME.app',
|
|
5
|
+
appName: 'PROJECT_NAME',
|
|
6
|
+
webDir: 'dist',
|
|
7
|
+
server: {
|
|
8
|
+
hostname: 'localhost',
|
|
9
|
+
androidScheme: 'https',
|
|
10
|
+
cleartext: true,
|
|
11
|
+
allowNavigation: ['*']
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default config;
|