-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathintent.js
More file actions
123 lines (109 loc) · 5.71 KB
/
Copy pathintent.js
File metadata and controls
123 lines (109 loc) · 5.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/* gameflix — Android: launch catalog games in the native RetroArch app.
*
* On Android there is no play:// OS handler, so instead we hand the game to the
* installed RetroArch via an intent:// deep link (a Chrome-for-Android feature,
* no companion app needed). RetroArch can't download ROMs itself, so the flow is:
*
* tap game → download ROM to /storage/emulated/0/Download
* → intent:// opens RetroArch with that file + the matching core
*
* The platform→core/src/ext mapping is cores.json, generated by generate.sh from
* the same launcher table the desktop uses (gen_cores.py launch.tsv). Desktop
* browsers are untouched.
*
* The pure helpers are also exported for Node so the URL/intent building can be
* unit-tested without a device (see test_intent.js).
*/
(function () {
"use strict";
// Target RetroArch app. "com.retroarch" = stable RetroArch;
// "com.retroarch.aarch64" = RetroArch Plus (64-bit). Core dir + component follow it.
var RETRO_PKG = "com.retroarch";
var RETRO_ACT = "com.retroarch.browser.retroactivity.RetroActivityFuture";
var CORE_DIR = "/data/data/" + RETRO_PKG + "/cores";
var DL_DIR = "/storage/emulated/0/Download"; // Chrome's default download dir
// Mirror of the bash urlenc(): keep /A-Za-z0-9._~-, percent-encode the rest (UTF-8).
function urlenc(s) {
return s.replace(/[^/A-Za-z0-9._~-]/g, function (c) {
return Array.prototype.map.call(new TextEncoder().encode(c), function (b) {
return "%" + b.toString(16).toUpperCase().padStart(2, "0");
}).join("");
});
}
// First entry whose pattern is a substring of the dir — same as the bash `case`.
function lookup(cores, dir) {
for (var i = 0; i < cores.length; i++) {
if (dir.indexOf(cores[i].pattern) !== -1) return cores[i];
}
return null;
}
// Android Intent.parseUri runs Uri.decode on extra values, so encodeURIComponent
// round-trips safely. package/component are structural and stay literal.
function buildIntent(romPath, corePath, fallback) {
var e = encodeURIComponent;
return "intent://gameflix/launch#Intent;" +
"action=android.intent.action.MAIN;" +
"package=" + RETRO_PKG + ";" +
"component=" + RETRO_PKG + "/" + RETRO_ACT + ";" +
"S.ROM=" + e(romPath) + ";" +
"S.LIBRETRO=" + e(corePath) + ";" +
(fallback ? "S.browser_fallback_url=" + e(fallback) + ";" : "") +
"end";
}
// play://-href → everything needed to download + launch, or { error }.
function resolve(cores, href, fallback) {
var path = decodeURIComponent(href.replace(/^play:\/\//, "")); // /plat/folder/file
var dir = path.replace(/\/[^/]*$/, "/");
var ent = lookup(cores, dir);
if (!ent) return { error: "Žiadne mapovanie core pre " + dir };
var core = ent.core || "";
if (core.indexOf(" ") !== -1 || core.slice(-9) !== "_libretro") {
return { error: "Cez intent:// zatiaľ nepodporované:\n" + core +
"\n(MAME / standalone potrebujú cmd-file)" };
}
if (!ent.src) return { error: "Žiadny zdroj na stiahnutie pre " + dir };
var segs = path.split("/").filter(Boolean);
var inner = segs.slice(2).join("/");
var fname = inner.split("/").pop();
var corePath = CORE_DIR + "/" + core + "_android.so";
var romPath = DL_DIR + "/" + fname;
return {
fname: fname,
romUrl: ent.src + urlenc(inner), // direct download (no CORS for navigations)
romPath: romPath,
corePath: corePath,
intent: buildIntent(romPath, corePath, fallback)
};
}
var api = { urlenc: urlenc, lookup: lookup, buildIntent: buildIntent, resolve: resolve,
RETRO_PKG: RETRO_PKG, CORE_DIR: CORE_DIR, DL_DIR: DL_DIR };
// ---- Node (unit tests) ------------------------------------------------
if (typeof module !== "undefined" && module.exports) { module.exports = api; return; }
// ---- Browser, Android only --------------------------------------------
if (typeof navigator === "undefined" || !/Android/i.test(navigator.userAgent)) return;
// Firefox/Fenix has a long-standing bug: if S.browser_fallback_url is present
// it jumps straight to the fallback and never tries to open the app
// (fenix#23397, bug 1795161). So on Firefox we omit the fallback entirely;
// Chrome honours it correctly (tries the app first, falls back only if absent).
var FIREFOX = /Firefox|FxiOS|Fennec/i.test(navigator.userAgent);
var coresPromise = fetch("cores.json").then(function (r) { return r.json(); });
function startDownload(url, fname) {
var a = document.createElement("a");
a.href = url; a.download = fname; a.rel = "noreferrer";
document.body.appendChild(a); a.click(); a.remove();
}
document.addEventListener("click", function (ev) {
var a = ev.target.closest && ev.target.closest('a[href^="play:"]');
if (!a) return;
ev.preventDefault();
coresPromise.then(function (cores) {
var r = resolve(cores, a.getAttribute("href"), FIREFOX ? null : location.href);
if (r.error) { alert(r.error); return; }
startDownload(r.romUrl, r.fname); // 1) get the ROM onto the device
if (confirm("Sťahujem „" + r.fname + "“ do Downloads.\n\n" +
"Po dokončení sťahovania daj OK = spustiť v RetroArch.")) {
window.location.href = r.intent; // 2) hand it to native RetroArch
}
});
}, true);
})();