Initial commit

This commit is contained in:
foxster-mp4
2023-04-27 20:27:37 -07:00
commit d35389cc9a
18 changed files with 1468 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Temporary files
*~
~$*.doc*
~$*.xls*
~$*.ppt*
*.xlk
*.pdf
# .DS_Store files
*.DS_Store
# Preference files
.vscode/

105
app.html Normal file
View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/uibanner.css">
</head>
<body class="loading">
<div id="loading">
<img src="img/loading.gif" alt="loading">
<p>Loading</p>
</div>
<div id="main">
<div id="top">
<!-- Navigation bar -->
<div id="nav-bar">
<button id="back" type="button">
<i class="bi bi-chevron-left"></i>
Back
</button>
<div id="title" class="hidden">
<img src="https://raw.githubusercontent.com/therealFoxster/AltSource/master/img/icons/generic_app.jpeg" alt="">
<p>AltSource</p>
</div>
<a href="https://example.com" class="install hidden">
<button class="uibutton">Free</button>
</a>
</div>
</div>
<!-- Content -->
<div class="item">
<div class="app-header">
<div class="content">
<img src="https://raw.githubusercontent.com/therealFoxster/AltSource/master/img/icons/generic_app.jpeg" alt="">
<div class="right">
<div class="text">
<p class="title">AltSource</p>
<p class="subtitle">therealFoxster</p>
</div>
<div class="ipa">
<a href="https://example.com" class="install">
<button class="uibutton">Free</button>
</a>
<a href="http://example.com" id="download">
Download IPA
</a>
</div>
</div>
</div>
<div class="background"></div>
</div>
</div>
<div id="preview" class="section">
<p id="subtitle">The quick brown fox jumps over the lazy dog.</p>
<div class="header">
<h2>Preview</h2>
</div>
<div id="screenshots"></div>
<p id="description">
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
</p>
</div>
<div id="whats-new" class="section">
<div class="header">
<h2>What's New</h2>
<p id="version-date">Apr 10, 2023</p>
</div>
<div class="header">
<p id="version">Version 2.0</p>
<p id="version-size">0 KB</p>
</div>
<p id="version-description">
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
</p>
</div>
<div id="permissions" class="section">
<div class="header">
<h2>Permissions</h2>
</div>
<div class="permission">
<i class="bi-person-fill-check"></i>
<div class="text">
<p class="title">None</p>
<p class="description">The developer has not specified any permission required by this app.</p>
</div>
</div>
</div>
</div>
<script src="js/main.js"></script>
<script src="js/app.js"></script>
</body>
</html>

24
apps.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/apps.css">
<link rel="stylesheet" href="css/uibanner.css">
</head>
<body class="loading">
<div id="loading">
<img src="img/loading.gif" alt="loading">
<p>Loading</p>
</div>
<!-- Add to AltStore banner & navigation bar -->
<div id="top"></div>
<div id="main">
<div id="apps"></div>
</div>
<script src="js/main.js"></script>
<script src="js/apps.js"></script>
</body>
</html>

156
css/app.css Normal file
View File

@@ -0,0 +1,156 @@
a {
color: var(--app-tint-color)
}
/* Main */
#main {
padding: unset;
overflow: hidden;
}
#main .item {
margin-top: 30%;
margin-bottom: 0;
padding: 1em;
}
#nav-bar #title,
#nav-bar .install {
transition: opacity 0.25s ease-in-out;
}
/* IPA install & download */
.ipa {
display: flex;
flex-direction: column;
align-items: center;
}
.ipa #download {
position: absolute;
margin-top: 36px; /* 32px button height + 4px gap */
font-size: 0.65em;
font-weight: 450;
}
/* Preview */
#preview {
position: relative;
}
#preview #subtitle {
text-align: center;
font-size: 0.9em;
margin: 0 1em;
}
#preview .header {
margin: 1em 0;
}
#preview #screenshots {
display: flex;
flex-direction: row;
overflow: scroll;
padding-right: 1em;
}
#preview #screenshots>img {
max-width: 70%;
margin-left: 1em;
border-radius: 0.25em;
}
#preview #description {
margin: 1em;
-webkit-line-clamp: 5;
line-clamp: 5;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
position: relative;
word-wrap: break-word
}
#more button {
min-width: 0 !important;
margin-left: 2px !important;
padding: 0;
font-size: 15px;
font-weight: 500;
text-transform: none;
border-radius: 4px;
border: none;
background: unset;
color: var(--accent-color);
cursor: pointer;
position: absolute;
bottom: 0;
right: 0;
padding-left: 36px;
background: linear-gradient(to right, transparent, var(--color-bg) 35%);
}
@media (hover:hover) {
#more button:hover {
opacity: 0.75;
}
}
/* What's new */
#whats-new,
#permissions {
padding: 1em;
}
#whats-new .header,
#permissions .header {
margin: 0.35em 0;
padding: 0;
}
#whats-new .header>p:first-of-type {
opacity: 0.5;
}
#whats-new .header>p {
opacity: 0.35;
}
#whats-new #version-description {
-webkit-line-clamp: 3;
line-clamp: 3;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
position: relative;
word-wrap: break-word
}
/* Permissions */
#permissions .permission {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75em;
margin: 0.5em 0;
}
#permissions .permission i {
font-size: 1.55em;
color: var(--app-tint-color);
}
#permissions .permission .title {
font-weight: 550;
}
#permissions .permission .description {
font-size: 0.9em;
opacity: 0.5;
}

27
css/apps.css Normal file
View File

@@ -0,0 +1,27 @@
#main {
padding-top: 7rem;
}
#apps .app-container {
margin: 1em 1rem 3.25em 1rem;
}
#apps .app-container:last-of-type {
margin-bottom: 1em;
}
.screenshots {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 1em;
}
.screenshots:last-of-type {
margin-bottom: 1em;
}
.screenshot {
max-width: 48%;
border-radius: 4px;
}

45
css/index.css Normal file
View File

@@ -0,0 +1,45 @@
#main {
padding-top: 2rem;
}
#main #title {
margin-bottom: 0.35rem;
padding: 0 1rem;
}
#main input {
width: 95%;
font-size: 1em;
padding: 0.5rem 0.8rem;
margin-bottom: 1rem;
border: none;
border-radius: 10px;
background-color: rgba(0, 0, 0, 0.07);
}
.suggestion {
padding: 1rem 1rem 1rem 0;
margin-left: 1rem;
font-size: 1.1rem;
border-bottom: 0.1px solid var(--color-separator);
}
.suggestion .bi {
margin-right: 4px;
}
@media (prefers-color-scheme: dark) {
#main input {
background-color: rgba(255, 255, 255, 0.07);
}
.suggestion {
border-bottom: 0.1px solid var(--color-separator-dark) !important;
}
}
#main .textfield {
display: flex;
align-items: center;
justify-content: center;
padding: 0 1rem;
}

389
css/main.css Normal file
View File

@@ -0,0 +1,389 @@
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css");
:root {
--accent-color: #018084;
--app-tint-color: #018084;
--color-bg: rgba(255, 255, 255, 255);
--color-separator: rgba(0, 0, 0, 0.15);
--color-primary: rgba(0, 122, 254, 255);
--color-transparent: rgba(255, 255, 255, 0.75);
--color-bg-dark: rgb(26, 25, 27);
--color-separator-dark: rgba(255, 255, 255, 0.15);
--color-primary-dark: rgba(11, 132, 254, 255);
--color-transparent-dark: rgba(26, 25, 27, 0.25);
}
* {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
}
/* Dark appearance */
@media (prefers-color-scheme: dark) {
body,
.uibanner,
#loading {
background-color: var(--color-bg-dark) !important;
}
p,
h1,
h2 {
color: white;
}
#nav-bar {
background-color: var(--color-transparent-dark) !important;
}
.uibanner,
#nav-bar {
border-color: var(--color-separator-dark) !important;
}
.app-header .background {
opacity: 0.35 !important;
}
#more button {
background: linear-gradient(to right, transparent, var(--color-bg-dark) 35%) !important;
}
}
body.loading {
overflow: hidden;
}
#loading {
position: fixed;
left: 0;
right: 0;
height: 100vh;
margin: 0 auto;
z-index: 4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
background: var(--color-bg);
}
#loading img {
width: 24px;
opacity: 0.5;
}
#loading p {
font-size: 0.75em;
font-weight: 500;
text-transform: uppercase;
opacity: 0.35;
}
/* h1#title {
padding-left: 1rem;
} */
body {
width: 100%;
padding-bottom: 20px;
}
@media screen and (min-device-width: 767px) {
body {
margin: 0 auto;
max-width: 414px;
}
}
.hidden {
opacity: 0;
pointer-events: none;
}
#top {
position: fixed;
z-index: 5;
left: 0;
right: 0;
}
#title {
margin-bottom: 0.35em;
}
#main>p:first-of-type {
text-transform: uppercase;
font-weight: 500;
font-size: 0.9em;
opacity: 0.5;
padding-left: 1rem;
margin-top: 1rem;
}
/* Navigation bar */
#nav-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 0.1px solid var(--color-separator);
background-color: var(--color-transparent);
-webkit-backdrop-filter: saturate(100%) blur(30px);
backdrop-filter: saturate(100%) blur(20px);
max-width: 414px;
margin: 0 auto;
padding: 0.25em 0.75em;
z-index: 2;
}
#nav-bar #title {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
min-height: 2.5em;
margin: 0;
}
#nav-bar #title>p {
font-weight: 600;
}
#nav-bar #title>img {
max-height: 2em;
border-radius: 0.4em;
}
#nav-bar #back {
cursor: pointer;
}
#nav-bar #back .bi {
margin-left: -8px;
-webkit-text-stroke: 1px;
font-size: 1.3em;
}
/* Main */
#main {
/* padding: 1em; */
padding-top: 3.55rem;
}
.item {
margin: 1rem 0;
}
/* Section header */
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
}
/* News */
#news-items {
display: grid;
height: 100%;
gap: 1em;
grid-auto-columns: 95%;
grid-auto-flow: column;
overflow-x: scroll;
padding: 1rem;
}
#news-items.one {
grid-auto-columns: 100% !important;
}
#news .item {
display: flex;
flex-direction: column;
justify-content: center;
border-radius: 1.5em;
overflow: hidden;
min-width: 100%;
height: 100%;
margin: 0;
}
#news .item>.text {
margin: 1.5em;
color: #fff;
}
#news .item>.text>p {
opacity: 0.75;
font-size: 0.9em;
}
#news .item .image-wrapper {
max-height: 15.15em;
overflow: hidden;
}
#news img {
width: 100%;
height: auto;
display: block;
}
/* App header */
#apps.section .item {
padding: 0 1rem;
}
.app-header {
position: relative;
display: flex;
}
.app-header>.content {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: 1rem;
}
.app-header img {
display: block;
border-radius: 13.5px;
max-width: 64px;
}
.app-header .background {
position: absolute;
background-color: var(--accent-color);
opacity: 0.2;
border-radius: 1.5em;
width: 100%;
height: 100%;
z-index: -1;
}
.app-header>.content>.right {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
margin-left: 0.65em;
}
.app-header>.content .text>.title {
font-weight: 600;
font-size: 1rem;
}
.app-header>.content .text>.subtitle {
opacity: 0.5;
font-size: 0.85em;
}
/* About */
#about a:visited {
color: var(--accent-color)
}
#about.section .item {
padding: 0 1rem;
}
/* Buttons */
button.uibutton {
min-width: 78px;
height: 32px;
padding: 0 12px;
font-size: 15px;
font-weight: 700;
text-transform: uppercase;
border: none;
border-radius: 20px;
color: white;
background-color: var(--accent-color);
cursor: pointer;
}
button.uibutton:active {
opacity: 0.8;
}
/* Links */
a {
cursor: pointer;
text-decoration: none;
color: var(--accent-color)
}
a>button {
cursor: pointer;
}
@media (hover:hover) {
a:hover {
opacity: 0.75;
}
}
.header a,
#nav-bar #back {
display: flex;
align-items: center;
font-size: 1.05em;
font-weight: 450;
border: none;
background: unset;
padding: 0;
color: var(--accent-color);
}
/* Badges */
.badge {
margin-left: 0.05rem !important;
margin-right: 0.05rem !important;
padding: 0.5px 6.5px;
font-size: 10px;
line-height: 1.3333733333;
font-weight: 700;
border-radius: 12px;
border-width: 1px;
border-style: solid;
box-sizing: border-box;
white-space: nowrap;
display: inline-block;
background: transparent;
text-transform: uppercase;
vertical-align: middle;
color: white;
border-color: transparent;
}
.beta.badge {
margin-bottom: 2px;
background-color: #f5a10d;
}
.beta.badge::before {
content: "Beta";
}
.small.badge {
font-size: 8px;
}

19
css/news.css Normal file
View File

@@ -0,0 +1,19 @@
#main {
padding-top: 8rem;
}
#news {
padding: 0 1rem;
}
.news-item-wrapper {
margin-bottom: 3em;
}
.news-item-wrapper:last-of-type {
margin-bottom: 1em;
}
.news-item-wrapper .item {
margin-bottom: 1rem !important;
}

68
css/uibanner.css Normal file
View File

@@ -0,0 +1,68 @@
.uibanner .icon {
max-width: 30px;
border-radius: 7px;
border: 0.75px solid rgba(0, 0, 0, 0.1);
}
.uibanner {
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 1px;
border-bottom: 0.1px solid var(--color-separator);
}
.uibanner {
background: white;
}
.uibanner>.icon {
margin-left: 14px;
display: flex;
}
.uibanner>.content {
min-height: 28px;
width: 100%;
margin-left: 8px;
padding: 8px 16px 8px 0px;
display: flex;
justify-content: space-between;
align-items: center;
}
.uibanner>.content>.text-container>.title-text {
padding-bottom: 0 !important;
font-weight: 600;
font-size: 12.95px !important;
}
.uibanner>.content>.text-container>.detail-text {
padding-top: 0 !important;
font-size: 11px !important;
font-weight: 400;
line-height: 12px !important;
opacity: 0.55;
}
.uibanner>.content button {
min-width: 68px !important;
padding: 5px 13px;
font-size: 15px;
font-weight: 600;
text-transform: uppercase;
border: none;
border-radius: 20px;
color: white;
background-color: rgb(0, 122, 254);
cursor: pointer;
}
.uibanner>.content a {
text-decoration: none;
color: unset;
font-weight: 600;
color: white;
}

48
home.html Normal file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/uibanner.css">
<link rel="stylesheet" href="css/main.css">
</head>
<body class="loading">
<div id="loading">
<img src="img/loading.gif" alt="loading">
<p>Loading</p>
</div>
<!-- Add to AltStore banner -->
<div id="top"></div>
<div id="main">
<p>Now viewing</p>
<div class="header">
<h1 id="title">altsource-v2</h1>
<a href="index.html"><i class="bi bi-pencil-square"></i></a>
</div>
<div id="news" class="section">
<div class="header">
<h2>News</h2>
<a href="index.html">View All</a>
</div>
<div id="news-items"></div>
</div>
<div id="apps" class="section">
<div class="header">
<h2>Featured Apps</h2>
<a href="index.html">View All Apps</a>
</div>
</div>
<div id="about" class="section">
<div class="header">
<h2>About</h2>
</div>
</div>
</div>
<script src="js/main.js"></script>
<script src="js/home.js"></script>
</body>
</html>

61
index.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/uibanner.css">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<div id="main">
<h1 id="title">AltSource Viewer</h1>
<div class="textfield">
<input type="text" placeholder="Source URL">
</div>
<div id="suggestions" class="section">
<div class="header">
<h2>Suggested</h2>
</div>
<a href="home.html?source=https://therealfoxster.github.io/altsource/data/apps.json">
<div class="suggestion">
<i class="bi bi-search"></i>
Foxster's AltSource
</div>
</a>
<a href="home.html?source=https://qnblackcat.github.io/AltStore/apps.json">
<div class="suggestion">
<i class="bi bi-search"></i>
Qn_'s AltStore Repo
</div>
</a>
<a href="home.html?source=https://alt.getutm.app">
<div class="suggestion">
<i class="bi bi-search"></i>
UTM Repository
</div>
</a>
<a href="home.html?source=https://flyinghead.github.io/flycast-builds/altstore.json">
<div class="suggestion">
<i class="bi bi-search"></i>
Flyinghead
</div>
</a>
<a href="home.html?source=https://provenance-emu.com/apps.json">
<div class="suggestion">
<i class="bi bi-search"></i>
Provenance EMU
</div>
</a>
<a href="https://therealfoxster.github.io/altsource-viewer" target="_blank">
<div class="suggestion">
<i class="bi bi-code-slash"></i>
Source Code
</div>
</a>
</div>
</div>
<script src="js/index.js"></script>
</body>
</html>

216
js/app.js Normal file
View File

@@ -0,0 +1,216 @@
const bundleId = urlSearchParams.get('id');
if (!urlSearchParams.has('id') || !bundleId) nope();
// Hide/show navigation bar title & install button
let hidden = false;
window.onscroll = function (e) {
const appName = document.querySelector(".app-header .text>.title");
const title = document.getElementById("title");
const button = document.querySelector("#nav-bar .install");
if (hidden && appName.getBoundingClientRect().y >= 72) { // App name not visible
hidden = false;
title.classList.add("hidden");
button.classList.add("hidden");
button.disaled = true;
} else if (!hidden && appName.getBoundingClientRect().y < 72) {
hidden = true;
title.classList.remove("hidden");
button.classList.remove("hidden");
button.disaled = false;
}
}
fetch(sourceURL)
.then(response => response.json())
.then(json => {
const apps = json.apps.filter(app => app.bundleIdentifier === bundleId);
const app = apps[0];
if (!app) {
alert(`Unable to find app matching bundle identifier "${bundleId}".\nYou will now be redirected to the home page.`);
nope();
}
document.title = `${app.name} - ${json.name}`;
const tintColor = `#${app.tintColor}`;
if (tintColor)
document.querySelector(':root').style.setProperty("--app-tint-color", `${tintColor}`);
// Tint back button
const backButton = document.getElementById("back");
backButton.style.color = tintColor;
const installButtons = document.querySelectorAll("a.install");
installButtons.forEach(button => {
button.href = `altstore://install?url=${app.downloadURL}`;
});
const downloadButton = document.getElementById("download");
downloadButton.href = app.downloadURL;
//
// Navigation bar
//
const navBar = document.getElementById("nav-bar");
const navBarIcon = navBar.querySelector("#title>img");
const navBarTitle = navBar.querySelector("#title>p");
const navBarInstallButton = navBar.querySelector(".uibutton");
navBarTitle.textContent = app.name;
navBarIcon.src = app.iconURL;
navBarInstallButton.style.backgroundColor = `${tintColor}`;
//
// App header
//
const appHeader = document.querySelector("#main .app-header");
const appHeaderIcon = appHeader.querySelector("img");
const appHeaderTitle = appHeader.querySelector(".title");
const appHeaderSubtitle = appHeader.querySelector(".subtitle");
const appHeaderInstallButton = appHeader.querySelector(".uibutton");
const appHeaderBackground = appHeader.querySelector(".background");
appHeaderIcon.src = app.iconURL;
appHeaderTitle.textContent = app.name;
appHeaderSubtitle.textContent = app.developerName;
appHeaderInstallButton.style.backgroundColor = tintColor;
appHeaderBackground.style.backgroundColor = tintColor;
//
// Preview
//
const preview = document.getElementById("preview");
const previewSubtitle = preview.querySelector("#subtitle");
const previewScreenshots = preview.querySelector("#screenshots");
const previewDescription = preview.querySelector("#description");
previewSubtitle.textContent = app.subtitle;
app.screenshotURLs.forEach(url => {
previewScreenshots.insertAdjacentHTML("beforeend", `<img src="${url}" alt="">`);
});
let localizedDescription = app.localizedDescription;
const perviewDescriptionURLs = [...new Set(localizedDescription.match(urlRegex))]; // Creating set from array to remove duplicates
perviewDescriptionURLs.forEach(url => {
localizedDescription = localizedDescription.replaceAll(url, `<a href="${url}">${url}</a>`)
});
previewDescription.innerHTML = localizedDescription.replaceAll("\n", "<br>");
const more = `
<a id="more" onclick="revealTruncatedText(this);">
<button style="color: ${tintColor};">more</button>
</a>`;
if (previewDescription.scrollHeight > previewDescription.clientHeight)
previewDescription.insertAdjacentHTML("beforeend", more);
//
// Version info
//
const versionDate = document.getElementById("version-date");
const version = document.getElementById("version");
const versionSize = document.getElementById("version-size");
const versionDescription = document.getElementById("version-description");
// Version date
const versionDateObject = new Date(app.versionDate),
month = versionDateObject.toUTCString().split(" ")[2],
date = versionDateObject.getDate(),
dateString = `${month} ${date}, ${versionDateObject.getFullYear()}`;
const today = new Date();
const msPerDay = 60 * 60 * 24 * 1000;
const msDifference = today.valueOf() - versionDateObject.valueOf();
versionDate.textContent = dateString;
if (msDifference <= msPerDay) // Today
versionDate.textContent = "Today";
else if (msDifference <= msPerDay * 2) // Yesterday
versionDate.textContent = "Yesterday";
// Version number
version.textContent = `Version ${app.version}`;
// Version size
const units = ["B", "KB", "MB", "GB"];
var appSize = app.size, c = 0;
while (appSize > 1024) {
appSize = parseFloat(appSize/1024).toFixed(1);
c++;
}
versionSize.textContent = `${appSize} ${units[c]}`;
// Version description
var appVersionDescription = app.versionDescription;
const urls = [...new Set(appVersionDescription.match(urlRegex))]; // Creating set from array to remove duplicates
urls.forEach(url =>
appVersionDescription = appVersionDescription.replaceAll(url, `<a href="${url}">${url}</a>`)
);
versionDescription.innerHTML = appVersionDescription.replaceAll("\n", "<br>");
if (versionDescription.scrollHeight > versionDescription.clientHeight)
versionDescription.insertAdjacentHTML("beforeend", more);
//
// Permissions
//
const permissions = document.getElementById("permissions");
if (app.permissions)
permissions.querySelector(".permission").remove();
app.permissions?.forEach(permission => {
let permissionType = "Unknown", icon = "gear-wide-connected";
switch (permission.type) {
case "background-audio":
permissionType = "Background Audio";
// icon = "audio";
icon = "volume-up-fill";
break;
case "background-fetch":
permissionType = "Background Fetch";
// icon = "fetch";
icon = "arrow-repeat"
break;
case "photos":
permissionType = "Photos"
// icon = "photos";
icon = "image-fill";
break;
default:
break;
}
const html = `
<div class="permission">
<i class="bi-${icon}" style="color: ${tintColor};"></i>
<div class="text">
<p class="title">${permissionType}</p>
<p class="description">${permission.usageDescription ?? "No description provided."}</p>
</div>
</div>`;
permissions.insertAdjacentHTML("beforeend", html);
});
waitForAllImagesToLoad();
});
function revealTruncatedText(moreButton) {
const textId = moreButton.parentNode.id;
const text = document.getElementById(textId);
text.style.display = "block";
text.style.overflow = "auto";
text.style.webkitLineClamp = "none";
text.style.lineClamp = "none";
text.removeChild(moreButton)
}
function nope() {
window.location.replace("index.html");
}

37
js/apps.js Normal file
View File

@@ -0,0 +1,37 @@
addNavigationBar("All Apps");
fetch(sourceURL)
.then(response => response.json())
.then(json => {
if (json.tintColor) setTintColor(json.tintColor)
document.title = `Apps - ${json.name}`;
json.apps.sort((a, b) => (new Date(b.versionDate)).valueOf() - (new Date(a.versionDate)).valueOf());
json.apps.forEach(app => {
if (app.beta) return; // Ignore beta apps
const urls = app.screenshotURLs;
let html = `
<div class="app-container">`;
html +=
appHeaderHTML(app);
html += `
<p style="text-align: center; font-size: 0.9em;">${app.subtitle ?? ""}</p>`;
if (urls) {
html += `
<div class="screenshots">`;
for (let i = 0; i < urls.length, i < 2; i++) html += `
<img src="${urls[i]}" class="screenshot">`;
html += `
</div>`;
}
html += `
</div>`;
document.getElementById("apps").insertAdjacentHTML("beforeend", html);
});
waitForAllImagesToLoad();
});

72
js/home.js Normal file
View File

@@ -0,0 +1,72 @@
fetch(sourceURL)
.then(response => response.json())
.then(json => {
document.querySelector("#news a").href = `news.html?source=${sourceURL}`;
document.querySelector("#apps a").href = `apps.html?source=${sourceURL}`;
if (json.tintColor) setTintColor(json.tintColor)
document.title = json.name;
document.getElementById("title").innerText = json.name;
// Sort apps in descending order
json.apps.sort((a, b) => {
// If b < a
return (new Date(b.versionDate)).valueOf() - (new Date(a.versionDate)).valueOf();
});
if (json.news && json.news.length >= 1) {
// Sort news in decending order (latest first)
json.news.sort((a, b) => (new Date(b.date)).valueOf() - (new Date(a.date)).valueOf());
// News
if (json.news.length == 1) {
document.getElementById("news-items").insertAdjacentHTML("beforeend", newsItemHTML(json.news[0], json.apps, true));
document.getElementById("news-items").classList.add("one");
} else if (json.news.length > 1) {
for (let i = 0; i < 5 && i < json.news.length; i++) {
document.getElementById("news-items").insertAdjacentHTML("beforeend", newsItemHTML(json.news[i], json.apps, true));
}
}
} else {
document.getElementById("news").remove();
}
// Apps
let count = 1;
json.apps.forEach(app => {
// Max: 3 featured apps if not specified
if (count > 3) return;
// Ignore beta apps
if (app.beta) return;
// If there are featured apps, ignore non-featured apps
if (json.featuredApps && !json.featuredApps.includes(app.bundleIdentifier)) return;
document.getElementById("apps").insertAdjacentHTML("beforeend", appHeaderHTML(app));
count++;
});
var description = json.description;
if (description) {
const urls = [...new Set(description.match(urlRegex))]; // Creating set from array to remove duplicates
urls.forEach(url =>
description = description.replaceAll(url, `<a href="${url}">${url}</a>`)
);
document.getElementById("about").insertAdjacentHTML("beforeend", `
<div class="item">
<p>${description.replaceAll("\n", "<br>")}</p>
</div>
`);
} else {
document.getElementById("about").remove();
}
waitForAllImagesToLoad();
});

16
js/index.js Normal file
View File

@@ -0,0 +1,16 @@
const textField = document.querySelector("input");
textField.addEventListener("keypress", function (event) {
// If the user presses the "Enter" key on the keyboard
if (event.key === "Enter") {
event.preventDefault();
const url = textField.value;
const urlRegex = /(https?:\/\/[^ ]*)/g;
if (!url.match(urlRegex))
alert("Invalid URL.");
else
window.location.replace(`home.html?source=${url}`);
}
});

132
js/main.js Normal file
View File

@@ -0,0 +1,132 @@
const urlSearchParams = new URLSearchParams(window.location.search);
const sourceURL = urlSearchParams.get('source');
// If no source
if (!urlSearchParams.has('source') || !sourceURL) {
alert(`No source provided.`);
window.location.replace("index.html");
}
// https://stackoverflow.com/a/31760088
const urlRegex = /(https?:\/\/[^ ]*)/g; // "g": global flag; without this, match() returns only the first matching result
// If source is not a URL
if (!sourceURL.match(urlRegex)) {
alert("Invalid URL.");
window.location.replace("index.html");
}
// Back button
document.getElementById("back")?.addEventListener("click", () => history.back(1));
// Add to AltStore banner
document.getElementById("top")?.insertAdjacentHTML("afterbegin", `
<div class="uibanner">
<img src="https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png" alt="altstore-icon" class="icon">
<div class="content">
<div class="text-container">
<p class="title-text">AltStore <span class="small beta badge"></span></p>
<p class="detail-text">
Add this source to AltStore to receive app updates (requires AltStore beta)
</p>
</div>
<a href="altstore://source?url=${sourceURL}">
<button>Add</button>
</a>
</div>
</div>`);
function waitForAllImagesToLoad() {
const allImages = document.querySelectorAll("img");
var count = 0;
allImages.forEach(image => {
// New img element that won't be rendered to the DOM
var newImage = document.createElement("img");
// Attach load listener
newImage.addEventListener("load", imageLoaded);
// Set src
newImage.src = image.src;
})
function imageLoaded() {
if (++count == allImages.length) {
document.querySelector("body").classList.remove("loading");
document.getElementById("loading").remove();
}
}
}
function setTintColor(color) {
document.querySelector(':root').style.setProperty("--accent-color", `#${color}`);
}
function addNavigationBar(title) {
document.getElementById("top").insertAdjacentHTML("beforeend", `
<div id="nav-bar">
<button id="back" type="button">
<i class="bi bi-chevron-left"></i>
Back
</button>
<div id="title">
<p>${title ?? ""}</p>
</div>
<button id="back" class="hidden">
<i class="bi bi-chevron-left"></i>
Back
</button>
</div>`);
document.getElementById("back")?.addEventListener("click", () => history.back(1));
}
function newsItemHTML(news, apps, minimal) {
let html = `
<div class="news-item-wrapper">`;
if (news.url) html += `
<a href="${news.url}">`;
html += `
<div class="item" style="background-color: #${news.tintColor};">
<div class="text">
<h3>${news.title}</h3>
<p>${news.caption}</p>
</div>`;
if (news.imageURL && !minimal) html += `
<div class="image-wrapper">
<img src="${news.imageURL}">
</div>`;
html += `
</div>`;
if (news.url) html +=
"</a>";
if (news.appID && !minimal) {
const app = apps.find(app => app.bundleIdentifier == news.appID);
if (app) html += appHeaderHTML(app);
}
html += "</div>";
return html;
}
function appHeaderHTML(app) {
return `
<div class="item">
<div class="app-header">
<div class="content">
<img src="${app.iconURL}" alt="">
<div class="right">
<div class="text">
<p class="title">${app.name}</p>
<p class="subtitle">${app.developerName}</p>
</div>
<a href="app.html?source=${sourceURL}&id=${app.bundleIdentifier}">
<button class="uibutton" style="background-color: #${app.tintColor};">View</button>
</a>
</div>
</div>
<div class="background" style="background-color: #${app.tintColor};"></div>
</div>
</div>`;
}

16
js/news.js Normal file
View File

@@ -0,0 +1,16 @@
addNavigationBar("All News");
fetch(sourceURL)
.then(response => response.json())
.then(json => {
if (json.tintColor) setTintColor(json.tintColor)
document.title = `News - ${json.name}`;
json.news.sort((a, b) => (new Date(b.date)).valueOf() - (new Date(a.date)).valueOf());
json.news.forEach(news =>
document.getElementById("news").insertAdjacentHTML("beforeend", newsItemHTML(news, json.apps))
);
waitForAllImagesToLoad();
});

24
news.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/news.css">
<link rel="stylesheet" href="css/uibanner.css">
</head>
<body class="loading">
<div id="loading">
<img src="img/loading.gif" alt="loading">
<p>Loading</p>
</div>
<!-- Add to AltStore banner & navigation bar -->
<div id="top"></div>
<div id="main">
<div id="news"></div>
</div>
<script src="js/main.js"></script>
<script src="js/news.js"></script>
</body>
</html>