Initial commit (v1.0.0)

This commit is contained in:
Aidan 2024-12-16 23:37:49 -05:00
parent 742572cf1e
commit 35b707b339
No known key found for this signature in database
GPG Key ID: 1773A01F0EFE4FC1
24 changed files with 937 additions and 34 deletions

35
.gitignore vendored
View File

@ -1,33 +1,4 @@
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
bun.lockb
node_modules/
public/css/styles.css

View File

@ -1,2 +1,50 @@
# modules-app
An open-source Magisk modules app store
# modules
[![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/)
[![Join Telegram Channel](https://img.shields.io/badge/join_channel-telegram-blue)](https://t.me/pontushub)
An open-source Magisk modules and root/FOSS app store
This is my first project which uses `bun`.
Modules does **NOT** host the modules themselves, and only provides downloads to third-party sources. It's database is open source, and is the `apps.json` and `modules.json` files.
Unless a module is labeled `SelfHost`, I am not hosting the file in question.
# Contributing
If you would like to contribute, please know I appreciate it very much!
The backend works on Node.js with Express, EJS, and Tailwind CSS. Each time the code is updated, make sure you run `bun run build:css` to use the latest needed styles.
You can either contribute code (start with `app.js`) or your time to the `apps.json` database and `modules.json`. JSON formatting is pretty easy to learn.
Please create pull requests and issues, and be generally respectful and patient.
# Setting up and self-hosting
1. First, clone the repository:
```bash
git clone https://github.com/ihatenodejs/modules.git
```
2. Next, let's install Bun (if you don't already have it). You can see more about Bun [here](https://bun.sh/).
**Windows (PowerShell)**
```
powershell -c "irm bun.sh/install.ps1 | iex"
```
**Linux/macOS:**
```bash
curl -fsSL https://bun.sh/install | bash
```
3. Now, let's install everything with:
```
bun install
```
4. After that, we'll build the Tailwind CSS:
```
bun run build:css
```
5. Finally, we start the server on port `3000`:
```
bun app.js
```
Your server can now be accessed at http://localhost:3000, and can be used in conjunction with a reverse proxy to get a functional site up.

94
app.js Normal file
View File

@ -0,0 +1,94 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const validator = require('validator');
const helmet = require('helmet');
const app = express();
const PORT = 3000;
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'src/views'));
app.use(helmet());
app.use(express.static(path.join(__dirname, 'public')));
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdnjs.cloudflare.com"],
styleSrc: ["'self'", "https://cdnjs.cloudflare.com"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
}));
// routes
app.get('/', (req, res) => {
res.render('pages/home', { title: 'modules.' });
});
app.get('/apps', (req, res) => {
fs.readFile('apps.json', (err, data) => {
if (err) throw err;
const apps = JSON.parse(data);
res.render('pages/apps', { title: 'Apps', apps: apps });
});
});
app.get('/modules', (req, res) => {
fs.readFile('modules.json', (err, data) => {
if (err) throw err;
const modules = JSON.parse(data);
res.render('pages/modules', { title: 'Modules | modules.', modules: modules });
});
});
app.get('/modules/:id', (req, res) => {
fs.readFile('modules.json', (err, data) => {
if (err) throw err;
const modules = JSON.parse(data);
const module = modules.find(m => m.id === req.params.id);
if (module) {
res.render('templates/module', { module: module });
} else {
res.status(404).send('Module not found');
}
});
});
app.get('/apps/:id', (req, res) => {
fs.readFile('apps.json', (err, data) => {
if (err) throw err;
const apps = JSON.parse(data);
const app = apps.find(m => m.id === req.params.id);
if (app) {
res.render('templates/app', { app: app });
} else {
res.status(404).send('App not found');
}
});
});
app.get('/download', (req, res) => {
const { name, type, version, arch } = req.query;
if (!validator.isAlphanumeric(name.replace(/\s/g, '')) || !validator.isAlphanumeric(type) || !validator.isAlphanumeric(version.replace(/\./g, '')) || !validator.isAlphanumeric(arch)) {
return res.status(400).send('Invalid input, mister!');
}
fs.readFile(`${type}s.json`, (err, data) => {
if (err) throw err;
const items = JSON.parse(data);
const item = items.find(i => i.name === name);
if (item && item.downloadLinks && item.downloadLinks[version] && item.downloadLinks[version][arch]) {
const downloadLink = item.downloadLinks[version][arch];
res.render('templates/download', { name: item.name, type, version, arch, downloadLink });
} else {
res.status(404).send('Download link not found');
}
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

101
apps.json Normal file
View File

@ -0,0 +1,101 @@
[
{
"id": "1",
"name": "Muzza",
"icon": "/img/apps/muzza.png",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v0.6.3-rc-2": {
"foss": "https://github.com/Maloy-Android/Muzza/releases/download/v0.6.3-rc-2/Muzza-0.6.3-rc-2.final.-foss-release.apk",
"full": "https://github.com/Maloy-Android/Muzza/releases/download/v0.6.3-rc-2/Muzza-0.6.3-rc-2.final.-full-release.apk"
},
"v0.6.3-rc-1": {
"foss": "https://github.com/Maloy-Android/Muzza/releases/download/v0.6.3-rc1/Muzza-0.6.3-rc1-foss-release.apk",
"full": "https://github.com/Maloy-Android/Muzza/releases/download/v0.6.3-rc1/Muzza-0.6.3-rc1-full-release.apk"
},
"v0.6.3": {
"foss": "https://github.com/Maloy-Android/Muzza/releases/download/v0.6.3/Muzza-0.6.3-foss-release.apk",
"full": "https://github.com/Maloy-Android/Muzza/releases/download/v0.6.3/Muzza-0.6.3-full-release.apk"
}
},
"description": "Muzza is a Material 3 YouTube Music client. It features ad-free playback from YouTube/YouTube Music, background playback, search via YouTube Music, cache/download, lyrics, Android Auto, and Discord rich presence support.\n\nIf you're in a region where YouTube Music is not supported, you won't be able to use this app unless you have a proxy or VPN to connect to a YouTube Music supported region.",
"github": "https://github.com/Maloy-Android/Muzza"
},
{
"id": "2",
"name": "NewPipe",
"icon": "/img/apps/newpipe.png",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v0.27.4": {
"all": "https://github.com/TeamNewPipe/NewPipe/releases/download/v0.27.4/NewPipe_v0.27.4.apk"
},
"v0.27.3": {
"all": "https://github.com/TeamNewPipe/NewPipe/releases/download/v0.27.3/NewPipe_v0.27.3.apk"
},
"v0.27.2": {
"all": "https://github.com/TeamNewPipe/NewPipe/releases/download/v0.27.2/NewPipe_v0.27.2.apk"
}
},
"description": "NewPipe is a 3rd party client that supports many services like Youtube, Peertube, SoundCloud, and Bandcamp.\n\nNewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.\n\nAlso, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.\n\nTHIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN THE NEWPIPE GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.",
"github": "https://github.com/TeamNewPipe/NewPipe"
},
{
"id": "3",
"name": "FetchIt",
"icon": "/img/apps/fetchit.jpg",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v1.4": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.4/app-release.apk"
},
"v1.3": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.3/app-release.apk"
},
"v1.2": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.2/app-release.apk"
},
"v1.1": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.1/app-release.apk"
},
"v1.0": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.0/app-release.apk"
}
},
"description": "NewPipe is a 3rd party client that supports many services like Youtube, Peertube, SoundCloud, and Bandcamp.\n\nNewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.\n\nAlso, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.\n\nTHIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN THE NEWPIPE GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.",
"github": "https://github.com/TeamNewPipe/NewPipe"
},
{
"id": "4",
"name": "Root Detector",
"icon": "/img/apps/rootdetector.jpg",
"price": "FREE",
"foss": false,
"selfHosted": true,
"downloadLinks": {
"v1.4": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.4/app-release.apk"
},
"v1.3": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.3/app-release.apk"
},
"v1.2": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.2/app-release.apk"
},
"v1.1": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.1/app-release.apk"
},
"v1.0": {
"all": "https://github.com/tharunbirla/FetchIt/releases/download/v1.0/app-release.apk"
}
},
"description": "NewPipe is a 3rd party client that supports many services like Youtube, Peertube, SoundCloud, and Bandcamp.\n\nNewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.\n\nAlso, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.\n\nTHIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN THE NEWPIPE GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.",
"github": "https://github.com/TeamNewPipe/NewPipe"
}
]

106
modules.json Normal file
View File

@ -0,0 +1,106 @@
[
{
"id": "1",
"name": "ReVanced eXtended",
"icon": "/img/modules/rvx.png",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v19.16.39": {
"all": "https://github.com/NoName-exe/revanced-extended/releases/download/126/youtube-revanced-extended-magisk-v19.16.39-all.zip"
},
"v19.25.39": {
"all": "https://github.com/NoName-exe/revanced-extended/releases/download/117/youtube-revanced-extended-magisk-v19.25.39-all.zip"
}
},
"description": "ReVanced eXtended is a mod that extends the functionality of ReVanced. It has additional features to the standard ReVanced patches to YouTube. It has a custom icon.\n\nIt features modifications such as SponsorBlock (skipping segments like sponsorships in videos), local YouTube Premium, and more. The app is themed to fit YouTube premium, and is configured through an option in the YouTube settings.\n\nThis option is lighter on battery, as it doesn't use MicroG, like the non-root option. This module will install the YouTube ReVanced eXtended app. You must install the seperate ReVanced eXtended Music module if you would like YouTube Music.\n\nYou can get more information about ReVanced eXtended at it's GitHub page (listed below).",
"github": "https://github.com/NoName-exe/revanced-extended"
},
{
"id": "2",
"name": "ReVanced eXtended Music",
"icon": "/img/modules/rvx-music.png",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v7.16.53": {
"arm-v7a": "https://github.com/NoName-exe/revanced-extended/releases/download/126/youtube-music-revanced-extended-magisk-v7.16.53-arm-v7a.zip",
"arm64-v8a": "https://github.com/NoName-exe/revanced-extended/releases/download/126/youtube-music-revanced-extended-magisk-v7.16.53-arm64-v8a.zip"
},
"v7.08.52": {
"arm-v7a": "https://github.com/NoName-exe/revanced-extended/releases/download/117/youtube-music-revanced-extended-magisk-v7.08.52-arm-v7a.zip",
"arm64-v8a": "https://github.com/NoName-exe/revanced-extended/releases/download/117/youtube-music-revanced-extended-magisk-v7.08.52-arm64-v8a.zip"
}
},
"description": "ReVanced eXtended Music is a mod that extends the functionality of ReVanced Music. It has additional features to the standard ReVanced patches to YouTube Music, with a custom icon.\n\nIt features modifications such as SponsorBlock (skipping segments like non-music segments in music videos), local YouTube Premium, and more. The app is themed to fit YouTube premium, and is configured through an option in the YouTube Music settings.\n\nThis option is lighter on battery, as it doesn't use MicroG, like the non-root option. This module will install the YouTube ReVanced eXtended Music app. You must install the seperate ReVanced eXtended module if you would like regular YouTube.\n\nYou can get more information about ReVanced eXtended at it's GitHub page (listed below).",
"github": "https://github.com/NoName-exe/revanced-extended"
},
{
"id": "3",
"name": "Tricky Store",
"icon": "/img/modules/ts.png",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v1.2.0": {
"all": "https://github.com/5ec1cff/TrickyStore/releases/download/1.2.0/Tricky-Store-v1.2.0-155-331f6fe-release.zip"
},
"v1.1.3 ": {
"all": "https://github.com/5ec1cff/TrickyStore/releases/download/1.1.3/Tricky-Store-v1.1.3-128-820c2b2-release.zip"
},
"v1.1.2 ": {
"all": "https://github.com/5ec1cff/TrickyStore/releases/download/1.1.2/Tricky-Store-v1.1.2-123-94176b9-release.zip"
}
},
"description": "A trick of keystore. Android 10 or above is required.\n\nThis module is used for modifying the certificate chain generated for Android Key Attestation. In combination with a keybox.xml file, you can spoof Play Integrity requests to return a STRONG hardware-backed attestation status.\n\nYou can get more information about Tricky Store at it's GitHub page (listed below).",
"github": "https://github.com/5ec1cff/TrickyStore"
},
{
"id": "4",
"name": "LSPosed Mod",
"icon": "/img/modules/lsposed.jpg",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v1.2.0": {
"all": "https://github.com/5ec1cff/TrickyStore/releases/download/1.2.0/Tricky-Store-v1.2.0-155-331f6fe-release.zip"
},
"v1.1.3 ": {
"all": "https://github.com/5ec1cff/TrickyStore/releases/download/1.1.3/Tricky-Store-v1.1.3-128-820c2b2-release.zip"
},
"v1.1.2 ": {
"all": "https://github.com/5ec1cff/TrickyStore/releases/download/1.1.2/Tricky-Store-v1.1.2-123-94176b9-release.zip"
}
},
"description": "LSPosed is a great XPosed Framework, but it has a big problem, only manager can manage scope. LSPosed team don't accept PR for CLI or API Module, the TODO issues are old more one year and never completed, is more important the GUI changed many times but not CLI or API Module. In my fork API Module and CLI are implemented. CLI require root user because must access files readable only by root.\n\nA Riru / Zygisk module trying to provide an ART hooking framework which delivers consistent APIs with the OG Xposed, leveraging LSPlant hooking framework.",
"github": "https://github.com/mywalkb/LSPosed_mod"
},
{
"id": "5",
"name": "Play Integrity Fix",
"icon": "/img/modules/ts.png",
"price": "FREE",
"foss": true,
"selfHosted": false,
"downloadLinks": {
"v18.1": {
"all": "https://github.com/chiteroman/PlayIntegrityFix/releases/download/v18.1/PlayIntegrityFix_v18.1.zip"
},
"v18.0": {
"all": "https://github.com/chiteroman/PlayIntegrityFix/releases/download/v18.0/PlayIntegrityFix_v18.0.zip"
},
"v17.9": {
"all": "https://github.com/chiteroman/PlayIntegrityFix/releases/download/v17.9/PlayIntegrityFix_v17.9.zip"
},
"v17.8": {
"all": "https://github.com/chiteroman/PlayIntegrityFix/releases/download/v17.8/PlayIntegrityFix_v17.8.zip"
}
},
"description": "This module tries to fix Play Integrity and SafetyNet verdicts to get a valid attestation.\n\nThis module is not made to hide root, nor to avoid detections in other apps. It only serves to pass Device verdict in the Play Integrity tests and certify your device. All issues created to report a non-Google app not working will be closed without notice.\n\nPLEASE SEE GITHUB FOR SETUP INSTRUCTIONS.",
"github": "https://github.com/chiteroman/PlayIntegrityFix"
}
]

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "modules",
"version": "1.0.0",
"description": "An open-source Magisk modules app store ",
"main": "app.js",
"devDependencies": {},
"repository": {
"type": "git",
"url": "git+https://github.com/ihatenodejs/modules.git"
},
"author": "ihatenodejs",
"license": "Unlicense",
"bugs": {
"url": "https://github.com/ihatenodejs/modules/issues"
},
"homepage": "https://github.com/ihatenodejs/modules#readme",
"scripts": {
"build:css": "tailwindcss -i ./src/css/styles.css -o ./public/css/styles.css"
},
"dependencies": {
"ejs": "^3.1.10",
"express": "^4.21.2",
"helmet": "^8.0.0",
"tailwindcss": "^3.4.16",
"validator": "^13.12.0"
}
}

BIN
public/img/apps/fetchit.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
public/img/apps/muzza.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/apps/newpipe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/img/modules/rvx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
public/img/modules/ts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

22
public/js/desc-pg.js Normal file
View File

@ -0,0 +1,22 @@
const dlBtn = document.getElementById('download-btn');
const ddMenu = document.getElementById('dropdown-menu');
const copyBtn = document.getElementById('copy-btn');
const sourceURL = copyBtn.getAttribute('data-source-url');
dlBtn.addEventListener('click', (event) => {
event.stopPropagation();
ddMenu.classList.toggle('hidden');
});
document.addEventListener('click', () => {
ddMenu.classList.add('hidden');
});
copyBtn.addEventListener('click', (event) => {
event.stopPropagation();
navigator.clipboard.writeText(sourceURL).then(() => {
alert('Source URL copied to clipboard');
}).catch((err) => {
console.error('Failed to copy: ', err);
});
});

6
public/js/main.js Normal file
View File

@ -0,0 +1,6 @@
const menuBtn = document.getElementById('menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
menuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});

3
src/css/styles.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

85
src/views/pages/apps.ejs Normal file
View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> | modules.</title>
<link href="/css/styles.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-slate-900">
<header class="sticky top-0 bg-slate-900 z-50">
<div class="container mx-auto p-4 flex justify-between items-center">
<h1 class="text-4xl font-bold text-white"><a href="/">modules.</a></h1>
<nav class="hidden md:flex space-x-4">
<a href="/" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<p class="text-gray-300 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</p>
<a href="/modules" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
=====================================================================
<a href="#" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
=====================================================================
-->
</nav>
<button id="menu-btn" class="md:hidden text-white focus:outline-none"><i class="fas fa-bars"></i></button>
</div>
<div id="mobile-menu" class="hidden md:hidden">
<a href="/" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<p class="block px-4 py-2 text-gray-300 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</p>
<a href="/modules" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
===================================================================================
<a href="#" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
===================================================================================
-->
</div>
</header>
<div class="container mx-auto p-4">
<h1 class="text-4xl font-bold text-white my-6">Apps</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<% apps.forEach(app => { %>
<a href="/apps/<%= app.id %>" class="bg-slate-700 rounded-lg shadow-md p-4 flex flex-col justify-between no-underline text-black">
<div>
<img src="<%= app.icon %>" alt="<%= app.name %> icon" class="w-16 h-16 mx-auto rounded-full">
<h2 class="text-xl font-bold text-center text-white mt-4"><%= app.name %></h2>
</div>
<p class="text-center text-gray-700 mt-2">
<span class="bg-slate-500 text-white font-bold rounded-full px-3 py-1">
<% if (app.price === 'FREE') { %>
<%= app.price %>
<% } else { %>
$<%= app.price %>
<% } %>
</span>
<% if (app.foss) { %>
<span class="bg-yellow-500 text-white font-bold rounded-full px-3 py-1 ml-2">FOSS</span>
<% } %>
<% if (app.selfHosted) { %>
<span class="bg-green-500 text-white font-bold rounded-full px-3 py-1 ml-2">SelfHost</span>
<% } %>
</p>
</a>
<% }) %>
</div>
</div>
<script src="/js/main.js"></script>
</body>
</html>

74
src/views/pages/home.ejs Normal file
View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link href="/css/styles.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-slate-900">
<header class="sticky top-0 bg-slate-900 z-50">
<div class="container mx-auto p-4 flex justify-between items-center">
<h1 class="text-4xl font-bold text-white"><a href="#">modules.</a></h1>
<nav class="hidden md:flex space-x-4">
<p class="text-gray-300 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</p>
<a href="/apps" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<a href="/modules" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
=====================================================================
<a href="#" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
=====================================================================
-->
</nav>
<button id="menu-btn" class="md:hidden text-white focus:outline-none"><i class="fas fa-bars"></i></button>
</div>
<div id="mobile-menu" class="hidden md:hidden">
<p class="block px-4 py-2 text-gray-300 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</p>
<a href="/apps" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<a href="/modules" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
===================================================================================
<a href="#" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
===================================================================================
-->
</div>
</header>
<div class="container mx-auto p-4">
<h1 class="text-4xl font-bold text-white text-center my-20">What are you looking for?</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<a href="/apps" class="bg-slate-800 bg-opacity-50 backdrop-blur-md rounded-lg shadow-md p-6 flex flex-col justify-between no-underline text-white hover:bg-opacity-70 transition duration-300">
<div class="flex flex-col items-center">
<i class="fas fa-th-large text-9xl text-gray-300"></i>
<h2 class="text-4xl font-bold text-gray-300 mt-4">Apps</h2>
</div>
</a>
<a href="/modules" class="bg-slate-800 bg-opacity-50 backdrop-blur-md rounded-lg shadow-md p-6 flex flex-col justify-between no-underline text-white hover:bg-opacity-70 transition duration-300">
<div class="flex flex-col items-center">
<i class="fas fa-puzzle-piece text-9xl text-gray-300"></i>
<h2 class="text-4xl font-bold text-gray-300 mt-4">Modules</h2>
</div>
</a>
</div>
</div>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link href="/css/styles.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-slate-900">
<header class="sticky top-0 bg-slate-900 z-50">
<div class="container mx-auto p-4 flex justify-between items-center">
<h1 class="text-4xl font-bold text-white"><a href="/">modules.</a></h1>
<nav class="hidden md:flex space-x-4">
<a href="/" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/apps" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<p class="text-gray-300 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</p>
<!--
NOT FINISHED YET
=====================================================================
<a href="#" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
=====================================================================
-->
</nav>
<button id="menu-btn" class="md:hidden text-white focus:outline-none"><i class="fas fa-bars"></i></button>
</div>
<div id="mobile-menu" class="hidden md:hidden">
<a href="/" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/apps" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<p class="block px-4 py-2 text-gray-300 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</p>
<!--
NOT FINISHED YET
===================================================================================
<a href="#" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
===================================================================================
-->
</div>
</header>
<div class="container mx-auto p-4">
<h1 class="text-4xl font-bold text-white my-6">Modules</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<% modules.forEach(module => { %>
<a href="/modules/<%= module.id %>" class="bg-slate-700 rounded-lg shadow-md p-4 flex flex-col justify-between no-underline text-black">
<div>
<img src="<%= module.icon %>" alt="<%= module.name %> icon" class="w-16 h-16 mx-auto rounded-full">
<h2 class="text-xl font-bold text-center text-white mt-4"><%= module.name %></h2>
</div>
<p class="text-center text-gray-700 mt-2">
<span class="bg-slate-500 text-white font-bold rounded-full px-3 py-1">
<% if (module.price === 'FREE') { %>
<%= module.price %>
<% } else { %>
$<%= module.price %>
<% } %>
</span>
<% if (module.foss) { %>
<span class="bg-yellow-500 text-white font-bold rounded-full px-3 py-1 ml-2">FOSS</span>
<% } %>
<% if (module.selfHosted) { %>
<span class="bg-green-500 text-white font-bold rounded-full px-3 py-1 ml-2">SelfHost</span>
<% } %>
</p>
</a>
<% }) %>
</div>
</div>
<script src="/js/main.js"></script>
</body>
</html>

122
src/views/templates/app.ejs Normal file
View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= app.name %> | modules.</title>
<link href="/css/styles.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-slate-900">
<header class="sticky top-0 bg-slate-900 z-50">
<div class="container mx-auto p-4 flex justify-between items-center">
<h1 class="text-4xl font-bold text-white"><a href="/">modules.</a></h1>
<nav class="hidden md:flex space-x-4">
<a href="/" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/apps" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<a href="/modules" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
=====================================================================
<a href="#" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
=====================================================================
-->
</nav>
<button id="menu-btn" class="md:hidden text-white focus:outline-none"><i class="fas fa-bars"></i></button>
</div>
<div id="mobile-menu" class="hidden md:hidden">
<a href="/" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/apps" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<a href="/modules" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
===================================================================================
<a href="#" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
===================================================================================
-->
</div>
</header>
<div class="w-full mx-auto p-4">
<div class="grid grid-cols-1 md:grid-cols-6 gap-4 w-full">
<div class="p-4 col-span-6 md:col-span-2 flex flex-col items-center bg-white bg-opacity-10 backdrop-blur-md rounded-lg border border-white border-opacity-25 shadow-md">
<img src="<%= app.icon %>" alt="<%= app.name %> icon" class="w-32 h-32 mb-4 rounded-full">
<h1 class="text-2xl font-bold text-white text-center mb-2"><%= app.name %></h1>
<p class="text-center mb-4">
<span class="bg-slate-500 text-white font-bold rounded-full px-3 py-1">
<% if (app.price === 'FREE') { %>
<%= app.price %>
<% } else { %>
$<%= app.price %>
<% } %>
</span>
<% if (app.foss) { %>
<span class="bg-yellow-500 text-white font-bold rounded-full px-3 py-1 ml-2">FOSS</span>
<% } %>
<% if (app.selfHosted) { %>
<span class="bg-green-500 text-white font-bold rounded-full px-3 py-1 ml-2">SelfHost</span>
<% } %>
</p>
<form id="download-form" action="/download" method="get" class="w-full">
<div class="relative inline-block text-left w-full">
<label for="version" class="block text-white text-sm font-bold mb-2">Select Version:</label>
<select id="version" name="version" class="block appearance-none w-full bg-gray-700 border border-gray-600 text-white py-2 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-gray-600 focus:border-gray-500">
<% Object.keys(app.downloadLinks).forEach(function(version) { %>
<option value="<%= version %>"><%= version %></option>
<% }); %>
</select>
</div>
<div class="relative inline-block text-left w-full mt-4">
<label for="arch" class="block text-white text-sm font-bold mb-2">Select Architecture:</label>
<select id="arch" name="arch" class="block appearance-none w-full bg-gray-700 border border-gray-600 text-white py-2 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-gray-600 focus:border-gray-500">
<% Object.keys(app.downloadLinks[Object.keys(app.downloadLinks)[0]]).forEach(function(arch) { %>
<option value="<%= arch %>"><%= arch %></option>
<% }); %>
</select>
</div>
<div class="mt-4 w-full">
<button type="submit" class="inline-flex justify-center w-full rounded-full border border-gray-300 shadow-sm px-4 py-2 bg-slate-500 text-sm font-medium text-white hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500">
<i class="fas fa-download mr-2"></i> Download
</button>
</div>
<input type="hidden" name="name" value="<%= app.name %>">
<input type="hidden" name="type" value="app">
</form>
</div>
<div class="p-4 col-span-6 md:col-span-4 bg-white bg-opacity-10 backdrop-blur-md rounded-lg border border-white border-opacity-25 shadow-md">
<p class="text-white"><%- app.description.replace(/\n/g, '<br>') %></p>
</div>
<div class="p-4 col-span-6 md:col-span-4 md:col-start-3 bg-white bg-opacity-10 backdrop-blur-md rounded-lg border border-white border-opacity-25 shadow-md">
<h2 class="text-xl font-bold text-white mb-4 flex items-center">
<i class="fab fa-github mr-2"></i> GitHub Repository
</h2>
<a href="<%= app.github %>" class="text-blue-400 hover:text-blue-500 break-all">
<i class="fas fa-external-link-alt mr-2"></i><%= app.github %>
</a>
</div>
</div>
</div>
<script>
const menuBtn = document.getElementById('menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
menuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
</script>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download <%= name %> | modules.</title>
<link href="/css/styles.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-slate-900">
<header class="sticky top-0 bg-slate-900 z-50">
<div class="container mx-auto p-4 flex justify-between items-center">
<h1 class="text-4xl font-bold text-white"><a href="/">modules.</a></h1>
</div>
</header>
<div class="container mx-auto p-4">
<h1 class="text-4xl font-bold text-white text-center mt-20 mb-10">I would like to download the <span class="text-slate-500"><%= name %></span> <%= type %>, version <span class="text-slate-500"><%= version %></span> for <span class="text-slate-500"><%= arch %></span></h1>
<div class="flex justify-center">
<a href="<%= downloadLink %>" class="bg-slate-500 text-white font-bold rounded-full px-6 py-3 text-center hover:bg-slate-600 transition duration-300">
<i class="fas fa-download mr-2"></i> Download
</a>
</div>
<p class="text-xl text-center font-bold text-gray-500 mt-10">Your file is being downloaded from: <span class="text-gray-600"><a href="<%= downloadLink %>"><%= downloadLink %></a></span></p>
</div>
</body>
</html>

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= module.name %> | modules.</title>
<link href="/css/styles.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-slate-900">
<header class="sticky top-0 bg-slate-900 z-50">
<div class="container mx-auto p-4 flex justify-between items-center">
<h1 class="text-4xl font-bold text-white"><a href="/">modules.</a></h1>
<nav class="hidden md:flex space-x-4">
<a href="/" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/apps" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<a href="/modules" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
=====================================================================
<a href="#" class="text-white hover:text-gray-400 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
=====================================================================
-->
</nav>
<button id="menu-btn" class="md:hidden text-white focus:outline-none"><i class="fas fa-bars"></i></button>
</div>
<div id="mobile-menu" class="hidden md:hidden">
<a href="/" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/apps" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-th-large mr-2"></i> Apps
</a>
<a href="/modules" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-puzzle-piece mr-2"></i> Modules
</a>
<!--
NOT FINISHED YET
===================================================================================
<a href="#" class="block px-4 py-2 text-white hover:bg-gray-700 flex items-center">
<i class="fas fa-tags mr-2"></i> Categories
</a>
===================================================================================
-->
</div>
</header>
<div class="w-full mx-auto p-4">
<div class="grid grid-cols-1 md:grid-cols-6 gap-4 w-full">
<div class="p-4 col-span-6 md:col-span-2 flex flex-col items-center bg-white bg-opacity-10 backdrop-blur-md rounded-lg border border-white border-opacity-25 shadow-md">
<img src="<%= module.icon %>" alt="<%= module.name %> icon" class="w-32 h-32 mb-4 rounded-full">
<h1 class="text-2xl font-bold text-white text-center mb-2"><%= module.name %></h1>
<p class="text-center mb-4">
<span class="bg-slate-500 text-white font-bold rounded-full px-3 py-1">
<% if (module.price === 'FREE') { %>
<%= module.price %>
<% } else { %>
$<%= module.price %>
<% } %>
</span>
<% if (module.foss) { %>
<span class="bg-yellow-500 text-white font-bold rounded-full px-3 py-1 ml-2">FOSS</span>
<% } %>
<% if (module.selfHosted) { %>
<span class="bg-green-500 text-white font-bold rounded-full px-3 py-1 ml-2">SelfHost</span>
<% } %>
</p>
<form id="download-form" action="/download" method="get" class="w-full">
<div class="relative inline-block text-left w-full">
<label for="version" class="block text-white text-sm font-bold mb-2">Select Version:</label>
<select id="version" name="version" class="block appearance-none w-full bg-gray-700 border border-gray-600 text-white py-2 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-gray-600 focus:border-gray-500">
<% Object.keys(module.downloadLinks).forEach(function(version) { %>
<option value="<%= version %>"><%= version %></option>
<% }); %>
</select>
</div>
<div class="relative inline-block text-left w-full mt-4">
<label for="arch" class="block text-white text-sm font-bold mb-2">Select Architecture:</label>
<select id="arch" name="arch" class="block appearance-none w-full bg-gray-700 border border-gray-600 text-white py-2 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-gray-600 focus:border-gray-500">
<% Object.keys(module.downloadLinks[Object.keys(module.downloadLinks)[0]]).forEach(function(arch) { %>
<option value="<%= arch %>"><%= arch %></option>
<% }); %>
</select>
</div>
<div class="mt-4 w-full">
<button type="submit" class="inline-flex justify-center w-full rounded-full border border-gray-300 shadow-sm px-4 py-2 bg-slate-500 text-sm font-medium text-white hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500">
<i class="fas fa-download mr-2"></i> Download
</button>
</div>
<input type="hidden" name="name" value="<%= module.name %>">
<input type="hidden" name="type" value="module">
</form>
</div>
<div class="p-4 col-span-6 md:col-span-4 bg-white bg-opacity-10 backdrop-blur-md rounded-lg border border-white border-opacity-25 shadow-md">
<p class="text-white"><%- module.description.replace(/\n/g, '<br>') %></p>
</div>
<div class="p-4 col-span-6 md:col-span-4 md:col-start-3 bg-white bg-opacity-10 backdrop-blur-md rounded-lg border border-white border-opacity-25 shadow-md">
<h2 class="text-xl font-bold text-white mb-4 flex items-center">
<i class="fab fa-github mr-2"></i> GitHub Repository
</h2>
<a href="<%= module.github %>" class="text-blue-400 hover:text-blue-500 break-all">
<i class="fas fa-external-link-alt mr-2"></i><%= module.github %>
</a>
</div>
</div>
</div>
<script>
const menuBtn = document.getElementById('menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
menuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
</script>
</body>
</html>

11
tailwind.config.js Normal file
View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/views/**/*.ejs'
],
theme: {
extend: {},
},
plugins: [],
};