offline try
This commit is contained in:
1 parent
8b13b1aacd
commit
d6bb8aec8c
7 files changed
+420
-350
No files matched your search
@@ -24,9 +24,7 @@
|
||||
"predictiveBackGestureEnabled": false,
|
||||
"package": "com.nessar.jeu21"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-asset"
|
||||
],
|
||||
"plugins": [],
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "039ce5f3-59ab-4635-8433-92189ff0b1b1"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"expo-status-bar": "~55.0.5",
|
||||
"react": "19.2.0",
|
||||
"react-native": "0.83.4",
|
||||
"react-native-gesture-handler": "~2.30.0",
|
||||
"react-native-gesture-handler": "~2.30.1",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "~5.6.2",
|
||||
"react-native-screens": "~4.23.0",
|
||||
@@ -30,13 +30,14 @@
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/react": "~19.2.14",
|
||||
"bun-types": "^1.3.11",
|
||||
"eslint": "^9.0.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-expo": "^55.0.0",
|
||||
"jest": "~29.7.0",
|
||||
"jest-expo": "^55.0.13",
|
||||
"prettier": "^3.8.1",
|
||||
"react-test-renderer": "^19.2.4",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript": "~5.9.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -243,6 +244,58 @@
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
@@ -803,6 +856,8 @@
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
+1
-7
@@ -1,9 +1,3 @@
|
||||
const { getDefaultConfig } = require('expo/metro-config');
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
if (!config.resolver.assetExts.includes('html')) {
|
||||
config.resolver.assetExts.push('html');
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
module.exports = getDefaultConfig(__dirname);
|
||||
+52
-49
@@ -1,50 +1,53 @@
|
||||
{
|
||||
"name": "21",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"lint": "bun x expo lint",
|
||||
"typecheck": "bun x tsc --noEmit",
|
||||
"test": "bun test",
|
||||
"coverage:check": "bun run scripts/check-coverage.ts",
|
||||
"test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && bun run coverage:check",
|
||||
"ci:quality": "bun run lint && bun run typecheck && bun run test:coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@3d-dice/dice-box": "^1.1.4",
|
||||
"@react-navigation/native": "^7.2.2",
|
||||
"@react-navigation/native-stack": "^7.14.10",
|
||||
"expo": "~55.0.11",
|
||||
"expo-asset": "~55.0.12",
|
||||
"expo-dev-client": "~55.0.22",
|
||||
"expo-file-system": "~55.0.14",
|
||||
"expo-sensors": "~55.0.11",
|
||||
"expo-status-bar": "~55.0.5",
|
||||
"react": "19.2.0",
|
||||
"react-native": "0.83.4",
|
||||
"react-native-gesture-handler": "~2.30.1",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "~5.6.2",
|
||||
"react-native-screens": "~4.23.0",
|
||||
"react-native-webview": "13.16.0",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/react": "~19.2.14",
|
||||
"bun-types": "^1.3.11",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-expo": "^55.0.0",
|
||||
"jest": "~29.7.0",
|
||||
"jest-expo": "^55.0.13",
|
||||
"prettier": "^3.8.1",
|
||||
"react-test-renderer": "^19.2.4",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
"name": "21",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"lint": "bun x expo lint",
|
||||
"typecheck": "bun x tsc --noEmit",
|
||||
"test": "bun test",
|
||||
"coverage:check": "bun run scripts/check-coverage.ts",
|
||||
"test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && bun run coverage:check",
|
||||
"ci:quality": "bun run lint && bun run typecheck && bun run test:coverage",
|
||||
"sync:dice": "node scripts/sync-dice-box-assets.mjs",
|
||||
"postinstall": "bun run sync:dice"
|
||||
},
|
||||
"dependencies": {
|
||||
"@3d-dice/dice-box": "^1.1.4",
|
||||
"@react-navigation/native": "^7.2.2",
|
||||
"@react-navigation/native-stack": "^7.14.10",
|
||||
"expo": "~55.0.11",
|
||||
"expo-asset": "~55.0.12",
|
||||
"expo-dev-client": "~55.0.22",
|
||||
"expo-file-system": "~55.0.14",
|
||||
"expo-sensors": "~55.0.11",
|
||||
"expo-status-bar": "~55.0.5",
|
||||
"react": "19.2.0",
|
||||
"react-native": "0.83.4",
|
||||
"react-native-gesture-handler": "~2.30.1",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "~5.6.2",
|
||||
"react-native-screens": "~4.23.0",
|
||||
"react-native-webview": "13.16.0",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/react": "~19.2.14",
|
||||
"bun-types": "^1.3.11",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-expo": "^55.0.0",
|
||||
"jest": "~29.7.0",
|
||||
"jest-expo": "^55.0.13",
|
||||
"prettier": "^3.8.1",
|
||||
"react-test-renderer": "^19.2.4",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Bundles @3d-dice/dice-box into a single IIFE file and copies
|
||||
* all required assets into android/app/src/main/assets/ so the
|
||||
* WebView can load everything offline from file:///android_asset/.
|
||||
*
|
||||
* Usage: node scripts/sync-dice-box-assets.mjs
|
||||
* Called automatically via the "postinstall" npm script.
|
||||
*/
|
||||
|
||||
import { build } from 'esbuild';
|
||||
import { cpSync, mkdirSync, copyFileSync, rmSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const ANDROID_ASSETS = resolve(ROOT, 'android/app/src/main/assets');
|
||||
const DICE_BOX_DIST = resolve(ROOT, 'node_modules/@3d-dice/dice-box/dist');
|
||||
|
||||
// 0. Clean previous output
|
||||
rmSync(resolve(ANDROID_ASSETS, 'dice-box'), { recursive: true, force: true });
|
||||
|
||||
// 1. Bundle dice-box ES module → IIFE (DiceBoxModule global)
|
||||
await build({
|
||||
entryPoints: [resolve(DICE_BOX_DIST, 'dice-box.es.js')],
|
||||
bundle: true,
|
||||
format: 'iife',
|
||||
globalName: 'DiceBoxModule',
|
||||
outfile: resolve(ANDROID_ASSETS, 'dice-box/dice-box-bundle.js'),
|
||||
minify: true,
|
||||
platform: 'browser',
|
||||
target: ['es2015'],
|
||||
logLevel: 'info',
|
||||
});
|
||||
|
||||
// 2. Copy static assets (ammo WASM, themes) preserving directory structure
|
||||
mkdirSync(resolve(ANDROID_ASSETS, 'dice-box/assets'), { recursive: true });
|
||||
|
||||
cpSync(
|
||||
resolve(DICE_BOX_DIST, 'assets'),
|
||||
resolve(ANDROID_ASSETS, 'dice-box/assets'),
|
||||
{ recursive: true },
|
||||
);
|
||||
|
||||
// 3. Copy the offline HTML template into android assets
|
||||
copyFileSync(
|
||||
resolve(ROOT, 'src/html/dice3d.html'),
|
||||
resolve(ANDROID_ASSETS, 'dice3d.html'),
|
||||
);
|
||||
|
||||
console.log('✓ dice-box assets synced to android/app/src/main/assets/');
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Asset } from 'expo-asset';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ActivityIndicator, LayoutChangeEvent, StyleSheet, View } from 'react-native';
|
||||
import { LayoutChangeEvent, StyleSheet, View } from 'react-native';
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||
import type { QualitySetting } from '../constants/quality';
|
||||
import { parseDicePayload } from '../services/dicePayloadService';
|
||||
import { ui } from '../styles/ui';
|
||||
|
||||
const DICE_HTML_URI = 'file:///android_asset/dice3d.html';
|
||||
|
||||
type Dice3DViewerProps = {
|
||||
rollSignal: number;
|
||||
onRollResult: (dice: [number, number]) => void;
|
||||
@@ -27,20 +28,12 @@ export function Dice3DViewer({
|
||||
const autoResetAttemptsRef = useRef(0);
|
||||
const onRollResultRef = useRef(onRollResult);
|
||||
const lastLayoutRef = useRef({ width: 0, height: 0 });
|
||||
const [htmlUri, setHtmlUri] = useState<string | null>(null);
|
||||
const [webReady, setWebReady] = useState(false);
|
||||
const [webViewKey, setWebViewKey] = useState(0);
|
||||
|
||||
const postBridgeMessage = useCallback(
|
||||
(payload: Record<string, unknown>) => {
|
||||
if (!htmlUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
webViewRef.current?.postMessage(JSON.stringify(payload));
|
||||
},
|
||||
[htmlUri],
|
||||
);
|
||||
const postBridgeMessage = useCallback((payload: Record<string, unknown>) => {
|
||||
webViewRef.current?.postMessage(JSON.stringify(payload));
|
||||
}, []);
|
||||
|
||||
const resetWebView = useCallback(() => {
|
||||
setWebReady(false);
|
||||
@@ -64,30 +57,6 @@ export function Dice3DViewer({
|
||||
onRollResultRef.current = onRollResult;
|
||||
}, [onRollResult]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const loadHtml = async () => {
|
||||
const asset = Asset.fromModule(require('../html/dice3d.html'));
|
||||
await asset.downloadAsync();
|
||||
const uri = asset.localUri ?? asset.uri;
|
||||
|
||||
if (mounted) {
|
||||
setHtmlUri(uri);
|
||||
}
|
||||
};
|
||||
|
||||
loadHtml().catch(() => {
|
||||
if (mounted) {
|
||||
setHtmlUri(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onReadyChange?.(webReady);
|
||||
}, [onReadyChange, webReady]);
|
||||
@@ -97,12 +66,12 @@ export function Dice3DViewer({
|
||||
}, [postResize, webViewKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webReady || !htmlUri) {
|
||||
if (!webReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
postQualitySetting();
|
||||
}, [htmlUri, postQualitySetting, webReady]);
|
||||
}, [postQualitySetting, webReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetSignal === undefined) {
|
||||
@@ -123,7 +92,7 @@ export function Dice3DViewer({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!htmlUri || autoResetAttemptsRef.current >= 2) {
|
||||
if (autoResetAttemptsRef.current >= 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,10 +104,10 @@ export function Dice3DViewer({
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [htmlUri, resetWebView, webReady, webViewKey]);
|
||||
}, [resetWebView, webReady, webViewKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webReady || !htmlUri) {
|
||||
if (!webReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,7 +117,7 @@ export function Dice3DViewer({
|
||||
|
||||
lastSignalRef.current = rollSignal;
|
||||
webViewRef.current?.postMessage(JSON.stringify({ type: 'ROLL' }));
|
||||
}, [htmlUri, rollSignal, webReady]);
|
||||
}, [rollSignal, webReady]);
|
||||
|
||||
const onWrapperLayout = useCallback(
|
||||
(event: LayoutChangeEvent) => {
|
||||
@@ -210,20 +179,12 @@ export function Dice3DViewer({
|
||||
resetWebView();
|
||||
}, [resetWebView]);
|
||||
|
||||
if (!htmlUri) {
|
||||
return (
|
||||
<View style={styles.loaderContainer}>
|
||||
<ActivityIndicator size="large" color={ui.color.highlight} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper} onLayout={onWrapperLayout}>
|
||||
<WebView
|
||||
key={`dice-webview-${webViewKey}`}
|
||||
ref={webViewRef}
|
||||
source={{ uri: htmlUri }}
|
||||
source={{ uri: DICE_HTML_URI }}
|
||||
style={styles.webView}
|
||||
onMessage={onWebMessage}
|
||||
onLoadStart={() => setWebReady(false)}
|
||||
@@ -234,7 +195,6 @@ export function Dice3DViewer({
|
||||
domStorageEnabled
|
||||
allowFileAccess
|
||||
allowUniversalAccessFromFileURLs
|
||||
mixedContentMode="always"
|
||||
setSupportMultipleWindows={false}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
@@ -253,11 +213,4 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
loaderContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: ui.color.surfaceDeep,
|
||||
borderRadius: ui.radius.xxl,
|
||||
},
|
||||
});
|
||||
+244
-228
@@ -48,269 +48,285 @@
|
||||
<body>
|
||||
<div id="dice-box"></div>
|
||||
|
||||
<script type="module">
|
||||
const emit = (payload) => {
|
||||
window.ReactNativeWebView?.postMessage(JSON.stringify(payload));
|
||||
};
|
||||
<!-- IIFE bundle: no ES modules, no dynamic import() -->
|
||||
<script src="./dice-box/dice-box-bundle.js"></script>
|
||||
|
||||
const randomDie = () => Math.floor(Math.random() * 6) + 1;
|
||||
const fallbackRoll = () => [randomDie(), randomDie()];
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
let diceBox = null;
|
||||
let isRolling = false;
|
||||
let resizeQueued = false;
|
||||
let healthCheckFailures = 0;
|
||||
let qualityLevel = 'high';
|
||||
let qualityResetQueued = false;
|
||||
var DiceBox =
|
||||
(typeof DiceBoxModule !== 'undefined' && DiceBoxModule.default) ||
|
||||
null;
|
||||
|
||||
const isQualityLevel = (value) => {
|
||||
return value === 'low' || value === 'medium' || value === 'high';
|
||||
};
|
||||
var emit = function (payload) {
|
||||
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify(payload));
|
||||
}
|
||||
};
|
||||
|
||||
const getQualityConfig = (level) => {
|
||||
switch (level) {
|
||||
case 'low':
|
||||
return {
|
||||
scale: 7.5,
|
||||
gravity: 0.9,
|
||||
throwForce: 2.8,
|
||||
spinForce: 3.1,
|
||||
};
|
||||
case 'medium':
|
||||
return {
|
||||
scale: 8.8,
|
||||
gravity: 0.96,
|
||||
throwForce: 3.4,
|
||||
spinForce: 4.0,
|
||||
};
|
||||
case 'high':
|
||||
default:
|
||||
return {
|
||||
scale: 10,
|
||||
gravity: 1,
|
||||
throwForce: 4,
|
||||
spinForce: 5,
|
||||
};
|
||||
}
|
||||
};
|
||||
var randomDie = function () {
|
||||
return Math.floor(Math.random() * 6) + 1;
|
||||
};
|
||||
|
||||
const extractValues = (result) => {
|
||||
if (!Array.isArray(result)) {
|
||||
return null;
|
||||
}
|
||||
var fallbackRoll = function () {
|
||||
return [randomDie(), randomDie()];
|
||||
};
|
||||
|
||||
const values = result
|
||||
.map((die) => Number(die?.value))
|
||||
.filter((value) => Number.isInteger(value) && value >= 1 && value <= 6);
|
||||
var diceBox = null;
|
||||
var isRolling = false;
|
||||
var resizeQueued = false;
|
||||
var healthCheckFailures = 0;
|
||||
var qualityLevel = 'high';
|
||||
var qualityResetQueued = false;
|
||||
|
||||
return values.length >= 2 ? values.slice(0, 2) : null;
|
||||
};
|
||||
var isQualityLevel = function (value) {
|
||||
return value === 'low' || value === 'medium' || value === 'high';
|
||||
};
|
||||
|
||||
const ensureDiceBox = async () => {
|
||||
if (diceBox) {
|
||||
return diceBox;
|
||||
}
|
||||
var getQualityConfig = function (level) {
|
||||
switch (level) {
|
||||
case 'low':
|
||||
return { scale: 7.5, gravity: 0.9, throwForce: 2.8, spinForce: 3.1 };
|
||||
case 'medium':
|
||||
return { scale: 8.8, gravity: 0.96, throwForce: 3.4, spinForce: 4.0 };
|
||||
case 'high':
|
||||
default:
|
||||
return { scale: 10, gravity: 1, throwForce: 4, spinForce: 5 };
|
||||
}
|
||||
};
|
||||
|
||||
const { default: DiceBox } = await import(
|
||||
'https://unpkg.com/@3d-dice/dice-box@1.1.4/dist/dice-box.es.js'
|
||||
);
|
||||
var extractValues = function (result) {
|
||||
if (!Array.isArray(result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const qualityConfig = getQualityConfig(qualityLevel);
|
||||
var values = [];
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var v = Number(result[i] && result[i].value);
|
||||
if (Number.isInteger(v) && v >= 1 && v <= 6) {
|
||||
values.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
diceBox = new DiceBox({
|
||||
container: '#dice-box',
|
||||
id: 'dice-box-canvas',
|
||||
origin: '',
|
||||
assetPath: 'https://unpkg.com/@3d-dice/dice-box@1.1.4/dist/assets/',
|
||||
theme: 'default',
|
||||
offscreen: false,
|
||||
scale: qualityConfig.scale,
|
||||
gravity: qualityConfig.gravity,
|
||||
throwForce: qualityConfig.throwForce,
|
||||
spinForce: qualityConfig.spinForce,
|
||||
});
|
||||
return values.length >= 2 ? values.slice(0, 2) : null;
|
||||
};
|
||||
|
||||
await diceBox.init();
|
||||
healthCheckFailures = 0;
|
||||
return diceBox;
|
||||
};
|
||||
var ensureDiceBox = function () {
|
||||
if (diceBox) {
|
||||
return Promise.resolve(diceBox);
|
||||
}
|
||||
|
||||
const resetDiceBox = async () => {
|
||||
try {
|
||||
const container = document.getElementById('dice-box');
|
||||
if (!DiceBox) {
|
||||
return Promise.reject(new Error('DiceBox module not loaded'));
|
||||
}
|
||||
|
||||
var qualityConfig = getQualityConfig(qualityLevel);
|
||||
|
||||
diceBox = new DiceBox({
|
||||
container: '#dice-box',
|
||||
id: 'dice-box-canvas',
|
||||
origin: '',
|
||||
assetPath: './dice-box/assets/',
|
||||
theme: 'default',
|
||||
offscreen: false,
|
||||
scale: qualityConfig.scale,
|
||||
gravity: qualityConfig.gravity,
|
||||
throwForce: qualityConfig.throwForce,
|
||||
spinForce: qualityConfig.spinForce,
|
||||
});
|
||||
|
||||
return diceBox.init().then(function () {
|
||||
healthCheckFailures = 0;
|
||||
return diceBox;
|
||||
});
|
||||
};
|
||||
|
||||
var resetDiceBox = function () {
|
||||
var container = document.getElementById('dice-box');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
|
||||
diceBox = null;
|
||||
await ensureDiceBox();
|
||||
queueResize();
|
||||
} catch (error) {
|
||||
console.warn('DiceBox reset fallback', error);
|
||||
}
|
||||
};
|
||||
|
||||
const applyQualityLevel = async (level) => {
|
||||
if (!isQualityLevel(level) || level === qualityLevel) {
|
||||
return;
|
||||
}
|
||||
return ensureDiceBox()
|
||||
.then(function () {
|
||||
queueResize();
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.warn('DiceBox reset fallback', error);
|
||||
});
|
||||
};
|
||||
|
||||
qualityLevel = level;
|
||||
|
||||
if (isRolling) {
|
||||
qualityResetQueued = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await resetDiceBox();
|
||||
};
|
||||
|
||||
const resizeDiceBox = async () => {
|
||||
try {
|
||||
const box = await ensureDiceBox();
|
||||
if (typeof box?.resizeWorld === 'function') {
|
||||
box.resizeWorld();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('DiceBox resize fallback', error);
|
||||
}
|
||||
};
|
||||
|
||||
const queueResize = () => {
|
||||
if (resizeQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
resizeQueued = true;
|
||||
requestAnimationFrame(() => {
|
||||
resizeQueued = false;
|
||||
resizeDiceBox();
|
||||
});
|
||||
};
|
||||
|
||||
const hasVisibleCanvas = () => {
|
||||
const container = document.getElementById('dice-box');
|
||||
const canvas = container?.querySelector('canvas');
|
||||
|
||||
if (!container || !canvas) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (container.clientWidth < 2 || container.clientHeight < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
return canvasRect.width >= 2 && canvasRect.height >= 2;
|
||||
};
|
||||
|
||||
const runHealthCheck = async () => {
|
||||
if (isRolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasVisibleCanvas()) {
|
||||
healthCheckFailures = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
healthCheckFailures += 1;
|
||||
|
||||
if (healthCheckFailures >= 3) {
|
||||
healthCheckFailures = 0;
|
||||
await resetDiceBox();
|
||||
return;
|
||||
}
|
||||
|
||||
queueResize();
|
||||
};
|
||||
|
||||
const rollDice = async () => {
|
||||
if (isRolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRolling = true;
|
||||
|
||||
try {
|
||||
let diceValues = null;
|
||||
|
||||
try {
|
||||
const box = await ensureDiceBox();
|
||||
const rollResult = await box.roll('2d6');
|
||||
diceValues = extractValues(rollResult);
|
||||
} catch (error) {
|
||||
console.warn('DiceBox roll fallback', error);
|
||||
var applyQualityLevel = function (level) {
|
||||
if (!isQualityLevel(level) || level === qualityLevel) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!diceValues) {
|
||||
diceValues = fallbackRoll();
|
||||
qualityLevel = level;
|
||||
|
||||
if (isRolling) {
|
||||
qualityResetQueued = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
emit({ type: 'ROLL_RESULT', payload: { dice: diceValues } });
|
||||
queueResize();
|
||||
} finally {
|
||||
isRolling = false;
|
||||
return resetDiceBox();
|
||||
};
|
||||
|
||||
if (qualityResetQueued) {
|
||||
qualityResetQueued = false;
|
||||
await resetDiceBox();
|
||||
}
|
||||
}
|
||||
};
|
||||
var resizeDiceBox = function () {
|
||||
ensureDiceBox()
|
||||
.then(function (box) {
|
||||
if (box && typeof box.resizeWorld === 'function') {
|
||||
box.resizeWorld();
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.warn('DiceBox resize fallback', error);
|
||||
});
|
||||
};
|
||||
|
||||
const onBridgeMessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data || '{}');
|
||||
if (message.type === 'ROLL') {
|
||||
rollDice();
|
||||
var queueResize = function () {
|
||||
if (resizeQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'RESIZE') {
|
||||
queueResize();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'RESET') {
|
||||
resetDiceBox();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'QUALITY') {
|
||||
applyQualityLevel(message?.payload?.level);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Bridge parse error', error);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', onBridgeMessage);
|
||||
document.addEventListener('message', onBridgeMessage);
|
||||
window.addEventListener('resize', queueResize);
|
||||
window.addEventListener('orientationchange', queueResize);
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
queueResize();
|
||||
runHealthCheck().catch((error) => {
|
||||
console.warn('DiceBox health fallback', error);
|
||||
resizeQueued = true;
|
||||
requestAnimationFrame(function () {
|
||||
resizeQueued = false;
|
||||
resizeDiceBox();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var hasVisibleCanvas = function () {
|
||||
var container = document.getElementById('dice-box');
|
||||
var canvas = container ? container.querySelector('canvas') : null;
|
||||
|
||||
if (!container || !canvas) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (container.clientWidth < 2 || container.clientHeight < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var canvasRect = canvas.getBoundingClientRect();
|
||||
return canvasRect.width >= 2 && canvasRect.height >= 2;
|
||||
};
|
||||
|
||||
var runHealthCheck = function () {
|
||||
if (isRolling) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (hasVisibleCanvas()) {
|
||||
healthCheckFailures = 0;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
healthCheckFailures += 1;
|
||||
|
||||
if (healthCheckFailures >= 3) {
|
||||
healthCheckFailures = 0;
|
||||
return resetDiceBox();
|
||||
}
|
||||
|
||||
ensureDiceBox()
|
||||
.catch((error) => {
|
||||
console.warn('DiceBox init fallback', error);
|
||||
})
|
||||
.finally(() => {
|
||||
queueResize();
|
||||
setInterval(() => {
|
||||
runHealthCheck().catch((error) => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
var rollDice = function () {
|
||||
if (isRolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRolling = true;
|
||||
|
||||
ensureDiceBox()
|
||||
.then(function (box) {
|
||||
return box.roll('2d6');
|
||||
})
|
||||
.then(function (rollResult) {
|
||||
var diceValues = extractValues(rollResult);
|
||||
if (!diceValues) {
|
||||
diceValues = fallbackRoll();
|
||||
}
|
||||
|
||||
emit({ type: 'ROLL_RESULT', payload: { dice: diceValues } });
|
||||
queueResize();
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.warn('DiceBox roll fallback', error);
|
||||
emit({
|
||||
type: 'ROLL_RESULT',
|
||||
payload: { dice: fallbackRoll() },
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
isRolling = false;
|
||||
|
||||
if (qualityResetQueued) {
|
||||
qualityResetQueued = false;
|
||||
resetDiceBox();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var onBridgeMessage = function (event) {
|
||||
try {
|
||||
var message = JSON.parse(event.data || '{}');
|
||||
if (message.type === 'ROLL') {
|
||||
rollDice();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'RESIZE') {
|
||||
queueResize();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'RESET') {
|
||||
resetDiceBox();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'QUALITY') {
|
||||
applyQualityLevel(
|
||||
message && message.payload && message.payload.level,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Bridge parse error', error);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', onBridgeMessage);
|
||||
document.addEventListener('message', onBridgeMessage);
|
||||
window.addEventListener('resize', queueResize);
|
||||
window.addEventListener('orientationchange', queueResize);
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') {
|
||||
queueResize();
|
||||
runHealthCheck().catch(function (error) {
|
||||
console.warn('DiceBox health fallback', error);
|
||||
});
|
||||
}, 1800);
|
||||
emit({ type: 'READY' });
|
||||
}
|
||||
});
|
||||
|
||||
ensureDiceBox()
|
||||
.catch(function (error) {
|
||||
console.warn('DiceBox init fallback', error);
|
||||
})
|
||||
.then(function () {
|
||||
queueResize();
|
||||
setInterval(function () {
|
||||
runHealthCheck().catch(function (error) {
|
||||
console.warn('DiceBox health fallback', error);
|
||||
});
|
||||
}, 1800);
|
||||
emit({ type: 'READY' });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in new issue
Block a user