pulse-js-framework 1.0.0 → 1.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/README.md +63 -0
- package/cli/build.js +124 -5
- package/cli/index.js +32 -5
- package/cli/mobile.js +1473 -0
- package/compiler/lexer.js +19 -2
- package/mobile/bridge/pulse-native.js +420 -0
- package/package.json +13 -6
- package/runtime/dom.js +363 -33
- package/runtime/index.js +2 -0
- package/runtime/native.js +368 -0
- package/runtime/pulse.js +247 -13
- package/runtime/router.js +10 -1
package/cli/mobile.js
ADDED
|
@@ -0,0 +1,1473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Mobile CLI - Mobile platform commands
|
|
3
|
+
* Zero-dependency mobile platform for Pulse Framework
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, readdirSync } from 'fs';
|
|
7
|
+
import { join, resolve, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
const MOBILE_DIR = 'mobile';
|
|
15
|
+
const CONFIG_FILE = 'pulse.mobile.json';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle mobile subcommands
|
|
19
|
+
*/
|
|
20
|
+
export async function handleMobileCommand(args) {
|
|
21
|
+
const subcommand = args[0];
|
|
22
|
+
const subargs = args.slice(1);
|
|
23
|
+
|
|
24
|
+
switch (subcommand) {
|
|
25
|
+
case 'init':
|
|
26
|
+
await initMobile(subargs);
|
|
27
|
+
break;
|
|
28
|
+
case 'build':
|
|
29
|
+
await buildMobile(subargs);
|
|
30
|
+
break;
|
|
31
|
+
case 'run':
|
|
32
|
+
await runMobile(subargs);
|
|
33
|
+
break;
|
|
34
|
+
case 'sync':
|
|
35
|
+
await syncAssets(subargs);
|
|
36
|
+
break;
|
|
37
|
+
case 'help':
|
|
38
|
+
default:
|
|
39
|
+
showMobileHelp();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize mobile platforms
|
|
45
|
+
*/
|
|
46
|
+
async function initMobile(args) {
|
|
47
|
+
const root = process.cwd();
|
|
48
|
+
const mobileDir = join(root, MOBILE_DIR);
|
|
49
|
+
const configPath = join(root, CONFIG_FILE);
|
|
50
|
+
|
|
51
|
+
console.log('Initializing Pulse Mobile...\n');
|
|
52
|
+
|
|
53
|
+
// Check if dist exists
|
|
54
|
+
if (!existsSync(join(root, 'dist'))) {
|
|
55
|
+
console.warn('Warning: No dist/ folder found. Run "pulse build" first.\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create mobile directory
|
|
59
|
+
if (!existsSync(mobileDir)) {
|
|
60
|
+
mkdirSync(mobileDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Read project name from package.json
|
|
64
|
+
let projectName = 'PulseApp';
|
|
65
|
+
let packageId = 'com.pulse.app';
|
|
66
|
+
|
|
67
|
+
const packageJsonPath = join(root, 'package.json');
|
|
68
|
+
if (existsSync(packageJsonPath)) {
|
|
69
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
70
|
+
projectName = toPascalCase(pkg.name || 'PulseApp');
|
|
71
|
+
packageId = `com.pulse.${pkg.name?.toLowerCase().replace(/[^a-z0-9]/g, '') || 'app'}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create default config
|
|
75
|
+
const config = {
|
|
76
|
+
name: projectName,
|
|
77
|
+
displayName: projectName,
|
|
78
|
+
packageId: packageId,
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
platforms: ['android', 'ios'],
|
|
81
|
+
webDir: 'dist',
|
|
82
|
+
android: {
|
|
83
|
+
minSdkVersion: 24,
|
|
84
|
+
targetSdkVersion: 34,
|
|
85
|
+
compileSdkVersion: 34
|
|
86
|
+
},
|
|
87
|
+
ios: {
|
|
88
|
+
deploymentTarget: '13.0'
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Write config if not exists
|
|
93
|
+
if (!existsSync(configPath)) {
|
|
94
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
95
|
+
console.log(`Created ${CONFIG_FILE}`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`${CONFIG_FILE} already exists, skipping...`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Copy Android template
|
|
101
|
+
const androidTemplateDir = join(__dirname, '..', 'mobile', 'templates', 'android');
|
|
102
|
+
const androidDir = join(mobileDir, 'android');
|
|
103
|
+
|
|
104
|
+
if (!existsSync(androidDir)) {
|
|
105
|
+
if (existsSync(androidTemplateDir)) {
|
|
106
|
+
console.log('Initializing Android project...');
|
|
107
|
+
copyAndProcessTemplate(androidTemplateDir, androidDir, config);
|
|
108
|
+
console.log('Android project created.');
|
|
109
|
+
} else {
|
|
110
|
+
console.log('Creating Android project structure...');
|
|
111
|
+
createAndroidProject(androidDir, config);
|
|
112
|
+
console.log('Android project created.');
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log('Android directory exists, skipping...');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Copy iOS template
|
|
119
|
+
const iosTemplateDir = join(__dirname, '..', 'mobile', 'templates', 'ios');
|
|
120
|
+
const iosDir = join(mobileDir, 'ios');
|
|
121
|
+
|
|
122
|
+
if (!existsSync(iosDir)) {
|
|
123
|
+
if (existsSync(iosTemplateDir)) {
|
|
124
|
+
console.log('Initializing iOS project...');
|
|
125
|
+
copyAndProcessTemplate(iosTemplateDir, iosDir, config);
|
|
126
|
+
console.log('iOS project created.');
|
|
127
|
+
} else {
|
|
128
|
+
console.log('Creating iOS project structure...');
|
|
129
|
+
createIOSProject(iosDir, config);
|
|
130
|
+
console.log('iOS project created.');
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
console.log('iOS directory exists, skipping...');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Copy bridge script to dist if it exists
|
|
137
|
+
const bridgeSource = join(__dirname, '..', 'mobile', 'bridge', 'pulse-native.js');
|
|
138
|
+
if (existsSync(join(root, 'dist')) && existsSync(bridgeSource)) {
|
|
139
|
+
cpSync(bridgeSource, join(root, 'dist', 'pulse-native.js'));
|
|
140
|
+
console.log('Native bridge script copied to dist/');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(`
|
|
144
|
+
Mobile platforms initialized!
|
|
145
|
+
|
|
146
|
+
Next steps:
|
|
147
|
+
1. Run "pulse build" to build your web app
|
|
148
|
+
2. Run "pulse mobile build android" or "pulse mobile build ios"
|
|
149
|
+
3. Run "pulse mobile run android" to test on device/emulator
|
|
150
|
+
|
|
151
|
+
Requirements:
|
|
152
|
+
- Android: Android SDK with build-tools installed
|
|
153
|
+
- iOS: macOS with Xcode (iOS builds only work on Mac)
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build for a mobile platform
|
|
159
|
+
*/
|
|
160
|
+
async function buildMobile(args) {
|
|
161
|
+
const platform = args[0]?.toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (!platform || !['android', 'ios'].includes(platform)) {
|
|
164
|
+
console.error('Please specify a platform: android or ios');
|
|
165
|
+
console.log('Usage: pulse mobile build <platform>');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const root = process.cwd();
|
|
170
|
+
const config = loadConfig(root);
|
|
171
|
+
|
|
172
|
+
// First, ensure web build exists
|
|
173
|
+
if (!existsSync(join(root, config.webDir))) {
|
|
174
|
+
console.log('Building web app first...');
|
|
175
|
+
const { buildProject } = await import('./build.js');
|
|
176
|
+
await buildProject([]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Sync web assets to native project
|
|
180
|
+
await syncWebAssets(root, platform, config);
|
|
181
|
+
|
|
182
|
+
// Build native app
|
|
183
|
+
if (platform === 'android') {
|
|
184
|
+
await buildAndroid(root, config);
|
|
185
|
+
} else if (platform === 'ios') {
|
|
186
|
+
await buildIOS(root, config);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build and run on device/emulator
|
|
192
|
+
*/
|
|
193
|
+
async function runMobile(args) {
|
|
194
|
+
const platform = args[0]?.toLowerCase();
|
|
195
|
+
|
|
196
|
+
if (!platform || !['android', 'ios'].includes(platform)) {
|
|
197
|
+
console.error('Please specify a platform: android or ios');
|
|
198
|
+
console.log('Usage: pulse mobile run <platform>');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build first
|
|
203
|
+
await buildMobile([platform]);
|
|
204
|
+
|
|
205
|
+
const root = process.cwd();
|
|
206
|
+
const config = loadConfig(root);
|
|
207
|
+
|
|
208
|
+
// Run on device/emulator
|
|
209
|
+
if (platform === 'android') {
|
|
210
|
+
await runAndroid(root, config);
|
|
211
|
+
} else if (platform === 'ios') {
|
|
212
|
+
await runIOS(root, config);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Sync assets only
|
|
218
|
+
*/
|
|
219
|
+
async function syncAssets(args) {
|
|
220
|
+
const platform = args[0]?.toLowerCase();
|
|
221
|
+
const root = process.cwd();
|
|
222
|
+
const config = loadConfig(root);
|
|
223
|
+
|
|
224
|
+
if (platform) {
|
|
225
|
+
await syncWebAssets(root, platform, config);
|
|
226
|
+
} else {
|
|
227
|
+
await syncWebAssets(root, 'android', config);
|
|
228
|
+
await syncWebAssets(root, 'ios', config);
|
|
229
|
+
}
|
|
230
|
+
console.log('Assets synced successfully!');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Sync web assets to native project
|
|
235
|
+
*/
|
|
236
|
+
async function syncWebAssets(root, platform, config) {
|
|
237
|
+
const webDir = join(root, config.webDir);
|
|
238
|
+
|
|
239
|
+
if (!existsSync(webDir)) {
|
|
240
|
+
console.error(`Web directory "${config.webDir}" not found. Run "pulse build" first.`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let assetsDir;
|
|
245
|
+
if (platform === 'android') {
|
|
246
|
+
assetsDir = join(root, MOBILE_DIR, 'android', 'app', 'src', 'main', 'assets', 'www');
|
|
247
|
+
} else {
|
|
248
|
+
assetsDir = join(root, MOBILE_DIR, 'ios', 'PulseApp', 'www');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Create assets directory
|
|
252
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
253
|
+
|
|
254
|
+
// Copy web files
|
|
255
|
+
cpSync(webDir, assetsDir, { recursive: true });
|
|
256
|
+
|
|
257
|
+
// Copy native bridge
|
|
258
|
+
const bridgeSource = join(__dirname, '..', 'mobile', 'bridge', 'pulse-native.js');
|
|
259
|
+
if (existsSync(bridgeSource)) {
|
|
260
|
+
cpSync(bridgeSource, join(assetsDir, 'pulse-native.js'));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(`Web assets synced to ${platform}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Build Android APK
|
|
268
|
+
*/
|
|
269
|
+
async function buildAndroid(root, config) {
|
|
270
|
+
const androidDir = join(root, MOBILE_DIR, 'android');
|
|
271
|
+
|
|
272
|
+
if (!existsSync(androidDir)) {
|
|
273
|
+
console.error('Android project not found. Run "pulse mobile init" first.');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log('Building Android APK...\n');
|
|
278
|
+
|
|
279
|
+
// Determine gradle executable
|
|
280
|
+
const isWindows = process.platform === 'win32';
|
|
281
|
+
const gradlew = isWindows ? 'gradlew.bat' : './gradlew';
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
execSync(`${gradlew} assembleDebug`, {
|
|
285
|
+
cwd: androidDir,
|
|
286
|
+
stdio: 'inherit'
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const apkPath = join(androidDir, 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
|
|
290
|
+
console.log(`
|
|
291
|
+
Build successful!
|
|
292
|
+
APK location: ${apkPath}
|
|
293
|
+
`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('Android build failed.');
|
|
296
|
+
console.error('Make sure Android SDK is installed and ANDROID_HOME is set.');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Build iOS app
|
|
303
|
+
*/
|
|
304
|
+
async function buildIOS(root, config) {
|
|
305
|
+
if (process.platform !== 'darwin') {
|
|
306
|
+
console.error('iOS builds are only supported on macOS');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const iosDir = join(root, MOBILE_DIR, 'ios');
|
|
311
|
+
|
|
312
|
+
if (!existsSync(iosDir)) {
|
|
313
|
+
console.error('iOS project not found. Run "pulse mobile init" first.');
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log('Building iOS app...\n');
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
execSync(`xcodebuild -project PulseApp.xcodeproj -scheme PulseApp -configuration Debug -destination 'generic/platform=iOS Simulator' build`, {
|
|
321
|
+
cwd: iosDir,
|
|
322
|
+
stdio: 'inherit'
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
console.log('\niOS build successful!');
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('iOS build failed.');
|
|
328
|
+
console.error('Make sure Xcode is installed and command line tools are configured.');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Run on Android device/emulator
|
|
335
|
+
*/
|
|
336
|
+
async function runAndroid(root, config) {
|
|
337
|
+
const androidDir = join(root, MOBILE_DIR, 'android');
|
|
338
|
+
const isWindows = process.platform === 'win32';
|
|
339
|
+
const gradlew = isWindows ? 'gradlew.bat' : './gradlew';
|
|
340
|
+
|
|
341
|
+
console.log('Installing and running on Android...\n');
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
execSync(`${gradlew} installDebug`, {
|
|
345
|
+
cwd: androidDir,
|
|
346
|
+
stdio: 'inherit'
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Launch the app
|
|
350
|
+
const packageId = config.packageId;
|
|
351
|
+
execSync(`adb shell am start -n ${packageId}/${packageId}.MainActivity`, {
|
|
352
|
+
stdio: 'inherit'
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
console.log('\nApp launched on Android device/emulator');
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('Failed to run on Android.');
|
|
358
|
+
console.error('Make sure a device/emulator is connected (check with "adb devices").');
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Run on iOS simulator
|
|
365
|
+
*/
|
|
366
|
+
async function runIOS(root, config) {
|
|
367
|
+
if (process.platform !== 'darwin') {
|
|
368
|
+
console.error('iOS development is only supported on macOS');
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const iosDir = join(root, MOBILE_DIR, 'ios');
|
|
373
|
+
|
|
374
|
+
console.log('Running on iOS Simulator...\n');
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
// Build for simulator
|
|
378
|
+
execSync(`xcodebuild -project PulseApp.xcodeproj -scheme PulseApp -destination 'platform=iOS Simulator,name=iPhone 15' -configuration Debug build`, {
|
|
379
|
+
cwd: iosDir,
|
|
380
|
+
stdio: 'inherit'
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Boot simulator if needed
|
|
384
|
+
try {
|
|
385
|
+
execSync('xcrun simctl boot "iPhone 15"', { stdio: 'pipe' });
|
|
386
|
+
} catch (e) {
|
|
387
|
+
// Simulator might already be booted
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Get app path and install
|
|
391
|
+
const buildDir = join(iosDir, 'build', 'Debug-iphonesimulator');
|
|
392
|
+
execSync(`xcrun simctl install booted "${join(buildDir, 'PulseApp.app')}"`, {
|
|
393
|
+
stdio: 'inherit'
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Launch app
|
|
397
|
+
execSync(`xcrun simctl launch booted ${config.packageId}`, {
|
|
398
|
+
stdio: 'inherit'
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
console.log('\nApp launched on iOS Simulator');
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error('Failed to run on iOS.');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Load mobile config
|
|
410
|
+
*/
|
|
411
|
+
function loadConfig(root) {
|
|
412
|
+
const configPath = join(root, CONFIG_FILE);
|
|
413
|
+
|
|
414
|
+
if (!existsSync(configPath)) {
|
|
415
|
+
console.error(`No ${CONFIG_FILE} found. Run "pulse mobile init" first.`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Copy and process template files
|
|
424
|
+
*/
|
|
425
|
+
function copyAndProcessTemplate(src, dest, config) {
|
|
426
|
+
if (!existsSync(src)) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
mkdirSync(dest, { recursive: true });
|
|
431
|
+
|
|
432
|
+
const files = readdirSync(src, { withFileTypes: true });
|
|
433
|
+
|
|
434
|
+
for (const file of files) {
|
|
435
|
+
const srcPath = join(src, file.name);
|
|
436
|
+
const destPath = join(dest, file.name);
|
|
437
|
+
|
|
438
|
+
if (file.isDirectory()) {
|
|
439
|
+
copyAndProcessTemplate(srcPath, destPath, config);
|
|
440
|
+
} else {
|
|
441
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
442
|
+
|
|
443
|
+
// Replace template variables
|
|
444
|
+
content = content
|
|
445
|
+
.replace(/\{\{APP_NAME\}\}/g, config.name)
|
|
446
|
+
.replace(/\{\{DISPLAY_NAME\}\}/g, config.displayName)
|
|
447
|
+
.replace(/\{\{PACKAGE_ID\}\}/g, config.packageId)
|
|
448
|
+
.replace(/\{\{VERSION\}\}/g, config.version)
|
|
449
|
+
.replace(/\{\{MIN_SDK\}\}/g, String(config.android?.minSdkVersion || 24))
|
|
450
|
+
.replace(/\{\{TARGET_SDK\}\}/g, String(config.android?.targetSdkVersion || 34))
|
|
451
|
+
.replace(/\{\{COMPILE_SDK\}\}/g, String(config.android?.compileSdkVersion || 34))
|
|
452
|
+
.replace(/\{\{IOS_TARGET\}\}/g, config.ios?.deploymentTarget || '13.0');
|
|
453
|
+
|
|
454
|
+
writeFileSync(destPath, content);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Create Android project from scratch
|
|
461
|
+
*/
|
|
462
|
+
function createAndroidProject(androidDir, config) {
|
|
463
|
+
const packagePath = config.packageId.replace(/\./g, '/');
|
|
464
|
+
|
|
465
|
+
// Create directory structure
|
|
466
|
+
const dirs = [
|
|
467
|
+
'app/src/main/java/' + packagePath,
|
|
468
|
+
'app/src/main/res/layout',
|
|
469
|
+
'app/src/main/res/values',
|
|
470
|
+
'app/src/main/res/drawable',
|
|
471
|
+
'app/src/main/res/mipmap-hdpi',
|
|
472
|
+
'app/src/main/res/mipmap-mdpi',
|
|
473
|
+
'app/src/main/res/mipmap-xhdpi',
|
|
474
|
+
'app/src/main/res/mipmap-xxhdpi',
|
|
475
|
+
'app/src/main/res/mipmap-xxxhdpi',
|
|
476
|
+
'app/src/main/assets/www',
|
|
477
|
+
'gradle/wrapper'
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
for (const dir of dirs) {
|
|
481
|
+
mkdirSync(join(androidDir, dir), { recursive: true });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// MainActivity.java
|
|
485
|
+
writeFileSync(join(androidDir, 'app/src/main/java', packagePath, 'MainActivity.java'), `
|
|
486
|
+
package ${config.packageId};
|
|
487
|
+
|
|
488
|
+
import android.os.Bundle;
|
|
489
|
+
import android.webkit.WebView;
|
|
490
|
+
import android.webkit.WebSettings;
|
|
491
|
+
import android.webkit.WebViewClient;
|
|
492
|
+
import android.webkit.WebChromeClient;
|
|
493
|
+
import android.webkit.ConsoleMessage;
|
|
494
|
+
import android.util.Log;
|
|
495
|
+
import android.app.Activity;
|
|
496
|
+
import android.view.View;
|
|
497
|
+
import android.view.Window;
|
|
498
|
+
import android.view.WindowManager;
|
|
499
|
+
import android.graphics.Color;
|
|
500
|
+
import android.os.Build;
|
|
501
|
+
|
|
502
|
+
public class MainActivity extends Activity {
|
|
503
|
+
private static final String TAG = "PulseApp";
|
|
504
|
+
private WebView webView;
|
|
505
|
+
private PulseBridge bridge;
|
|
506
|
+
|
|
507
|
+
@Override
|
|
508
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
509
|
+
super.onCreate(savedInstanceState);
|
|
510
|
+
|
|
511
|
+
// Full screen
|
|
512
|
+
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
513
|
+
getWindow().setFlags(
|
|
514
|
+
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
515
|
+
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// Create WebView
|
|
519
|
+
webView = new WebView(this);
|
|
520
|
+
setContentView(webView);
|
|
521
|
+
|
|
522
|
+
// Configure WebView
|
|
523
|
+
WebSettings settings = webView.getSettings();
|
|
524
|
+
settings.setJavaScriptEnabled(true);
|
|
525
|
+
settings.setDomStorageEnabled(true);
|
|
526
|
+
settings.setDatabaseEnabled(true);
|
|
527
|
+
settings.setAllowFileAccess(true);
|
|
528
|
+
settings.setAllowContentAccess(true);
|
|
529
|
+
settings.setMediaPlaybackRequiresUserGesture(false);
|
|
530
|
+
|
|
531
|
+
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
|
532
|
+
|
|
533
|
+
if (BuildConfig.DEBUG) {
|
|
534
|
+
WebView.setWebContentsDebuggingEnabled(true);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Set up native bridge
|
|
538
|
+
bridge = new PulseBridge(this);
|
|
539
|
+
webView.addJavascriptInterface(bridge, "PulseNative");
|
|
540
|
+
|
|
541
|
+
webView.setWebViewClient(new WebViewClient() {
|
|
542
|
+
@Override
|
|
543
|
+
public void onPageFinished(WebView view, String url) {
|
|
544
|
+
super.onPageFinished(view, url);
|
|
545
|
+
injectBridgeInit();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
webView.setWebChromeClient(new WebChromeClient() {
|
|
550
|
+
@Override
|
|
551
|
+
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
|
552
|
+
Log.d(TAG, consoleMessage.message());
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
webView.loadUrl("file:///android_asset/www/index.html");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private void injectBridgeInit() {
|
|
561
|
+
String script = "if(typeof window.initPulseNative === 'function') { window.initPulseNative(); }";
|
|
562
|
+
webView.evaluateJavascript(script, null);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
public void executeJS(String script) {
|
|
566
|
+
runOnUiThread(() -> webView.evaluateJavascript(script, null));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
@Override
|
|
570
|
+
public void onBackPressed() {
|
|
571
|
+
if (webView.canGoBack()) {
|
|
572
|
+
webView.goBack();
|
|
573
|
+
} else {
|
|
574
|
+
super.onBackPressed();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
@Override
|
|
579
|
+
protected void onResume() {
|
|
580
|
+
super.onResume();
|
|
581
|
+
webView.onResume();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
@Override
|
|
585
|
+
protected void onPause() {
|
|
586
|
+
webView.onPause();
|
|
587
|
+
super.onPause();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
@Override
|
|
591
|
+
protected void onDestroy() {
|
|
592
|
+
if (webView != null) webView.destroy();
|
|
593
|
+
super.onDestroy();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
`.trim());
|
|
597
|
+
|
|
598
|
+
// PulseBridge.java
|
|
599
|
+
writeFileSync(join(androidDir, 'app/src/main/java', packagePath, 'PulseBridge.java'), `
|
|
600
|
+
package ${config.packageId};
|
|
601
|
+
|
|
602
|
+
import android.content.Context;
|
|
603
|
+
import android.content.SharedPreferences;
|
|
604
|
+
import android.os.Build;
|
|
605
|
+
import android.webkit.JavascriptInterface;
|
|
606
|
+
import android.provider.Settings;
|
|
607
|
+
import android.os.Vibrator;
|
|
608
|
+
import android.os.VibrationEffect;
|
|
609
|
+
import android.widget.Toast;
|
|
610
|
+
import android.content.pm.PackageManager;
|
|
611
|
+
import android.content.pm.PackageInfo;
|
|
612
|
+
import android.net.ConnectivityManager;
|
|
613
|
+
import android.net.NetworkInfo;
|
|
614
|
+
import org.json.JSONObject;
|
|
615
|
+
import org.json.JSONException;
|
|
616
|
+
|
|
617
|
+
public class PulseBridge {
|
|
618
|
+
private static final String PREFS_NAME = "PulseStorage";
|
|
619
|
+
private Context context;
|
|
620
|
+
private MainActivity activity;
|
|
621
|
+
|
|
622
|
+
public PulseBridge(MainActivity activity) {
|
|
623
|
+
this.activity = activity;
|
|
624
|
+
this.context = activity.getApplicationContext();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Storage API
|
|
628
|
+
@JavascriptInterface
|
|
629
|
+
public void setItem(String key, String value) {
|
|
630
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
631
|
+
prefs.edit().putString(key, value).apply();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
@JavascriptInterface
|
|
635
|
+
public String getItem(String key) {
|
|
636
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
637
|
+
return prefs.getString(key, null);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
@JavascriptInterface
|
|
641
|
+
public void removeItem(String key) {
|
|
642
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
643
|
+
prefs.edit().remove(key).apply();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
@JavascriptInterface
|
|
647
|
+
public void clearStorage() {
|
|
648
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
649
|
+
prefs.edit().clear().apply();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
@JavascriptInterface
|
|
653
|
+
public String getAllKeys() {
|
|
654
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
655
|
+
return String.join(",", prefs.getAll().keySet());
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Device Info
|
|
659
|
+
@JavascriptInterface
|
|
660
|
+
public String getDeviceInfo() {
|
|
661
|
+
try {
|
|
662
|
+
JSONObject info = new JSONObject();
|
|
663
|
+
info.put("platform", "android");
|
|
664
|
+
info.put("model", Build.MODEL);
|
|
665
|
+
info.put("manufacturer", Build.MANUFACTURER);
|
|
666
|
+
info.put("version", Build.VERSION.RELEASE);
|
|
667
|
+
info.put("sdkVersion", Build.VERSION.SDK_INT);
|
|
668
|
+
info.put("appVersion", getAppVersion());
|
|
669
|
+
return info.toString();
|
|
670
|
+
} catch (JSONException e) {
|
|
671
|
+
return "{}";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private String getAppVersion() {
|
|
676
|
+
try {
|
|
677
|
+
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
|
678
|
+
return pInfo.versionName;
|
|
679
|
+
} catch (PackageManager.NameNotFoundException e) {
|
|
680
|
+
return "1.0.0";
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
@JavascriptInterface
|
|
685
|
+
public String getNetworkStatus() {
|
|
686
|
+
try {
|
|
687
|
+
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
688
|
+
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
|
689
|
+
JSONObject status = new JSONObject();
|
|
690
|
+
status.put("connected", activeNetwork != null && activeNetwork.isConnected());
|
|
691
|
+
status.put("type", activeNetwork != null ? activeNetwork.getTypeName() : "none");
|
|
692
|
+
return status.toString();
|
|
693
|
+
} catch (JSONException e) {
|
|
694
|
+
return "{\\"connected\\":false,\\"type\\":\\"none\\"}";
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// UI API
|
|
699
|
+
@JavascriptInterface
|
|
700
|
+
public void showToast(String message, boolean isLong) {
|
|
701
|
+
int duration = isLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT;
|
|
702
|
+
activity.runOnUiThread(() -> Toast.makeText(context, message, duration).show());
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
@JavascriptInterface
|
|
706
|
+
public void vibrate(int duration) {
|
|
707
|
+
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
|
708
|
+
if (vibrator != null && vibrator.hasVibrator()) {
|
|
709
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
710
|
+
vibrator.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE));
|
|
711
|
+
} else {
|
|
712
|
+
vibrator.vibrate(duration);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Clipboard
|
|
718
|
+
@JavascriptInterface
|
|
719
|
+
public void copyToClipboard(String text) {
|
|
720
|
+
android.content.ClipboardManager clipboard =
|
|
721
|
+
(android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
722
|
+
android.content.ClipData clip = android.content.ClipData.newPlainText("Pulse", text);
|
|
723
|
+
clipboard.setPrimaryClip(clip);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
@JavascriptInterface
|
|
727
|
+
public String getClipboardText() {
|
|
728
|
+
android.content.ClipboardManager clipboard =
|
|
729
|
+
(android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
730
|
+
if (clipboard.hasPrimaryClip()) {
|
|
731
|
+
android.content.ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
|
|
732
|
+
CharSequence text = item.getText();
|
|
733
|
+
return text != null ? text.toString() : "";
|
|
734
|
+
}
|
|
735
|
+
return "";
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// App Lifecycle
|
|
739
|
+
@JavascriptInterface
|
|
740
|
+
public void exitApp() {
|
|
741
|
+
activity.runOnUiThread(() -> activity.finish());
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
@JavascriptInterface
|
|
745
|
+
public void minimizeApp() {
|
|
746
|
+
activity.runOnUiThread(() -> activity.moveTaskToBack(true));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
`.trim());
|
|
750
|
+
|
|
751
|
+
// AndroidManifest.xml
|
|
752
|
+
writeFileSync(join(androidDir, 'app/src/main/AndroidManifest.xml'), `
|
|
753
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
754
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
755
|
+
package="${config.packageId}">
|
|
756
|
+
|
|
757
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
758
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
759
|
+
<uses-permission android:name="android.permission.VIBRATE" />
|
|
760
|
+
|
|
761
|
+
<application
|
|
762
|
+
android:allowBackup="true"
|
|
763
|
+
android:icon="@mipmap/ic_launcher"
|
|
764
|
+
android:label="${config.displayName}"
|
|
765
|
+
android:supportsRtl="true"
|
|
766
|
+
android:theme="@style/Theme.PulseApp"
|
|
767
|
+
android:usesCleartextTraffic="true"
|
|
768
|
+
android:hardwareAccelerated="true">
|
|
769
|
+
|
|
770
|
+
<activity
|
|
771
|
+
android:name=".MainActivity"
|
|
772
|
+
android:exported="true"
|
|
773
|
+
android:configChanges="orientation|screenSize|keyboardHidden"
|
|
774
|
+
android:windowSoftInputMode="adjustResize"
|
|
775
|
+
android:launchMode="singleTask">
|
|
776
|
+
|
|
777
|
+
<intent-filter>
|
|
778
|
+
<action android:name="android.intent.action.MAIN" />
|
|
779
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
780
|
+
</intent-filter>
|
|
781
|
+
</activity>
|
|
782
|
+
</application>
|
|
783
|
+
</manifest>
|
|
784
|
+
`.trim());
|
|
785
|
+
|
|
786
|
+
// app/build.gradle
|
|
787
|
+
writeFileSync(join(androidDir, 'app/build.gradle'), `
|
|
788
|
+
plugins {
|
|
789
|
+
id 'com.android.application'
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
android {
|
|
793
|
+
namespace '${config.packageId}'
|
|
794
|
+
compileSdk ${config.android?.compileSdkVersion || 34}
|
|
795
|
+
|
|
796
|
+
defaultConfig {
|
|
797
|
+
applicationId "${config.packageId}"
|
|
798
|
+
minSdk ${config.android?.minSdkVersion || 24}
|
|
799
|
+
targetSdk ${config.android?.targetSdkVersion || 34}
|
|
800
|
+
versionCode 1
|
|
801
|
+
versionName "${config.version}"
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
buildTypes {
|
|
805
|
+
release {
|
|
806
|
+
minifyEnabled true
|
|
807
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
808
|
+
}
|
|
809
|
+
debug {
|
|
810
|
+
debuggable true
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
compileOptions {
|
|
815
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
816
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
dependencies {}
|
|
821
|
+
`.trim());
|
|
822
|
+
|
|
823
|
+
// build.gradle (project level)
|
|
824
|
+
writeFileSync(join(androidDir, 'build.gradle'), `
|
|
825
|
+
buildscript {
|
|
826
|
+
repositories {
|
|
827
|
+
google()
|
|
828
|
+
mavenCentral()
|
|
829
|
+
}
|
|
830
|
+
dependencies {
|
|
831
|
+
classpath 'com.android.tools.build:gradle:8.2.0'
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
allprojects {
|
|
836
|
+
repositories {
|
|
837
|
+
google()
|
|
838
|
+
mavenCentral()
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
task clean(type: Delete) {
|
|
843
|
+
delete rootProject.buildDir
|
|
844
|
+
}
|
|
845
|
+
`.trim());
|
|
846
|
+
|
|
847
|
+
// settings.gradle
|
|
848
|
+
writeFileSync(join(androidDir, 'settings.gradle'), `
|
|
849
|
+
pluginManagement {
|
|
850
|
+
repositories {
|
|
851
|
+
google()
|
|
852
|
+
mavenCentral()
|
|
853
|
+
gradlePluginPortal()
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
rootProject.name = "${config.name}"
|
|
858
|
+
include ':app'
|
|
859
|
+
`.trim());
|
|
860
|
+
|
|
861
|
+
// gradle-wrapper.properties
|
|
862
|
+
writeFileSync(join(androidDir, 'gradle/wrapper/gradle-wrapper.properties'), `
|
|
863
|
+
distributionBase=GRADLE_USER_HOME
|
|
864
|
+
distributionPath=wrapper/dists
|
|
865
|
+
distributionUrl=https\\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
|
866
|
+
zipStoreBase=GRADLE_USER_HOME
|
|
867
|
+
zipStorePath=wrapper/dists
|
|
868
|
+
`.trim());
|
|
869
|
+
|
|
870
|
+
// gradlew (Unix)
|
|
871
|
+
writeFileSync(join(androidDir, 'gradlew'), `#!/bin/sh
|
|
872
|
+
exec gradle "$@"
|
|
873
|
+
`.trim());
|
|
874
|
+
|
|
875
|
+
// gradlew.bat (Windows)
|
|
876
|
+
writeFileSync(join(androidDir, 'gradlew.bat'), `@echo off
|
|
877
|
+
gradle %*
|
|
878
|
+
`.trim());
|
|
879
|
+
|
|
880
|
+
// styles.xml
|
|
881
|
+
writeFileSync(join(androidDir, 'app/src/main/res/values/styles.xml'), `
|
|
882
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
883
|
+
<resources>
|
|
884
|
+
<style name="Theme.PulseApp" parent="android:Theme.Material.Light.NoActionBar">
|
|
885
|
+
<item name="android:windowBackground">@android:color/white</item>
|
|
886
|
+
<item name="android:statusBarColor">@android:color/transparent</item>
|
|
887
|
+
</style>
|
|
888
|
+
</resources>
|
|
889
|
+
`.trim());
|
|
890
|
+
|
|
891
|
+
// strings.xml
|
|
892
|
+
writeFileSync(join(androidDir, 'app/src/main/res/values/strings.xml'), `
|
|
893
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
894
|
+
<resources>
|
|
895
|
+
<string name="app_name">${config.displayName}</string>
|
|
896
|
+
</resources>
|
|
897
|
+
`.trim());
|
|
898
|
+
|
|
899
|
+
// proguard-rules.pro
|
|
900
|
+
writeFileSync(join(androidDir, 'app/proguard-rules.pro'), `
|
|
901
|
+
# Pulse WebView bridge
|
|
902
|
+
-keepclassmembers class ${config.packageId}.PulseBridge {
|
|
903
|
+
@android.webkit.JavascriptInterface <methods>;
|
|
904
|
+
}
|
|
905
|
+
-keepattributes JavascriptInterface
|
|
906
|
+
`.trim());
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Create iOS project from scratch
|
|
911
|
+
*/
|
|
912
|
+
function createIOSProject(iosDir, config) {
|
|
913
|
+
// Create directory structure
|
|
914
|
+
const dirs = [
|
|
915
|
+
'PulseApp',
|
|
916
|
+
'PulseApp/www',
|
|
917
|
+
'PulseApp/Assets.xcassets',
|
|
918
|
+
'PulseApp.xcodeproj'
|
|
919
|
+
];
|
|
920
|
+
|
|
921
|
+
for (const dir of dirs) {
|
|
922
|
+
mkdirSync(join(iosDir, dir), { recursive: true });
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// AppDelegate.swift
|
|
926
|
+
writeFileSync(join(iosDir, 'PulseApp/AppDelegate.swift'), `
|
|
927
|
+
import UIKit
|
|
928
|
+
|
|
929
|
+
@main
|
|
930
|
+
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
931
|
+
var window: UIWindow?
|
|
932
|
+
|
|
933
|
+
func application(_ application: UIApplication,
|
|
934
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
935
|
+
return true
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
func application(_ application: UIApplication,
|
|
939
|
+
configurationForConnecting connectingSceneSession: UISceneSession,
|
|
940
|
+
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
941
|
+
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
`.trim());
|
|
945
|
+
|
|
946
|
+
// SceneDelegate.swift
|
|
947
|
+
writeFileSync(join(iosDir, 'PulseApp/SceneDelegate.swift'), `
|
|
948
|
+
import UIKit
|
|
949
|
+
|
|
950
|
+
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
951
|
+
var window: UIWindow?
|
|
952
|
+
|
|
953
|
+
func scene(_ scene: UIScene,
|
|
954
|
+
willConnectTo session: UISceneSession,
|
|
955
|
+
options connectionOptions: UIScene.ConnectionOptions) {
|
|
956
|
+
guard let windowScene = (scene as? UIWindowScene) else { return }
|
|
957
|
+
|
|
958
|
+
window = UIWindow(windowScene: windowScene)
|
|
959
|
+
window?.rootViewController = ViewController()
|
|
960
|
+
window?.makeKeyAndVisible()
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
`.trim());
|
|
964
|
+
|
|
965
|
+
// ViewController.swift
|
|
966
|
+
writeFileSync(join(iosDir, 'PulseApp/ViewController.swift'), `
|
|
967
|
+
import UIKit
|
|
968
|
+
import WebKit
|
|
969
|
+
|
|
970
|
+
class ViewController: UIViewController, WKNavigationDelegate {
|
|
971
|
+
private var webView: WKWebView!
|
|
972
|
+
private var bridge: PulseBridge!
|
|
973
|
+
|
|
974
|
+
override func viewDidLoad() {
|
|
975
|
+
super.viewDidLoad()
|
|
976
|
+
setupWebView()
|
|
977
|
+
loadApp()
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private func setupWebView() {
|
|
981
|
+
let config = WKWebViewConfiguration()
|
|
982
|
+
config.allowsInlineMediaPlayback = true
|
|
983
|
+
|
|
984
|
+
bridge = PulseBridge(viewController: self)
|
|
985
|
+
config.userContentController.add(bridge, name: "PulseNative")
|
|
986
|
+
|
|
987
|
+
webView = WKWebView(frame: view.bounds, configuration: config)
|
|
988
|
+
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
989
|
+
webView.navigationDelegate = self
|
|
990
|
+
webView.scrollView.bounces = false
|
|
991
|
+
|
|
992
|
+
#if DEBUG
|
|
993
|
+
if #available(iOS 16.4, *) {
|
|
994
|
+
webView.isInspectable = true
|
|
995
|
+
}
|
|
996
|
+
#endif
|
|
997
|
+
|
|
998
|
+
view.addSubview(webView)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
private func loadApp() {
|
|
1002
|
+
guard let indexPath = Bundle.main.path(forResource: "index", ofType: "html", inDirectory: "www") else {
|
|
1003
|
+
print("Error: Could not find www/index.html")
|
|
1004
|
+
return
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
let indexURL = URL(fileURLWithPath: indexPath)
|
|
1008
|
+
webView.loadFileURL(indexURL, allowingReadAccessTo: indexURL.deletingLastPathComponent())
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
func executeJS(_ script: String) {
|
|
1012
|
+
webView.evaluateJavaScript(script, completionHandler: nil)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
1016
|
+
webView.evaluateJavaScript("if(typeof window.initPulseNative === 'function') { window.initPulseNative(); }", completionHandler: nil)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
|
|
1020
|
+
}
|
|
1021
|
+
`.trim());
|
|
1022
|
+
|
|
1023
|
+
// PulseBridge.swift
|
|
1024
|
+
writeFileSync(join(iosDir, 'PulseApp/PulseBridge.swift'), `
|
|
1025
|
+
import UIKit
|
|
1026
|
+
import WebKit
|
|
1027
|
+
|
|
1028
|
+
class PulseBridge: NSObject, WKScriptMessageHandler {
|
|
1029
|
+
private weak var viewController: ViewController?
|
|
1030
|
+
private let userDefaults = UserDefaults.standard
|
|
1031
|
+
private let prefix = "pulse_"
|
|
1032
|
+
|
|
1033
|
+
init(viewController: ViewController) {
|
|
1034
|
+
self.viewController = viewController
|
|
1035
|
+
super.init()
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
1039
|
+
guard let body = message.body as? [String: Any],
|
|
1040
|
+
let action = body["action"] as? String,
|
|
1041
|
+
let callbackId = body["callbackId"] as? String else { return }
|
|
1042
|
+
|
|
1043
|
+
let args = body["args"] as? [String: Any] ?? [:]
|
|
1044
|
+
|
|
1045
|
+
switch action {
|
|
1046
|
+
case "setItem":
|
|
1047
|
+
if let key = args["key"] as? String, let value = args["value"] as? String {
|
|
1048
|
+
userDefaults.set(value, forKey: prefix + key)
|
|
1049
|
+
}
|
|
1050
|
+
sendSuccess(callbackId: callbackId, data: nil)
|
|
1051
|
+
|
|
1052
|
+
case "getItem":
|
|
1053
|
+
if let key = args["key"] as? String {
|
|
1054
|
+
let value = userDefaults.string(forKey: prefix + key)
|
|
1055
|
+
sendSuccess(callbackId: callbackId, data: value)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
case "removeItem":
|
|
1059
|
+
if let key = args["key"] as? String {
|
|
1060
|
+
userDefaults.removeObject(forKey: prefix + key)
|
|
1061
|
+
}
|
|
1062
|
+
sendSuccess(callbackId: callbackId, data: nil)
|
|
1063
|
+
|
|
1064
|
+
case "clearStorage":
|
|
1065
|
+
let keys = userDefaults.dictionaryRepresentation().keys.filter { $0.hasPrefix(prefix) }
|
|
1066
|
+
for key in keys { userDefaults.removeObject(forKey: key) }
|
|
1067
|
+
sendSuccess(callbackId: callbackId, data: nil)
|
|
1068
|
+
|
|
1069
|
+
case "getAllKeys":
|
|
1070
|
+
let keys = userDefaults.dictionaryRepresentation().keys
|
|
1071
|
+
.filter { $0.hasPrefix(prefix) }
|
|
1072
|
+
.map { String($0.dropFirst(prefix.count)) }
|
|
1073
|
+
sendSuccess(callbackId: callbackId, data: keys)
|
|
1074
|
+
|
|
1075
|
+
case "getDeviceInfo":
|
|
1076
|
+
let device = UIDevice.current
|
|
1077
|
+
let info: [String: Any] = [
|
|
1078
|
+
"platform": "ios",
|
|
1079
|
+
"model": device.model,
|
|
1080
|
+
"systemVersion": device.systemVersion,
|
|
1081
|
+
"name": device.name,
|
|
1082
|
+
"appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
|
1083
|
+
]
|
|
1084
|
+
sendSuccess(callbackId: callbackId, data: info)
|
|
1085
|
+
|
|
1086
|
+
case "getNetworkStatus":
|
|
1087
|
+
let status: [String: Any] = ["connected": true, "type": "unknown"]
|
|
1088
|
+
sendSuccess(callbackId: callbackId, data: status)
|
|
1089
|
+
|
|
1090
|
+
case "showToast":
|
|
1091
|
+
if let message = args["message"] as? String {
|
|
1092
|
+
DispatchQueue.main.async {
|
|
1093
|
+
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
|
|
1094
|
+
self.viewController?.present(alert, animated: true) {
|
|
1095
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { alert.dismiss(animated: true) }
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
sendSuccess(callbackId: callbackId, data: nil)
|
|
1100
|
+
|
|
1101
|
+
case "vibrate":
|
|
1102
|
+
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
|
1103
|
+
sendSuccess(callbackId: callbackId, data: nil)
|
|
1104
|
+
|
|
1105
|
+
case "copyToClipboard":
|
|
1106
|
+
if let text = args["text"] as? String {
|
|
1107
|
+
UIPasteboard.general.string = text
|
|
1108
|
+
}
|
|
1109
|
+
sendSuccess(callbackId: callbackId, data: nil)
|
|
1110
|
+
|
|
1111
|
+
case "getClipboardText":
|
|
1112
|
+
sendSuccess(callbackId: callbackId, data: UIPasteboard.general.string ?? "")
|
|
1113
|
+
|
|
1114
|
+
default:
|
|
1115
|
+
sendError(callbackId: callbackId, message: "Unknown action")
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private func sendSuccess(callbackId: String, data: Any?) {
|
|
1120
|
+
var response: [String: Any] = ["success": true]
|
|
1121
|
+
if let data = data { response["data"] = data }
|
|
1122
|
+
sendResponse(callbackId: callbackId, response: response)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private func sendError(callbackId: String, message: String) {
|
|
1126
|
+
sendResponse(callbackId: callbackId, response: ["success": false, "error": message])
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
private func sendResponse(callbackId: String, response: [String: Any]) {
|
|
1130
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: response),
|
|
1131
|
+
let jsonString = String(data: jsonData, encoding: .utf8) else { return }
|
|
1132
|
+
|
|
1133
|
+
DispatchQueue.main.async {
|
|
1134
|
+
self.viewController?.executeJS("window.__pulseNativeCallback('\\(callbackId)', \\(jsonString));")
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
`.trim());
|
|
1139
|
+
|
|
1140
|
+
// Info.plist
|
|
1141
|
+
writeFileSync(join(iosDir, 'PulseApp/Info.plist'), `
|
|
1142
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1143
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1144
|
+
<plist version="1.0">
|
|
1145
|
+
<dict>
|
|
1146
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
1147
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
1148
|
+
<key>CFBundleDisplayName</key>
|
|
1149
|
+
<string>${config.displayName}</string>
|
|
1150
|
+
<key>CFBundleExecutable</key>
|
|
1151
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
1152
|
+
<key>CFBundleIdentifier</key>
|
|
1153
|
+
<string>${config.packageId}</string>
|
|
1154
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
1155
|
+
<string>6.0</string>
|
|
1156
|
+
<key>CFBundleName</key>
|
|
1157
|
+
<string>$(PRODUCT_NAME)</string>
|
|
1158
|
+
<key>CFBundlePackageType</key>
|
|
1159
|
+
<string>APPL</string>
|
|
1160
|
+
<key>CFBundleShortVersionString</key>
|
|
1161
|
+
<string>${config.version}</string>
|
|
1162
|
+
<key>CFBundleVersion</key>
|
|
1163
|
+
<string>1</string>
|
|
1164
|
+
<key>LSRequiresIPhoneOS</key>
|
|
1165
|
+
<true/>
|
|
1166
|
+
<key>UIApplicationSceneManifest</key>
|
|
1167
|
+
<dict>
|
|
1168
|
+
<key>UIApplicationSupportsMultipleScenes</key>
|
|
1169
|
+
<false/>
|
|
1170
|
+
<key>UISceneConfigurations</key>
|
|
1171
|
+
<dict>
|
|
1172
|
+
<key>UIWindowSceneSessionRoleApplication</key>
|
|
1173
|
+
<array>
|
|
1174
|
+
<dict>
|
|
1175
|
+
<key>UISceneConfigurationName</key>
|
|
1176
|
+
<string>Default Configuration</string>
|
|
1177
|
+
<key>UISceneDelegateClassName</key>
|
|
1178
|
+
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
|
1179
|
+
</dict>
|
|
1180
|
+
</array>
|
|
1181
|
+
</dict>
|
|
1182
|
+
</dict>
|
|
1183
|
+
<key>UILaunchStoryboardName</key>
|
|
1184
|
+
<string>LaunchScreen</string>
|
|
1185
|
+
<key>UISupportedInterfaceOrientations</key>
|
|
1186
|
+
<array>
|
|
1187
|
+
<string>UIInterfaceOrientationPortrait</string>
|
|
1188
|
+
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
1189
|
+
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
1190
|
+
</array>
|
|
1191
|
+
</dict>
|
|
1192
|
+
</plist>
|
|
1193
|
+
`.trim());
|
|
1194
|
+
|
|
1195
|
+
// Minimal Xcode project file (project.pbxproj)
|
|
1196
|
+
writeFileSync(join(iosDir, 'PulseApp.xcodeproj/project.pbxproj'), generateXcodeProject(config));
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Generate minimal Xcode project file
|
|
1201
|
+
*/
|
|
1202
|
+
function generateXcodeProject(config) {
|
|
1203
|
+
return `// !$*UTF8*$!
|
|
1204
|
+
{
|
|
1205
|
+
archiveVersion = 1;
|
|
1206
|
+
classes = {
|
|
1207
|
+
};
|
|
1208
|
+
objectVersion = 56;
|
|
1209
|
+
objects = {
|
|
1210
|
+
|
|
1211
|
+
/* Begin PBXBuildFile section */
|
|
1212
|
+
1A0000000000000000000001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000011; };
|
|
1213
|
+
1A0000000000000000000002 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000012; };
|
|
1214
|
+
1A0000000000000000000003 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000013; };
|
|
1215
|
+
1A0000000000000000000004 /* PulseBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000014; };
|
|
1216
|
+
1A0000000000000000000005 /* www in Resources */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000015; };
|
|
1217
|
+
/* End PBXBuildFile section */
|
|
1218
|
+
|
|
1219
|
+
/* Begin PBXFileReference section */
|
|
1220
|
+
1A0000000000000000000010 /* PulseApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PulseApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
1221
|
+
1A0000000000000000000011 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
|
1222
|
+
1A0000000000000000000012 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
|
1223
|
+
1A0000000000000000000013 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
|
1224
|
+
1A0000000000000000000014 /* PulseBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulseBridge.swift; sourceTree = "<group>"; };
|
|
1225
|
+
1A0000000000000000000015 /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; path = www; sourceTree = "<group>"; };
|
|
1226
|
+
1A0000000000000000000016 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
1227
|
+
/* End PBXFileReference section */
|
|
1228
|
+
|
|
1229
|
+
/* Begin PBXGroup section */
|
|
1230
|
+
1A0000000000000000000020 = {
|
|
1231
|
+
isa = PBXGroup;
|
|
1232
|
+
children = (
|
|
1233
|
+
1A0000000000000000000021 /* PulseApp */,
|
|
1234
|
+
1A0000000000000000000022 /* Products */,
|
|
1235
|
+
);
|
|
1236
|
+
sourceTree = "<group>";
|
|
1237
|
+
};
|
|
1238
|
+
1A0000000000000000000021 /* PulseApp */ = {
|
|
1239
|
+
isa = PBXGroup;
|
|
1240
|
+
children = (
|
|
1241
|
+
1A0000000000000000000011,
|
|
1242
|
+
1A0000000000000000000012,
|
|
1243
|
+
1A0000000000000000000013,
|
|
1244
|
+
1A0000000000000000000014,
|
|
1245
|
+
1A0000000000000000000015,
|
|
1246
|
+
1A0000000000000000000016,
|
|
1247
|
+
);
|
|
1248
|
+
path = PulseApp;
|
|
1249
|
+
sourceTree = "<group>";
|
|
1250
|
+
};
|
|
1251
|
+
1A0000000000000000000022 /* Products */ = {
|
|
1252
|
+
isa = PBXGroup;
|
|
1253
|
+
children = (
|
|
1254
|
+
1A0000000000000000000010,
|
|
1255
|
+
);
|
|
1256
|
+
name = Products;
|
|
1257
|
+
sourceTree = "<group>";
|
|
1258
|
+
};
|
|
1259
|
+
/* End PBXGroup section */
|
|
1260
|
+
|
|
1261
|
+
/* Begin PBXNativeTarget section */
|
|
1262
|
+
1A0000000000000000000030 /* PulseApp */ = {
|
|
1263
|
+
isa = PBXNativeTarget;
|
|
1264
|
+
buildConfigurationList = 1A0000000000000000000050;
|
|
1265
|
+
buildPhases = (
|
|
1266
|
+
1A0000000000000000000031,
|
|
1267
|
+
1A0000000000000000000032,
|
|
1268
|
+
);
|
|
1269
|
+
buildRules = (
|
|
1270
|
+
);
|
|
1271
|
+
dependencies = (
|
|
1272
|
+
);
|
|
1273
|
+
name = PulseApp;
|
|
1274
|
+
productName = PulseApp;
|
|
1275
|
+
productReference = 1A0000000000000000000010;
|
|
1276
|
+
productType = "com.apple.product-type.application";
|
|
1277
|
+
};
|
|
1278
|
+
/* End PBXNativeTarget section */
|
|
1279
|
+
|
|
1280
|
+
/* Begin PBXProject section */
|
|
1281
|
+
1A0000000000000000000040 /* Project object */ = {
|
|
1282
|
+
isa = PBXProject;
|
|
1283
|
+
attributes = {
|
|
1284
|
+
BuildIndependentTargetsInParallel = 1;
|
|
1285
|
+
LastSwiftUpdateCheck = 1500;
|
|
1286
|
+
LastUpgradeCheck = 1500;
|
|
1287
|
+
TargetAttributes = {
|
|
1288
|
+
1A0000000000000000000030 = {
|
|
1289
|
+
CreatedOnToolsVersion = 15.0;
|
|
1290
|
+
};
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
buildConfigurationList = 1A0000000000000000000041;
|
|
1294
|
+
compatibilityVersion = "Xcode 14.0";
|
|
1295
|
+
developmentRegion = en;
|
|
1296
|
+
hasScannedForEncodings = 0;
|
|
1297
|
+
knownRegions = (
|
|
1298
|
+
en,
|
|
1299
|
+
Base,
|
|
1300
|
+
);
|
|
1301
|
+
mainGroup = 1A0000000000000000000020;
|
|
1302
|
+
productRefGroup = 1A0000000000000000000022;
|
|
1303
|
+
projectDirPath = "";
|
|
1304
|
+
projectRoot = "";
|
|
1305
|
+
targets = (
|
|
1306
|
+
1A0000000000000000000030,
|
|
1307
|
+
);
|
|
1308
|
+
};
|
|
1309
|
+
/* End PBXProject section */
|
|
1310
|
+
|
|
1311
|
+
/* Begin PBXResourcesBuildPhase section */
|
|
1312
|
+
1A0000000000000000000032 /* Resources */ = {
|
|
1313
|
+
isa = PBXResourcesBuildPhase;
|
|
1314
|
+
buildActionMask = 2147483647;
|
|
1315
|
+
files = (
|
|
1316
|
+
1A0000000000000000000005,
|
|
1317
|
+
);
|
|
1318
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
1319
|
+
};
|
|
1320
|
+
/* End PBXResourcesBuildPhase section */
|
|
1321
|
+
|
|
1322
|
+
/* Begin PBXSourcesBuildPhase section */
|
|
1323
|
+
1A0000000000000000000031 /* Sources */ = {
|
|
1324
|
+
isa = PBXSourcesBuildPhase;
|
|
1325
|
+
buildActionMask = 2147483647;
|
|
1326
|
+
files = (
|
|
1327
|
+
1A0000000000000000000001,
|
|
1328
|
+
1A0000000000000000000002,
|
|
1329
|
+
1A0000000000000000000003,
|
|
1330
|
+
1A0000000000000000000004,
|
|
1331
|
+
);
|
|
1332
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
1333
|
+
};
|
|
1334
|
+
/* End PBXSourcesBuildPhase section */
|
|
1335
|
+
|
|
1336
|
+
/* Begin XCBuildConfiguration section */
|
|
1337
|
+
1A0000000000000000000051 /* Debug */ = {
|
|
1338
|
+
isa = XCBuildConfiguration;
|
|
1339
|
+
buildSettings = {
|
|
1340
|
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
1341
|
+
CODE_SIGN_STYLE = Automatic;
|
|
1342
|
+
CURRENT_PROJECT_VERSION = 1;
|
|
1343
|
+
GENERATE_INFOPLIST_FILE = NO;
|
|
1344
|
+
INFOPLIST_FILE = PulseApp/Info.plist;
|
|
1345
|
+
IPHONEOS_DEPLOYMENT_TARGET = ${config.ios?.deploymentTarget || '13.0'};
|
|
1346
|
+
MARKETING_VERSION = ${config.version};
|
|
1347
|
+
PRODUCT_BUNDLE_IDENTIFIER = ${config.packageId};
|
|
1348
|
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
1349
|
+
SWIFT_VERSION = 5.0;
|
|
1350
|
+
TARGETED_DEVICE_FAMILY = "1,2";
|
|
1351
|
+
};
|
|
1352
|
+
name = Debug;
|
|
1353
|
+
};
|
|
1354
|
+
1A0000000000000000000052 /* Release */ = {
|
|
1355
|
+
isa = XCBuildConfiguration;
|
|
1356
|
+
buildSettings = {
|
|
1357
|
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
1358
|
+
CODE_SIGN_STYLE = Automatic;
|
|
1359
|
+
CURRENT_PROJECT_VERSION = 1;
|
|
1360
|
+
GENERATE_INFOPLIST_FILE = NO;
|
|
1361
|
+
INFOPLIST_FILE = PulseApp/Info.plist;
|
|
1362
|
+
IPHONEOS_DEPLOYMENT_TARGET = ${config.ios?.deploymentTarget || '13.0'};
|
|
1363
|
+
MARKETING_VERSION = ${config.version};
|
|
1364
|
+
PRODUCT_BUNDLE_IDENTIFIER = ${config.packageId};
|
|
1365
|
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
1366
|
+
SWIFT_VERSION = 5.0;
|
|
1367
|
+
TARGETED_DEVICE_FAMILY = "1,2";
|
|
1368
|
+
};
|
|
1369
|
+
name = Release;
|
|
1370
|
+
};
|
|
1371
|
+
1A0000000000000000000061 /* Debug */ = {
|
|
1372
|
+
isa = XCBuildConfiguration;
|
|
1373
|
+
buildSettings = {
|
|
1374
|
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
1375
|
+
CLANG_ENABLE_MODULES = YES;
|
|
1376
|
+
CLANG_ENABLE_OBJC_ARC = YES;
|
|
1377
|
+
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
1378
|
+
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
1379
|
+
GCC_OPTIMIZATION_LEVEL = 0;
|
|
1380
|
+
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
1381
|
+
ONLY_ACTIVE_ARCH = YES;
|
|
1382
|
+
SDKROOT = iphoneos;
|
|
1383
|
+
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
|
1384
|
+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
1385
|
+
};
|
|
1386
|
+
name = Debug;
|
|
1387
|
+
};
|
|
1388
|
+
1A0000000000000000000062 /* Release */ = {
|
|
1389
|
+
isa = XCBuildConfiguration;
|
|
1390
|
+
buildSettings = {
|
|
1391
|
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
1392
|
+
CLANG_ENABLE_MODULES = YES;
|
|
1393
|
+
CLANG_ENABLE_OBJC_ARC = YES;
|
|
1394
|
+
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
1395
|
+
ENABLE_NS_ASSERTIONS = NO;
|
|
1396
|
+
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
1397
|
+
MTL_ENABLE_DEBUG_INFO = NO;
|
|
1398
|
+
SDKROOT = iphoneos;
|
|
1399
|
+
SWIFT_COMPILATION_MODE = wholemodule;
|
|
1400
|
+
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
|
1401
|
+
VALIDATE_PRODUCT = YES;
|
|
1402
|
+
};
|
|
1403
|
+
name = Release;
|
|
1404
|
+
};
|
|
1405
|
+
/* End XCBuildConfiguration section */
|
|
1406
|
+
|
|
1407
|
+
/* Begin XCConfigurationList section */
|
|
1408
|
+
1A0000000000000000000041 /* Build configuration list for PBXProject "PulseApp" */ = {
|
|
1409
|
+
isa = XCConfigurationList;
|
|
1410
|
+
buildConfigurations = (
|
|
1411
|
+
1A0000000000000000000061,
|
|
1412
|
+
1A0000000000000000000062,
|
|
1413
|
+
);
|
|
1414
|
+
defaultConfigurationIsVisible = 0;
|
|
1415
|
+
defaultConfigurationName = Release;
|
|
1416
|
+
};
|
|
1417
|
+
1A0000000000000000000050 /* Build configuration list for PBXNativeTarget "PulseApp" */ = {
|
|
1418
|
+
isa = XCConfigurationList;
|
|
1419
|
+
buildConfigurations = (
|
|
1420
|
+
1A0000000000000000000051,
|
|
1421
|
+
1A0000000000000000000052,
|
|
1422
|
+
);
|
|
1423
|
+
defaultConfigurationIsVisible = 0;
|
|
1424
|
+
defaultConfigurationName = Release;
|
|
1425
|
+
};
|
|
1426
|
+
/* End XCConfigurationList section */
|
|
1427
|
+
};
|
|
1428
|
+
rootObject = 1A0000000000000000000040 /* Project object */;
|
|
1429
|
+
}
|
|
1430
|
+
`;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Convert string to PascalCase
|
|
1435
|
+
*/
|
|
1436
|
+
function toPascalCase(str) {
|
|
1437
|
+
return str
|
|
1438
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
1439
|
+
.replace(/^(.)/, (_, char) => char.toUpperCase());
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Show mobile help
|
|
1444
|
+
*/
|
|
1445
|
+
function showMobileHelp() {
|
|
1446
|
+
console.log(`
|
|
1447
|
+
Pulse Mobile - Zero-Dependency Mobile Platform
|
|
1448
|
+
|
|
1449
|
+
Usage: pulse mobile <command> [options]
|
|
1450
|
+
|
|
1451
|
+
Commands:
|
|
1452
|
+
init Initialize mobile platforms (Android & iOS)
|
|
1453
|
+
build <platform> Build for android or ios
|
|
1454
|
+
run <platform> Build and run on device/emulator
|
|
1455
|
+
sync [platform] Sync web assets to native projects
|
|
1456
|
+
|
|
1457
|
+
Examples:
|
|
1458
|
+
pulse mobile init
|
|
1459
|
+
pulse mobile build android
|
|
1460
|
+
pulse mobile build ios
|
|
1461
|
+
pulse mobile run android
|
|
1462
|
+
pulse mobile run ios
|
|
1463
|
+
|
|
1464
|
+
Configuration:
|
|
1465
|
+
Edit pulse.mobile.json to customize your mobile app settings.
|
|
1466
|
+
|
|
1467
|
+
Requirements:
|
|
1468
|
+
Android: Android SDK with build-tools
|
|
1469
|
+
iOS: macOS with Xcode (builds only work on Mac)
|
|
1470
|
+
`);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
export default { handleMobileCommand };
|