Update WuppiMini, Add TiVuOcto, Update Build.sh
This commit is contained in:
parent
53204d067b
commit
260dc1e61d
15
Build.sh
15
Build.sh
|
@ -1,13 +1,14 @@
|
|||
#!/bin/sh
|
||||
SourceApps="SpiderADB WuppiMini"
|
||||
SourceApps="SpiderADB TiVuOcto WuppiMini"
|
||||
HubSdkApps="${SourceApps} MatrixStickerHelper TiktOctt"
|
||||
HtmlHeadInject='<script src="../../shared/OctoHub-Global.js"></script>'
|
||||
|
||||
quoteVar(){ echo '"'"$1"'"' ;}
|
||||
|
||||
getMetaAttr(){
|
||||
file="$1"
|
||||
name="$2"
|
||||
key="$([ -n "$3" ] && echo "$3" || echo "property")"
|
||||
key="$([ -n "$3" ] && echo "$3" || echo property)"
|
||||
grep '<meta '"$key"'="'"$name"'"' "$file" | grep '>' | cut -d '"' -f4
|
||||
}
|
||||
|
||||
|
@ -30,9 +31,9 @@ node ../WriteRedirectPages.js
|
|||
for App in ${HubSdkApps}
|
||||
do
|
||||
file="./${App}/index.html"
|
||||
name="$(getMetaAttr "${file}" og:title)"
|
||||
description="$(getMetaAttr "${file}" og:description property)"
|
||||
url="$(getMetaAttr "${file}" Url OctoSpaccHubSdk)" #"$(getMetaAttr "${file}" og:url property)"
|
||||
name="$( getMetaAttr "${file}" og:title)"
|
||||
description="$(getMetaAttr "${file}" og:description)"
|
||||
url="$( getMetaAttr "${file}" Url OctoSpaccHubSdk)"
|
||||
cat << [OctoSpaccHubSdk-WebManifest-EOF] > "./${App}/WebManifest.json"
|
||||
{
|
||||
$(getMetaAttr "${file}" WebManifestExtra OctoSpaccHubSdk | sed s/\'/\"/g)
|
||||
|
@ -42,5 +43,7 @@ do
|
|||
"name": "${name}"
|
||||
}
|
||||
[OctoSpaccHubSdk-WebManifest-EOF]
|
||||
sed -i 's|</head>|<title>'"${name}"'</title><link rel="manifest" href="./WebManifest.json"/></head>|' "${file}"
|
||||
htmltitle='<title>'"${name}"'</title>'
|
||||
htmlcanonical='<link rel="canonical" href="'"${url}"'"/>'
|
||||
sed -i 's|</head>|<link rel="manifest" href="./WebManifest.json"/>'"${htmltitle}${htmlcanonical}${htmlmanifest}${HtmlHeadInject}"'</head>|' "${file}"
|
||||
done
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta property="og:title" content="🕷️ SpiderADB"/>
|
||||
<meta property="og:title" content="SpiderADB"/>
|
||||
<meta OctoSpaccHubSdk="Url" content="https://hub.octt.eu.org/SpiderADB/"/>
|
||||
<meta OctoSpaccHubSdk="WebManifestExtra" content="'display':'standalone', 'icons':[{ 'src':'./icon.png', 'type':'image/png', 'sizes':'512x512' }],"/>
|
||||
<link rel="apple-touch-icon" href="./icon.png"/>
|
||||
|
@ -15,7 +15,6 @@
|
|||
<script src="./util.js"></script>
|
||||
<script src="./holo-web/holo-extra-octt.js"></script>
|
||||
<script src="./holo-web/holo-touch.js"></script>
|
||||
<script src="../../shared/OctoHub-Global.js"></script>
|
||||
<style>
|
||||
.floatRight { float: right; }
|
||||
body { overflow-x: hidden; padding-bottom: 0; overflow-wrap: break-word; }
|
||||
|
|
|
@ -107,9 +107,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
echo index.html node_modules
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
npm update
|
||||
npm install
|
|
@ -0,0 +1,519 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TiVuOcto</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta property="og:title" content="TiVuOcto"/>
|
||||
<meta OctoSpaccHubSdk="Url" content="https://hub.octt.eu.org/TiVuOcto/"/>
|
||||
<meta OctoSpaccHubSdk="WebManifestExtra" content="'display':'standalone',"/>
|
||||
<link href="./node_modules/muicss/dist/css/mui.min.css" rel="stylesheet"/>
|
||||
<link href="./node_modules/video.js/dist/video-js.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
:root {
|
||||
--headerHeight: 48px;
|
||||
--sidebarWidth: 200px;
|
||||
--colorBackground: #000;
|
||||
--colorBackground2: #102;
|
||||
--colorForeground: #fff;
|
||||
--colorForeground2: #bbb;
|
||||
--colorAccent: #305;
|
||||
--colorAccent2: #203;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: var(--colorBackground);
|
||||
color: var(--colorForeground);
|
||||
}
|
||||
|
||||
body a {
|
||||
color: var(--colorForeground2);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004);
|
||||
}
|
||||
|
||||
#header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
#sidedrawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebarWidth);
|
||||
left: calc(0px - var(--sidebarWidth));
|
||||
overflow: auto;
|
||||
z-index: 2;
|
||||
background-color: var(--colorBackground2);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
#content-wrapper {
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
margin-left: 0px;
|
||||
transition: margin-left 0.2s;
|
||||
}
|
||||
|
||||
#content-wrapper > * {
|
||||
position: relative;
|
||||
top: var(--headerHeight);
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--headerHeight));
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#header {
|
||||
left: var(--sidebarWidth);
|
||||
}
|
||||
|
||||
#sidedrawer {
|
||||
transform: translate(var(--sidebarWidth));
|
||||
}
|
||||
|
||||
#content-wrapper {
|
||||
margin-left: var(--sidebarWidth);
|
||||
}
|
||||
|
||||
#footer {
|
||||
margin-left: var(--sidebarWidth);
|
||||
}
|
||||
|
||||
body[data-hide-sidedrawer="1"] #header {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
body[data-hide-sidedrawer="1"] #sidedrawer {
|
||||
transform: translate(0px);
|
||||
}
|
||||
|
||||
body[data-hide-sidedrawer="1"] #content-wrapper,
|
||||
body[data-hide-sidedrawer="1"] #footer {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#sidedrawer[data-active="true"] {
|
||||
transform: translate(var(--sidebarWidth));
|
||||
}
|
||||
|
||||
#header .sidedrawer-toggle,
|
||||
#header .sidemenu-toggle {
|
||||
color: var(--colorForeground);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#header .sidedrawer-toggle {
|
||||
margin-right: 10px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#header .sidedrawer-toggle:hover,
|
||||
#header .sidemenu-toggle:hover {
|
||||
color: var(--colorForeground2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#header .mui-dropdown {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#sidedrawer-brand {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#sidedrawer ul {
|
||||
list-style: none;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#sidedrawer > ul {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
#sidedrawer > ul > li:first-child {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
#sidedrawer > ul > li > ul > li {
|
||||
padding: 1em;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#sidedrawer > ul > li > ul > li:hover {
|
||||
background: var(--colorAccent);
|
||||
}
|
||||
|
||||
#sidedrawer > ul > li > ul > li:target {
|
||||
background: var(--colorAccent2) !important;
|
||||
}
|
||||
|
||||
#sidedrawer > ul > li > a {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#sidedrawer ul > li > a > strong {
|
||||
display: block;
|
||||
padding: 15px 22px;
|
||||
background-color: var(--colorBackground2);
|
||||
}
|
||||
|
||||
#sidedrawer ul > li > a > strong:hover {
|
||||
background-color: var(--colorAccent);
|
||||
}
|
||||
|
||||
#sidedrawer strong + ul > li {
|
||||
padding: 6px 0px;
|
||||
}
|
||||
|
||||
#sidedrawer > ul > li > ul > li > a {
|
||||
font-size: x-large;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#sidedrawer > ul > li > ul > li > a > img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 160px;
|
||||
/* <https://stackoverflow.com/questions/12690444/css-border-on-png-image-with-transparent-parts/70470266#70470266> */
|
||||
filter:
|
||||
drop-shadow(2px 0 0 var(--colorForeground))
|
||||
drop-shadow(0 2px 0 var(--colorForeground))
|
||||
drop-shadow(-2px 0 0 var(--colorForeground))
|
||||
drop-shadow(0 -2px 0 var(--colorForeground));
|
||||
}
|
||||
|
||||
#header .mui-appbar {
|
||||
height: var(--headerHeight);
|
||||
min-height: var(--headerHeight);
|
||||
line-height: var(--headerHeight);
|
||||
background-color: var(--colorAccent);
|
||||
}
|
||||
|
||||
#no-video,
|
||||
#app-info {
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sidedrawer" class="mui--no-user-select">
|
||||
<div id="sidedrawer-brand" class="mui--appbar-line-height">
|
||||
<span class="mui--text-title"></span>
|
||||
</div>
|
||||
<div class="mui-divider"></div>
|
||||
</div>
|
||||
<header id="header">
|
||||
<div class="mui-appbar mui--appbar-line-height">
|
||||
<div class="mui-container-fluid"><!--
|
||||
--><a class="sidedrawer-toggle mui--visible-xs-inline-block mui--visible-sm-inline-block js-show-sidedrawer">☰</a><!--
|
||||
--><a class="sidedrawer-toggle mui--hidden-xs mui--hidden-sm js-hide-sidedrawer">☰</a><!--
|
||||
--><span class="mui--text-title"></span>
|
||||
<div class="mui-dropdown">
|
||||
<a class="sidemenu-toggle" data-mui-toggle="dropdown">︙</a>
|
||||
<ul class="mui-dropdown__menu mui-dropdown__menu--right" style="top: 31px;">
|
||||
<li><a href="#/about">About TiVuOcto</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div id="content-wrapper">
|
||||
<div id="app-info">
|
||||
<p id="no-javascript" style="padding-top: 1em;">
|
||||
⚠️ You need to <a href="https://enable-javascript.com">enable JavaScript</a> to run this app.
|
||||
</p>
|
||||
<h2>About TiVuOcto</h2><p>
|
||||
A minimal webapp for playing IPTV streams.
|
||||
Includes free TV channels from the entire world
|
||||
with a responsive UI, clean UX, no ads, no spyware.
|
||||
</p>
|
||||
<h3>Disclaimer</h3><p>
|
||||
This app allows users to play video live streams
|
||||
based on Internet standards, from either embedded sources,
|
||||
or user-specified sources that the app is not affiliated with.
|
||||
<br/>
|
||||
The embedded sources are specified below and publicly available,
|
||||
only including fully legal free-to-watch/public-domain content.
|
||||
This app does not make any pay-per-view content freely available,
|
||||
nor does it include any DRM/geoblocking circumvention measures.
|
||||
</p>
|
||||
<h3>Licensing</h3><p>
|
||||
Copyright (C) 2024 OctoSpacc
|
||||
<br/>
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
<br/>
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
<br/>
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.
|
||||
</p>
|
||||
<h3>Thanks and Third Party Libraries</h3>
|
||||
<p>This app wouldn't have been possible without the following:</p><ul>
|
||||
<li><a href="https://github.com/Free-TV/IPTV">Free-TV IPTV</a> playlist</li>
|
||||
<li><a href="https://videojs.com">Video.js</a> player [<a href="https://github.com/videojs/video.js/blob/main/LICENSE">Apache 2.0</a>]</li>
|
||||
<li><a href="https://muicss.com">MUI CSS</a> framework [<a href="https://github.com/muicss/mui/blob/master/LICENSE.txt">MIT</a>]</li>
|
||||
</ul>
|
||||
<h3>Changelog</h3>
|
||||
<h4>2024-07-30</h4>
|
||||
<ul>
|
||||
<li>First MVP version using the biggest free IPTV list, and about screen.</li>
|
||||
<li>Channel list in sidebar, divided by country with flag emojis.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./node_modules/muicss/dist/js/mui.min.js"></script>
|
||||
<script src="./node_modules/video.js/dist/video.min.js"></script>
|
||||
<script>(async function(){
|
||||
var COUNTRIES = {
|
||||
ar: '🇸🇦️',
|
||||
en: '🏴️',
|
||||
es: '🇪🇸️',
|
||||
albania: '🇦🇱️',
|
||||
andorra: '🇦🇩️',
|
||||
argentina: '🇦🇷️',
|
||||
australia: '🇦🇺️',
|
||||
austria: '🇦🇹️',
|
||||
azerbaijan: '🇦🇿️',
|
||||
belarus: '🇧🇾️',
|
||||
belgium: '🇧🇪️',
|
||||
bosnia_and_herzegovina: '🇧🇦️',
|
||||
brazil: '🇧🇷️',
|
||||
bulgaria: '🇧🇬️',
|
||||
canada: '🇨🇦️',
|
||||
chad: '🇹🇩️',
|
||||
chile: '🇨🇱️',
|
||||
china: '🇨🇳️',
|
||||
costa_rica: '🇨🇷️',
|
||||
croatia: '🇭🇷️',
|
||||
cyprus: '🇨🇾️',
|
||||
czech_republic: '🇨🇿️',
|
||||
denmark: '🇩🇰️',
|
||||
dominican_republic: '🇩🇴️',
|
||||
estonia: '🇪🇪️',
|
||||
faroe_islands: '🇫🇴️',
|
||||
finland: '🇫🇮️',
|
||||
france: '🇫🇷️',
|
||||
georgia: '🇬🇪️',
|
||||
germany: '🇩🇪️',
|
||||
greece: '🇬🇷️',
|
||||
greenland: '🇬🇱️',
|
||||
hong_kong: '🇭🇰️',
|
||||
hungary: '🇭🇺️',
|
||||
iceland: '🇮🇸️',
|
||||
india: '🇮🇳️',
|
||||
iran: '🇮🇷️',
|
||||
iraq: '🇮🇶️',
|
||||
ireland: '🇮🇪️',
|
||||
israel: '🇮🇱️',
|
||||
italy: '🇮🇹️',
|
||||
japan: '🇯🇵️',
|
||||
korea: '🇰🇷️',
|
||||
kosovo: '🇽🇰️',
|
||||
latvia: '🇱🇻️',
|
||||
lithuania: '🇱🇹️',
|
||||
luxembourg: '🇱🇺️',
|
||||
macau: '🇲🇴️',
|
||||
malta: '🇲🇹️',
|
||||
mexico: '🇲🇽️',
|
||||
moldova: '🇲🇩️',
|
||||
monaco: '🇲🇨️',
|
||||
montenegro: '🇲🇪️',
|
||||
netherlands: '🇳🇱️',
|
||||
north_korea: '🇰🇵️',
|
||||
north_macedonia: '🇲🇰️',
|
||||
norway: '🇳🇴️',
|
||||
paraguay: '🇵🇾️',
|
||||
peru: '🇵🇪️',
|
||||
poland: '🇵🇱️',
|
||||
portugal: '🇵🇹️',
|
||||
qatar: '🇶🇦️',
|
||||
romania: '🇷🇴️',
|
||||
russia: '🇷🇺️',
|
||||
san_marino: '🇸🇲️',
|
||||
saudi_arabia: '🇸🇦️',
|
||||
serbia: '🇷🇸️',
|
||||
slovakia: '🇸🇰️',
|
||||
slovenia: '🇸🇮️',
|
||||
somalia: '🇸🇴️',
|
||||
spain: '🇪🇸️',
|
||||
sweden: '🇸🇪️',
|
||||
switzerland: '🇨🇭️',
|
||||
taiwan: '🇹🇼️',
|
||||
trinidad: '🇹🇹️',
|
||||
turkey: '🇹🇷️',
|
||||
uk: '🇬🇧️',
|
||||
ukraine: '🇺🇦️',
|
||||
united_arab_emirates: '🇦🇪️',
|
||||
usa: '🇺🇸️',
|
||||
venezuela: '🇻🇪️',
|
||||
};
|
||||
|
||||
var APPNAME = document.title;
|
||||
//var APPINFO = document.querySelector('#app-info-container > div').innerHTML;
|
||||
|
||||
function setTitle (title='') {
|
||||
title = title.trim();
|
||||
document.title = (title ? `${title} — ${APPNAME}` : APPNAME);
|
||||
document.querySelector('#header .mui--text-title').textContent = (title || APPNAME);
|
||||
}
|
||||
|
||||
setTitle();
|
||||
document.querySelector('#sidedrawer .mui--text-title').textContent = APPNAME;
|
||||
|
||||
function getQuotedValue (data, key) {
|
||||
return data.split(`${key}="`)?.[1]?.split('"')?.[0];
|
||||
}
|
||||
|
||||
function getToActiveChannel () {
|
||||
var channelEl = document.querySelector(`#sidedrawer a[href="${location.hash}"]`);
|
||||
if (!channelEl) {
|
||||
return;
|
||||
}
|
||||
channelEl.parentElement.parentElement.hidden = false;
|
||||
channelEl.parentElement.scrollIntoView();
|
||||
//document.querySelector('#sidedrawer').scrollTop -= parseInt(getComputedStyle(document.querySelector('#header')).height.slice(0, -2));
|
||||
return channelEl;
|
||||
}
|
||||
|
||||
async function loadFromHash (event) {
|
||||
var videoEl = document.querySelector('#video-player');
|
||||
var noVideoEl = document.querySelector('#no-video');
|
||||
var appInfoEl = document.querySelector('#app-info');
|
||||
var channelEl = getToActiveChannel();
|
||||
var video = videojs('video-player');
|
||||
video.pause();
|
||||
if (channelEl) {
|
||||
noVideoEl.hidden = true;
|
||||
appInfoEl.hidden = true;
|
||||
channelEl.click();
|
||||
setTitle(channelEl.textContent);
|
||||
var src = `https://hlb0.octt.eu.org/cors-main.php/${channelEl.dataset.src}`;
|
||||
var headers = (await fetch(src)).headers;
|
||||
if (event.videoInitialLoad) {
|
||||
video.muted(true);
|
||||
}
|
||||
video.src({ src, type: (headers.get('content-type') || 'application/x-mpegURL') });
|
||||
video.play();
|
||||
videoEl.hidden = false;
|
||||
} else {
|
||||
setTitle();
|
||||
var hashMain = location.hash.split('/')[1];
|
||||
videoEl.hidden = true;
|
||||
switch (hashMain) {
|
||||
default:
|
||||
noVideoEl.hidden = false;
|
||||
appInfoEl.hidden = true;
|
||||
break; case 'about':
|
||||
noVideoEl.hidden = true;
|
||||
appInfoEl.hidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sideDrawerEl = document.querySelector('#sidedrawer');
|
||||
document.querySelector('.js-show-sidedrawer').onclick = (function(){
|
||||
var overlayEl = mui.overlay('on', { onclose: function(){
|
||||
sideDrawerEl.dataset.active = false;
|
||||
document.body.appendChild(sideDrawerEl);
|
||||
}});
|
||||
document.body.appendChild(overlayEl);
|
||||
overlayEl.appendChild(sideDrawerEl);
|
||||
sideDrawerEl.dataset.active = true;
|
||||
getToActiveChannel();
|
||||
});
|
||||
|
||||
document.querySelector('.js-hide-sidedrawer').onclick = (function(){
|
||||
document.body.dataset.hideSidedrawer = Number(!Number(document.body.dataset.hideSidedrawer || 0));
|
||||
});
|
||||
|
||||
document.querySelector('#no-javascript').remove();
|
||||
document.querySelector('#app-info').hidden = true;
|
||||
document.querySelector('#content-wrapper').innerHTML += `
|
||||
<div id="no-video" hidden="true">
|
||||
<p style="margin-top: 1em;">
|
||||
Select a valid channel to play video.
|
||||
</p>
|
||||
</div>
|
||||
<video id="video-player" class="video-js" controls="controls" data-setup="{}"></video>
|
||||
`;
|
||||
|
||||
// TODO error handling
|
||||
var iptvRequest = await fetch('https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8');
|
||||
var iptvData = await iptvRequest.text();
|
||||
var iptvChannels = iptvData.split('\n#EXTINF:-1').slice(1);
|
||||
|
||||
var channelsHtml = '';
|
||||
var channelGroupEls = {};
|
||||
for (var channel of iptvChannels) {
|
||||
var group = getQuotedValue(channel, 'group-title');
|
||||
var channelId = getQuotedValue(channel, 'tvg-id');
|
||||
if (!channelId) {
|
||||
continue;
|
||||
}
|
||||
channelGroupEls[group] ||= '';
|
||||
channelGroupEls[group] += `<li id="/0/${channelId}">
|
||||
<a href="#/0/${channelId}" data-src="${channel.split('\n')[1]}">
|
||||
<img loading="lazy" src="${getQuotedValue(channel, 'tvg-logo')}"/>
|
||||
${getQuotedValue(channel, 'tvg-name')}
|
||||
</a>
|
||||
</li>`;
|
||||
}
|
||||
for (var group in channelGroupEls) {
|
||||
var groupLower = group.toLowerCase();
|
||||
channelsHtml += `<li>
|
||||
<a class="category-toggle" href="javascript:void(0);">
|
||||
<strong>${
|
||||
COUNTRIES[groupLower.replaceAll(' ', '_')] ||
|
||||
COUNTRIES[groupLower.replace('vod', '').trim().replaceAll(' ', '_')] ||
|
||||
COUNTRIES[groupLower.split('(')[1]?.split(')')[0]] ||
|
||||
''} ${group}</strong>
|
||||
</a><ul>${channelGroupEls[group]}</ul>
|
||||
</li>`;
|
||||
}
|
||||
document.querySelector('#sidedrawer').innerHTML += `<ul>${channelsHtml}</ul>`;
|
||||
|
||||
Array.from(document.querySelectorAll('#sidedrawer a.category-toggle')).forEach(function(titleEl){
|
||||
var groupEl = titleEl.nextElementSibling;
|
||||
groupEl.hidden = true;
|
||||
titleEl.onclick = (function(){
|
||||
groupEl.hidden = !groupEl.hidden;
|
||||
titleEl.scrollIntoView();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', loadFromHash);
|
||||
loadFromHash({ videoInitialLoad: true });
|
||||
|
||||
})();</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,358 @@
|
|||
{
|
||||
"name": "TiVuOcto",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"muicss": "^0.10.3",
|
||||
"video.js": "^8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
|
||||
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/http-streaming": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.1.tgz",
|
||||
"integrity": "sha512-G7YrgNEq9ETaUmtkoTnTuwkY9U+xP7Xncedzgxio/Rmz2Gn2zmodEbBIVQinb2UDznk7X8uY5XBr/Ew6OD/LWg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "4.0.0",
|
||||
"aes-decrypter": "4.0.1",
|
||||
"global": "^4.4.0",
|
||||
"m3u8-parser": "^7.1.0",
|
||||
"mpd-parser": "^1.3.0",
|
||||
"mux.js": "7.0.3",
|
||||
"video.js": "^7 || ^8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"video.js": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/vhs-utils": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz",
|
||||
"integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"global": "^4.4.0",
|
||||
"url-toolkit": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/xhr": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz",
|
||||
"integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"global": "~4.4.0",
|
||||
"is-function": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aes-decrypter": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz",
|
||||
"integrity": "sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^3.0.5",
|
||||
"global": "^4.4.0",
|
||||
"pkcs7": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
|
||||
"integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"global": "^4.4.0",
|
||||
"url-toolkit": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"node_modules/global": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"dependencies": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
"node_modules/individual": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
|
||||
"integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g=="
|
||||
},
|
||||
"node_modules/is-function": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/m3u8-parser": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-7N+pk79EH4oLKPEYdgRXgAsKDyA/VCo0qCHlUwacttQA0WqsjZQYmNfywMvjlY9MpEBVZEt0jKFd73Kv15EBYQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^3.0.5",
|
||||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/m3u8-parser/node_modules/@videojs/vhs-utils": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
|
||||
"integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"global": "^4.4.0",
|
||||
"url-toolkit": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/min-document": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
|
||||
"dependencies": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mpd-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.0.0",
|
||||
"@xmldom/xmldom": "^0.8.3",
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"bin": {
|
||||
"mpd-to-m3u8-json": "bin/parse.js"
|
||||
}
|
||||
},
|
||||
"node_modules/muicss": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/muicss/-/muicss-0.10.3.tgz",
|
||||
"integrity": "sha512-CuVrxnns64RZogHhrAxpMw2BF74Mably9KGUwk5LxHkG1yXUGArf8ZWP8jYd1ueNplqIwgrRdkau8GgrmXTMUQ==",
|
||||
"dependencies": {
|
||||
"react-addons-shallow-compare": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14.0 || ^15.0.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mux.js": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.3.tgz",
|
||||
"integrity": "sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"bin": {
|
||||
"muxjs-transmux": "bin/transmux.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pkcs7": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
|
||||
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5"
|
||||
},
|
||||
"bin": {
|
||||
"pkcs7": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-addons-shallow-compare": {
|
||||
"version": "15.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.3.tgz",
|
||||
"integrity": "sha512-EDJbgKTtGRLhr3wiGDXK/+AEJ59yqGS+tKE6mue0aNXT6ZMR7VJbbzIiT6akotmHg1BLj46ElJSb+NBMp80XBg==",
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/rust-result": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
|
||||
"integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==",
|
||||
"dependencies": {
|
||||
"individual": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-json-parse": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
|
||||
"integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==",
|
||||
"dependencies": {
|
||||
"rust-result": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-toolkit": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz",
|
||||
"integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="
|
||||
},
|
||||
"node_modules/video.js": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.17.1.tgz",
|
||||
"integrity": "sha512-MKW/oRs5B9UeN6TiF+CsVNGacxV4mPWlyDt1VzRkNXy6gPkCK04oQKB2XEhHHQCtACv3PeOkOXnr5b1ID2LwPg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "3.13.1",
|
||||
"@videojs/vhs-utils": "^4.0.0",
|
||||
"@videojs/xhr": "2.7.0",
|
||||
"aes-decrypter": "^4.0.1",
|
||||
"global": "4.4.0",
|
||||
"m3u8-parser": "^7.1.0",
|
||||
"mpd-parser": "^1.2.2",
|
||||
"mux.js": "^7.0.1",
|
||||
"safe-json-parse": "4.0.0",
|
||||
"videojs-contrib-quality-levels": "4.1.0",
|
||||
"videojs-font": "4.2.0",
|
||||
"videojs-vtt.js": "0.15.5"
|
||||
}
|
||||
},
|
||||
"node_modules/videojs-contrib-quality-levels": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
|
||||
"integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
|
||||
"dependencies": {
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"video.js": "^8"
|
||||
}
|
||||
},
|
||||
"node_modules/videojs-font": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz",
|
||||
"integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ=="
|
||||
},
|
||||
"node_modules/videojs-vtt.js": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
||||
"integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
|
||||
"dependencies": {
|
||||
"global": "^4.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"muicss": "^0.10.3",
|
||||
"video.js": "^8.17.1"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
node ./index.js html > /dev/null
|
||||
echo index.js index.html icon.png node_modules/SpaccDotWeb/SpaccDotWeb.Server.js
|
||||
node ./index.js writeStaticHtml > /dev/null
|
||||
echo index.js index.css index.html icon.png node_modules node_modules/SpaccDotWeb/SpaccDotWeb.Server.js node_modules/SpaccDotWeb/SpaccDotWeb.Alt.js
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
code {
|
||||
font-family: revert;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
color: #000;
|
||||
background: #eee url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwBAMAAADMe/ShAAAAHlBMVEX29vb09PTy8vL19fXz8/Px8fHw8PDv7+/u7u739/dHdNvrAAACSElEQVR4AezYNX7DMBiGcafcKTRlC0hdy7T1Z7iAcS5YOkEl38DeQlNOW24YX63fsxr+Ztm2RmvjMiu2bWbOXPmjtW2A7S8YKlfMCK4lKPzeMoJvYhROHYIJno9gggkmmGCCCSaYYIIJJphgggkmGP/PZQZf4XDHCK5JFFYtE7hXkwWYap0awEMPh8NHA7jPNQprv2EAH5zh8MsTDo9vY/xGxuCeLXFYsVMY7nNZwCm/AcM3scZhnTooPBwfafBYP4JwiZvBfgWDh7Xxkc71DmXjY916hOAbLrMxK7ZuTOfKdxC4ZMf6z1Ui2iEh/2Sdssru8L4dy+zXFVGbuVtntyPxK+cqZRc7wr1Ll0v95/qser1DVZv/yVr5bv10Bczq+ML4Rl/als/ww4WfJptbwuACMbgwhaUVfkvgt6LU1icpdkAAAADAEEz/1EKshHOBgMAnwZ7gNgG9DEwybTKpyZiLRCT6ItqUCJuIYqKoosaJlEsMJBki+ZOElwRfkpqSuElcS9Qnd4LcGN3enRUACMRQDFSDJRzg3wIKuNnNzygYA83rGbx9g1cwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPB/bFgfx7ZH4TWJ7DB0W9w5hwcdgen7MHxfp8r9IFGkKQEEU6WHWWhVZaWZTFdlg9mwWSdiAZRbJABB+FzkHoHcXuQ82cDBtlkQzVSkc1yZEMk2fRKNjZTzetkg0LlhJK1KjAYDAaDwWAwGAwGg8FgcAD3L/H6J4DLS/j67eEOl9GYoKbAV2YAAAAASUVORK5CYII=');
|
||||
background-size: 10px;
|
||||
}
|
||||
a {
|
||||
color: #777;
|
||||
}
|
||||
h1 {
|
||||
display: inline;
|
||||
}
|
||||
h1 > a {
|
||||
color: #000;
|
||||
}
|
||||
form > input, form > select, form > textarea, form > label {
|
||||
display: block;
|
||||
margin: 8px 0px;
|
||||
width: 100%;
|
||||
}
|
||||
form > input[type="text"], form > input[type="url"], form > input[type="password"], form > input[type="submit"], /*form > input[type="button"],*/ form > select, textarea {
|
||||
min-height: 2em;
|
||||
}
|
||||
form > textarea {
|
||||
font-size: medium;
|
||||
}
|
||||
form > textarea, form > select[multiple] {
|
||||
resize: vertical;
|
||||
}
|
||||
div#header {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
border-bottom: 4px solid #5ac800;
|
||||
background: #fff;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
div#header > * {
|
||||
display: inline;
|
||||
padding: 0px 0.25em;
|
||||
}
|
||||
div#main {
|
||||
width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
div.channels > a {
|
||||
display: block;
|
||||
min-height: 2em;
|
||||
}
|
||||
div.channels > a > img {
|
||||
width: 2em;
|
||||
height: auto;
|
||||
}
|
||||
div.channels > a > span {
|
||||
background: #777;
|
||||
color: #fff;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
div.posts > article {
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
margin: 8px;
|
||||
border: 1px solid #ccc;
|
||||
word-break: break-word;
|
||||
}
|
||||
div.posts > article > header > a {
|
||||
float: right;
|
||||
}
|
||||
div.posts > article img, div.posts > article video, div.posts > article iframe {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
}
|
||||
div#transition {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background: black;
|
||||
opacity: 0.25;
|
||||
cursor: progress;
|
||||
}
|
||||
p.notice, blockquote {
|
||||
background: white;
|
||||
padding: 1em;
|
||||
border-width: 4px;
|
||||
border-left-width: 1em;
|
||||
border-style: solid;
|
||||
}
|
||||
blockquote {
|
||||
border-color: #777;
|
||||
}
|
||||
p.notice.success {
|
||||
border-color: #5ac800;
|
||||
}
|
||||
p.notice.error {
|
||||
border-color: #de0000;
|
||||
}
|
||||
p.notice.info {
|
||||
border-color: #0097ce;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env node
|
||||
// search "Copyright" in this file for licensing info
|
||||
|
||||
// configuration
|
||||
|
@ -6,7 +7,9 @@ const serverPort = 8135;
|
|||
const detailedLogging = true;
|
||||
const serverLanUpstreams = false;
|
||||
const serverPlaintextUpstreams = false;
|
||||
let resFiles = [ 'package.json', 'package-lock.json' ];
|
||||
const appSelfContained = false;
|
||||
let staticFiles = [ 'package.json', 'package-lock.json' ];
|
||||
let linkStyles = [ 'index.css' ];
|
||||
const appTerms = `
|
||||
<p >(These terms apply to the server-hosted version of the app only.)
|
||||
<br/>This service is offered for free, in the hope that it can be useful, but without any warranty.
|
||||
|
@ -18,113 +21,144 @@ const appTerms = `
|
|||
<br/>If you don't agree with these terms, discontinue usage of this site immediately, and instead <a href="/info#h-floss">get the source code</a> to host it yourself, find another instance, or use the <a href="/info#h-versions">local, client-side version</a>.
|
||||
</p>`;
|
||||
const suggestedTags = [ 'fromWuppiMini' ];
|
||||
const corsProxies = [ 'corsproxy.io', 'corsproxy.org' ];
|
||||
const corsProxies = [ 'corsproxy.io?', 'corsproxy.org?', 'hlb0.octt.eu.org/cors-main.php/' ];
|
||||
|
||||
const SpaccDotWebServer = require('SpaccDotWeb/SpaccDotWeb.Server.js');
|
||||
let crypto;
|
||||
var crypto, escapeHtml;
|
||||
let isEnvServer = SpaccDotWebServer.envIsNode;
|
||||
let isEnvBrowser = SpaccDotWebServer.envIsBrowser;
|
||||
const httpCodes = { success: [200,201] };
|
||||
const strings = {
|
||||
csrfErrorHtml: `<p class="notice error">Authorization token mismatch. Please try resubmitting.</p>`,
|
||||
upstreamDisallowedHtml: `<p class="notice error">Upstream destination is not allowed from backend.</p>`,
|
||||
const httpCodes = { success: [ 200, 201 ] };
|
||||
const appPlatforms = {
|
||||
"wp.org": { name: "WordPress.org (Community/Self-hosted)", writing: true },
|
||||
//"greader": { name: "RSS with Google Reader API (FreshRSS, ...)", reading: true },
|
||||
};
|
||||
const appLanguages = {
|
||||
en: "🇬🇧️ English",
|
||||
it: "🇮🇹️ Italiano",
|
||||
};
|
||||
const appStrings = {
|
||||
compose: { en: `Compose`, it: `Componi` },
|
||||
read: { en: `Read`, it: `Leggi` },
|
||||
settings: { en: `Settings`, it: `Impostazioni` },
|
||||
language: { en: `Language`, it: `Lingua` },
|
||||
composePost: {
|
||||
en: `Compose Post`,
|
||||
it: `Componi Post`,
|
||||
},
|
||||
postTitle: {
|
||||
en: `Post Title`,
|
||||
it: `Titolo del Post`,
|
||||
},
|
||||
includeTags: {
|
||||
en: `Include suggested Tags`,
|
||||
it: `Includi Tag suggeriti`,
|
||||
},
|
||||
postingHint: {
|
||||
en: `What's on your mind?`,
|
||||
it: `A cosa stai pensando?`,
|
||||
},
|
||||
uploadDraft: {
|
||||
en: `Upload Draft`,
|
||||
it: `Carica Bozza`,
|
||||
},
|
||||
publish: {
|
||||
en: `Publish!`,
|
||||
it: `Pubblica!`,
|
||||
},
|
||||
postPublished: {
|
||||
en: `Post published`,
|
||||
it: `Post pubblicato`,
|
||||
},
|
||||
draftUploaded: {
|
||||
en: `Draft uploaded`,
|
||||
it: `Bozza caricata`,
|
||||
},
|
||||
unknownError: {
|
||||
en: `An unknown error just happened. Please check that your data is correct, and try again.`,
|
||||
it: `Si è verificato un errore sconosciuto. Per favore, controlla che i dati inseriti siano corretti, e riprova.`,
|
||||
},
|
||||
upstreamError: {
|
||||
en: `Upstream server responded with error`,
|
||||
it: `Il server di upstream ha risposto con errore`,
|
||||
},
|
||||
csrfError: {
|
||||
en: `Authorization token mismatch. Please try resubmitting.`,
|
||||
it: `Il token di autorizzazione non combacia. Per favore ritenta l'invio.`,
|
||||
},
|
||||
upstreamDisallowed: {
|
||||
en: `Upstream destination is not allowed from backend. (Only global HTTPS sites are allowed.)`,
|
||||
it: `La destinazione di upstream non è permessa dal backend. (Solo i siti HTTPS globali sono permessi.)`,
|
||||
},
|
||||
currentAccounts: {
|
||||
en: `Current Accounts`,
|
||||
it: `Account Correnti`,
|
||||
},
|
||||
addNewAccount: {
|
||||
en: `Add New Account`,
|
||||
it: `Aggiungi Nuovo Account`,
|
||||
},
|
||||
siteInstanceUrl: {
|
||||
en: `Site/Instance URL`,
|
||||
it: `URL Sito/Istanza`,
|
||||
},
|
||||
rememberMe: {
|
||||
en: `Remember me`,
|
||||
it: `Ricordami`,
|
||||
},
|
||||
loginAndSave: {
|
||||
en: `Login and Save`,
|
||||
it: `Login e Salva`,
|
||||
},
|
||||
mustAddAccount: {
|
||||
en: (type) => `You must add ${type === 'compose' ? 'a writing' : type === 'read' ? 'a reading' : 'an'} account to continue. Go to <a href="/settings">Settings</a>.`,
|
||||
it: (type) => `Devi aggiungere un account ${type === 'compose' ? 'di scrittura' : type === 'read' ? 'di lettura' : ''} per continuare. Vai alle <a href="/settings">Impostazioni</a>.`,
|
||||
},
|
||||
accountExists: {
|
||||
en: `The account you tried to add is already registered.`,
|
||||
it: `L'account che hai provato ad aggiungere è già registrato.`,
|
||||
},
|
||||
logoutAccounts: {
|
||||
en: `Logout Selected Accounts`,
|
||||
it: `Logout Account Selezionati`,
|
||||
},
|
||||
noAccountSelected: {
|
||||
en: `You haven't selected any account to be removed.`,
|
||||
it: `Non hai selezionato alcun account da rimuovere.`
|
||||
},
|
||||
applyLanguage: {
|
||||
en: `Apply Language`,
|
||||
it: `Applica Lingua`,
|
||||
},
|
||||
postEmpty: {
|
||||
en: `Post content is empty. Please write some text or upload a media.`,
|
||||
it: `Il post è vuoto. Per favore scrivi del testo o carica un file.`,
|
||||
},
|
||||
};
|
||||
appStrings.get = (string, language='en') => (appStrings[string][language] || Object.values(appStrings[string])[0]);
|
||||
|
||||
const appPager = (content, title) => `${title ? `<h2>${title}</h2>` : ''}${content}`;
|
||||
const appPager = (content, title, opts={}, ctx) => `<div id="header">
|
||||
<h1><a href="/">${appName}</a></h1>
|
||||
<a href="/compose">${appStrings.get('compose', getUserLanguage(opts.context))} 📝️</a>
|
||||
<!--${ctx.envIsBrowser ? '' : `<a href="/read">${appStrings.get('read', getUserLanguage(opts.context))} 📜️</a>`}-->
|
||||
<a href="/info">Info ℹ️</a>
|
||||
<a href="/settings">${appStrings.get('settings', getUserLanguage(opts.context))} ⚙️</a>
|
||||
</div><div id="main">${title ? `<h2>${title}</h2>` : ''}${content}</div>`;
|
||||
|
||||
const newHtmlPage = (content, title) => `<!DOCTYPE html><html><head>
|
||||
const htmlPager = (content, title, opts={}, ctx) => `<!DOCTYPE html><html><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta property="og:title" content="${title ? `${title} — ` : ''}${appName}"/>
|
||||
<meta OctoSpaccHubSdk="Url" content="https://hub.octt.eu.org/WuppiMini/"/>
|
||||
<meta OctoSpaccHubSdk="WebManifestExtra" content="'display':'standalone', 'icons':[{ 'src':'./icon.png', 'type':'image/png', 'sizes':'256x256' }],"/>
|
||||
<title>${title ? `${title} — ` : ''}${appName}</title>
|
||||
<link rel="apple-touch-icon" href="./icon.png"/>
|
||||
<script src="../../shared/OctoHub-Global.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
code {
|
||||
font-family: revert;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
color: #000;
|
||||
background: #eee url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwBAMAAADMe/ShAAAAHlBMVEX29vb09PTy8vL19fXz8/Px8fHw8PDv7+/u7u739/dHdNvrAAACSElEQVR4AezYNX7DMBiGcafcKTRlC0hdy7T1Z7iAcS5YOkEl38DeQlNOW24YX63fsxr+Ztm2RmvjMiu2bWbOXPmjtW2A7S8YKlfMCK4lKPzeMoJvYhROHYIJno9gggkmmGCCCSaYYIIJJphgggkmGP/PZQZf4XDHCK5JFFYtE7hXkwWYap0awEMPh8NHA7jPNQprv2EAH5zh8MsTDo9vY/xGxuCeLXFYsVMY7nNZwCm/AcM3scZhnTooPBwfafBYP4JwiZvBfgWDh7Xxkc71DmXjY916hOAbLrMxK7ZuTOfKdxC4ZMf6z1Ui2iEh/2Sdssru8L4dy+zXFVGbuVtntyPxK+cqZRc7wr1Ll0v95/qser1DVZv/yVr5bv10Bczq+ML4Rl/als/ww4WfJptbwuACMbgwhaUVfkvgt6LU1icpdkAAAADAEEz/1EKshHOBgMAnwZ7gNgG9DEwybTKpyZiLRCT6ItqUCJuIYqKoosaJlEsMJBki+ZOElwRfkpqSuElcS9Qnd4LcGN3enRUACMRQDFSDJRzg3wIKuNnNzygYA83rGbx9g1cwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPB/bFgfx7ZH4TWJ7DB0W9w5hwcdgen7MHxfp8r9IFGkKQEEU6WHWWhVZaWZTFdlg9mwWSdiAZRbJABB+FzkHoHcXuQ82cDBtlkQzVSkc1yZEMk2fRKNjZTzetkg0LlhJK1KjAYDAaDwWAwGAwGg8FgcAD3L/H6J4DLS/j67eEOl9GYoKbAV2YAAAAASUVORK5CYII=');
|
||||
background-size: 10px;
|
||||
}
|
||||
a {
|
||||
color: #777;
|
||||
}
|
||||
h1 {
|
||||
display: inline;
|
||||
}
|
||||
h1 > a {
|
||||
color: #000;
|
||||
}
|
||||
form > input, form > select, form > textarea, form > label {
|
||||
display: block;
|
||||
margin: 8px 0px;
|
||||
width: 100%;
|
||||
}
|
||||
form > input[type="text"], form > input[type="url"], form > input[type="password"], form > input[type="submit"], /*form > input[type="button"],*/ form > select, textarea {
|
||||
min-height: 2em;
|
||||
}
|
||||
textarea {
|
||||
font-size: medium;
|
||||
}
|
||||
div#header {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
border-bottom: 4px solid #5ac800;
|
||||
background: #fff;
|
||||
}
|
||||
div#header > * {
|
||||
display: inline;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
div#app {
|
||||
width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
div#transition {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: black;
|
||||
opacity: 0.25;
|
||||
}
|
||||
p.notice {
|
||||
background: white;
|
||||
padding: 1em;
|
||||
border-width: 4px;
|
||||
border-left-width: 1em;
|
||||
border-style: solid;
|
||||
}
|
||||
p.notice.success {
|
||||
border-color: #5ac800;
|
||||
}
|
||||
p.notice.error {
|
||||
border-color: #de0000;
|
||||
}
|
||||
</style>
|
||||
${linkStyles.map((path) => SpaccDotWebServer.makeHtmlStyleFragment(path, appSelfContained)).join('')}
|
||||
</head><body><!--
|
||||
-->${isEnvBrowser ? `<div id="transition"></div>` : ''}<!--
|
||||
--><div id="header"><!--
|
||||
--><h1><a href="/">${appName}</a></h1><!--
|
||||
--><a href="/info">Info</a><!--
|
||||
--><a href="/settings">Settings</a><!--
|
||||
--></div><!--
|
||||
--><div id="app">${appPager(content, title)}</div><!--
|
||||
--><div id="app">${appPager(content, title, opts, ctx)}</div><!--
|
||||
--></body></html>`;
|
||||
|
||||
const A = (href) => `<a href="${href}">${href}</a>`
|
||||
const A = (href) => `<a href="${href}">${href}</a>`;
|
||||
|
||||
const Log = (type, msg) => ((type !== 'D' || detailedLogging) && console.log(`${type}: ${msg}`));
|
||||
|
||||
|
@ -158,7 +192,7 @@ const genCsrfToken = (accountString, time) => (isEnvServer && time && crypto.scr
|
|||
|
||||
const matchCsrfToken = (bodyParams, accountString) => (isEnvServer ? bodyParams.formToken === genCsrfToken(accountString, bodyParams.formTime) : true);
|
||||
|
||||
const corsProxyIfNeed = (need) => (isEnvBrowser /*&& need*/ ? `https://${corsProxies[~~(Math.random() * corsProxies.length)]}?` : '');
|
||||
const corsProxyIfNeed = (/*need*/) => (isEnvBrowser /*&& need*/ ? `https://${corsProxies[~~(Math.random() * corsProxies.length)]}` : '');
|
||||
|
||||
/*const handleRequest = async (req, res={}) => {
|
||||
// TODO warn if the browser has cookies disabled when running on server side
|
||||
|
@ -176,54 +210,105 @@ const corsProxyIfNeed = (need) => (isEnvBrowser /*&& need*/ ? `https://${corsPro
|
|||
};
|
||||
};*/
|
||||
|
||||
// todo handle optional options field(s)
|
||||
const accountDataFromString = (accountString) => {
|
||||
const tokens = accountString.split(',');
|
||||
return { instance: tokens[0], username: tokens[1], password: tokens.slice(2).join(',') };
|
||||
}
|
||||
const getUserLanguage = (ctx) => (ctx.getCookie?.('language') || ctx.clientLanguages?.[0]?.split('-')[0]);
|
||||
|
||||
const makeFragmentLoggedIn = (accountString) => {
|
||||
const accountData = accountDataFromString(accountString);
|
||||
return `<p>Logged in as <i>${accountData.username} @ ${A(accountData.instance)}</i>.</p>`;
|
||||
}
|
||||
//const getAccountsData = (ctx) => accountsDataFromCookieString(ctx.getCookie('account'));
|
||||
|
||||
const main = () => {
|
||||
if (SpaccDotWebServer.envIsNode && process.argv[2] !== 'html') {
|
||||
resFiles = [__filename.split(require('path').sep).slice(-1)[0], ...resFiles];
|
||||
const getPlatformActionIcon = platform => (platform.writing && platform.reading ? '💱️'
|
||||
: (platform.writing && '📤️') || (platform.reading && '📥️'));
|
||||
|
||||
const makeAccountFormId = (account) => (btoa(account.username) + ":" + btoa(account.instance));
|
||||
|
||||
const accountDataFromFormId = (accountsData, accountId) => accountsData.filter(account => {
|
||||
let [username, instance] = accountId.split(':');
|
||||
username = atob(username);
|
||||
instance = atob(instance);
|
||||
return accountsData.filter(account => (account.username === username && account.instance === instance))[0];
|
||||
})[0];
|
||||
|
||||
const accountsDataFromCookieString = (accountString) => {
|
||||
const accounts = [];
|
||||
if (!accountString) {
|
||||
return accounts;
|
||||
}
|
||||
// old format, kept in just to support old sessions
|
||||
let tokens = accountString.split(',');
|
||||
if (tokens.length >= 3 && ['http', 'https'].includes(accountString.split('://')[0].toLowerCase())) {
|
||||
return [{ platform: "wp.org", instance: tokens[0], username: tokens[1], password: tokens.slice(2).join(','), options: {} }];
|
||||
}
|
||||
// new format
|
||||
for (const account of accountString.split(',')) {
|
||||
const tokens = account.split(':');
|
||||
const options = {};
|
||||
for (const option of tokens.slice(4)) {
|
||||
const [key, value] = option.split('=');
|
||||
options[key] = (value || true);
|
||||
}
|
||||
accounts.push({
|
||||
platform: tokens[0],
|
||||
instance: atob(tokens[1]),
|
||||
username: atob(tokens[2]),
|
||||
password: atob(tokens[3]),
|
||||
options,
|
||||
});
|
||||
}
|
||||
return accounts;
|
||||
};
|
||||
|
||||
const accountsCookieStringFromData = (accountData) => {
|
||||
let accounts = '';
|
||||
for (const account of [].concat(accountData)) {
|
||||
accounts += `,${account.platform}:${btoa(account.instance)}:${btoa(account.username)}:${btoa(account.password)}`;
|
||||
for (const [key, value] of Object.entries(account.options)) {
|
||||
accounts += `:${key}` + (value === true ? '' : `=${value}`);
|
||||
}
|
||||
}
|
||||
return accounts.slice(1);
|
||||
};
|
||||
|
||||
const makeCookieFlags = (opts) => ((opts === true || opts.remember === 'on') ? `; max-age=${365*24*60*60}` : '');
|
||||
|
||||
const main = async () => {
|
||||
if (isEnvServer && process.argv[2] !== 'writeStaticHtml') {
|
||||
staticFiles = [__filename.split(require('path').sep).slice(-1)[0], ...staticFiles];
|
||||
};
|
||||
|
||||
const server = SpaccDotWebServer.setup({
|
||||
appName: appName,
|
||||
staticPrefix: '/res/',
|
||||
staticFiles: resFiles,
|
||||
appPager: appPager,
|
||||
htmlPager: newHtmlPage,
|
||||
staticFiles, linkStyles,
|
||||
appPager, htmlPager,
|
||||
});
|
||||
|
||||
if (SpaccDotWebServer.envIsNode && process.argv[2] === 'html') {
|
||||
server.writeStaticHtml();
|
||||
if (isEnvServer && process.argv[2] === 'writeStaticHtml') {
|
||||
server.writeStaticHtml(appSelfContained);
|
||||
} else {
|
||||
if (SpaccDotWebServer.envIsNode) {
|
||||
escapeHtml = (await require('escape-html/index.js') || window.escapeHtml);
|
||||
if (isEnvServer) {
|
||||
crypto = require('crypto');
|
||||
console.log('Running Server...');
|
||||
console.log(`Running Server on :${serverPort}...`);
|
||||
};
|
||||
server.initServer({
|
||||
port: serverPort,
|
||||
address: '0.0.0.0',
|
||||
maxBodyUploadSize: 4e6, // 4 MB
|
||||
endpoints: [ endpointRoot, endpointInfo, endpointCompose, endpointSettings, endpointCatch ],
|
||||
endpoints: [ endpointRoot, endpointInfo, /* endpointHub, */ endpointCompose, /* endpointRead, */ endpointSettings, endpointCatch ],
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const endpointRoot = [ (ctx) => (!ctx.urlSections[0]), (ctx) => ctx.redirectTo(ctx.getCookie('account') ? '/compose' : '/info') ];
|
||||
// TODO fix this for now that we have 2 sections but may not always have the needed account
|
||||
const endpointRoot = [ (ctx) => (!ctx.urlSections[0]), (ctx) => {
|
||||
const account = accountsDataFromCookieString(ctx.getCookie('account'))?.[0];
|
||||
ctx.redirectTo((account ? (appPlatforms[account.platform].writing ? '/compose' : '/read') : '/info') + `?${ctx.urlQuery}`);
|
||||
} ];
|
||||
|
||||
const endpointCatch = [ (ctx) => true, (ctx) => ctx.renderPage('', (ctx.response.statusCode = 404)) ];
|
||||
const endpointCatch = [ (ctx) => true, (ctx) => ctx.renderPage('<p>This page does not exist. <a href="/">Go back to the home page</a>.</p>', (ctx.response.statusCode = 404)) ];
|
||||
|
||||
const endpointInfo = [ (ctx) => (ctx.urlSections[0] === 'info' && ctx.request.method === 'GET'), (ctx) => {
|
||||
const endpointInfo = [ 'GET /info/', (ctx) => {
|
||||
ctx.response.statusCode = 200;
|
||||
ctx.renderPage(`
|
||||
${!ctx.getCookie('account') ? `<p>You must login first. Go to <a href="/settings">Settings</a> to continue.</p>` : ''}
|
||||
${!ctx.getCookie('account') ? `<p class="notice info">${appStrings.get('mustAddAccount', getUserLanguage(ctx))(ctx.urlParameters.ref)}</p>` : ''}
|
||||
<h3>About</h3>
|
||||
<p>
|
||||
${appName} (temporary name?) is a minimalist, basic HTML-based frontend, designed for quickly and efficiently publishing to social media and content management services (note that only WordPress is currently supported).
|
||||
|
@ -261,29 +346,32 @@ const endpointInfo = [ (ctx) => (ctx.urlSections[0] === 'info' && ctx.request.me
|
|||
</p>
|
||||
<p>
|
||||
${isEnvServer ? `You can obtain the full source code and assets by downloading the following files:
|
||||
${resFiles.map((file) => ` • <a href="/res/${file}">${file}</a>`).join('')}.
|
||||
${staticFiles.map(file => ` • <a href="/res/${file}">${file}</a>`).join('')}.
|
||||
` : 'To get the original, unminified source code, visit this same page on the server-side version (refer to the Versions section above).'}
|
||||
Alternatively, you can also find the source code on my shared Git repo: ${A('https://gitlab.com/octospacc/octospacc.gitlab.io/-/tree/master/source/WuppiMini/')}.
|
||||
</p>
|
||||
${isEnvServer ? `<h3>Terms of Use and Privacy Policy</h3>${appTerms}` : ''}
|
||||
<h3>Changelog</h3>
|
||||
<h4>2024-02-24</h3>
|
||||
<ul>
|
||||
<h4>2024-07-15 (deployed 2024-07-30)</h4><ul>
|
||||
<!--<li>New Read function, connecting to RSS servers via Google Reader API.</li>-->
|
||||
<li>Multi-account support (required changes to the cookie format), allowing for selection of destination in Compose and filtering of sources in Read.</li>
|
||||
<li>New multi-language support, using browser language as a default and allowing change in Settings.</li>
|
||||
<li>Slight UX improvements and bugfixes to existing forms and screens.</li>
|
||||
<li>Made a preview of the Compose screen visible before logging-in.</li>
|
||||
</ul>
|
||||
<h4>2024-02-24</h4><ul>
|
||||
<li>Allow uploading posts as published or draft, via 2 distinct buttons.</li>
|
||||
<li>Migrated fancy portable-server-codebase to <a href="https://gitlab.com/SpaccInc/SpaccDotWeb">SpaccDotWeb</a> for code reuse and slimming down of the application core.</li>
|
||||
</ul>
|
||||
<h4>2024-02-12</h3>
|
||||
<ul>
|
||||
<h4>2024-02-12</h4><ul>
|
||||
<li>First working client-side version of the current app, without backend server (still a bit buggy).</li>
|
||||
<li>Fixed suggested tags handling not working and making the post error out, by instead simply writing them in the post body</li>
|
||||
</ul>
|
||||
<h4>2024-02-10</h4>
|
||||
<ul>
|
||||
<h4>2024-02-10</h4><ul>
|
||||
<li>Add "remember me" login option.</li>
|
||||
<li>Add "suggested tags" publishing option, will automatically add this list of tags to the post: [${suggestedTags}].</li>
|
||||
</ul>
|
||||
<h4>2024-02-09</h4>
|
||||
<ul>
|
||||
<h4>2024-02-09</h4><ul>
|
||||
<li>First working version, with an UI reminiscent of [that dead social network that rhymes with Meterse], and Info, Settings, and Composition pages!</li>
|
||||
<li>Allow logging in with a WordPress.org profile, and creating new posts, including uploading images.</li>
|
||||
<li>Add licensing and proper source code listing.</li>
|
||||
|
@ -292,41 +380,40 @@ const endpointInfo = [ (ctx) => (ctx.urlSections[0] === 'info' && ctx.request.me
|
|||
`);
|
||||
} ];
|
||||
|
||||
const endpointCompose = [ (ctx) => (ctx.urlSections[0] === 'compose' && ['GET', 'POST'].includes(ctx.request.method)), async (ctx) => {
|
||||
let noticeHtml = '';
|
||||
const endpointCompose = [ 'GET|POST /compose/', async (ctx) => {
|
||||
let [noticeErrorHtml, noticeSuccessHtml] = ['', ''];
|
||||
const language = getUserLanguage(ctx);
|
||||
const accountString = ctx.getCookie('account');
|
||||
if (!accountString) {
|
||||
return ctx.redirectTo('/');
|
||||
}
|
||||
const accounts = accountsDataFromCookieString(accountString)?.filter(account => appPlatforms[account.platform].writing);//getAccountsData(ctx);
|
||||
ctx.response.statusCode = 200;
|
||||
const postUploadStatus = ((ctx.bodyParameters?.publish && 'publish') || (ctx.bodyParameters?.draft && 'draft'));
|
||||
if (ctx.request.method === 'POST' && postUploadStatus) {
|
||||
if (!matchCsrfToken(ctx.bodyParameters, accountString)) {
|
||||
ctx.response.statusCode = 401;
|
||||
noticeHtml = strings.csrfErrorHtml;
|
||||
noticeErrorHtml = appStrings.get('csrfError', language);
|
||||
}
|
||||
const isThereAnyFile = ((ctx.bodyParameters.file?.data?.length || ctx.bodyParameters.file?.size) > 0);
|
||||
if (!ctx.bodyParameters.text?.trim() && !isThereAnyFile) {
|
||||
ctx.response.statusCode = 500;
|
||||
noticeHtml = `<p class="notice error">Post content is empty. Please write some text or upload a media.</p>`;
|
||||
noticeErrorHtml = appStrings.get('postEmpty', language);
|
||||
}
|
||||
const account = accountDataFromString(accountString);
|
||||
const account = accountDataFromFormId(accounts, ctx.account); //accountsDataFromCookieString(accountString)[0];
|
||||
if (!checkUpstreamAllowed(account.instance)) {
|
||||
ctx.response.statusCode = 500;
|
||||
noticeHtml = strings.upstreamDisallowedHtml;
|
||||
noticeErrorHtml = appStrings.get('upstreamDisallowed', language);
|
||||
}
|
||||
let mediaData;
|
||||
try {
|
||||
// there is a media to upload first
|
||||
if (httpCodes.success.includes(ctx.response.statusCode) && isThereAnyFile) {
|
||||
const mediaReq = await fetch(`${corsProxyIfNeed(account.cors)}${account.instance}/wp-json/wp/v2/media`, { headers: {
|
||||
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
|
||||
...getBackendHeaders(account),
|
||||
"Content-Type": ctx.bodyParameters.file.type,
|
||||
"Content-Disposition": `attachment; filename=${ctx.bodyParameters.file.filename}`,
|
||||
}, method: "POST", body: (ctx.bodyParameters.file.data || ctx.bodyParameters.file) });
|
||||
mediaData = await mediaReq.json();
|
||||
if (!httpCodes.success.includes(mediaReq.status)) {
|
||||
noticeHtml = `<p class="notice error">Upstream server responded with error ${ctx.response.statusCode = mediaReq.status}: ${JSON.stringify(mediaData)}</p>`;
|
||||
noticeErrorHtml = `${appStrings.get('upstreamError', language)} ${ctx.response.statusCode = mediaReq.status}: ${escapeHtml(JSON.stringify(mediaData))}`;
|
||||
}
|
||||
}
|
||||
// upload actual post if nothing has errored before
|
||||
|
@ -342,7 +429,7 @@ const endpointCompose = [ (ctx) => (ctx.urlSections[0] === 'compose' && ['GET',
|
|||
<!-- /wp:image -->
|
||||
` : ''}`;
|
||||
const postReq = await fetch(`${corsProxyIfNeed(account.cors)}${account.instance}/wp-json/wp/v2/posts`, { headers: {
|
||||
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
|
||||
...getBackendHeaders(account),
|
||||
"Content-Type": "application/json",
|
||||
}, method: "POST", body: JSON.stringify({
|
||||
status: postUploadStatus,
|
||||
|
@ -364,107 +451,240 @@ ${figureHtml}`.trim()),
|
|||
}) });
|
||||
const postData = await postReq.json();
|
||||
if (httpCodes.success.includes(postReq.status)) {
|
||||
noticeHtml = `<p class="notice success">${postUploadStatus === 'publish' ? 'Post published' : ''}${postUploadStatus === 'draft' ? 'Draft uploaded' : ''}! ${A(postData.link)}</p>`;
|
||||
noticeSuccessHtml = `${appStrings.get((postUploadStatus === 'publish' ? 'postPublished' : postUploadStatus === 'draft' ? 'draftUploaded' : ''), language)}! ${A(postData.link)}`;
|
||||
} else {
|
||||
noticeHtml = `<p class="notice error">Upstream server responded with error ${ctx.response.statusCode = postReq.status}: ${JSON.stringify(postData)}</p>`;
|
||||
noticeErrorHtml = `${appStrings.get('upstreamError', language)} ${ctx.response.statusCode = postReq.status}: ${escapeHtml(JSON.stringify(postData))}`;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
ctx.response.statusCode = 500;
|
||||
// display only generic error from server-side, for security
|
||||
noticeHtml = `<p class="notice error">${isEnvServer ? 'Some unknown error just happened. Please check that your data is correct, and try again.' : err}</p>`;
|
||||
noticeErrorHtml = (isEnvServer ? appStrings.get('unknownError', language) : err);
|
||||
}
|
||||
// TODO handle media upload success but post fail, either delete the remote media or find a way to reuse it when the user probably retries posting
|
||||
}
|
||||
ctx.renderPage(`${noticeHtml}
|
||||
${makeFragmentLoggedIn(accountString)}
|
||||
const disabledHtml = (!Object.keys(accounts).length ? 'disabled="true"' : '');
|
||||
ctx.renderPage(`
|
||||
${disabledHtml ? `<p class="notice info">${appStrings.get('mustAddAccount', getUserLanguage(ctx))('compose')}</p>` : ''}
|
||||
${noticeErrorHtml ? `<p class="notice error">${noticeErrorHtml}</p>` : ''}
|
||||
${noticeSuccessHtml ? `<p class="notice success">${noticeSuccessHtml}</p>` : ''}
|
||||
<form method="POST" enctype="multipart/form-data">${makeFormCsrf(accountString)}
|
||||
<input type="text" name="title" placeholder="Post Title" value="${ctx.bodyParameters.title && ctx.response.statusCode !== 200 ? ctx.bodyParameters.title : ''}"/>
|
||||
<input type="file" accept="image/jpeg,image/gif,image/png,image/webp,image/bmp" name="file"/>
|
||||
<textarea name="text" rows="10" placeholder="What's on your mind?">${ctx.bodyParameters.text && ctx.response.statusCode !== 200 ? ctx.bodyParameters.text : ''}</textarea>
|
||||
<select name="account" ${disabledHtml}>${accounts.map(account => `<option value="${makeAccountFormId(account)}">
|
||||
${account.username}@${account.instance} (${account.platform})
|
||||
</option>`).join('')}</select>
|
||||
<input type="text" name="title" placeholder="${appStrings.get('postTitle', language)}" value="${ctx.bodyParameters.title && ctx.response.statusCode !== 200 ? ctx.bodyParameters.title : ''}" ${disabledHtml}/>
|
||||
<input type="file" accept="image/jpeg,image/gif,image/png,image/webp,image/bmp" name="file" ${disabledHtml}/>
|
||||
<textarea name="text" rows="10" placeholder="${appStrings.get('postingHint', language)}" ${disabledHtml}>${ctx.bodyParameters.text && ctx.response.statusCode !== 200 ? ctx.bodyParameters.text : ''}</textarea>
|
||||
<!-- TODO: fix the turn off-on on submit of these checkboxes... -->
|
||||
<label><input type="checkbox" name="html" ${ctx.bodyParameters.html === 'on' && ctx.response.statusCode !== 200 ? 'checked="true"' : ''}/> Raw HTML mode</label>
|
||||
<label><input type="checkbox" name="tags" ${ctx.request.method === 'GET' || (ctx.bodyParameters.tags === 'on' && ctx.response.statusCode !== 200) ? 'checked="true"' : ''}/> Include suggested tags</label>
|
||||
<input type="submit" name="draft" value="Upload Draft"/>
|
||||
<input type="submit" name="publish" value="Publish!"/>
|
||||
<label><input type="checkbox" name="html" ${ctx.bodyParameters.html === 'on' && ctx.response.statusCode !== 200 ? 'checked="true"' : ''} ${disabledHtml}/> Raw HTML mode</label>
|
||||
<label><input type="checkbox" name="tags" ${ctx.request.method === 'GET' || (ctx.bodyParameters.tags === 'on' && ctx.response.statusCode !== 200) ? 'checked="true"' : ''} ${disabledHtml}/> ${appStrings.get('includeTags', language)}</label>
|
||||
<input type="submit" name="draft" value="${appStrings.get('uploadDraft', language)}" ${disabledHtml}/>
|
||||
<input type="submit" name="publish" value="${appStrings.get('publish', language)}" ${disabledHtml}/>
|
||||
</form>
|
||||
`, 'Compose Post');
|
||||
`, appStrings.get('composePost', language));
|
||||
} ];
|
||||
|
||||
const endpointSettings = [ (ctx) => (ctx.urlSections[0] === 'settings' && ['GET', 'POST'].includes(ctx.request.method)), async (ctx) => {
|
||||
let noticeHtml = '';
|
||||
const endpointRead = [ 'GET /read/', async (ctx) => {
|
||||
const accountString = ctx.getCookie('account');
|
||||
const accounts = accountsDataFromCookieString(accountString)?.filter(account => appPlatforms[account.platform].reading);
|
||||
if (!accounts || !Object.keys(accounts).length) {
|
||||
return ctx.redirectTo('/?ref=read');
|
||||
}
|
||||
ctx.response.statusCode = 200;
|
||||
const [accountIndex, channelIndex] = ctx.urlSections.slice(1);
|
||||
if (accountIndex && channelIndex) {
|
||||
const account = accounts[accountIndex];
|
||||
const postsReq = await fetch(`${corsProxyIfNeed()}${account.instance}/api/greader.php/reader/api/0/stream/contents/feed/${channelIndex}?output=json`/*`reading-list?output=json`*/, { headers: getBackendHeaders(account) });
|
||||
const postsData = await postsReq.json();
|
||||
ctx.renderPage(`<div class="posts">${postsData.items.map(post => `<article>
|
||||
<header>
|
||||
<b><a href="${post.origin.htmlUrl}">${post.origin.title}</a></b>
|
||||
<a href="${post.canonical[0].href}">${post.published}</a>
|
||||
<h3>${post.title}</h3>
|
||||
</header>
|
||||
<p>${post.summary.content}</p>
|
||||
</article>`).join('')}</div>`);
|
||||
} else {
|
||||
//if (ctx.request.method === 'POST') {
|
||||
// // TODO handle reading of selected accounts, folders, other options
|
||||
//}
|
||||
const account = accounts[0];
|
||||
const channelsReq = await fetch(`${corsProxyIfNeed()}${account.instance}/api/greader.php/reader/api/0/subscription/list?output=json`, { headers: getBackendHeaders(account) });
|
||||
const channelsData = await channelsReq.json();
|
||||
const unreadReq = await fetch(`${corsProxyIfNeed()}${account.instance}/api/greader.php/reader/api/0/unread-count?output=json`, { headers: getBackendHeaders(account) });
|
||||
const unreadData = await unreadReq.json();
|
||||
const unreads = {};
|
||||
for (const unread of unreadData.unreadcounts) {
|
||||
unreads[unread.id] = unread.count;
|
||||
}
|
||||
const folders = {};
|
||||
for (const channel of channelsData.subscriptions) {
|
||||
const category = channel.categories[0];
|
||||
folders[category.id.slice('user/-/label/'.length)] = category.label;
|
||||
}
|
||||
ctx.renderPage(`
|
||||
<form>
|
||||
<select name="account" multiple="true">${Object.entries(accounts).map(account => `<option value="${account[0]/*makeAccountFormId(account)*/}" ${(!ctx.bodyParameters.account || ctx.bodyParameters.account.includes(account[0])) ? 'selected="true"' : ''}>
|
||||
${account[1].username}@${account[1].instance} (${account[1].platform})
|
||||
</option>`).join('')}</select>
|
||||
<select name="folder" multiple="true">${Object.entries(folders).map(folder => `<option value="${0}/${folder[0]}" selected="true">
|
||||
${folder[1]}
|
||||
</option>`).join('')}</select>
|
||||
</form>
|
||||
<div class="channels">${channelsData.subscriptions.map(channel => `<a href="/read/${0}/${channel.id.split('/')[1]}">
|
||||
<img src="${channel.iconUrl}"/> ${channel.title} ${unreads[channel.id] ? `<span>${unreads[channel.id]}</span>` : ''}
|
||||
</a>`).join('')}</div>
|
||||
`, 'Channels');
|
||||
}
|
||||
} ];
|
||||
|
||||
const endpointSettings = [ 'GET|POST /settings/', async (ctx) => {
|
||||
let [noticeErrorHtml] = [''];
|
||||
const language = getUserLanguage(ctx);
|
||||
const accountString = ctx.getCookie('account');
|
||||
const accounts = accountsDataFromCookieString(accountString);//getAccountsData(ctx);
|
||||
ctx.response.statusCode = 200;
|
||||
if (ctx.request.method === 'POST') {
|
||||
if (accountString && !matchCsrfToken(ctx.bodyParameters, accountString)) {
|
||||
ctx.response.statusCode = 401;
|
||||
noticeHtml = strings.csrfErrorHtml;
|
||||
}
|
||||
if (ctx.response.statusCode === 200 && ctx.bodyParameters.login) {
|
||||
noticeErrorHtml = appStrings.get('csrfError', language);
|
||||
} else if (ctx.bodyParameters.language) {
|
||||
ctx.setCookie(`language=${ctx.bodyParameters.language}${makeCookieFlags(true)}`);
|
||||
return ctx.redirectTo('/settings'); // TODO remove, just a workaround to the server not updating readable cookies by itself
|
||||
} else if (ctx.bodyParameters.login) {
|
||||
ctx.bodyParameters.instance = ctx.bodyParameters.instance.trim();
|
||||
if (!checkUpstreamAllowed(ctx.bodyParameters.instance)) {
|
||||
ctx.response.statusCode = 500;
|
||||
noticeHtml = strings.upstreamDisallowedHtml;
|
||||
}
|
||||
try {
|
||||
const upstreamReq = await fetch(`${corsProxyIfNeed(ctx.bodyParameters.cors === 'on')}${ctx.bodyParameters.instance}/wp-json/wp/v2/users?context=edit`, { headers: {
|
||||
Authorization: `Basic ${btoa(ctx.bodyParameters.username + ':' + ctx.bodyParameters.password)}`,
|
||||
} });
|
||||
const upstreamData = await upstreamReq.json();
|
||||
if (upstreamReq.status === 200) {
|
||||
let cookieFlags = (ctx.bodyParameters.remember === 'on' ? `; max-age=${365*24*60*60}` : '');
|
||||
ctx.setCookie(`account=${ctx.bodyParameters.instance},${ctx.bodyParameters.username},${ctx.bodyParameters.password}${cookieFlags}`); // TODO: add cookie renewal procedure
|
||||
return ctx.redirectTo('/');
|
||||
} else {
|
||||
ctx.response.statusCode = upstreamReq.status;
|
||||
noticeHtml = `<p class="notice error">Upstream server responded with error ${upstreamReq.status}: ${JSON.stringify(upstreamData)}</p>`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
noticeErrorHtml = appStrings.get('upstreamDisallowed', language);
|
||||
} else if (accounts.filter(account => (makeAccountFormId(account) === makeAccountFormId(ctx.bodyParameters))).length) {
|
||||
ctx.response.statusCode = 500;
|
||||
// display only generic error from server-side, for security
|
||||
noticeHtml = `<p class="notice error">${isEnvServer ? 'Some unknown error just happened. Please check that your data is correct, and try again.' : err}</p>`;
|
||||
noticeErrorHtml = appStrings.get('accountExists', language);
|
||||
} else {
|
||||
const loginResult = await loginBackend(ctx.bodyParameters, language);
|
||||
if (loginResult.success) {
|
||||
ctx.setCookie(`account=${accountsCookieStringFromData([...accounts, {
|
||||
...ctx.bodyParameters,
|
||||
options: { remember: (ctx.bodyParameters.remember === 'on') },
|
||||
...loginResult.data,
|
||||
}])}${makeCookieFlags(ctx.bodyParameters)}`); // TODO: add cookie renewal procedure
|
||||
}
|
||||
if (loginResult.code) {
|
||||
ctx.response.statusCode = loginResult.code;
|
||||
}
|
||||
if (loginResult.error) {
|
||||
noticeErrorHtml = loginResult.error;
|
||||
}
|
||||
if (loginResult.redirect) {
|
||||
return ctx.redirectTo(loginResult.redirect);
|
||||
}
|
||||
}
|
||||
} else if (ctx.bodyParameters.logout) {
|
||||
if (ctx.bodyParameters.account) {
|
||||
for (const accountId of [].concat(ctx.bodyParameters.account)) {
|
||||
const formAccount = JSON.stringify(accountDataFromFormId(accounts, accountId));
|
||||
for (const dataAccountIndex in accounts) {
|
||||
if (formAccount === JSON.stringify(accounts[dataAccountIndex])) {
|
||||
accounts.splice(dataAccountIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.setCookie(`account=${accountsCookieStringFromData(accounts)}${makeCookieFlags(ctx.bodyParameters)}`);
|
||||
return ctx.redirectTo('/');
|
||||
} else {
|
||||
ctx.response.statusCode = 500;
|
||||
noticeErrorHtml = appStrings.get('noAccountSelected', language);
|
||||
}
|
||||
} else if (ctx.response.statusCode === 200 && ctx.bodyParameters.logout) {
|
||||
ctx.setCookie(`account=`);
|
||||
return ctx.redirectTo('/');
|
||||
}
|
||||
}
|
||||
ctx.renderPage(`${noticeHtml}
|
||||
${accountString ? `
|
||||
<h3>Current Account</h3>
|
||||
${makeFragmentLoggedIn(accountString)}
|
||||
const mustRememberAccount = accounts?.slice(-1)?.[0]?.options?.remember;
|
||||
const formSharedHtml = `${mustRememberAccount ? '<input type="hidden" name="remember" value="on"/>' : ''}`;
|
||||
ctx.renderPage(`
|
||||
${noticeErrorHtml ? `<p class="notice error">${noticeErrorHtml}</p>` : ''}
|
||||
<form method="POST">${makeFormCsrf(accountString)}
|
||||
<input type="submit" name="logout" value="Logout"/>
|
||||
</form>
|
||||
` : '<p>You must login first.</p>'}
|
||||
${!accountString ? `<h3><!--Add New Account-->Login</h3>
|
||||
<h3>${appStrings.get('language', language)}</h3>
|
||||
<select name="language">${Object.entries(appLanguages).map(lang => `<option value="${lang[0]}" ${language === lang[0] ? 'selected="true"' : ''}>${lang[1]}</option>`).join('')}</select>
|
||||
<input type="submit" value="${appStrings.get('applyLanguage', language)}"/>
|
||||
${formSharedHtml}</form>
|
||||
${accountString ? `<h3>${appStrings.get('currentAccounts', language)}</h3>
|
||||
<form method="POST">${makeFormCsrf(accountString)}
|
||||
<select name="backend">
|
||||
<option value="wp.org">
|
||||
WordPress.org (Community/Self-hosted)
|
||||
</option>
|
||||
</select>
|
||||
<label><i>Note: For WordPress.org you must use an "application password" (<code>/wp-admin/profile.php#application-passwords-section</code>)</i></label>
|
||||
<input type="url" name="instance" placeholder="Site/Instance URL" value="${ctx.bodyParameters.instance || ''}" required="true"/>
|
||||
${accounts.map(account => `<label><input type="checkbox" name="account" value="${makeAccountFormId(account)}"/>
|
||||
${getPlatformActionIcon(appPlatforms[account.platform])}
|
||||
${account.username}@${account.instance} (${account.platform})</label>`).join('')}
|
||||
<input type="submit" name="logout" value="${appStrings.get('logoutAccounts', language)}"/>
|
||||
${formSharedHtml}</form>
|
||||
` : ''}
|
||||
<h3>${appStrings.get('addNewAccount', language)}</h3>
|
||||
<form method="POST">${makeFormCsrf(accountString)}
|
||||
<select name="platform">${Object.entries(appPlatforms).map(platform => `<option value="${platform[0]}" ${ctx.bodyParameters.platform === platform[0] ? 'selected="true"' : ''}>
|
||||
${getPlatformActionIcon(platform[1])} ${platform[1].name}
|
||||
</option>`).join('')}</select>
|
||||
<p><i>
|
||||
Note: For WordPress.org you must use an "application password"
|
||||
( <code>/wp-admin/profile.php#application-passwords-section</code> )
|
||||
</i></p>
|
||||
<input type="url" name="instance" placeholder="${appStrings.get('siteInstanceUrl', language)}" value="${ctx.bodyParameters.instance || ''}" required="true"/>
|
||||
<input type="text" name="username" placeholder="Username" value="${ctx.bodyParameters.username || ''}" required="true"/>
|
||||
<input type="password" name="password" placeholder="Password" value="${ctx.bodyParameters.password || ''}" required="true"/>
|
||||
<!--${isEnvBrowser ? `<label><input type="checkbox" name="cors" ${ctx.bodyParameters.cors === 'on' && ctx.response.statusCode !== 200 ? 'checked="true"' : ''}/> Site disallows CORS, use proxy</label>` : ''}-->
|
||||
<label><input type="checkbox" name="remember" ${ctx.request.method === 'POST' && ctx.bodyParameters.remember !== 'on' ? '' : 'checked="true"'}/> Remember me</label>
|
||||
<input type="submit" name="login" value="Login and Save"/>
|
||||
</form>` : ''}
|
||||
<!--${true ? `
|
||||
<h3>Select and Manage Accounts</h3>
|
||||
<form method="POST">${makeFormCsrf(accountString)}
|
||||
<ul>
|
||||
<li>
|
||||
<input type="submit" name="select" value="username@url"/>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
` : ''}-->
|
||||
`, 'Settings');
|
||||
<label>
|
||||
<input type="checkbox" name="remember" ${ctx.request.method === 'POST'
|
||||
? (ctx.bodyParameters.remember === 'on' ? 'checked="true"' : '')
|
||||
: (mustRememberAccount ? 'checked="true"' : '')}
|
||||
${accounts?.length > 0 ? 'disabled="true"' : ''}
|
||||
/> ${appStrings.get('rememberMe', language)}</label>
|
||||
<input type="submit" name="login" value="${appStrings.get('loginAndSave', language)}"/>
|
||||
${formSharedHtml}</form>
|
||||
`, appStrings.get('settings', language));
|
||||
} ];
|
||||
|
||||
const getBackendHeaders = (account) => {
|
||||
switch (account.platform) {
|
||||
case 'wp.org':
|
||||
return { "Authorization": `Basic ${btoa(account.username + ':' + account.password)}` };
|
||||
break;
|
||||
case 'greader':
|
||||
return { "Authorization": `GoogleLogin auth=${account.username}/${account.password}` };
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const loginBackend = async (loginData, language) => {
|
||||
const result = {};
|
||||
let upstreamReq, upstreamData;
|
||||
try {
|
||||
switch (loginData.platform) {
|
||||
case 'wp.org':
|
||||
upstreamReq = await fetch(`${corsProxyIfNeed(/*loginData.cors === 'on'*/)}${loginData.instance}/wp-json/wp/v2/users?context=edit`, { headers: getBackendHeaders(loginData) });
|
||||
upstreamData = await upstreamReq.json();
|
||||
if (upstreamReq.status === 200) {
|
||||
result.success = true;
|
||||
result.redirect = '/';
|
||||
} else {
|
||||
result.code = upstreamReq.status;
|
||||
result.error = `${appStrings.get('upstreamError', language)} ${upstreamReq.status}: ${escapeHtml(JSON.stringify(upstreamData))}`;
|
||||
}
|
||||
break;
|
||||
case 'greader':
|
||||
upstreamReq = await fetch(`${corsProxyIfNeed()}${loginData.instance}/api/greader.php/accounts/ClientLogin?Email=${encodeURIComponent(loginData.username)}&Passwd=${encodeURIComponent(loginData.password)}`);
|
||||
upstreamData = await upstreamReq.text();
|
||||
if (upstreamReq.status === 200) {
|
||||
const authToken = upstreamData.split('\n').filter(line => line.startsWith('Auth='))[0].split('=')[1].split('/');
|
||||
result.data = { username: authToken[0], password: authToken[1] };
|
||||
result.success = true;
|
||||
result.redirect = '/';
|
||||
} else {
|
||||
result.code = upstreamReq.status;
|
||||
result.error = `${appStrings.get('upstreamError', language)} ${upstreamReq.status}: ${escapeHtml(upstreamData)}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// display only generic error from server-side, for security
|
||||
result.code = 500;
|
||||
result.error = (isEnvServer ? appStrings.get('unknownError', language) : err);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
|
@ -5,11 +5,17 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3",
|
||||
"mime-types": "^2.1.35",
|
||||
"parse-multipart-data": "^1.5.0",
|
||||
"SpaccDotWeb": "gitlab:SpaccInc/SpaccDotWeb"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
|
@ -36,7 +42,7 @@
|
|||
},
|
||||
"node_modules/SpaccDotWeb": {
|
||||
"version": "indev",
|
||||
"resolved": "git+ssh://git@gitlab.com/SpaccInc/SpaccDotWeb.git#63dc2e648f93c7917cced6b08305255f8e4ce330",
|
||||
"resolved": "git+ssh://git@gitlab.com/SpaccInc/SpaccDotWeb.git#495c7a8d2e0e570108b9d2ed23e555949270666e",
|
||||
"dependencies": {
|
||||
"mime-types": "^2.1.35",
|
||||
"parse-multipart-data": "^1.5.0"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"mime-types": "^2.1.35",
|
||||
"parse-multipart-data": "^1.5.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"SpaccDotWeb": "gitlab:SpaccInc/SpaccDotWeb"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<meta property="og:title" content="🎵️ TiktOctt"/>
|
||||
<meta OctoSpaccHubSdk="Url" content="https://hub.octt.eu.org/TiktOctt/"/>
|
||||
<meta OctoSpaccHubSdk="WebManifestExtra" content="'display':'standalone', 'icons':[{ 'src':'./logo.png', 'type':'image/png', 'sizes':'1024x1024' }],"/>
|
||||
<link rel="apple-touch-icon" href="./logo.png"/>
|
||||
<style>
|
||||
:root {
|
||||
--footerHeight: 40px;
|
||||
|
|
Loading…
Reference in New Issue