From 917b9136eb614de93800dda8c0bcfe3883cc31b3 Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Mon, 6 Oct 2025 14:10:25 -0400 Subject: [PATCH 01/38] License project as MPLv2 --- LICENSE.txt | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d0a1fa1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. From cccee14b645604e0d0df4b9e62dcfa19c1755280 Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Tue, 14 Oct 2025 14:11:37 -0400 Subject: [PATCH 02/38] FFAA beta public. Closes #7. --- README.md | 10 ++++- build/deploys.txt | 2 + public/install.js | 57 ++++++++++++++++++++----- public/manifest.json | 17 ++++++++ public/manifest.webapp | 19 +++++++++ public/rocket_16.png | Bin 0 -> 567 bytes public/rocket_bag_128.png | Bin 0 -> 3504 bytes public/rocket_bag_48.png | Bin 0 -> 1346 bytes public/rocket_bag_512_maskable.png | Bin 0 -> 31694 bytes public/rocket_bag_60.png | Bin 0 -> 1680 bytes public/rocket_bag_64.png | Bin 0 -> 1726 bytes src/assets/astro.svg | 1 - src/assets/background.svg | 1 - src/components/common_imports.astro | 3 ++ src/components/common_metadata.astro | 1 + src/components/details_header.astro | 3 -- src/components/details_resources.astro | 3 -- src/components/footer.astro | 1 + src/content.config.js | 2 +- src/pages/all.astro | 8 ++-- src/pages/app/[slug].astro | 14 +++--- src/pages/dead.astro | 8 ++-- src/pages/hosted.astro | 8 ++-- src/pages/index.astro | 31 ++++++++------ src/pages/live.astro | 8 ++-- src/pages/packaged.astro | 8 ++-- src/pages/privileged.astro | 8 ++-- 27 files changed, 149 insertions(+), 64 deletions(-) create mode 100644 public/manifest.json create mode 100644 public/manifest.webapp create mode 100644 public/rocket_16.png create mode 100644 public/rocket_bag_128.png create mode 100644 public/rocket_bag_48.png create mode 100644 public/rocket_bag_512_maskable.png create mode 100644 public/rocket_bag_60.png create mode 100644 public/rocket_bag_64.png delete mode 100644 src/assets/astro.svg delete mode 100644 src/assets/background.svg create mode 100644 src/components/common_imports.astro delete mode 100644 src/components/details_header.astro delete mode 100644 src/components/details_resources.astro diff --git a/README.md b/README.md index df1bbfc..0647cc7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # FF Apps Archive -https://ffapps.danielherr.software \ No newline at end of file +https://ffapps.danielherr.software + +The actual apps are available from the Internet Archive. Torrent is available. After downloading app collection, run apps_cleanup.mjs to convert the apps collection into a structure suitable for hosting. You only need to run that once after downloading. Then move the apps to ./public/app/ + +https://archive.org/details/Firefox_Marketplace_2018_03_Capture + +magnet:?xt=urn:btih:d3a4a134c4014cff23830e65973ffd80642d4952&dn=Firefox_Marketplace_2018_03_Capture&xl=29368516608&tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&ws=http://ia601502.us.archive.org/4/items/&ws=http://ia801502.us.archive.org/4/items/&ws=https://archive.org/download/ + +See deploys.txt for build commands. You'll need a system with a good amount of RAM, adjust the build command according to how much you want to use. I used 20GB, when I tried before increasing the RAM limit of Node the build would crash. \ No newline at end of file diff --git a/build/deploys.txt b/build/deploys.txt index 72d51c0..1f58377 100644 --- a/build/deploys.txt +++ b/build/deploys.txt @@ -1,3 +1,5 @@ node .\build\apps_cleanup.mjs +$env:NODE_OPTIONS="--max-old-space-size=20000"; npx astro build + netlify deploy --dir=dist --no-build \ No newline at end of file diff --git a/public/install.js b/public/install.js index 54e5ca9..c690217 100644 --- a/public/install.js +++ b/public/install.js @@ -1,15 +1,52 @@ "use strict" +var manifest = install_button.dataset.manifest +var app + +if(navigator.mozApps == undefined) { + install_button.disabled = true +} + +if(manifest == "manifest.webapp") { + var check = navigator.mozApps.checkInstalled(location.href + manifest) + check.addEventListener("success", function() { + if(this.result) { + app = this.result + install_button.textContent = install_button.textContent.replace("Install", "Launch") + } }) +} else { + var check = navigator.mozApps.getInstalled() + check.addEventListener("success", function() { + app = this.result.find(function(app) { + return app.manifestURL == manifest + }) + if(app) { + install_button.textContent = install_button.textContent.replace("Install", "Launch") +} }) } +check.addEventListener("error", function() { + console.error(this.error) +}) + install_button.addEventListener("click", function() { - if(this.dataset.manifest == "manifest.webapp") { - var request = navigator.mozApps.installPackage(location.href + this.dataset.manifest) - request.addEventListener("success", function(event) { - console.log(event) + if(install_button.textContent.indexOf("Install") == 0) { + if(manifest == "manifest.webapp") { + var install = navigator.mozApps.installPackage(location.href + manifest) + } else { + var install = navigator.mozApps.install(manifest) + } + install.addEventListener("success", function() { + app = this.result + install_button.textContent = install_button.textContent.replace("Install", "Launch") }) - request.addEventListener("error", function(event) { - console.log(event) + install.addEventListener("error", function() { + console.error(this.error) + if(this.error.name == "REINSTALL_FORBIDDEN") { + install_button.textContent = install_button.textContent.replace("Install", "Launch") + install_button.disabled = true + } else if(this.error.name != "DENIED") { + alert("An error occurred while installing: " + this.error.name) + } }) - } else { - navigator.mozApps.install(this.dataset.manifest) - } -}) \ No newline at end of file + } else if(install_button.textContent.indexOf("Launch") == 0) { + app.launch() +} }) \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..40e6336 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "FF Apps Archive", + "short_name": "FFAA", + "description": "FF Apps Archive contains thousands of apps from the defunct Firefox Marketplace.", + "icons": [ + { "src": "/rocket_16.png", "sizes": "16x16", "type": "image/png", "purpose": "monochrome" }, + { "src": "/rocket_32.png", "sizes": "32x32", "type": "image/png", "purpose": "monochrome" }, + { "src": "/rocket_bag_48.png", "sizes": "48x48", "type": "image/png" }, + { "src": "/rocket_bag_60.png", "sizes": "60x60", "type": "image/png" }, + { "src": "/rocket_bag_64.png", "sizes": "64x64", "type": "image/png" }, + { "src": "/rocket_bag_128.png", "sizes": "128x128", "type": "image/png" }, + { "src": "/rocket_bag_512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/rocket_bag_512_maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ], + "start_url": "/", + "display": "standalone" +} \ No newline at end of file diff --git a/public/manifest.webapp b/public/manifest.webapp new file mode 100644 index 0000000..5c0bcba --- /dev/null +++ b/public/manifest.webapp @@ -0,0 +1,19 @@ +{ + "name": "FFAA", + "description": "FF Apps Archive contains thousands of apps from the defunct Firefox Marketplace.", + "developer": { + "name": "Daniel Herr", + "url": "https://danielherr.software" + }, + "icons": { + "16": "/rocket_16.png", + "32": "/rocket_32.png", + "48": "/rocket_bag_48.png", + "60": "/rocket_bag_60.png", + "64": "/rocket_bag_64.png", + "128": "/rocket_bag_128.png", + "512": "/rocket_bag_512.png" + }, + "launch_path": "/", + "version": "1" +} \ No newline at end of file diff --git a/public/rocket_16.png b/public/rocket_16.png new file mode 100644 index 0000000000000000000000000000000000000000..621455413a4aab52b64dc08679c53b42a3fd88e7 GIT binary patch literal 567 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6XVU3I`u#fXMsm#F#`j) zFbFd;%$g$s6nqun6XFV_ON@Goje1Iqx+@I3%MH7LsM?^T%Af;C0#Tho8!~FrZw8`9 zgXRWE*`T>iuMvn^!DN?SeWz|+Cz$Ngt?$#V>4hM@nu$796F{hPs!sV-or)>i zEaktF()~tfx*q~*nR=Fmh*>B9QeT<*)VtFNBhg> zEez%X$L*i>b1*Wx8C0-}3$VXpcvZ25C_xLH?H-KY;FH@O1TaS?83{1OVH1-F^T7 literal 0 HcmV?d00001 diff --git a/public/rocket_bag_128.png b/public/rocket_bag_128.png new file mode 100644 index 0000000000000000000000000000000000000000..82834d97b8b84a58b7ee27cfc55110231f228418 GIT binary patch literal 3504 zcmV;h4NvlkP)=*k>D?|a{$VI~(0LwPxB6V>QNNA2`LJZ7OHw3;%j z&=NG?L7RwuPJhW!lc;tXnAC^#{LHqTw24D-pxNjmYC&K|TJH6|2G=phQJc_@^gPX` zoYm9&;?VriO#~L89u**K(zB($ob2QJIBOFUowbS02rSH3PTB-J(le&ITr>&J7=R`O z7Dip1F)Qe%`t^qHteM5O;x#Sbxub1B#qQ>u2+#{G%(+^HxxWlTReP5gNT67yfuv!|lm)p4)7YvMp^+%$2k zNY9>%!eJ~Vy%~hmxvOIyBRzlO<)MlD34s*3t7FHKo5~1G-LiTDl<*kkxgjx{@=vN%*05SUY z)5P?_^Dhv%UsjqJ{Ze1J@Oa^e8J#3@&yS-^0{48@OBF4xZy51aNA>klM;}8VBpl{w zv3tkI$rQS8^rF~(Q&qU&iKtERL8_t$i{E?9n<4~YrPzH_Q{JkmKlrMn4}G+`MJy;Sy@ta(eQ%MbFb^M3DbyPLHIa|t`6l3buZ!NhN>`2Z zde>haY5s2w15}Y}TudaYLSSP1Q7rZl(R;ewfZsO8RK3@u!~k`qiu4|Ky8@KDzJaR9 z!8kYw^Kn z3nN+)$Rwc#c?2mU`Y^$&h&NFa0-2;V1S=y(umRXbK{GNk<})FxY1Ig1l60C4Kr01} z$jE45LY33%Lh%EcBvmnC%4yfbl;I$gq;nX6@CphVk&$tT30H<6Lm-o+y=(x=!j1|W-qMohZ-pGFizQ$rK{z8Y%k zp~$t6c#kIJ%INGvlXc1vkczdTtD*UBbEnPksQT(Vkn^8;#QQV}`;f^PfRJejq@dv4 zoc6!v#_GnI>yZ3x0`Y!L;NMJ?GQ=YaKd^M^Ba@)|QkBuo@2Eq?g;^GWZP)0a&t<=yl!N{En*nr4o{d z#}e;fM9(lW_Q9hNSgMRsNpPpKu15i2>+`ugiLXH{(Tbo!7=WOm2rN;oDg=&g`Ju;~ z&sJkyBg}arnfO{nV#mZN1MOnjVOgNUcNet(Ti)9$UtR`r%3$K_5u7sUQ3e0amDDS{8b5HJ7W6-nOysF3cJcM|^DpI;9_g^YIG2 z&_b;7EHt{IXG%{(%3mVeZ(sP%d}yd?FnXV@@#~1MO+epp0Z32;97kXY5>-KP;ov#m zjbuOHpZP-41NZxU_Ew_-sI0gNNdrTPuTda7^aF5^f`$aK@LzL`zVhFlQqt*u$%Dh; z*4Z0IL$Kt3^N6oeAZxe)Bq{>FL|_Syu0P1TfweboL;BOYhu(Kn*eYmi;}1{yl6}P2 zD3ClZ0Mixz+Y%N2VD8eMjDUt;@9=Kmp41Y9_fH-W(*88hSL{C@DTBg@uT=o)`T32>uHexMmi_&wV9Cp(hSIbHI?@pM%XIL*-d<{0`S!jz=97IeqhdK;nP3+ zUY)OQow){;=Wf8=n)*&J6k8gb;Z9{OTs?Xbn(7;Q&vWC{RYXjqK-+YqOxd0?9d?!=TP>;+S918n0w+r%R-|kfHwO?V? zlq|s65uZU1{Zl^x)j8vz zw>MR_G_}Ca*kVXi`xBo{H$1@wAl1(MZ>e}8?Pl8h!UoR*xO1zP^qzMr_BBXpdI@Aa z5k!1WU6Eqz{gi$H#vq{G=D?-xpU$PV*bwZ?*i3v*UGyv$fSGpQ6A;j>Z0bkE=XyH}UxyWa~Aa3qZP^S3tV07trX^w`WN2V~^{^#)HJ?$73QF zfLXSl(G)affdMEdK0m`eC+i0w1p$rBJpiQV$HPfK0P`qlNDhE$d2<2Cu=QM&Ve0{S z1lo?~9sttwGko+6;{q_-)?)<%8eKN?0Fa&^j|BYyd`dw>1_D6-3qCz9$^($W1z?V? z#}))MGUouC!~l?8lf*BgHlMBGdHg3le(8$~aKze>W4sZdOYvXnf0gcQ(0Ho)~LtW`*04jDJfx2tPe*?(Fs->wJF77|k3eW3R!9|xOV7lj{`t@emrjK2cUt1 zh6D{j%`Z1$;jmyqddM3R(0)D(3qZrY&3EfF&&H(<0gZYRfX3VPuzYlkpnVjL)ImdK z_r4kRGzNh5{P^JB_haVUxb~lqAJD410cdM!g|%;`h|){(_!-dBjc-b213-Fy{9GSr zvTR(nKewTptm zKZa%5I0LP^6MzFV)*3v|>Q~~q+g`={JCDk;#tSHQwTl5bx_A#{+qoJ%Ps!vt+-)nifB(?ibX4W3%SQY9v2ah<0zknG zc|64i;57ua>Pi4Ab{>XYl?U&6wmM{Tw?Dt@hfdco!0>cb~hg{C2jr6VPghdp7KgSz%DAZW$B|@-coMw`^`>YAUXGx_6UGIrB6gP#U$|U~eUpW)q*E5zk`)oTehMbQ|7IePDxEKCgS=`h#;eao06BG!gID zL|$QXtew0NSh|&?A|D9A+0PDg*A))+f8afS^_i>0`!$jA7y!p`1ePw>))|_9_gln{ z7VLs`uO~tkzUb4h6Dz(X-mi&FWOA(?6LZ-CNxe3>!@8G3yAk>3OQDeli2{w zCf$gsd=b3J=;pUJHN(=Oe#Cn<5oadP${{z;+5x0sp}h;#UE%-FBabcKO}tN&v2yTc z@~s_~=HI(O3dhnA!@p3Hef7V5cmNfBn&=qLyrUNZujB;93}_J6OAf<|OyRIvdlMIe); zrA$%(DWjJ5pHhcMkV#4<24G10&j;cM&!TOpm4e1(;%Y{pph1jWa>*r^Tyn`Jmt1nm eC6`=MtN#N>eNppX;5ktM0000 z62(nLoj4tcTosVY<-gNX8brDH@N`L&o#gl7ee%BNe@@{|nv^k)@^(*lA|I0q(n!v6 zXd)*`7FkYS;F*Q1eB6^FecY2r2n6Y&p6mXT;r-Occm32!-2{SNq2A7RUqGWa<`dY# zs79T*-tG(ds}pY#*vV154j-sWSQMyEzzjx6GijX0-C3UR2~-(|^ zMKpxd3?G1+?)hb=9%PkWh7VE2IR&fY>j-A>!EAkkRfhGm^oX;|0$Wva9%PhYI<#cq z9QtgSsRwB%D})WMRq9^SD)nBWD*ZnMv$#(8t|hNcV-4MNdbU1VmA;AYT{`C>!<4#( zolj1fnG_(xaiU4n22!P#sk% zyuVd5)4N5Ui&I2Gqho(cQKkVC3O>mZnqfw>pJnlG5w!#&RRr(D@X%vSuCCTj zBrb>+nnO;q1h?=S0#Q;H$D*&T7tgAnj6Vp=u{z8W-NGvgL^+mn$TqO~uQ1p>V7u

9UC1(jlK%y8*(x9;E`>KBf3fT6O`*lrILD@6@W;oIme8 zD^Hf{8WK)0A8CqEoZnk-`;=#^zngC0tA!Es$~61jHRMB<<{BJ8z$@!b9WGU$o%8S$ z)pFtz^4>Ob=UTnbzHr?VOu!>|r2#F+FFx1clk4`G7Gy8gab;VrV_&)kMG^4myw=7& z{6uxNv~y)!d9ZX@U_!bq2t4}l&F!U~smwii^J(@V_O&b^m4Jt+1$gsGG|P|$WM;_x z!9&yny!j-B?UHKpcF_e7Q48?qXG%4hEK8~>Bj7RJ0)4IbpE?^c4P(&yJ!x_cP!HaG zQp~cYew73~2Bs`v{sMQg{5RyPf>6132ddZYv?*H|i$kAg;!)SbrxxJNH_y&?OMMR$ z@R+(l%aQZQQwAVMuE9~SY!tj2f?PL$)NJ|&dlqc5JoB?A52M4QPb|QjPmZu$7oT4V zcuZd4c3m?{Uk|sOmn_ku>(4vbzcL27GGBBy-^O?E=*OR<>juO3Pb|QjZ=RjyxppF18d@@JZpW<)t4*V>J;1K;&p*QD5*E@ z)L+I>?*RI4bzrcg4`oZXip>~zffcxHzDyumZD^tG-{i`H20HT+oa!a8Mm=pYz65WTS&xChW(+|BJK!1$CyOEjVb5A^-pY07*qoM6N<$ Eg1QHYg8%>k literal 0 HcmV?d00001 diff --git a/public/rocket_bag_512_maskable.png b/public/rocket_bag_512_maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..019c00c658b242da6dc405cfdd8537c95359fac7 GIT binary patch literal 31694 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�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 literal 0 HcmV?d00001 diff --git a/public/rocket_bag_60.png b/public/rocket_bag_60.png new file mode 100644 index 0000000000000000000000000000000000000000..99f051efcc23c55b140740264068dbcdbada56b4 GIT binary patch literal 1680 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 U=pkb!s{jB107*qoM6N<$f;GD{0{{R3 literal 0 HcmV?d00001 diff --git a/src/assets/astro.svg b/src/assets/astro.svg deleted file mode 100644 index 8cf8fb0..0000000 --- a/src/assets/astro.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/assets/background.svg b/src/assets/background.svg deleted file mode 100644 index 4b2be0a..0000000 --- a/src/assets/background.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/common_imports.astro b/src/components/common_imports.astro new file mode 100644 index 0000000..d0ab7f5 --- /dev/null +++ b/src/components/common_imports.astro @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/common_metadata.astro b/src/components/common_metadata.astro index 188eab8..1f19fe7 100644 --- a/src/components/common_metadata.astro +++ b/src/components/common_metadata.astro @@ -1,3 +1,4 @@ + \ No newline at end of file diff --git a/src/components/details_header.astro b/src/components/details_header.astro deleted file mode 100644 index fc68257..0000000 --- a/src/components/details_header.astro +++ /dev/null @@ -1,3 +0,0 @@ -

-

FF Apps Archive (alpha)

-
\ No newline at end of file diff --git a/src/components/details_resources.astro b/src/components/details_resources.astro deleted file mode 100644 index 5127823..0000000 --- a/src/components/details_resources.astro +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/components/footer.astro b/src/components/footer.astro index f2fb4b7..9cdd50f 100644 --- a/src/components/footer.astro +++ b/src/components/footer.astro @@ -1,4 +1,5 @@ \ No newline at end of file diff --git a/src/content.config.js b/src/content.config.js index 3135792..64c92bd 100644 --- a/src/content.config.js +++ b/src/content.config.js @@ -2,7 +2,7 @@ import * as Content from "astro:content" import * as Loaders from "astro/loaders" let metadata = Content.defineCollection({ - loader: Loaders.glob({ pattern: "*/info.json", base: "./raw" }) + loader: Loaders.glob({ pattern: "*/info.json", base: "./public/app/" }) }) export let collections = { metadata } \ No newline at end of file diff --git a/src/pages/all.astro b/src/pages/all.astro index a9416ab..fd8901c 100644 --- a/src/pages/all.astro +++ b/src/pages/all.astro @@ -1,6 +1,7 @@ --- import * as Content from "astro:content" +import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" import AppsList from "../components/apps_list.astro" @@ -10,16 +11,15 @@ let apps = await Content.getCollection("metadata") - - + - All Apps - FF Apps Archive (alpha) + All Apps - FF Apps Archive
-

All Apps - FF Apps Archive (alpha)

+

All Apps - FF Apps Archive (beta)

diff --git a/src/pages/app/[slug].astro b/src/pages/app/[slug].astro index 0de2b21..f46f315 100644 --- a/src/pages/app/[slug].astro +++ b/src/pages/app/[slug].astro @@ -2,8 +2,6 @@ import * as Content from "astro:content" import Footer from "../../components/footer.astro" -import DetailsHeader from "../../components/details_header.astro" -import DetailsResources from "../../components/details_resources.astro" import Metadata from "../../components/common_metadata.astro" export async function getStaticPaths() { @@ -27,18 +25,22 @@ let releasenotes = app.data.release_notes ? app.data.release_notes["en-US"] ?? a - + + + - { name } - FF Apps Archive (alpha) + { name } - FF Apps Archive - +
+

FF Apps Archive (beta)

+

{ name }

- + { app.data.app_type != "privileged" && } { app.data.package_path && Download }
diff --git a/src/pages/dead.astro b/src/pages/dead.astro index 9ab0ec5..af83524 100644 --- a/src/pages/dead.astro +++ b/src/pages/dead.astro @@ -1,4 +1,5 @@ --- +import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" import AppsList from "../components/apps_list.astro" @@ -8,15 +9,14 @@ import { dead, total } from "../utils/validateHostedApps" - - + - All Dead Hosted Apps - FF Apps Archive (alpha) + All Dead Hosted Apps - FF Apps Archive
-

All Dead Hosted Apps - FF Apps Archive (alpha)

+

All Dead Hosted Apps - FF Apps Archive (beta)

Found { dead.length } dead hosted apps out of { total } total hosted apps.

diff --git a/src/pages/hosted.astro b/src/pages/hosted.astro index a0dad6b..61313e5 100644 --- a/src/pages/hosted.astro +++ b/src/pages/hosted.astro @@ -1,6 +1,7 @@ --- import * as Content from "astro:content" +import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" import AppsList from "../components/apps_list.astro" @@ -11,15 +12,14 @@ let apps = await Content.getCollection("metadata", ({ data }) => data.app_type = - - + - All Hosted Apps - FF Apps Archive (alpha) + All Hosted Apps - FF Apps Archive
-

All Hosted Apps - FF Apps Archive (alpha)

+

All Hosted Apps - FF Apps Archive (beta)

diff --git a/src/pages/index.astro b/src/pages/index.astro index e157a91..a937789 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,33 +1,36 @@ --- -import Footer from '../components/footer.astro' -import Metadata from '../components/common_metadata.astro' +import Imports from "../components/common_imports.astro" +import Footer from "../components/footer.astro" +import Metadata from "../components/common_metadata.astro" --- - - + - FF Apps Archive (alpha) + FF Apps Archive
-

FF Apps Archive (alpha)

- +

FF Apps Archive (beta)

-

FF Apps Archive will contain thousands of apps from the defunct Firefox Marketplace. +

FF Apps Archive contains thousands of apps from the defunct Firefox Marketplace.

All Apps

-

Packaged Apps

-

Privileged Apps

+

Packaged Apps - can be installed here

+

Privileged Apps - cannot be installed directly and must be sideloaded using developer tools

All Hosted Apps

-

Live Hosted Apps

-

Dead Hosted Apps

+

Live Hosted Apps - can be installed, but might no longer function

+

Dead Hosted Apps - cannot be installed

+

+ Firefox apps can be installed on any version of Firefox OS and on old versions of the Firefox web browser v16 - v46.
+ Firefox desktop v45
+ Firefox Android v45
+ Firefox OS 2.5 Android launcher +

diff --git a/src/pages/live.astro b/src/pages/live.astro index 9d75cc3..391e5a3 100644 --- a/src/pages/live.astro +++ b/src/pages/live.astro @@ -1,4 +1,5 @@ --- +import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" import AppsList from "../components/apps_list.astro" @@ -8,15 +9,14 @@ import { live, total } from "../utils/validateHostedApps" - - + - All Live Hosted Apps - FF Apps Archive (alpha) + All Live Hosted Apps - FF Apps Archive
-

All Live Hosted Apps - FF Apps Archive (alpha)

+

All Live Hosted Apps - FF Apps Archive (beta)

Found { live.length } live hosted apps out of { total } total hosted apps.

diff --git a/src/pages/packaged.astro b/src/pages/packaged.astro index 2d25ac1..a45df0d 100644 --- a/src/pages/packaged.astro +++ b/src/pages/packaged.astro @@ -1,6 +1,7 @@ --- import * as Content from "astro:content" +import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" import AppsList from "../components/apps_list.astro" @@ -11,15 +12,14 @@ let packaged = await Content.getCollection("metadata", ({ data }) => data.app_ty - - + - All Packaged Apps - FF Apps Archive (alpha) + All Packaged Apps - FF Apps Archive
-

All Packaged Apps - FF Apps Archive (alpha)

+

All Packaged Apps - FF Apps Archive (beta)

diff --git a/src/pages/privileged.astro b/src/pages/privileged.astro index b45841c..a4ef718 100644 --- a/src/pages/privileged.astro +++ b/src/pages/privileged.astro @@ -1,6 +1,7 @@ --- import * as Content from "astro:content" +import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" import AppsList from "../components/apps_list.astro" @@ -11,15 +12,14 @@ let privileged = await Content.getCollection("metadata", ({ data }) => data.app_ - - + - All Privileged Apps - FF Apps Archive (alpha) + All Privileged Apps - FF Apps Archive
-

All Privileged Apps - FF Apps Archive (alpha)

+

All Privileged Apps - FF Apps Archive (beta)

From 25103b6a41c7585c2229372ed20699c29bb514f8 Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Sat, 18 Oct 2025 01:38:19 -0400 Subject: [PATCH 03/38] Add category pages and minor UI improvements. Closes #2 --- public/common.css | 8 + public/details.css | 24 +- public/icons/books-comics.png | Bin 0 -> 421 bytes public/icons/business.png | Bin 0 -> 378 bytes public/icons/categories-sprite-alt.svg | 590 ++++++++++++++++++ public/icons/categories-sprite-black.svg | 471 ++++++++++++++ public/icons/categories-sprite.svg | 590 ++++++++++++++++++ public/icons/cats-sprite.svg | 136 ++++ public/icons/creativity.png | Bin 0 -> 470 bytes public/icons/education.png | Bin 0 -> 402 bytes public/icons/entertainment.png | Bin 0 -> 580 bytes public/icons/games.png | Bin 0 -> 515 bytes public/icons/groundbreaking.png | Bin 0 -> 621 bytes public/icons/health-fitness.png | Bin 0 -> 528 bytes public/icons/lifestyle.png | Bin 0 -> 498 bytes public/icons/maps-navigation.png | Bin 0 -> 493 bytes public/icons/music.png | Bin 0 -> 492 bytes public/icons/news.png | Bin 0 -> 625 bytes public/icons/photo-video.png | Bin 0 -> 386 bytes public/{ => icons}/rocket_16.png | Bin public/{ => icons}/rocket_32.png | Bin public/{ => icons}/rocket_bag_128.png | Bin public/{ => icons}/rocket_bag_48.png | Bin public/{ => icons}/rocket_bag_512.png | Bin .../{ => icons}/rocket_bag_512_maskable.png | Bin public/{ => icons}/rocket_bag_60.png | Bin public/{ => icons}/rocket_bag_64.png | Bin public/icons/shopping.png | Bin 0 -> 367 bytes public/icons/social.png | Bin 0 -> 540 bytes public/icons/sports.png | Bin 0 -> 517 bytes public/icons/tools.png | Bin 0 -> 556 bytes public/icons/travel.png | Bin 0 -> 351 bytes public/icons/weather.png | Bin 0 -> 625 bytes public/manifest.json | 16 +- public/manifest.webapp | 14 +- src/categories.json | 29 + src/components/apps_list.astro | 5 +- src/components/categories.astro | 11 + src/components/common_imports.astro | 2 +- src/components/header.astro | 5 + src/components/nav.astro | 13 + src/components/search.astro | 6 + src/pages/[category].astro | 42 ++ src/pages/all.astro | 11 +- src/pages/all_images.astro | 29 + src/pages/app/[slug].astro | 40 +- src/pages/dead.astro | 14 +- src/pages/hosted.astro | 15 +- src/pages/index.astro | 5 +- src/pages/live.astro | 14 +- src/pages/packaged.astro | 13 +- src/pages/privileged.astro | 11 +- src/{utils => }/validateHostedApps.js | 0 53 files changed, 2042 insertions(+), 72 deletions(-) create mode 100644 public/icons/books-comics.png create mode 100644 public/icons/business.png create mode 100644 public/icons/categories-sprite-alt.svg create mode 100644 public/icons/categories-sprite-black.svg create mode 100644 public/icons/categories-sprite.svg create mode 100644 public/icons/cats-sprite.svg create mode 100644 public/icons/creativity.png create mode 100644 public/icons/education.png create mode 100644 public/icons/entertainment.png create mode 100644 public/icons/games.png create mode 100644 public/icons/groundbreaking.png create mode 100644 public/icons/health-fitness.png create mode 100644 public/icons/lifestyle.png create mode 100644 public/icons/maps-navigation.png create mode 100644 public/icons/music.png create mode 100644 public/icons/news.png create mode 100644 public/icons/photo-video.png rename public/{ => icons}/rocket_16.png (100%) rename public/{ => icons}/rocket_32.png (100%) rename public/{ => icons}/rocket_bag_128.png (100%) rename public/{ => icons}/rocket_bag_48.png (100%) rename public/{ => icons}/rocket_bag_512.png (100%) rename public/{ => icons}/rocket_bag_512_maskable.png (100%) rename public/{ => icons}/rocket_bag_60.png (100%) rename public/{ => icons}/rocket_bag_64.png (100%) create mode 100644 public/icons/shopping.png create mode 100644 public/icons/social.png create mode 100644 public/icons/sports.png create mode 100644 public/icons/tools.png create mode 100644 public/icons/travel.png create mode 100644 public/icons/weather.png create mode 100644 src/categories.json create mode 100644 src/components/categories.astro create mode 100644 src/components/header.astro create mode 100644 src/components/nav.astro create mode 100644 src/components/search.astro create mode 100644 src/pages/[category].astro create mode 100644 src/pages/all_images.astro rename src/{utils => }/validateHostedApps.js (100%) diff --git a/public/common.css b/public/common.css index c5fbb94..64b7480 100644 --- a/public/common.css +++ b/public/common.css @@ -1,3 +1,11 @@ * { box-sizing: border-box; +} +header img { + float: left; + margin-right: 1em; +} +#categories_list, #apps_list { + list-style: none; + padding: 0; } \ No newline at end of file diff --git a/public/details.css b/public/details.css index bac87af..6ec52e0 100644 --- a/public/details.css +++ b/public/details.css @@ -1,15 +1,31 @@ -div { +h1 { + font-size: x-large; +} +h2 { + font-size: xx-large; +} +#screenshots_list { display: -moz-box; display: flex; overflow: auto; + clear: both; } img { display: block; flex-shrink: 0; + max-width: 100%; } -button { +#action_buttons > * { display: block; + width: 100%; font-size: large; - padding: 0.5em 5em; - /* border-radius: 10em; */ + padding: 0.5em 2em; + margin-bottom: 1em; + border: thin solid lightgrey; + border-radius: 10em; + text-align: center; + text-decoration: none; +} +.app_text_blocks { + white-space: pre-wrap; } \ No newline at end of file diff --git a/public/icons/books-comics.png b/public/icons/books-comics.png new file mode 100644 index 0000000000000000000000000000000000000000..d25dee9f7eaea18b7ae614a2971e0f6f5335bf24 GIT binary patch literal 421 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx{O~ji-xah=kN_-lFsQ$yqFttkV!Rknkw5o~LK5cA#rmt*o zpI{4M@aIaKRwCA2{BY~$&rA9A_PE?VaxCqO-Hw?#d%hoB<|?bUMP>`<&O!;*Oz~-K z!R#Hoyk%!}?{m4p_+^E|1?3BjB}`U~Z|4|`TlmLJ`s}i&Xzzms@|M=u_wRos?8hS{ U!}!zJ9O!QbPgg&ebxsLQ0Ldi6X#fBK literal 0 HcmV?d00001 diff --git a/public/icons/business.png b/public/icons/business.png new file mode 100644 index 0000000000000000000000000000000000000000..5073ae7a0e27675f51aeed227d2d703fbc405307 GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx{O~pr?yth{fr%S5FHaQs8m8xMYe~Fw^({`>(ainukfvy_k8eU6Ef-=|9JJ zg_g%(Sq|AZzAvjf`*cyky&#rXT;2>>i=Usk)|RPqymQ^cB(rISS9e`oq-30XN3L{% z_}VAVJC-Y-Tp9e*!8Yf|F|+QDC4HSIQuZ!$o+151$vfRvI&s~F$}p>0rAATvo>k^d zQ4GGF9&%3qVO&yX@;3?BKTCheWeKcmW{q`OmHc3NkgV{9;79{OfzoG+bMk&)UgKK- cm-!CkfwdwFVdQ&MBb@0D3#MX8-^I literal 0 HcmV?d00001 diff --git a/public/icons/categories-sprite-alt.svg b/public/icons/categories-sprite-alt.svg new file mode 100644 index 0000000..029c316 --- /dev/null +++ b/public/icons/categories-sprite-alt.svg @@ -0,0 +1,590 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/public/icons/categories-sprite-black.svg b/public/icons/categories-sprite-black.svg new file mode 100644 index 0000000..d7aae24 --- /dev/null +++ b/public/icons/categories-sprite-black.svg @@ -0,0 +1,471 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/public/icons/categories-sprite.svg b/public/icons/categories-sprite.svg new file mode 100644 index 0000000..2f48544 --- /dev/null +++ b/public/icons/categories-sprite.svg @@ -0,0 +1,590 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/public/icons/cats-sprite.svg b/public/icons/cats-sprite.svg new file mode 100644 index 0000000..6c6498a --- /dev/null +++ b/public/icons/cats-sprite.svg @@ -0,0 +1,136 @@ + + + + + Cat Icons + + + + image/svg+xml + + Cat Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/creativity.png b/public/icons/creativity.png new file mode 100644 index 0000000000000000000000000000000000000000..3f31869e0fb78c40a6ce17149c0e9cfe09e3bf57 GIT binary patch literal 470 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRqpYWkV~EA+-mAAKwJ7koUKCy$c;Udm|LfG15()l6zKp(mPm zz1}=SNN4h1qv(Vwp$&hWoLf~g70L*e|z_5MX7^K-Mx?kHaGo4jnjj%@n5^F31M zme-$HTb~{Nu2xUB@{IDnQ=jkLds4bDw{kzb?$7G@c}p!@Zr99TV%hrZyo{?{amlpx zmR6p{61Oa^mIQyS>oa*c?d7wKfL||9H~X;o{FVdQ&MBb@0AX|Lpa1{> literal 0 HcmV?d00001 diff --git a/public/icons/education.png b/public/icons/education.png new file mode 100644 index 0000000000000000000000000000000000000000..d0edf75d7792b1fd4fd09a67996c5372e0c461de GIT binary patch literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx{O~rl*Tzh{fr*QzC_06nI<|8wCSYzT3ZTIVzP>XwmRj*=&7~ZwJ>$El=y^ z;VT^0d^n@lHS_gPpWb_ESI%V1b{?KP?bjQZ3!#Ecj}*;UXjtXWW!UE75K|FXv0R@k z-}3UaP>s#ZT315EoWI*G?iHEsTYqCk#Fq4x_H5h&s~!mlOi9|$Q9NPkm9zPdH!C-8 zWS&qJ@P6;gTDyjv-8arlJ(*XX9`WtO-}Z|RLJM`xuiOtRF7R9LGpX%od%^5=?1jf_ zIZNhvuN76X5_1r?JoI1T(~-p!wH5c)*DqhY@6i-N%e2dpu|Q8Uc)I$ztaD0e0ssj5 By>0*i literal 0 HcmV?d00001 diff --git a/public/icons/entertainment.png b/public/icons/entertainment.png new file mode 100644 index 0000000000000000000000000000000000000000..eb764fd0324d9342eae2b72fb1d88a3fe25ddd32 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRW4EV^V~EA+qf-tRJyGCsk)GgqjOpJ0`nPg9=Y7m(KH0EU^zND})hk(% zm-O8<>(kfNa57!2H8wV0eUJSE-yf0u13B*sTg|=m56(CKFMMXLn&#DMs*b-676kpS zYZRTKsh_a>nZcui^7n@CEE~3Due^dEtzUp)6 zf}PuI>f#eJ<369Z`?~s|_A%CJ3=8b%g+=M7yjs)g$#(IKy0Wd0W_M+2DznawDPmts z8Y*^$i}Bnyli%QDz3y1f4$^ZZW literal 0 HcmV?d00001 diff --git a/public/icons/games.png b/public/icons/games.png new file mode 100644 index 0000000000000000000000000000000000000000..4a4a6873e05ebcd6add0893bfdf6ffe40c314a1c GIT binary patch literal 515 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRqnD?PV~EA+y&;~HjwtX5XIryu`1k*oWeda1CBZRiHH#cL^qmgucmJWo zWc*hu;N`S}dAr;qW=t#S%nHA!n^Mz0V@6R}&|z!8DJm;xS1v!DBKrQW6YKn~*Y@%q zcAJ>Nn2^6#+PXNZQ{`mJxrJw~g=1B_Pn`39qtv?Qo6(9_j0dJNYV$GFq)P7aFg^J-?FYG03FA@Ez Q0vO^9p00i_>zopr06j$Ua{vGU literal 0 HcmV?d00001 diff --git a/public/icons/groundbreaking.png b/public/icons/groundbreaking.png new file mode 100644 index 0000000000000000000000000000000000000000..c67b729861a36cfec402f78789fff46ff592a18e GIT binary patch literal 621 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jR;}%aB#}JFtbEjUMwnTx)MVqCOPvX`8{bh0tTX&bHoqxt@c|AgLAC{I{V8r4gv}rPw)Ken`R{-lCdQ^FPBU5A&*yEb>2ib&n29<&97&CR9L)lV{nf3 zj)1hMQRwnfb^p z|Jdsjc1#T~R#VtuuC3X&+bz6r1;d2j6)(&mbgwa#%MwUd)bG9abJe06nb~i078!5V zo1o_*(_C$GK;ax~++(k-dYL=E6VH9j-*Wr4#?6zwZ<&%_=e(5dcAK=!;&hE+mh#J| z7CxSSmo_taF{?~xP)SpWQ18EIaZ-wrMI}u|y;^Xo;u@XOxHS`FrQ>w0GnZ~wf0toZ zy7l`(QL&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRW4Nb_V~B+0+k=dg9w~@89AsB)QFyrj_xv*Buq{`%X6YH+{qys#k&P_( z^x4x7&K9|6s<)e;dwrL&^X5+>+w87B@cqb;r?=+wg{W6XVMd7v>rqE?_sa7C5-_R>M0k%PV_sH72PjFl}nSoW~d@ zz2R3DbKA^E=U%2Ub@g68D=vCNSB2ePXnEG-OKWU+g?)ti54WCv^gz+FG3|NK92;Nm z>3PhTwiIq}(e_`xvEcfg&dWl5vovD0^;dm!T6H*LM|4~H#!qRy?~fgF&P@I?>&JvF g`Ng9D{=Aak?JAb=C}`3pVDK||y85}Sb4q9e0LDc6-2eap literal 0 HcmV?d00001 diff --git a/public/icons/lifestyle.png b/public/icons/lifestyle.png new file mode 100644 index 0000000000000000000000000000000000000000..ccaf52f133d5b62741c647505c7762493e63cf72 GIT binary patch literal 498 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRqot>dV~B+0+k>1@W4v3vl*wFCrf3~>%%)+GSteZoBJT5Ve{u^?} zVO79lg@o8~Cv`&`?xME%}hR z-@zw5Gp4lZOBuSBFPS8(^4Pq+uz(IoHJQR+6x%K44$rjF6*2UngDMbL&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRqlu@BV~EA+-YFNEniV))Px}-~-uwUm*34aAN5%A68xIK1S{AlN(WvT| za+>qRvo<@{@@&`~6*5hl9k6&2(U0KnlDkwXc zLuuNI` z&uncp{E?Vb`cma>LT^ULGpoa~UBVYU+OHh9SCCrp^Q2{Z>Z!IOC8L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRqp_!pV~EA+yHh-;H5>4-K4m!z}UQ-1EU#|8tsn zGWtGaEa6&f8eq;EyIe|e<5W9V>B8MhU*3Pro?Lz3H)rnFAA&wQ);n?*1Ycl#!k8mt zY_5Od>-A+UuIUQ`4}IHP)bLZps@r1$-|bm^kN@dRb#;+C-ZpKaZM6dDi|6Os7{BE7 z_B^m>uaioAk>MBlQvdpE1@TONb)_Wk1A20)23p^o|L&0&e|2|b#mtv4Q^Ix^-t|@8 s;lOF9Sz9UaWrya(->$!dHv~W8XId&O$8tT=2N<*rp00i_>zopr0OC^SoB#j- literal 0 HcmV?d00001 diff --git a/public/icons/news.png b/public/icons/news.png new file mode 100644 index 0000000000000000000000000000000000000000..c9dd09d28fdd9ba28409a55806fa65bc3f42f8e2 GIT binary patch literal 625 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jR;|@<3#}En0y9c9#R4fHt4=QU;;o9p|{(tpc9Sw~QjobdoNSFWfj(Ye$ z<72s>!%WE)Q|%+YN+f59?LV=+`E-QWk_jKXOH!sx%#___W&d&J!czzJ*0U-J*}t^n z_4>BBeMgYe`m_7DF7~`cr|vs+5I^qqG%%}jmJwd24l)dhLHaeZD=RUG~vS1z2LWPbT!aOi`shjvSQk6g-d zzPnU%RgJ_emB)(xokxTg%w02e-NE+7PWx*7_8Tc|WXVxo@Zv*=sE@6#b5dqopU#$` z6`%4}Y%bqC=d80+l4rVi?z_pm*xvVX&0nMV(3ts{^Tmh0p$_|M^zx7SJ_^&@AIs`A zwSD2d?eor^@KE8mb$u&kBO`h_FKkon;};5sOK-lpwk+!6!!Xf3ORi1aVe?sN)qw#2 zYc+dc<|#~Ey}taeZYl2rjW1`XY};pPm9XpW!>f5$-CvyjboyRJ{4UANzF9w%ep}A_ gaq5@PpZR~7Lt~^Tg=SVQ0LCwar>mdKI;Vst0KkhjH2?qr literal 0 HcmV?d00001 diff --git a/public/icons/photo-video.png b/public/icons/photo-video.png new file mode 100644 index 0000000000000000000000000000000000000000..0e2ff6cb1da284a516c5942a42424ce269be02f0 GIT binary patch literal 386 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$Fk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XbOU@sT!Hj|qQOFs)w6*b_)3EOf*Ax93>*Ry8v57oKY#yy z-tk})pv*;27sn8d^T`Pk83pg}vick>e}8WwcjP{2=E;&0Gu|)c-e;Qdpk{4@0eiyR2!8yPezSjQf{XT+a~OxPMaJ_dMlUlNmdKI;Vst09dEGWdHyG literal 0 HcmV?d00001 diff --git a/public/rocket_16.png b/public/icons/rocket_16.png similarity index 100% rename from public/rocket_16.png rename to public/icons/rocket_16.png diff --git a/public/rocket_32.png b/public/icons/rocket_32.png similarity index 100% rename from public/rocket_32.png rename to public/icons/rocket_32.png diff --git a/public/rocket_bag_128.png b/public/icons/rocket_bag_128.png similarity index 100% rename from public/rocket_bag_128.png rename to public/icons/rocket_bag_128.png diff --git a/public/rocket_bag_48.png b/public/icons/rocket_bag_48.png similarity index 100% rename from public/rocket_bag_48.png rename to public/icons/rocket_bag_48.png diff --git a/public/rocket_bag_512.png b/public/icons/rocket_bag_512.png similarity index 100% rename from public/rocket_bag_512.png rename to public/icons/rocket_bag_512.png diff --git a/public/rocket_bag_512_maskable.png b/public/icons/rocket_bag_512_maskable.png similarity index 100% rename from public/rocket_bag_512_maskable.png rename to public/icons/rocket_bag_512_maskable.png diff --git a/public/rocket_bag_60.png b/public/icons/rocket_bag_60.png similarity index 100% rename from public/rocket_bag_60.png rename to public/icons/rocket_bag_60.png diff --git a/public/rocket_bag_64.png b/public/icons/rocket_bag_64.png similarity index 100% rename from public/rocket_bag_64.png rename to public/icons/rocket_bag_64.png diff --git a/public/icons/shopping.png b/public/icons/shopping.png new file mode 100644 index 0000000000000000000000000000000000000000..2cfe3ce42c6eee2458cab8c70998e40bf73054e6 GIT binary patch literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$Fk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xv;urWT!Hj|;y~N(Q~E&Fyd^Yr|c) z4#T^m4;V7@685ZS5Kq~C@&MS)_87?2L5j(N>%!BHpXJ?#R<_a07pWAupfi>gZ>NySvd0#Z$374qQU%<`e zsv+`ZXOCXSZpJ4JCJZG{6oA|oy%p7sY71LJ1+rxl`+e6ml^Y%qWs~(e=zQckH$$9G W>qR~;_sc-vFnGH9xvXL&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRW3s1c&jDTaA1E$d(>o_(J91Cgo##%`ab1U@4l(TeDc~Lilcm>MjFa zr9~3`H&knPw|HkPd2B1_dekbmJiK-@vy5OQ&)Z!=)vo7n1$U_G_(@9_Tze(zSTd?75n~x{*MdoXsSKDFz>z~$+tGYh!Re{Ck(H_3NG16@vduA;+ z6%jE^^Lqb|)0OvmZNXzHN3K~j_OESP`}s`n>JJ_(r>~aW+wx;aOO~zkZvBT#OvLm! z8VVo$xmO(B@Gh*{pi5P$i^*e7ixXqO)>$XcoGG&HUTa`{n&Cq8srkTxw_rt r4p+C1lI7e#F)8cLoUH#X|AARSO}OIl{pCHts9^AP^>bP0l+XkK7oGxj literal 0 HcmV?d00001 diff --git a/public/icons/sports.png b/public/icons/sports.png new file mode 100644 index 0000000000000000000000000000000000000000..d65996545a3f51b13921168316bab1de08348fb4 GIT binary patch literal 517 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRqmQSHV~EA+wIQBmM-_O46WVwh-q+vKJHWT_!~aEBway1_(CKDq@Mh{~ zj61z9JTL6R;l<0Vz6U+>ad8o`v_EoYW2mwEN>R&~&os7bJQtY0(A$uK;q+G910mbq zF6^n&v$XxVb@iJ4@n0geqgGCnJbWSeck4xVhK9*v+rqN5wYez2;9G&$h1(>vil2SpI?Pl1e8;}S z2FV5+_8e-c>{tF|FEHCXL|d|k`RnYmTm2@*M$bha1y-DUsI#fWKk4pmUYR+eb5>W& z-~ayja9UrPtW#=U=BL}C(hTa;-M(~Jm~Z(pv%}3~wgs2q3zt80Z)*Cp0K=TY)78&qol`;+05|;k6aWAK literal 0 HcmV?d00001 diff --git a/public/icons/tools.png b/public/icons/tools.png new file mode 100644 index 0000000000000000000000000000000000000000..8da74e489b052d21486b72626c482527fd45fc77 GIT binary patch literal 556 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jRW3i`;V~EA+-l=byS_OF8CQk~x@i+eMO`ekzKb`(k@jWp;eb%F(%iCEy zUL>AdR+!R$TF-m2j`FiJT+!2^^p5!)ENH$x@y+T}g1w+vNJU6j7H+^ZtH{9E*i6Pk6N zee+^;YqB=lBr0*cD_L2^sx0L7yRl$Gt8v%)lA_q}0V@563Xc}>Px5Jh!k@lM^vROl zDkuD;o^4rm`{2Yc+i!ipHgWI%fFVdQ I&MBb@07J0|Q~&?~ literal 0 HcmV?d00001 diff --git a/public/icons/travel.png b/public/icons/travel.png new file mode 100644 index 0000000000000000000000000000000000000000..0be6f97ee1ca43ea4b1887022620082f3f94945e GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$Fk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X3<7*YT!Hj|Qh>EDivds%e@T#EFoS@CfkQw-LBss@`_JEh z&({4&5Gb|E)5S4F<9u?$0=|S#8KDm1$#;Pk%x5t<`0M}n?|=UHsomH=v(fQ4`y_^F zcX#J{W@hH&{C$0V9fEP+|1WOX-`#ks;Y;sNgNd>Z*X&u@{>CeQHvdsy$k_ao_n&*r zdH;kJ_6IKPKlq>d$}YD5ygVPJ8N*&l;es89ZJ6T-G@yGywpM CG^LmT literal 0 HcmV?d00001 diff --git a/public/icons/weather.png b/public/icons/weather.png new file mode 100644 index 0000000000000000000000000000000000000000..c9dd09d28fdd9ba28409a55806fa65bc3f42f8e2 GIT binary patch literal 625 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G#sNMdu0Z-fiD3KGM@>L&0wqCy!3+Wl1`YuU1r7c4*Y7`n z|GlzQQx^jR;|@<3#}En0y9c9#R4fHt4=QU;;o9p|{(tpc9Sw~QjobdoNSFWfj(Ye$ z<72s>!%WE)Q|%+YN+f59?LV=+`E-QWk_jKXOH!sx%#___W&d&J!czzJ*0U-J*}t^n z_4>BBeMgYe`m_7DF7~`cr|vs+5I^qqG%%}jmJwd24l)dhLHaeZD=RUG~vS1z2LWPbT!aOi`shjvSQk6g-d zzPnU%RgJ_emB)(xokxTg%w02e-NE+7PWx*7_8Tc|WXVxo@Zv*=sE@6#b5dqopU#$` z6`%4}Y%bqC=d80+l4rVi?z_pm*xvVX&0nMV(3ts{^Tmh0p$_|M^zx7SJ_^&@AIs`A zwSD2d?eor^@KE8mb$u&kBO`h_FKkon;};5sOK-lpwk+!6!!Xf3ORi1aVe?sN)qw#2 zYc+dc<|#~Ey}taeZYl2rjW1`XY};pPm9XpW!>f5$-CvyjboyRJ{4UANzF9w%ep}A_ gaq5@PpZR~7Lt~^Tg=SVQ0LCwar>mdKI;Vst0KkhjH2?qr literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json index 40e6336..b8484da 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,14 +3,14 @@ "short_name": "FFAA", "description": "FF Apps Archive contains thousands of apps from the defunct Firefox Marketplace.", "icons": [ - { "src": "/rocket_16.png", "sizes": "16x16", "type": "image/png", "purpose": "monochrome" }, - { "src": "/rocket_32.png", "sizes": "32x32", "type": "image/png", "purpose": "monochrome" }, - { "src": "/rocket_bag_48.png", "sizes": "48x48", "type": "image/png" }, - { "src": "/rocket_bag_60.png", "sizes": "60x60", "type": "image/png" }, - { "src": "/rocket_bag_64.png", "sizes": "64x64", "type": "image/png" }, - { "src": "/rocket_bag_128.png", "sizes": "128x128", "type": "image/png" }, - { "src": "/rocket_bag_512.png", "sizes": "512x512", "type": "image/png" }, - { "src": "/rocket_bag_512_maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + { "src": "/icons/rocket_16.png", "sizes": "16x16", "type": "image/png", "purpose": "monochrome" }, + { "src": "/icons/rocket_32.png", "sizes": "32x32", "type": "image/png", "purpose": "monochrome" }, + { "src": "/icons/rocket_bag_48.png", "sizes": "48x48", "type": "image/png" }, + { "src": "/icons/rocket_bag_60.png", "sizes": "60x60", "type": "image/png" }, + { "src": "/icons/rocket_bag_64.png", "sizes": "64x64", "type": "image/png" }, + { "src": "/icons/rocket_bag_128.png", "sizes": "128x128", "type": "image/png" }, + { "src": "/icons/rocket_bag_512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icons/rocket_bag_512_maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ], "start_url": "/", "display": "standalone" diff --git a/public/manifest.webapp b/public/manifest.webapp index 5c0bcba..9456870 100644 --- a/public/manifest.webapp +++ b/public/manifest.webapp @@ -6,13 +6,13 @@ "url": "https://danielherr.software" }, "icons": { - "16": "/rocket_16.png", - "32": "/rocket_32.png", - "48": "/rocket_bag_48.png", - "60": "/rocket_bag_60.png", - "64": "/rocket_bag_64.png", - "128": "/rocket_bag_128.png", - "512": "/rocket_bag_512.png" + "16": "/icons/rocket_16.png", + "32": "/icons/rocket_32.png", + "48": "/icons/rocket_bag_48.png", + "60": "/icons/rocket_bag_60.png", + "64": "/icons/rocket_bag_64.png", + "128": "/icons/rocket_bag_128.png", + "512": "/icons/rocket_bag_512.png" }, "launch_path": "/", "version": "1" diff --git a/src/categories.json b/src/categories.json new file mode 100644 index 0000000..15997b7 --- /dev/null +++ b/src/categories.json @@ -0,0 +1,29 @@ +[ + { "slug": "games", "name": "Games" }, + { "slug": "books-comics", "name": "Books & Comics" }, + { "slug": "business", "name": "Business" }, + { "slug": "education", "name": "Education" }, + { "slug": "entertainment", "name": "Entertainment" }, + { "slug": "food-drink", "name": "Food & Drink" }, + { "slug": "health-fitness", "name": "Health & Fitness" }, + { "slug": "humor", "name": "Humor" }, + { "slug": "internet", "name": "Internet" }, + { "slug": "kids", "name": "Kids" }, + { "slug": "lifestyle", "name": "Lifestyle" }, + { "slug": "maps-navigation", "name": "Maps & Navigation" }, + { "slug": "music", "name": "Music" }, + { "slug": "news", "name": "News" }, + { "slug": "personalization", "name": "Personalization" }, + { "slug": "photo-video", "name": "Photo & Video" }, + { "slug": "productivity", "name": "Productivity" }, + { "slug": "reference", "name": "Reference" }, + { "slug": "science-tech", "name": "Science & Tech" }, + { "slug": "shopping", "name": "Shopping" }, + { "slug": "social", "name": "Social" }, + { "slug": "sports", "name": "Sports" }, + { "slug": "travel", "name": "Travel" }, + { "slug": "utilities", "name": "Utilities" }, + { "slug": "weather", "name": "Weather" }, + { "slug": "news-weather", "name": "News & Weather" }, + { "slug": "books", "name": "Books" } +] \ No newline at end of file diff --git a/src/components/apps_list.astro b/src/components/apps_list.astro index d2710c2..f21d962 100644 --- a/src/components/apps_list.astro +++ b/src/components/apps_list.astro @@ -1,9 +1,10 @@ --- -const { apps } = Astro.props +const { apps, images } = Astro.props --- -
    +
      { apps.map((app) => (
    • + { images && } { app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] }
    • )) } diff --git a/src/components/categories.astro b/src/components/categories.astro new file mode 100644 index 0000000..8005fa3 --- /dev/null +++ b/src/components/categories.astro @@ -0,0 +1,11 @@ +--- +import categories from "../categories.json" with { type: "json" } +--- + + \ No newline at end of file diff --git a/src/components/common_imports.astro b/src/components/common_imports.astro index d0ab7f5..0bfe711 100644 --- a/src/components/common_imports.astro +++ b/src/components/common_imports.astro @@ -1,3 +1,3 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/header.astro b/src/components/header.astro new file mode 100644 index 0000000..d037f40 --- /dev/null +++ b/src/components/header.astro @@ -0,0 +1,5 @@ + +
      + +

      FF Apps Archive (beta)

      +
      \ No newline at end of file diff --git a/src/components/nav.astro b/src/components/nav.astro new file mode 100644 index 0000000..eb44fe1 --- /dev/null +++ b/src/components/nav.astro @@ -0,0 +1,13 @@ +--- +import Categories from "../components/categories.astro" +--- + + \ No newline at end of file diff --git a/src/components/search.astro b/src/components/search.astro new file mode 100644 index 0000000..841242b --- /dev/null +++ b/src/components/search.astro @@ -0,0 +1,6 @@ + +
      + + +
      +
      \ No newline at end of file diff --git a/src/pages/[category].astro b/src/pages/[category].astro new file mode 100644 index 0000000..e7e58a2 --- /dev/null +++ b/src/pages/[category].astro @@ -0,0 +1,42 @@ +--- +import * as Content from "astro:content" + +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" +import Metadata from "../components/common_metadata.astro" +import AppsList from "../components/apps_list.astro" + +import categories from "../categories.json" with { type: "json" } + +export async function getStaticPaths() { + return categories.map(category => ({ + params: { category: category.slug }, props: { category }, +})) } + +let { category } = Astro.props + +let apps = await Content.getCollection("metadata", ({ data }) => data.categories.includes(category.slug)) +--- + + + + + + + + + + { category.name } - FF Apps Archive + + +
      +
      +
      + +

      { category.name }

      +
      + +
      +
      + + \ No newline at end of file diff --git a/src/pages/all.astro b/src/pages/all.astro index fd8901c..f6edf88 100644 --- a/src/pages/all.astro +++ b/src/pages/all.astro @@ -2,8 +2,9 @@ import * as Content from "astro:content" import Imports from "../components/common_imports.astro" -import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" import AppsList from "../components/apps_list.astro" let apps = await Content.getCollection("metadata") @@ -13,15 +14,15 @@ let apps = await Content.getCollection("metadata") - All Apps - FF Apps Archive -
      -

      All Apps - FF Apps Archive (beta)

      -
      +
      +
      +

      All Apps

      +
      diff --git a/src/pages/all_images.astro b/src/pages/all_images.astro new file mode 100644 index 0000000..6c5934e --- /dev/null +++ b/src/pages/all_images.astro @@ -0,0 +1,29 @@ +--- +import * as Content from "astro:content" + +import Imports from "../components/common_imports.astro" +import Footer from "../components/footer.astro" +import Metadata from "../components/common_metadata.astro" +import AppsList from "../components/apps_list.astro" + +let apps = await Content.getCollection("metadata") +--- + + + + + + + + All Apps - FF Apps Archive + + +
      +

      All Apps - FF Apps Archive (beta)

      +
      +
      + +
      +
      + + \ No newline at end of file diff --git a/src/pages/app/[slug].astro b/src/pages/app/[slug].astro index f46f315..3a0c137 100644 --- a/src/pages/app/[slug].astro +++ b/src/pages/app/[slug].astro @@ -1,9 +1,12 @@ --- import * as Content from "astro:content" +import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import Metadata from "../../components/common_metadata.astro" +import categories from "../../categories.json" with { type: "json" } + export async function getStaticPaths() { let apps = await Content.getCollection("metadata") return apps.map(app => ({ @@ -34,30 +37,39 @@ let releasenotes = app.data.release_notes ? app.data.release_notes["en-US"] ?? a { name } - FF Apps Archive -
      -

      FF Apps Archive (beta)

      -
      +
      - -

      { name }

      - { app.data.app_type != "privileged" && } - { app.data.package_path && Download } -
      -
      +
      + +

      { name }

      +

      Developer: { app.data.author }

      +

      Rating: { app.data.ratings.average }, { app.data.ratings.count } ratings

      +
      +
      + { app.data.app_type != "privileged" && } + { app.data.package_path && Download } +
      +
      { app.data.previews.map(preview => ) }
      -

      +

      +
      { homepage &&

      Homepage: { homepage }

      } -

      Developer: { app.data.author }

      Support: Web, Email

      -

      Categories: { app.data.categories.join(", ") }

      +

      Categories: +

      +

      { app.data.is_offline &&

      Works Offline

      } -

      Rating: { app.data.ratings.average }, { app.data.ratings.count } ratings

      { app.data.app_type != "hosted" &&

      Size: { Math.round(app.data.file_size / 1024 / 1024) } MB

      }

      Created: { app.data.created }

      Updated: { app.data.last_updated }

      Version: { app.data.current_version }

      - { releasenotes &&

      Release Notes:

      } +
      + { releasenotes &&

      Release Notes:

      } +
      +

      App Type: { app.data.app_type }

      diff --git a/src/pages/dead.astro b/src/pages/dead.astro index af83524..da07b65 100644 --- a/src/pages/dead.astro +++ b/src/pages/dead.astro @@ -1,24 +1,26 @@ --- import Imports from "../components/common_imports.astro" -import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" import AppsList from "../components/apps_list.astro" -import { dead, total } from "../utils/validateHostedApps" + +import { dead, total } from "../validateHostedApps.js" --- - All Dead Hosted Apps - FF Apps Archive -
      -

      All Dead Hosted Apps - FF Apps Archive (beta)

      -
      +
      +
      +

      All Dead Hosted Apps

      +

      Found { dead.length } dead hosted apps out of { total } total hosted apps.

      diff --git a/src/pages/hosted.astro b/src/pages/hosted.astro index 61313e5..817f219 100644 --- a/src/pages/hosted.astro +++ b/src/pages/hosted.astro @@ -2,27 +2,28 @@ import * as Content from "astro:content" import Imports from "../components/common_imports.astro" -import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" import AppsList from "../components/apps_list.astro" -let apps = await Content.getCollection("metadata", ({ data }) => data.app_type == "hosted") +let hosted = await Content.getCollection("metadata", ({ data }) => data.app_type == "hosted") --- - All Hosted Apps - FF Apps Archive -
      -

      All Hosted Apps - FF Apps Archive (beta)

      -
      +
      - +
      +

      All Hosted Apps

      +
      +
      diff --git a/src/pages/index.astro b/src/pages/index.astro index a937789..1ef8d61 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,18 +2,20 @@ import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" +import Header from "../components/header.astro" +import Categories from "../components/categories.astro" --- - FF Apps Archive
      +

      FF Apps Archive (beta)

      @@ -25,6 +27,7 @@ import Metadata from "../components/common_metadata.astro"

      All Hosted Apps

      Live Hosted Apps - can be installed, but might no longer function

      Dead Hosted Apps - cannot be installed

      +

      Firefox apps can be installed on any version of Firefox OS and on old versions of the Firefox web browser v16 - v46.
      Firefox desktop v45
      diff --git a/src/pages/live.astro b/src/pages/live.astro index 391e5a3..3f1a74e 100644 --- a/src/pages/live.astro +++ b/src/pages/live.astro @@ -1,24 +1,26 @@ --- import Imports from "../components/common_imports.astro" -import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" import AppsList from "../components/apps_list.astro" -import { live, total } from "../utils/validateHostedApps" + +import { live, total } from "../validateHostedApps.js" --- - All Live Hosted Apps - FF Apps Archive -

      -

      All Live Hosted Apps - FF Apps Archive (beta)

      -
      +
      +
      +

      All Live Hosted Apps

      +

      Found { live.length } live hosted apps out of { total } total hosted apps.

      diff --git a/src/pages/packaged.astro b/src/pages/packaged.astro index a45df0d..1820a18 100644 --- a/src/pages/packaged.astro +++ b/src/pages/packaged.astro @@ -2,8 +2,9 @@ import * as Content from "astro:content" import Imports from "../components/common_imports.astro" -import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" import AppsList from "../components/apps_list.astro" let packaged = await Content.getCollection("metadata", ({ data }) => data.app_type == "packaged") @@ -13,16 +14,16 @@ let packaged = await Content.getCollection("metadata", ({ data }) => data.app_ty - All Packaged Apps - FF Apps Archive -
      -

      All Packaged Apps - FF Apps Archive (beta)

      -
      +
      - +
      +

      All Packaged Apps

      +
      +
      diff --git a/src/pages/privileged.astro b/src/pages/privileged.astro index a4ef718..e0c9c1b 100644 --- a/src/pages/privileged.astro +++ b/src/pages/privileged.astro @@ -2,8 +2,9 @@ import * as Content from "astro:content" import Imports from "../components/common_imports.astro" -import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" import AppsList from "../components/apps_list.astro" let privileged = await Content.getCollection("metadata", ({ data }) => data.app_type == "privileged") @@ -13,15 +14,15 @@ let privileged = await Content.getCollection("metadata", ({ data }) => data.app_ - All Privileged Apps - FF Apps Archive -
      -

      All Privileged Apps - FF Apps Archive (beta)

      -
      +
      +
      +

      All Privileged Apps

      +
      diff --git a/src/utils/validateHostedApps.js b/src/validateHostedApps.js similarity index 100% rename from src/utils/validateHostedApps.js rename to src/validateHostedApps.js From caa7330b47f3a71625348459eecf63618a0a57e9 Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Mon, 3 Nov 2025 21:13:08 -0500 Subject: [PATCH 04/38] Add details to app lists. Closes #18. Paginate app type lists. Closes #10. Paginate app category lists. Closes #19. Add FFOS sideloading instructions. Closes #3. --- public/common.css | 4 - public/details.css | 6 +- public/install.mjs | 3 + public/lists.css | 39 ++++++++ src/components/apps_list.astro | 20 +++- src/components/categories.astro | 2 +- src/components/header.astro | 7 +- src/pages/[category]/[page].astro | 93 +++++++++++++++++++ .../index.astro} | 16 ++-- src/pages/all/[page].astro | 81 ++++++++++++++++ src/pages/{all.astro => all/index.astro} | 11 ++- src/pages/all_images.astro | 29 ------ src/pages/app/[slug].astro | 3 +- src/pages/dead/[page].astro | 83 +++++++++++++++++ src/pages/{dead.astro => dead/index.astro} | 13 +-- src/pages/hosted/[page].astro | 84 +++++++++++++++++ .../{hosted.astro => hosted/index.astro} | 11 ++- src/pages/index.astro | 20 ++-- src/pages/live/[page].astro | 83 +++++++++++++++++ src/pages/{live.astro => live/index.astro} | 13 +-- src/pages/packaged/[page].astro | 84 +++++++++++++++++ .../{packaged.astro => packaged/index.astro} | 11 ++- src/pages/privileged/[page].astro | 84 +++++++++++++++++ .../index.astro} | 11 ++- src/pages/sideloading.astro | 91 ++++++++++++++++++ src/paginate_apps.mjs | 38 ++++++++ 26 files changed, 849 insertions(+), 91 deletions(-) create mode 100644 public/install.mjs create mode 100644 public/lists.css create mode 100644 src/pages/[category]/[page].astro rename src/pages/{[category].astro => [category]/index.astro} (63%) create mode 100644 src/pages/all/[page].astro rename src/pages/{all.astro => all/index.astro} (53%) delete mode 100644 src/pages/all_images.astro create mode 100644 src/pages/dead/[page].astro rename src/pages/{dead.astro => dead/index.astro} (50%) create mode 100644 src/pages/hosted/[page].astro rename src/pages/{hosted.astro => hosted/index.astro} (56%) create mode 100644 src/pages/live/[page].astro rename src/pages/{live.astro => live/index.astro} (50%) create mode 100644 src/pages/packaged/[page].astro rename src/pages/{packaged.astro => packaged/index.astro} (57%) create mode 100644 src/pages/privileged/[page].astro rename src/pages/{privileged.astro => privileged/index.astro} (57%) create mode 100644 src/pages/sideloading.astro create mode 100644 src/paginate_apps.mjs diff --git a/public/common.css b/public/common.css index 64b7480..5f6b5c7 100644 --- a/public/common.css +++ b/public/common.css @@ -4,8 +4,4 @@ header img { float: left; margin-right: 1em; -} -#categories_list, #apps_list { - list-style: none; - padding: 0; } \ No newline at end of file diff --git a/public/details.css b/public/details.css index 6ec52e0..f0e2512 100644 --- a/public/details.css +++ b/public/details.css @@ -1,8 +1,8 @@ h1 { - font-size: x-large; + font-size: 2em; } h2 { - font-size: xx-large; + font-size: 3em; } #screenshots_list { display: -moz-box; @@ -18,7 +18,7 @@ img { #action_buttons > * { display: block; width: 100%; - font-size: large; + font-size: 1.5em; padding: 0.5em 2em; margin-bottom: 1em; border: thin solid lightgrey; diff --git a/public/install.mjs b/public/install.mjs new file mode 100644 index 0000000..f3221f0 --- /dev/null +++ b/public/install.mjs @@ -0,0 +1,3 @@ +if(install_button.dataset.manifest) { + install_button.disabled = true +} \ No newline at end of file diff --git a/public/lists.css b/public/lists.css new file mode 100644 index 0000000..67dced1 --- /dev/null +++ b/public/lists.css @@ -0,0 +1,39 @@ +#apps_list { + list-style: none; + padding: 0; +} +#apps_list > li { + margin-top: 20px; +} +#apps_list a { + display: block; + overflow: auto; + text-decoration: none; +} +.icon { + float: left; + margin-right: 1em; +} +.name { + display: inline; + margin: 0; +} +.author { + float: right; +} +.secondary { + clear: right; +} +.rating {} +.type {} +.categories { + display: inline; + list-style: none; + padding: 0; +} +.categories li { + display: inline; +} +.categories li:not(:last-child)::after { + content: ", " +} \ No newline at end of file diff --git a/src/components/apps_list.astro b/src/components/apps_list.astro index f21d962..c41dc10 100644 --- a/src/components/apps_list.astro +++ b/src/components/apps_list.astro @@ -1,11 +1,25 @@ --- -const { apps, images } = Astro.props +import categories from "../categories.json" with { type: "json" } + +const { apps } = Astro.props --- \ No newline at end of file diff --git a/src/components/categories.astro b/src/components/categories.astro index 8005fa3..834ae95 100644 --- a/src/components/categories.astro +++ b/src/components/categories.astro @@ -5,7 +5,7 @@ import categories from "../categories.json" with { type: "json" } \ No newline at end of file diff --git a/src/components/header.astro b/src/components/header.astro index d037f40..e42f47e 100644 --- a/src/components/header.astro +++ b/src/components/header.astro @@ -1,5 +1,6 @@ -
      - -

      FF Apps Archive (beta)

      + + +

      FF Apps Archive (beta)

      +
      \ No newline at end of file diff --git a/src/pages/[category]/[page].astro b/src/pages/[category]/[page].astro new file mode 100644 index 0000000..167e676 --- /dev/null +++ b/src/pages/[category]/[page].astro @@ -0,0 +1,93 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +import categories from "../../categories.json" with { type: "json" } + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + const pages = characters.flatMap(page => + categories.map(category => ({ + params: { category: category.slug, page }, + props: { category } + })) + ) + return pages +} + +let { category, page } = Astro.params + +let apps = await Content.getCollection("metadata", function(app) { + if(! app.data.categories.includes(category)) { + return false + } + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + + Page { page } - { Astro.props.category.name } - FF Apps Archive + + + +
      +
      +
      + +

      { Astro.props.category.name } - Page { page }

      +
      + + + +
      +
      + + \ No newline at end of file diff --git a/src/pages/[category].astro b/src/pages/[category]/index.astro similarity index 63% rename from src/pages/[category].astro rename to src/pages/[category]/index.astro index e7e58a2..df6cb67 100644 --- a/src/pages/[category].astro +++ b/src/pages/[category]/index.astro @@ -1,17 +1,16 @@ --- import * as Content from "astro:content" -import Header from "../components/header.astro" -import Footer from "../components/footer.astro" -import Metadata from "../components/common_metadata.astro" -import AppsList from "../components/apps_list.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import Metadata from "../../components/common_metadata.astro" +import AppsList from "../../components/apps_list.astro" -import categories from "../categories.json" with { type: "json" } +import categories from "../../categories.json" with { type: "json" } export async function getStaticPaths() { - return categories.map(category => ({ - params: { category: category.slug }, props: { category }, -})) } + return categories.map(category => ({ params: { category: category.slug }, props: { category } })) +} let { category } = Astro.props @@ -23,6 +22,7 @@ let apps = await Content.getCollection("metadata", ({ data }) => data.categories + diff --git a/src/pages/all/[page].astro b/src/pages/all/[page].astro new file mode 100644 index 0000000..8d11d4e --- /dev/null +++ b/src/pages/all/[page].astro @@ -0,0 +1,81 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + return characters.map(page => ({ + params: { page }, +})) } + +let { page } = Astro.params + +let apps = await Content.getCollection("metadata", function(app) { + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + Page { page } - All Apps - FF Apps Archive + + + +
      +
      +
      +

      All Apps - Page { page }

      +
      + + + +
      +
      + + \ No newline at end of file diff --git a/src/pages/all.astro b/src/pages/all/index.astro similarity index 53% rename from src/pages/all.astro rename to src/pages/all/index.astro index f6edf88..f28d987 100644 --- a/src/pages/all.astro +++ b/src/pages/all/index.astro @@ -1,11 +1,11 @@ --- import * as Content from "astro:content" -import Imports from "../components/common_imports.astro" -import Metadata from "../components/common_metadata.astro" -import Header from "../components/header.astro" -import Footer from "../components/footer.astro" -import AppsList from "../components/apps_list.astro" +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" let apps = await Content.getCollection("metadata") --- @@ -14,6 +14,7 @@ let apps = await Content.getCollection("metadata") + All Apps - FF Apps Archive diff --git a/src/pages/all_images.astro b/src/pages/all_images.astro deleted file mode 100644 index 6c5934e..0000000 --- a/src/pages/all_images.astro +++ /dev/null @@ -1,29 +0,0 @@ ---- -import * as Content from "astro:content" - -import Imports from "../components/common_imports.astro" -import Footer from "../components/footer.astro" -import Metadata from "../components/common_metadata.astro" -import AppsList from "../components/apps_list.astro" - -let apps = await Content.getCollection("metadata") ---- - - - - - - - - All Apps - FF Apps Archive - - -
      -

      All Apps - FF Apps Archive (beta)

      -
      -
      - -
      -
      - - \ No newline at end of file diff --git a/src/pages/app/[slug].astro b/src/pages/app/[slug].astro index 3a0c137..5b8819d 100644 --- a/src/pages/app/[slug].astro +++ b/src/pages/app/[slug].astro @@ -30,7 +30,8 @@ let releasenotes = app.data.release_notes ? app.data.release_notes["en-US"] ?? a - + + diff --git a/src/pages/dead/[page].astro b/src/pages/dead/[page].astro new file mode 100644 index 0000000..d65ebf5 --- /dev/null +++ b/src/pages/dead/[page].astro @@ -0,0 +1,83 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +import { dead, total } from "../../validateHostedApps.js" + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + return characters.map(page => ({ + params: { page }, +})) } + +let { page } = Astro.params + +let apps = dead.filter(app => { + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + Page { page } - Dead Hosted Apps - FF Apps Archive + + + +
      +
      +
      +

      Dead Hosted Apps - Page { page }

      +
      + + + +
      +
      + + \ No newline at end of file diff --git a/src/pages/dead.astro b/src/pages/dead/index.astro similarity index 50% rename from src/pages/dead.astro rename to src/pages/dead/index.astro index da07b65..ebccac6 100644 --- a/src/pages/dead.astro +++ b/src/pages/dead/index.astro @@ -1,17 +1,18 @@ --- -import Imports from "../components/common_imports.astro" -import Metadata from "../components/common_metadata.astro" -import Header from "../components/header.astro" -import Footer from "../components/footer.astro" -import AppsList from "../components/apps_list.astro" +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" -import { dead, total } from "../validateHostedApps.js" +import { dead, total } from "../../validateHostedApps.js" --- + All Dead Hosted Apps - FF Apps Archive diff --git a/src/pages/hosted/[page].astro b/src/pages/hosted/[page].astro new file mode 100644 index 0000000..1e3a3fa --- /dev/null +++ b/src/pages/hosted/[page].astro @@ -0,0 +1,84 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + return characters.map(page => ({ + params: { page }, +})) } + +let { page } = Astro.params + +let apps = await Content.getCollection("metadata", function(app) { + if(app.data.app_type != "hosted") { + return false + } + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + Page { page } - Hosted Apps - FF Apps Archive + + + +
      +
      +
      +

      Hosted Apps - Page { page }

      +
      + + + +
      +
      + + \ No newline at end of file diff --git a/src/pages/hosted.astro b/src/pages/hosted/index.astro similarity index 56% rename from src/pages/hosted.astro rename to src/pages/hosted/index.astro index 817f219..8b4836e 100644 --- a/src/pages/hosted.astro +++ b/src/pages/hosted/index.astro @@ -1,11 +1,11 @@ --- import * as Content from "astro:content" -import Imports from "../components/common_imports.astro" -import Metadata from "../components/common_metadata.astro" -import Header from "../components/header.astro" -import Footer from "../components/footer.astro" -import AppsList from "../components/apps_list.astro" +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" let hosted = await Content.getCollection("metadata", ({ data }) => data.app_type == "hosted") --- @@ -14,6 +14,7 @@ let hosted = await Content.getCollection("metadata", ({ data }) => data.app_type + All Hosted Apps - FF Apps Archive diff --git a/src/pages/index.astro b/src/pages/index.astro index 1ef8d61..b927244 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,7 +2,6 @@ import Imports from "../components/common_imports.astro" import Footer from "../components/footer.astro" import Metadata from "../components/common_metadata.astro" -import Header from "../components/header.astro" import Categories from "../components/categories.astro" --- @@ -21,12 +20,12 @@ import Categories from "../components/categories.astro"

      FF Apps Archive contains thousands of apps from the defunct Firefox Marketplace.

      -

      All Apps

      -

      Packaged Apps - can be installed here

      -

      Privileged Apps - cannot be installed directly and must be sideloaded using developer tools

      -

      All Hosted Apps

      -

      Live Hosted Apps - can be installed, but might no longer function

      -

      Dead Hosted Apps - cannot be installed

      +

      All Apps

      +

      Packaged Apps - can be installed here

      +

      Privileged Apps - cannot be installed directly and must be sideloaded using developer tools

      +

      All Hosted Apps

      +

      Live Hosted Apps - can be installed, but might no longer function

      +

      Dead Hosted Apps - cannot be installed

      Firefox apps can be installed on any version of Firefox OS and on old versions of the Firefox web browser v16 - v46.
      @@ -34,6 +33,13 @@ import Categories from "../components/categories.astro" Firefox Android v45
      Firefox OS 2.5 Android launcher

      +

      + For instructions on sideloading privileged apps on Firefox OS, see Sideloading Instructions. +

      +

      Credits:
      + Daniel Herr - Project Creator and Engineer
      + Dimitrios Vlachos - Community Manager +

      diff --git a/src/pages/live/[page].astro b/src/pages/live/[page].astro new file mode 100644 index 0000000..44cb33f --- /dev/null +++ b/src/pages/live/[page].astro @@ -0,0 +1,83 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +import { live, total } from "../../validateHostedApps.js" + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + return characters.map(page => ({ + params: { page }, +})) } + +let { page } = Astro.params + +let apps = live.filter(app => { + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + Page { page } - Live Hosted Apps - FF Apps Archive + + + +
      +
      +
      +

      Live Hosted Apps - Page { page }

      +
      + + + +
      +
      + + \ No newline at end of file diff --git a/src/pages/live.astro b/src/pages/live/index.astro similarity index 50% rename from src/pages/live.astro rename to src/pages/live/index.astro index 3f1a74e..7b91f9b 100644 --- a/src/pages/live.astro +++ b/src/pages/live/index.astro @@ -1,17 +1,18 @@ --- -import Imports from "../components/common_imports.astro" -import Metadata from "../components/common_metadata.astro" -import Header from "../components/header.astro" -import Footer from "../components/footer.astro" -import AppsList from "../components/apps_list.astro" +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" -import { live, total } from "../validateHostedApps.js" +import { live, total } from "../../validateHostedApps.js" --- + All Live Hosted Apps - FF Apps Archive diff --git a/src/pages/packaged/[page].astro b/src/pages/packaged/[page].astro new file mode 100644 index 0000000..330954e --- /dev/null +++ b/src/pages/packaged/[page].astro @@ -0,0 +1,84 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + return characters.map(page => ({ + params: { page }, +})) } + +let { page } = Astro.params + +let apps = await Content.getCollection("metadata", function(app) { + if(app.data.app_type != "packaged") { + return false + } + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + Page { page } - Packaged Apps - FF Apps Archive + + + +
      +
      +
      +

      Packaged Apps - Page { page }

      +
      + + + +
      +
      + + \ No newline at end of file diff --git a/src/pages/packaged.astro b/src/pages/packaged/index.astro similarity index 57% rename from src/pages/packaged.astro rename to src/pages/packaged/index.astro index 1820a18..ba8be92 100644 --- a/src/pages/packaged.astro +++ b/src/pages/packaged/index.astro @@ -1,11 +1,11 @@ --- import * as Content from "astro:content" -import Imports from "../components/common_imports.astro" -import Metadata from "../components/common_metadata.astro" -import Header from "../components/header.astro" -import Footer from "../components/footer.astro" -import AppsList from "../components/apps_list.astro" +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" let packaged = await Content.getCollection("metadata", ({ data }) => data.app_type == "packaged") --- @@ -14,6 +14,7 @@ let packaged = await Content.getCollection("metadata", ({ data }) => data.app_ty + All Packaged Apps - FF Apps Archive diff --git a/src/pages/privileged/[page].astro b/src/pages/privileged/[page].astro new file mode 100644 index 0000000..c72e319 --- /dev/null +++ b/src/pages/privileged/[page].astro @@ -0,0 +1,84 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + return characters.map(page => ({ + params: { page }, +})) } + +let { page } = Astro.params + +let apps = await Content.getCollection("metadata", function(app) { + if(app.data.app_type != "privileged") { + return false + } + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + Page { page } - Privileged Apps - FF Apps Archive + + + +
      +
      +
      +

      Privileged Apps - Page { page }

      +
      + + + +
      +
      + + \ No newline at end of file diff --git a/src/pages/privileged.astro b/src/pages/privileged/index.astro similarity index 57% rename from src/pages/privileged.astro rename to src/pages/privileged/index.astro index e0c9c1b..fddc5f8 100644 --- a/src/pages/privileged.astro +++ b/src/pages/privileged/index.astro @@ -1,11 +1,11 @@ --- import * as Content from "astro:content" -import Imports from "../components/common_imports.astro" -import Metadata from "../components/common_metadata.astro" -import Header from "../components/header.astro" -import Footer from "../components/footer.astro" -import AppsList from "../components/apps_list.astro" +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" let privileged = await Content.getCollection("metadata", ({ data }) => data.app_type == "privileged") --- @@ -14,6 +14,7 @@ let privileged = await Content.getCollection("metadata", ({ data }) => data.app_ + All Privileged Apps - FF Apps Archive diff --git a/src/pages/sideloading.astro b/src/pages/sideloading.astro new file mode 100644 index 0000000..cf33631 --- /dev/null +++ b/src/pages/sideloading.astro @@ -0,0 +1,91 @@ +--- +import * as Content from "astro:content" + +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" +import Imports from "../components/common_imports.astro" +import Metadata from "../components/common_metadata.astro" +--- + + + + + + + Sideloading - FF Apps Archive + + +
      +
      +
      +

      Firefox OS v1.2+ Sideloading Instructions

      +
      + +
        +
      1. Download an old version of Firefox on Windows, Linux, or Mac which has WebIDE, such as v45 ESR.
      2. + +
      3. Enable ADB and Devtools debugging on Firefox OS device in Developer settings
      4. + +
      5. Choose one of the following options: +
          +
        1. + Option 1: ADB Helper Firefox extension +
            +
          1. Download ADB Helper extension
          2. + +
          3. Run Firefox and navigate to "about:config"
          4. + +
          5. Search for "xpinstall.signatures.required" and switch to false
          6. + +
          7. Restart Firefox and open Addons page
          8. + +
          9. Use the addons menu to install extension from file or drag onto page
          10. + +
          11. Open WebIDE from the Developers entry in the Firefox menu
          12. + +
          13. Select Firefox OS device under USB Devices
          14. +
          +
        2. + +
        3. + Option 2: ADB system tool installation +
            +
          1. Install ADB using your package manager using a terminal or command prompt +
            +Debian/Ubuntu Linux: +
            +sudo apt install adb +
            +Windows 10/11: +
            +winget install Google.PlatformTools
          2. + +
          3. Setup forwarding: +
            +adb forward tcp:6000 localfilesystem:/data/local/debugger-socket +
          4. + +
          5. Open WebIDE from the Developers entry in the Firefox menu
          6. + +
          7. Select Remote Runtime and enter localhost:6000
          8. +
          +
        4. +
        +
      6. + +
      7. Approve debugging connection on Firefox OS device
      8. + +
      9. Open Packaged App and select desired extracted app
      10. + +
      11. Install and Run desired app
      12. +
      + +

      On Windows you may need to install drivers. + On the Fx0 this can be done by enabling "Install PC programs" in device settings on the Fx0 then running the executable inside the mounted storage drive. + Drivers for other models may be found online. + If you can’t get recognized by ADB and can’t find drivers, try using Linux. +

      +
      +
      + + \ No newline at end of file diff --git a/src/paginate_apps.mjs b/src/paginate_apps.mjs new file mode 100644 index 0000000..ba63f64 --- /dev/null +++ b/src/paginate_apps.mjs @@ -0,0 +1,38 @@ +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export function paginate(page, appsSource) { + let apps = appsSource.filter(app => { + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page + }) + + const aIndex = 1 + let currentIndex = characters.indexOf(page) + let prev = currentIndex > 0 ? characters[currentIndex - 1] : null + let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null + let cluster = [] + if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) + } + if(characters.indexOf(page) > aIndex) { + cluster.push(page) + } + if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) + } + let display = [characters[0], characters[1]] + if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] + } + if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") + } + + return { apps, prev, next, display } +} \ No newline at end of file From 3f87d7b664c49dcf613d2917c8a94ec8c7122794 Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Thu, 6 Nov 2025 19:22:31 -0500 Subject: [PATCH 05/38] Enhance hosted apps liveliness classification --- src/pages/[category]/[page].astro | 5 +- src/pages/[category]/index.astro | 5 +- src/pages/all/[page].astro | 6 ++- src/pages/all/index.astro | 5 +- src/pages/dead/[page].astro | 2 +- src/pages/dead/index.astro | 3 +- src/pages/hosted/[page].astro | 7 ++- src/pages/hosted/index.astro | 6 +-- src/pages/index.astro | 5 +- src/pages/live/[page].astro | 83 ------------------------------- src/pages/live/index.astro | 30 ----------- src/validateHostedApps.js | 20 -------- src/validate_hosted_apps.js | 40 +++++++++++++++ 13 files changed, 66 insertions(+), 151 deletions(-) delete mode 100644 src/pages/live/[page].astro delete mode 100644 src/pages/live/index.astro delete mode 100644 src/validateHostedApps.js create mode 100644 src/validate_hosted_apps.js diff --git a/src/pages/[category]/[page].astro b/src/pages/[category]/[page].astro index 167e676..bb2b88d 100644 --- a/src/pages/[category]/[page].astro +++ b/src/pages/[category]/[page].astro @@ -8,6 +8,7 @@ import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" import categories from "../../categories.json" with { type: "json" } +import { live } from "../../validate_hosted_apps.js" const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] @@ -24,7 +25,9 @@ export async function getStaticPaths() { let { category, page } = Astro.params -let apps = await Content.getCollection("metadata", function(app) { +let packaged = await Content.getCollection("metadata", app => app.data.app_type != "hosted") + +let apps = packaged.concat(live).filter(function(app) { if(! app.data.categories.includes(category)) { return false } diff --git a/src/pages/[category]/index.astro b/src/pages/[category]/index.astro index df6cb67..e32c9f4 100644 --- a/src/pages/[category]/index.astro +++ b/src/pages/[category]/index.astro @@ -7,6 +7,7 @@ import Metadata from "../../components/common_metadata.astro" import AppsList from "../../components/apps_list.astro" import categories from "../../categories.json" with { type: "json" } +import { live } from "../../validate_hosted_apps.js" export async function getStaticPaths() { return categories.map(category => ({ params: { category: category.slug }, props: { category } })) @@ -14,7 +15,9 @@ export async function getStaticPaths() { let { category } = Astro.props -let apps = await Content.getCollection("metadata", ({ data }) => data.categories.includes(category.slug)) +let packaged = await Content.getCollection("metadata", app => app.data.app_type != "hosted") +let apps = packaged.concat(live).filter(app => app.data.categories.includes(category.slug)) + --- diff --git a/src/pages/all/[page].astro b/src/pages/all/[page].astro index 8d11d4e..8c3016a 100644 --- a/src/pages/all/[page].astro +++ b/src/pages/all/[page].astro @@ -7,6 +7,8 @@ import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" +import { live } from "../../validate_hosted_apps.js" + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] export async function getStaticPaths() { @@ -17,7 +19,9 @@ export async function getStaticPaths() { let { page } = Astro.params -let apps = await Content.getCollection("metadata", function(app) { +let packaged = await Content.getCollection("metadata", app => app.data.app_type != "hosted") + +let apps = packaged.concat(live).filter(function(app) { let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] let starting = name[0].toUpperCase() if(! characters.includes(starting)) { diff --git a/src/pages/all/index.astro b/src/pages/all/index.astro index f28d987..142e27f 100644 --- a/src/pages/all/index.astro +++ b/src/pages/all/index.astro @@ -7,7 +7,10 @@ import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" -let apps = await Content.getCollection("metadata") +import { live } from "../../validate_hosted_apps.js" + +let packaged = await Content.getCollection("metadata", app => app.data.app_type != "hosted") +let apps = packaged.concat(live) --- diff --git a/src/pages/dead/[page].astro b/src/pages/dead/[page].astro index d65ebf5..11b4f23 100644 --- a/src/pages/dead/[page].astro +++ b/src/pages/dead/[page].astro @@ -7,7 +7,7 @@ import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" -import { dead, total } from "../../validateHostedApps.js" +import { dead } from "../../validate_hosted_apps.js" const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] diff --git a/src/pages/dead/index.astro b/src/pages/dead/index.astro index ebccac6..c790c80 100644 --- a/src/pages/dead/index.astro +++ b/src/pages/dead/index.astro @@ -5,7 +5,7 @@ import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" -import { dead, total } from "../../validateHostedApps.js" +import { dead } from "../../validate_hosted_apps.js" --- @@ -22,7 +22,6 @@ import { dead, total } from "../../validateHostedApps.js"

      All Dead Hosted Apps

      -

      Found { dead.length } dead hosted apps out of { total } total hosted apps.

      diff --git a/src/pages/hosted/[page].astro b/src/pages/hosted/[page].astro index 1e3a3fa..7e6400e 100644 --- a/src/pages/hosted/[page].astro +++ b/src/pages/hosted/[page].astro @@ -7,6 +7,8 @@ import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" +import { live } from "../../validate_hosted_apps.js" + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] export async function getStaticPaths() { @@ -17,10 +19,7 @@ export async function getStaticPaths() { let { page } = Astro.params -let apps = await Content.getCollection("metadata", function(app) { - if(app.data.app_type != "hosted") { - return false - } +let apps = live.filter(app => { let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] let starting = name[0].toUpperCase() if(! characters.includes(starting)) { diff --git a/src/pages/hosted/index.astro b/src/pages/hosted/index.astro index 8b4836e..c1529ca 100644 --- a/src/pages/hosted/index.astro +++ b/src/pages/hosted/index.astro @@ -1,13 +1,11 @@ --- -import * as Content from "astro:content" - import Imports from "../../components/common_imports.astro" import Metadata from "../../components/common_metadata.astro" import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" -let hosted = await Content.getCollection("metadata", ({ data }) => data.app_type == "hosted") +import { live } from "../../validate_hosted_apps.js" --- @@ -24,7 +22,7 @@ let hosted = await Content.getCollection("metadata", ({ data }) => data.app_type

      All Hosted Apps

      - +
diff --git a/src/pages/index.astro b/src/pages/index.astro index b927244..f6a8820 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -23,12 +23,11 @@ import Categories from "../components/categories.astro"

All Apps

Packaged Apps - can be installed here

Privileged Apps - cannot be installed directly and must be sideloaded using developer tools

-

All Hosted Apps

-

Live Hosted Apps - can be installed, but might no longer function

+

Hosted Apps - possibly installable, but might no longer function

Dead Hosted Apps - cannot be installed

- Firefox apps can be installed on any version of Firefox OS and on old versions of the Firefox web browser v16 - v46.
+ Firefox apps can be installed on any version of Firefox OS and on old versions of the Firefox web browser until v46.
Firefox desktop v45
Firefox Android v45
Firefox OS 2.5 Android launcher diff --git a/src/pages/live/[page].astro b/src/pages/live/[page].astro deleted file mode 100644 index 44cb33f..0000000 --- a/src/pages/live/[page].astro +++ /dev/null @@ -1,83 +0,0 @@ ---- -import * as Content from "astro:content" - -import Imports from "../../components/common_imports.astro" -import Metadata from "../../components/common_metadata.astro" -import Header from "../../components/header.astro" -import Footer from "../../components/footer.astro" -import AppsList from "../../components/apps_list.astro" - -import { live, total } from "../../validateHostedApps.js" - -const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] - -export async function getStaticPaths() { - const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] - return characters.map(page => ({ - params: { page }, -})) } - -let { page } = Astro.params - -let apps = live.filter(app => { - let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] - let starting = name[0].toUpperCase() - if(! characters.includes(starting)) { - starting = "1" - } - return starting == page -}) - -const aIndex = 1 -let currentIndex = characters.indexOf(page) -let prev = currentIndex > 0 ? characters[currentIndex - 1] : null -let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null -let cluster = [] -if(prev && characters.indexOf(prev) > aIndex) { - cluster.push(prev) -} -if(characters.indexOf(page) > aIndex) { - cluster.push(page) -} -if(next && characters.indexOf(next) > aIndex) { - cluster.push(next) -} -let display = [characters[0], characters[1]] -if(cluster.length > 0) { - display.push("...") - display = [...display, ...cluster] -} -if(! cluster.includes("Z")) { - display.push("...") - display.push("Z") -} ---- - - - - - - - - Page { page } - Live Hosted Apps - FF Apps Archive - - - -

-
-
-

Live Hosted Apps - Page { page }

-
- - - -
-
- - \ No newline at end of file diff --git a/src/pages/live/index.astro b/src/pages/live/index.astro deleted file mode 100644 index 7b91f9b..0000000 --- a/src/pages/live/index.astro +++ /dev/null @@ -1,30 +0,0 @@ ---- -import Imports from "../../components/common_imports.astro" -import Metadata from "../../components/common_metadata.astro" -import Header from "../../components/header.astro" -import Footer from "../../components/footer.astro" -import AppsList from "../../components/apps_list.astro" - -import { live, total } from "../../validateHostedApps.js" ---- - - - - - - - - All Live Hosted Apps - FF Apps Archive - - -
-
-
-

All Live Hosted Apps

-
-

Found { live.length } live hosted apps out of { total } total hosted apps.

- -
-
- - \ No newline at end of file diff --git a/src/validateHostedApps.js b/src/validateHostedApps.js deleted file mode 100644 index afcf238..0000000 --- a/src/validateHostedApps.js +++ /dev/null @@ -1,20 +0,0 @@ -import * as Content from "astro:content" - -let apps = await Content.getCollection("metadata", ({ data }) => data.app_type == "hosted") -let total = apps.length -let live = [], dead = [] -for await(let app of apps) { - try { - let response = await fetch(app.data.manifest_url) - if(response.ok) { - let result = await response.json() - live.push(app) - } else { - dead.push(app) - } - } catch { - dead.push(app) - } -} - -export { live, dead, total } \ No newline at end of file diff --git a/src/validate_hosted_apps.js b/src/validate_hosted_apps.js new file mode 100644 index 0000000..4a3bec9 --- /dev/null +++ b/src/validate_hosted_apps.js @@ -0,0 +1,40 @@ +import * as Content from "astro:content" + +async function check_html(url) { + try { + let response = await fetch(url) + if(response.ok) { + let type = response.headers.get("content-type") + return type && (type.includes("text/html") || type.includes("application/xhtml+xml")) + } + } catch { + return false +} } + +let apps = await Content.getCollection("metadata", ({ data }) => data.app_type == "hosted") +let total = apps.length +let live = [], dead = [] + +for await(let app of apps) { + let manifest = new URL(app.data.manifest_url) + try { + let response = await fetch(manifest) + if(response.ok) { + await response.json() + live.push(app) + } else { + throw new Error() + } + } catch { + let parent = new URL(manifest.pathname.split("/").slice(0, -1).join("/"), manifest.origin) + let root = new URL("/", manifest.origin) + + if(await check_html(parent)) { + live.push(app) + } else if(await check_html(root)) { + live.push(app) + } else { + dead.push(app) +} } } + +export { live, dead, total } \ No newline at end of file From a60014838958266e71e404d91a438c550f85430c Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Thu, 6 Nov 2025 22:53:24 -0500 Subject: [PATCH 06/38] Add Works Offline Category. Closes #17 --- astro.config.mjs | 3 +- src/pages/app/[slug].astro | 2 +- src/pages/works-offline/[page].astro | 88 ++++++++++++++++++++++++++++ src/pages/works-offline/index.astro | 36 ++++++++++++ src/validate_hosted_apps.js | 16 ++++- 5 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 src/pages/works-offline/[page].astro create mode 100644 src/pages/works-offline/index.astro diff --git a/astro.config.mjs b/astro.config.mjs index 88e8afd..4a06acc 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -11,7 +11,6 @@ export default defineConfig({ site: "https://ffapps.danielherr.software", compressHTML: false, build: { - inlineStylesheets: "never", - concurrency: 1 + inlineStylesheets: "never" } }) \ No newline at end of file diff --git a/src/pages/app/[slug].astro b/src/pages/app/[slug].astro index 5b8819d..99a9887 100644 --- a/src/pages/app/[slug].astro +++ b/src/pages/app/[slug].astro @@ -59,7 +59,7 @@ let releasenotes = app.data.release_notes ? app.data.release_notes["en-US"] ?? a

Support: Web, Email

Categories:

{ app.data.is_offline &&

Works Offline

} diff --git a/src/pages/works-offline/[page].astro b/src/pages/works-offline/[page].astro new file mode 100644 index 0000000..75a67a4 --- /dev/null +++ b/src/pages/works-offline/[page].astro @@ -0,0 +1,88 @@ +--- +import * as Content from "astro:content" + +import Imports from "../../components/common_imports.astro" +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +import { live } from "../../validate_hosted_apps.js" + +const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + +export async function getStaticPaths() { + const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] + return characters.map(page => ({ + params: { page }, +})) } + +let { page } = Astro.params + +let packaged = await Content.getCollection("metadata", app => app.data.app_type != "hosted") + +let apps = packaged.concat(live).filter(function(app) { + if(! app.data.is_offline) { + return false + } + let name = app.data.name["en-US"] ?? app.data.name.en ?? app.data.name[app.data.default_locale] + let starting = name[0].toUpperCase() + if(! characters.includes(starting)) { + starting = "1" + } + return starting == page +}) + +const aIndex = 1 +let currentIndex = characters.indexOf(page) +let prev = currentIndex > 0 ? characters[currentIndex - 1] : null +let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null +let cluster = [] +if(prev && characters.indexOf(prev) > aIndex) { + cluster.push(prev) +} +if(characters.indexOf(page) > aIndex) { + cluster.push(page) +} +if(next && characters.indexOf(next) > aIndex) { + cluster.push(next) +} +let display = [characters[0], characters[1]] +if(cluster.length > 0) { + display.push("...") + display = [...display, ...cluster] +} +if(! cluster.includes("Z")) { + display.push("...") + display.push("Z") +} +--- + + + + + + + + Page { page } - Works Offline - FF Apps Archive + + + +
+
+
+

Works Offline - Page { page }

+
+ + + +
+
+ + \ No newline at end of file diff --git a/src/pages/works-offline/index.astro b/src/pages/works-offline/index.astro new file mode 100644 index 0000000..6127008 --- /dev/null +++ b/src/pages/works-offline/index.astro @@ -0,0 +1,36 @@ +--- +import * as Content from "astro:content" + +import Metadata from "../../components/common_metadata.astro" +import Header from "../../components/header.astro" +import Footer from "../../components/footer.astro" +import AppsList from "../../components/apps_list.astro" + +import { live } from "../../validate_hosted_apps.js" + +let packaged = await Content.getCollection("metadata", app => app.data.app_type != "hosted") +let apps = packaged.concat(live).filter(app => app.data.is_offline) +--- + + + + + + + + + + Works Offline - FF Apps Archive + + +
+
+
+ +

Works Offline

+
+ +
+
+ + \ No newline at end of file diff --git a/src/validate_hosted_apps.js b/src/validate_hosted_apps.js index 4a3bec9..ca90b0b 100644 --- a/src/validate_hosted_apps.js +++ b/src/validate_hosted_apps.js @@ -1,8 +1,20 @@ import * as Content from "astro:content" +async function fetch_timed(url, timeout = 60000) { + let controller = new AbortController() + let id = setTimeout(() => controller.abort(), timeout) + try { + let response = await fetch(url, { signal: controller.signal }) + clearTimeout(id) + return response + } catch(error) { + clearTimeout(id) + throw error +} } + async function check_html(url) { try { - let response = await fetch(url) + let response = await fetch_timed(url) if(response.ok) { let type = response.headers.get("content-type") return type && (type.includes("text/html") || type.includes("application/xhtml+xml")) @@ -18,7 +30,7 @@ let live = [], dead = [] for await(let app of apps) { let manifest = new URL(app.data.manifest_url) try { - let response = await fetch(manifest) + let response = await fetch_timed(manifest) if(response.ok) { await response.json() live.push(app) From 797a6ecb6755c9d858573357e732e69c593cbc02 Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Sun, 9 Nov 2025 14:22:21 -0500 Subject: [PATCH 07/38] Add Works Offline Category to Homepage. Closes #17 --- src/components/categories.astro | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/categories.astro b/src/components/categories.astro index 834ae95..45a53d8 100644 --- a/src/components/categories.astro +++ b/src/components/categories.astro @@ -5,7 +5,10 @@ import categories from "../categories.json" with { type: "json" } \ No newline at end of file From fee5fdadb46dbd17e917abdd6ed6eba891525859 Mon Sep 17 00:00:00 2001 From: Daniel Herr Date: Sun, 16 Nov 2025 21:43:29 -0500 Subject: [PATCH 08/38] Enhance pagination. Closes #20. Add Contact form. Closes #21. --- package.json | 4 +- public/common.css | 3 + public/contact.css | 29 +++ public/pagination.css | 67 ++++++ public/pagination.js | 41 ++++ .../{nav.astro => navigation.astro} | 0 src/components/pagination.astro | 197 ++++++++++++++++++ src/pages/[category]/[page].astro | 37 +--- src/pages/all/[page].astro | 37 +--- src/pages/contact.astro | 57 +++++ src/pages/contacted.astro | 24 +++ src/pages/dead/[page].astro | 37 +--- src/pages/hosted/[page].astro | 38 +--- src/pages/index.astro | 1 + src/pages/packaged/[page].astro | 37 +--- src/pages/privileged/[page].astro | 37 +--- src/pages/works-offline/[page].astro | 37 +--- 17 files changed, 456 insertions(+), 227 deletions(-) create mode 100644 public/contact.css create mode 100644 public/pagination.css create mode 100644 public/pagination.js rename src/components/{nav.astro => navigation.astro} (100%) create mode 100644 src/components/pagination.astro create mode 100644 src/pages/contact.astro create mode 100644 src/pages/contacted.astro diff --git a/package.json b/package.json index 148bbbe..7ab0fc9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "@astrojs/netlify": "^6.5.8", "@astrojs/sitemap": "^3.5.1", "@zip.js/zip.js": "^2.7.73", - "astro": "^5.13.3" + "astro": "^5.15.7" } -} +} \ No newline at end of file diff --git a/public/common.css b/public/common.css index 5f6b5c7..f7cd2fa 100644 --- a/public/common.css +++ b/public/common.css @@ -1,6 +1,9 @@ * { box-sizing: border-box; } +body { + font-family: sans-serif; +} header img { float: left; margin-right: 1em; diff --git a/public/contact.css b/public/contact.css new file mode 100644 index 0000000..cebb28c --- /dev/null +++ b/public/contact.css @@ -0,0 +1,29 @@ +fieldset { + border: none; + padding: 0; + margin: 0; + margin-bottom: 1em; +} + +label { + display: block; +} +fieldset label { + margin-top: 0; +} + +input, button { + font-size: 1em; +} +input:user-invalid { + border-color: red; +} + +textarea { + display: block; + font-size: 1em; +} + +button { + margin-top: 1em; +} \ No newline at end of file diff --git a/public/pagination.css b/public/pagination.css new file mode 100644 index 0000000..5a1fd09 --- /dev/null +++ b/public/pagination.css @@ -0,0 +1,67 @@ +nav ol { + padding: 0; +} +nav div ol, .hybrid ol { + white-space: nowrap; + overflow-x: auto; + text-align: center; +} +nav li { + display: inline-block; + margin: 5px; + border: thin solid lightgrey; + border-radius: 10px; + background: aliceblue; +} +nav li a { + display: inline-block; + font-size: 1.5em; + padding-top: 10px; + padding-left: 20px; + padding-right: 20px; + padding-bottom: 10px; + text-decoration: none; +} +nav li.current { + font-weight: bold; + border-width: medium; + background: lightskyblue; +} +select { + font-size: 1em; + width: 4em; + padding: 5px; + border-radius: 0.4em; + text-align: center; + background: aliceblue; +} +details { + display: block; + margin-top: 20px; + margin-bottom: 40px; +} + +.hybrid::details-content { + content-visibility: visible; +} +.hybrid[open] ol { + white-space: normal; +} + +@media(prefers-color-scheme: dark) { + nav li { + background: midnightblue; + } + select { + background: midnightblue; +} } + +.details_polyfill summary { + display: block; +} +.details_polyfill details > summary:before { + content: "▶ "; +} +.details_polyfill details[open] > summary:before { + content: "▼ "; +} \ No newline at end of file diff --git a/public/pagination.js b/public/pagination.js new file mode 100644 index 0000000..b263d40 --- /dev/null +++ b/public/pagination.js @@ -0,0 +1,41 @@ +"use strict" + +/*var selects = document.querySelectorAll(".native_select") +selects.forEach(function(select) { + select.addEventListener("change", function() { + location.href = "../" + this.value +}) })*/ + +if(! ("open" in document.querySelector("details"))) { + window.addEventListener("click", function(event) { + if(event.target.nodeName == "SUMMARY") { + var details = event.target.parentNode + if(details.getAttribute("open") === "") { + details.removeAttribute("open") + } else { + details.setAttribute("open", "") + } } }) + document.body.classList.add("details_polyfill") +} + +var supported = false +var test = Object.defineProperty({}, "inline", { + get: function() { supported = true } +}) +document.body.scrollIntoView(test) +if(supported) { + var options = { inline: "center", container: "nearest", behavior: "instant" } +} else { + var options = false +} + +var navs = document.querySelectorAll("details") +function scroll_link(element) { + element.querySelector("li.current").scrollIntoView(options) +} +scroll_link(navs[1]) +scroll_link(navs[0]) +window.addEventListener("load", function() { + scroll_link(navs[1]) + scroll_link(navs[0]) +}) \ No newline at end of file diff --git a/src/components/nav.astro b/src/components/navigation.astro similarity index 100% rename from src/components/nav.astro rename to src/components/navigation.astro diff --git a/src/components/pagination.astro b/src/components/pagination.astro new file mode 100644 index 0000000..1ca67c8 --- /dev/null +++ b/src/components/pagination.astro @@ -0,0 +1,197 @@ +--- +let { current } = Astro.props +--- + + \ No newline at end of file diff --git a/src/pages/[category]/[page].astro b/src/pages/[category]/[page].astro index bb2b88d..46203c6 100644 --- a/src/pages/[category]/[page].astro +++ b/src/pages/[category]/[page].astro @@ -6,6 +6,7 @@ import Metadata from "../../components/common_metadata.astro" import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" +import Pagination from "../../components/pagination.astro" import categories from "../../categories.json" with { type: "json" } import { live } from "../../validate_hosted_apps.js" @@ -38,30 +39,6 @@ let apps = packaged.concat(live).filter(function(app) { } return starting == page }) - -const aIndex = 1 -let currentIndex = characters.indexOf(page) -let prev = currentIndex > 0 ? characters[currentIndex - 1] : null -let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null -let cluster = [] -if(prev && characters.indexOf(prev) > aIndex) { - cluster.push(prev) -} -if(characters.indexOf(page) > aIndex) { - cluster.push(page) -} -if(next && characters.indexOf(next) > aIndex) { - cluster.push(next) -} -let display = [characters[0], characters[1]] -if(cluster.length > 0) { - display.push("...") - display = [...display, ...cluster] -} -if(! cluster.includes("Z")) { - display.push("...") - display.push("Z") -} --- @@ -69,6 +46,8 @@ if(! cluster.includes("Z")) { + + Page { page } - { Astro.props.category.name } - FF Apps Archive @@ -81,15 +60,9 @@ if(! cluster.includes("Z")) {

{ Astro.props.category.name } - Page { page }

- + - +
diff --git a/src/pages/all/[page].astro b/src/pages/all/[page].astro index 8c3016a..42f78f6 100644 --- a/src/pages/all/[page].astro +++ b/src/pages/all/[page].astro @@ -6,6 +6,7 @@ import Metadata from "../../components/common_metadata.astro" import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" +import Pagination from "../../components/pagination.astro" import { live } from "../../validate_hosted_apps.js" @@ -29,30 +30,6 @@ let apps = packaged.concat(live).filter(function(app) { } return starting == page }) - -const aIndex = 1 -let currentIndex = characters.indexOf(page) -let prev = currentIndex > 0 ? characters[currentIndex - 1] : null -let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null -let cluster = [] -if(prev && characters.indexOf(prev) > aIndex) { - cluster.push(prev) -} -if(characters.indexOf(page) > aIndex) { - cluster.push(page) -} -if(next && characters.indexOf(next) > aIndex) { - cluster.push(next) -} -let display = [characters[0], characters[1]] -if(cluster.length > 0) { - display.push("...") - display = [...display, ...cluster] -} -if(! cluster.includes("Z")) { - display.push("...") - display.push("Z") -} --- @@ -60,6 +37,8 @@ if(! cluster.includes("Z")) { + + Page { page } - All Apps - FF Apps Archive @@ -70,15 +49,9 @@ if(! cluster.includes("Z")) {

All Apps - Page { page }

- + - +
diff --git a/src/pages/contact.astro b/src/pages/contact.astro new file mode 100644 index 0000000..083d4a8 --- /dev/null +++ b/src/pages/contact.astro @@ -0,0 +1,57 @@ +--- +import * as Content from "astro:content"; + +import Header from "../components/header.astro"; +import Footer from "../components/footer.astro"; +import Imports from "../components/common_imports.astro"; +import Metadata from "../components/common_metadata.astro"; +--- + + + + + + + + Contact - FF Apps Archive + + +
+
+
+

Contact

+
+
+
+
+ What is the nature of this message? + + + + +
+
+
+
+ +
+
+ + diff --git a/src/pages/contacted.astro b/src/pages/contacted.astro new file mode 100644 index 0000000..35dd687 --- /dev/null +++ b/src/pages/contacted.astro @@ -0,0 +1,24 @@ +--- +import * as Content from "astro:content" + +import Header from "../components/header.astro" +import Footer from "../components/footer.astro" +import Imports from "../components/common_imports.astro" +import Metadata from "../components/common_metadata.astro" +--- + + + + + + + Contact - FF Apps Archive + + +
+
+

Your message has been sent.

+
+
+ + \ No newline at end of file diff --git a/src/pages/dead/[page].astro b/src/pages/dead/[page].astro index 11b4f23..73cfa32 100644 --- a/src/pages/dead/[page].astro +++ b/src/pages/dead/[page].astro @@ -8,6 +8,7 @@ import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" import { dead } from "../../validate_hosted_apps.js" +import Pagination from "../../components/pagination.astro" const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] @@ -27,30 +28,6 @@ let apps = dead.filter(app => { } return starting == page }) - -const aIndex = 1 -let currentIndex = characters.indexOf(page) -let prev = currentIndex > 0 ? characters[currentIndex - 1] : null -let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null -let cluster = [] -if(prev && characters.indexOf(prev) > aIndex) { - cluster.push(prev) -} -if(characters.indexOf(page) > aIndex) { - cluster.push(page) -} -if(next && characters.indexOf(next) > aIndex) { - cluster.push(next) -} -let display = [characters[0], characters[1]] -if(cluster.length > 0) { - display.push("...") - display = [...display, ...cluster] -} -if(! cluster.includes("Z")) { - display.push("...") - display.push("Z") -} --- @@ -58,6 +35,8 @@ if(! cluster.includes("Z")) { + + Page { page } - Dead Hosted Apps - FF Apps Archive @@ -68,15 +47,9 @@ if(! cluster.includes("Z")) {

Dead Hosted Apps - Page { page }

- + - +
diff --git a/src/pages/hosted/[page].astro b/src/pages/hosted/[page].astro index 7e6400e..76e894a 100644 --- a/src/pages/hosted/[page].astro +++ b/src/pages/hosted/[page].astro @@ -6,7 +6,7 @@ import Metadata from "../../components/common_metadata.astro" import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" - +import Pagination from "../../components/pagination.astro" import { live } from "../../validate_hosted_apps.js" const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] @@ -27,30 +27,6 @@ let apps = live.filter(app => { } return starting == page }) - -const aIndex = 1 -let currentIndex = characters.indexOf(page) -let prev = currentIndex > 0 ? characters[currentIndex - 1] : null -let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null -let cluster = [] -if(prev && characters.indexOf(prev) > aIndex) { - cluster.push(prev) -} -if(characters.indexOf(page) > aIndex) { - cluster.push(page) -} -if(next && characters.indexOf(next) > aIndex) { - cluster.push(next) -} -let display = [characters[0], characters[1]] -if(cluster.length > 0) { - display.push("...") - display = [...display, ...cluster] -} -if(! cluster.includes("Z")) { - display.push("...") - display.push("Z") -} --- @@ -58,6 +34,8 @@ if(! cluster.includes("Z")) { + + Page { page } - Hosted Apps - FF Apps Archive @@ -68,15 +46,9 @@ if(! cluster.includes("Z")) {

Hosted Apps - Page { page }

- + - +
diff --git a/src/pages/index.astro b/src/pages/index.astro index f6a8820..3716621 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -35,6 +35,7 @@ import Categories from "../components/categories.astro"

For instructions on sideloading privileged apps on Firefox OS, see Sideloading Instructions.

+

Contact FFAA

Credits:
Daniel Herr - Project Creator and Engineer
Dimitrios Vlachos - Community Manager diff --git a/src/pages/packaged/[page].astro b/src/pages/packaged/[page].astro index 330954e..04eeaad 100644 --- a/src/pages/packaged/[page].astro +++ b/src/pages/packaged/[page].astro @@ -6,6 +6,7 @@ import Metadata from "../../components/common_metadata.astro" import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" +import Pagination from "../../components/pagination.astro" const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] @@ -28,30 +29,6 @@ let apps = await Content.getCollection("metadata", function(app) { } return starting == page }) - -const aIndex = 1 -let currentIndex = characters.indexOf(page) -let prev = currentIndex > 0 ? characters[currentIndex - 1] : null -let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null -let cluster = [] -if(prev && characters.indexOf(prev) > aIndex) { - cluster.push(prev) -} -if(characters.indexOf(page) > aIndex) { - cluster.push(page) -} -if(next && characters.indexOf(next) > aIndex) { - cluster.push(next) -} -let display = [characters[0], characters[1]] -if(cluster.length > 0) { - display.push("...") - display = [...display, ...cluster] -} -if(! cluster.includes("Z")) { - display.push("...") - display.push("Z") -} --- @@ -59,6 +36,8 @@ if(! cluster.includes("Z")) { + + Page { page } - Packaged Apps - FF Apps Archive @@ -69,15 +48,9 @@ if(! cluster.includes("Z")) {

Packaged Apps - Page { page }

- + - +
diff --git a/src/pages/privileged/[page].astro b/src/pages/privileged/[page].astro index c72e319..e7b79fc 100644 --- a/src/pages/privileged/[page].astro +++ b/src/pages/privileged/[page].astro @@ -6,6 +6,7 @@ import Metadata from "../../components/common_metadata.astro" import Header from "../../components/header.astro" import Footer from "../../components/footer.astro" import AppsList from "../../components/apps_list.astro" +import Pagination from "../../components/pagination.astro" const characters = [ "1", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ] @@ -28,30 +29,6 @@ let apps = await Content.getCollection("metadata", function(app) { } return starting == page }) - -const aIndex = 1 -let currentIndex = characters.indexOf(page) -let prev = currentIndex > 0 ? characters[currentIndex - 1] : null -let next = currentIndex < characters.length - 1 ? characters[currentIndex + 1] : null -let cluster = [] -if(prev && characters.indexOf(prev) > aIndex) { - cluster.push(prev) -} -if(characters.indexOf(page) > aIndex) { - cluster.push(page) -} -if(next && characters.indexOf(next) > aIndex) { - cluster.push(next) -} -let display = [characters[0], characters[1]] -if(cluster.length > 0) { - display.push("...") - display = [...display, ...cluster] -} -if(! cluster.includes("Z")) { - display.push("...") - display.push("Z") -} --- @@ -59,6 +36,8 @@ if(! cluster.includes("Z")) { + + Page { page } - Privileged Apps - FF Apps Archive @@ -69,15 +48,9 @@ if(! cluster.includes("Z")) {

Privileged Apps - Page { page }

- + - +