├── LICENSE ├── README.md ├── app.js ├── fap.js ├── github.js ├── index.html ├── repos.js ├── reposlist.js ├── script.js └── unknown.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Milk-Cool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlipperAppStore 2 | An online app store for Flipper Zero 3 | 4 | ## Running 5 | It's a completey static site, just download everything and open index.html in your browser (or just go to https://flipp-app.onrender.com/). 6 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | class App { 2 | constructor(path, category){ 3 | path = path.split("/"); 4 | this.category = category; 5 | this.author = path[0]; 6 | this.name = path[1]; 7 | this.branch = path[2]; 8 | this.path = this.author + "/" + this.name; 9 | this.full = path.join("/"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fap.js: -------------------------------------------------------------------------------- 1 | const buildFap = app => new Promise((resolve, reject) => { 2 | fetch(`https://corsanywhere-taloud.onrender.com/https://flipc.org/api/v2/${app.path}?branch=${app.branch}&nowerr=1`, { 3 | "headers": { 4 | "accept": "application/json" 5 | }, 6 | "method": "GET", 7 | }).then(res => res.json()).then(res => resolve(res.app.id + ".fap")).catch(reject); 8 | }); 9 | const getFap = app => new Promise((resolve, reject) => { 10 | fetch(`https://corsanywhere-taloud.onrender.com/https://flipc.org/api/v2/${app.path}/elf?branch=${app.branch}&nowerr=1`, { 11 | "headers": { 12 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" 13 | }, 14 | "method": "GET" 15 | }).then(res => res.arrayBuffer()).then(resolve).catch(reject); 16 | }); 17 | -------------------------------------------------------------------------------- /github.js: -------------------------------------------------------------------------------- 1 | const getIconUrl = app => new Promise((resolve, reject) => { 2 | fetch(`https://raw.githubusercontent.com/${app.full}/application.fam`).then(res => res.text()).then(async res => { 3 | resolve(`https://raw.githubusercontent.com/${app.full}/${res.match(/fap_icon="(?.+)"/).groups.icon}`); 4 | }).catch(() => resolve("unknown.png")); 5 | }); 6 | const getAppName = app => new Promise((resolve, reject) => { 7 | fetch(`https://raw.githubusercontent.com/${app.full}/application.fam`).then(res => res.text()).then(async res => { 8 | resolve(res.match(/name="(?.+)"/).groups.name); 9 | }).catch(() => resolve(app.name)); 10 | }); 11 | const getAppDescription = app => new Promise((resolve, reject) => { 12 | fetch(`https://api.github.com/repos/${app.path}`).then(res => res.json()).then(async res => { 13 | resolve(res.description || "No description"); 14 | }).catch(() => resolve("No description")); 15 | }); 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flipper Zero App Store 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 |
37 |
38 | Debug information
39 |

Output:

40 | 41 | 42 |

Input:

43 | 44 | 45 |
46 |

GitHub | Based on FAP builder.

47 |

48 |
49 |

50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /repos.js: -------------------------------------------------------------------------------- 1 | let applications = []; 2 | let categories = []; 3 | const loadRepos = () => { 4 | for(let i of Object.keys(reposList)){ 5 | categories.push(i); 6 | vi = reposList[i]; 7 | for(let j of vi) 8 | applications.push(new App(j, i)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reposlist.js: -------------------------------------------------------------------------------- 1 | const reposList = { 2 | "Games": [ 3 | "ahumeniy/flipperzero_tetris_game/main", 4 | "ahumeniy/doom-flipper-zero/main", 5 | "x27/flipperzero-game15/main", 6 | "eugene-kirzhanov/flipper-zero-2048-game/main", 7 | "panki27/minesweeper/master", 8 | "jasniec/flipper-scorched-tanks-game/main", 9 | "Ka3u6y6a/flipper-zero-dice/main", 10 | "emfleak/flipperzero-yatzee/main", 11 | "antirez/flipper-asteroids/main", 12 | "DroomOne/flipperzero-tamagotch-p1/master", 13 | "Dooskington/flipperzero-zombiez/main", 14 | "xMasterX/heap-defence/main", 15 | "Willzvul/Snake_2.0/main" 16 | ], 17 | "Misc": [ 18 | "akopachov/flipper-zero_authenticator/master", 19 | "Th3Un1q3/flipp_pomodoro/main", 20 | "sbrin/flipperzero_pomodoro/main", 21 | "panki27/caesar-cipher/master", 22 | "Krulknul/dolphin-counter/main", 23 | "anakod/flipper_passgen/main", 24 | "scrolltex/flipper_analog_clock/master" 25 | ], 26 | "Tools": [ 27 | "pbek/usb_hid_autofire/main", 28 | "QtRoS/flipper-zero-hex-viewer/master", 29 | "bmatcuk/flipperzero-qrcode/main", 30 | "litui/dtmf_dolphin/main", 31 | "Hong5489/ir_remote/main", 32 | "polarikus/flipper-zero_bc_scanner_emulator/main", 33 | "zacharyweiss/magspoof_flipper/main" 34 | ], 35 | "GPIO": [ 36 | "quen0n/unitemp-flipperzero/master", 37 | "H4ckd4ddy/flipperzero-sentry-safe-plugin/master", 38 | "ezod/flipperzero-gps/main", 39 | "NaejEL/flipperzero-i2ctools/main", 40 | "oleksiikutuzov/flipperzero-lightmeter/main", 41 | "xMasterX/flipper-flashlight/main", 42 | "ginkage/FlippAirMouse/main", 43 | "ezod/flipperzero-rc2014-coleco/main", 44 | "biotinker/flipperzero-gpioreader/main", 45 | "theageoflove/flipperzero-zeitraffer/main", 46 | "nmrr/flipperzero-geigercounter/main", 47 | "flyandi/flipper_zero_rgb_led/master" 48 | ], 49 | "Music": [ 50 | "ltva1/wav_player/main", 51 | "panki27/Metronome/master", 52 | "panki27/bpm-tapper/master", 53 | "besya/flipperzero-tuning-fork/main", 54 | "DrZlo13/flipper-zero-usb-midi/main", 55 | "DrZlo13/flipper-zero-music-tracker/master", 56 | "invalidna-me/flipperzero-ocarina/main" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // Global device variable 2 | let flipper = null; 3 | 4 | const $ = selector => document.querySelector(selector); 5 | const sleep = time => new Promise(resolve => setTimeout(resolve, time)); 6 | 7 | let reader; 8 | let connected = false; 9 | 10 | let state = 0; 11 | 12 | // Write text to Flipper 13 | const textEncoder = new TextEncoderStream(); 14 | let writableStreamClosed; 15 | let writer; 16 | 17 | const writeText = data => { 18 | writer.write((new TextEncoder()).encode(data)); 19 | $("#Dout_hex").value += data.split("").map(x => x.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase() + " ").join(""); 20 | $("#Dout_text").value += data; 21 | }; 22 | const writeRaw = data => { 23 | writer.write(data); 24 | $("#Dout_hex").value += String.fromCharCode(...data).split("").map(x => x.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase() + " ").join(""); 25 | $("#Dout_text").value += String.fromCharCode(...data); 26 | } 27 | const send = text => writeText(text + "\r\n"); 28 | 29 | // This function disconnects the flipper 30 | const disconnect = async () => { 31 | reader.releaseLock(); 32 | writer.releaseLock(); 33 | await flipper.close(); 34 | flipper = null; 35 | }; 36 | 37 | const main = async () => { 38 | if(flipper === null){ 39 | flipper = await navigator.serial.requestPort({ "filters": [{ "usbVendorId": 0x0483 }] }); 40 | await flipper.open({ "baudRate": 9600 }); 41 | } 42 | navigator.serial.addEventListener("disconnect", () => { 43 | flipper = null; 44 | connected = false; 45 | }); 46 | writer = flipper.writable.getWriter(); 47 | setTimeout(async () => { 48 | while(flipper.readable){ 49 | reader = flipper.readable.getReader(); 50 | let dataIn = ""; 51 | while(true){ 52 | const { value, done } = await reader.read(); 53 | if(value) connected = true; 54 | const textValue = Array.from(value).filter(x => x != 0x07).map(x => String.fromCharCode(x)).join(""); 55 | $("#Din_hex").value += Array.from(value).map(x => x.toString(16).toUpperCase().padStart(2, "0") + " ").join(""); 56 | $("#Din_text").value += textValue; 57 | } 58 | } 59 | }); 60 | }; 61 | 62 | const install = async app => { 63 | if(flipper == null) return alert("The Flipper Zero is not connected!"); 64 | state = 1; 65 | const name = await buildFap(app); 66 | state = 2; 67 | const fap = new Uint8Array(await getFap(app)); 68 | state = 3; 69 | send(`storage mkdir /ext/apps/${app.category}`); 70 | await sleep(500); 71 | send(`storage remove /ext/apps/${app.category}/${name}`); 72 | await sleep(500); 73 | writeText(`storage write_chunk /ext/apps/${app.category}/${name} ${fap.byteLength}\r`); 74 | await sleep(500); 75 | writeRaw(fap); 76 | state = 0; 77 | return fap.byteLength; 78 | } 79 | const installScreen = async app => { 80 | $(".loading").style.display = "block"; 81 | await install(app); 82 | $(".loading").style.display = "none"; 83 | } 84 | 85 | (async () => { 86 | loadRepos(); 87 | for(let i of categories){ 88 | let sel = document.createElement("input"); 89 | sel.type = "checkbox"; 90 | sel.id = "selcat_" + i; 91 | sel.style.width = "48px"; 92 | sel.checked = true; 93 | sel.onchange = () => { 94 | console.log(1) 95 | console.log(document.querySelectorAll(".category_" + i)) 96 | document.querySelectorAll(".category_" + i).forEach(j => j.style.display = sel.checked ? "flex" : "none"); 97 | }; 98 | $("#categories").appendChild(sel); 99 | 100 | let label = document.createElement("label"); 101 | label.setAttribute("for", "selcat_" + i); 102 | label.innerText = i; 103 | $("#categories").appendChild(label); 104 | } 105 | let n = 0; 106 | for(let i of applications){ 107 | let appDiv = document.createElement("div"); 108 | appDiv.classList.add(`category_${i.category}`); 109 | appDiv.style.height = "100px"; 110 | appDiv.style.borderBottom = "2px solid gray"; 111 | appDiv.style.display = "flex"; 112 | 113 | let icon = document.createElement("img"); 114 | icon.style.display = "inline-block"; 115 | icon.style.height = "80px"; 116 | icon.style.margin = "10px"; 117 | icon.style.imageRendering = "pixelated"; 118 | icon.src = await getIconUrl(i); 119 | appDiv.appendChild(icon); 120 | 121 | let name = document.createElement("h1"); 122 | name.style.marginLeft = "30px"; 123 | name.style.display = "inline-block"; 124 | name.style.height = "100px"; 125 | name.classList.add("app_name"); 126 | name.innerText = await getAppName(i); 127 | appDiv.appendChild(name); 128 | 129 | let info = document.createElement("h1"); 130 | info.style.marginLeft = "30px"; 131 | info.style.display = "inline-block"; 132 | info.style.height = "100px"; 133 | info.style.color = "gray"; 134 | info.innerText = `${i.author} | ${i.category}`; 135 | appDiv.appendChild(info); 136 | 137 | let right = document.createElement("div"); 138 | right.style.position = "absolute"; 139 | right.style.right = "40px"; 140 | right.style.display = "inline-block"; 141 | appDiv.appendChild(right); 142 | 143 | let downloadButton = document.createElement("button"); 144 | downloadButton.style.display = "inline-block"; 145 | downloadButton.style.height = "80px"; 146 | downloadButton.style.margin = "10px"; 147 | downloadButton.innerText = "Install"; 148 | downloadButton.onclick = () => installScreen(i); 149 | right.appendChild(downloadButton); 150 | 151 | let moreButton = document.createElement("button"); 152 | moreButton.style.display = "inline-block"; 153 | moreButton.style.height = "80px"; 154 | moreButton.style.margin = "10px"; 155 | moreButton.innerText = "More"; 156 | moreButton.onclick = () => window.open("https://github.com/" + i.path); 157 | right.appendChild(moreButton); 158 | 159 | $("#apps").appendChild(appDiv); 160 | 161 | $("#load").innerText = ` (${++n}/${applications.length})`; 162 | } 163 | $("#load").innerText = ""; 164 | const sortedApps = Array.from($("#apps").children).sort((x, y) => x.querySelector(".app_name").innerText > y.querySelector(".app_name").innerText ? 1 : -1); 165 | for(let i of $("#apps").children) 166 | i.remove(); 167 | for(let i of sortedApps) 168 | $("#apps").appendChild(i); 169 | $(".loading").style.display = "none"; 170 | })(); 171 | 172 | setInterval(() => { 173 | switch(state){ 174 | case 1: 175 | $("#status").innerText = "Building FAP..."; 176 | break; 177 | case 2: 178 | $("#status").innerText = "Downloading FAP..."; 179 | break; 180 | case 3: 181 | $("#status").innerText = "Uploading FAP to Flipper..."; 182 | break; 183 | default: 184 | $("#status").innerText = "Loading..."; 185 | } 186 | if(!connected){ 187 | $("#connect").innerText = "Connect"; 188 | $("#connect").disabled = false; 189 | }else{ 190 | $("#connect").innerText = "Connected"; 191 | $("#connect").disabled = true; 192 | } 193 | }, 100); 194 | -------------------------------------------------------------------------------- /unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milk-Cool/FlipperAppStore/111112f262f45d94b5c9f862c60493126a80378e/unknown.png --------------------------------------------------------------------------------