flotorrent/index.html
2023-01-09 20:19:33 +05:30

1402 lines
72 KiB
HTML

<!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">
<title>FLO Torrent</title>
<link rel="shortcut icon" href="flo-torrent.png" type="image/png">
<link rel="stylesheet" href="css/main.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..900&display=swap" rel="stylesheet">
</head>
<body data-theme="">
<sm-notifications id="notification_drawer"></sm-notifications>
<sm-popup id="confirmation_popup">
<h4 id="confirm_title"></h4>
<p id="confirm_message"></p>
<div class="flex align-center gap-0-5 margin-left-auto">
<button class="button cancel-button">Cancel</button>
<button class="button button--primary confirm-button">OK</button>
</div>
</sm-popup>
<sm-popup id="filter_popup">
<header class="popup__header" slot="header">
<button class="popup__header__close" onclick="getRef('filter_popup').hide()">
<svg class="icon close-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>
</button>
<h3>Filter</h3>
</header>
<form id="filter_form" onsubmit="return false">
<h4>Category</h4>
<section id="category_selector" class="option-selector"></section>
<h4>Tags</h4>
<section id="tags_selector" class="option-selector"></section>
<div class="flex gap-1">
<button class="justify-right" onclick="resetFilter()">Clear</button>
<button onclick="addFilter()" class="button button--primary">Fliter</button>
</div>
</form>
</section>
</sm-popup>
<header id="main_header">
<div class="flex align-center">
<svg id="main_header__logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z" />
</svg>
<a href="#/home" class="header__company-name">RanchiMall</a>
</div>
<a href="#/how_it_works" class="button justify-right">How it works?</a>
<theme-toggle></theme-toggle>
</header>
<main>
<section id="loading_page" class="page">
<svg class="app-icon icon app-icon-loader" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path
d="M54.69,14.28l8.83-8.83a1.62,1.62,0,0,0-1.14-2.77H21.15A1.63,1.63,0,0,0,19.53,4.3V59.7a1.62,1.62,0,0,0,2.78,1.13l8.83-9a1.62,1.62,0,0,0,.46-1.14V38.06a1.58,1.58,0,0,1,.48-1.14l8.81-8.83a1.62,1.62,0,0,0-1.15-2.77H33.22A1.62,1.62,0,0,1,31.6,23.7V16.37a1.63,1.63,0,0,1,1.62-1.62H53.55A1.63,1.63,0,0,0,54.69,14.28Z" />
<path
d="M1.62,14.75H12.36A1.62,1.62,0,0,0,14,13.13V4.3a1.63,1.63,0,0,0-1.62-1.62H7.47a1.6,1.6,0,0,0-1.35.73L.27,12.24A1.62,1.62,0,0,0,1.62,14.75Z" />
</svg>
<h4>FLO Torrent</h4>
<p>Getting torrents from FLO blockchain</p>
</section>
<section id="home" class="page hidden">
<section id="search_section" class="page-layout">
<svg class="app-icon icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path
d="M54.69,14.28l8.83-8.83a1.62,1.62,0,0,0-1.14-2.77H21.15A1.63,1.63,0,0,0,19.53,4.3V59.7a1.62,1.62,0,0,0,2.78,1.13l8.83-9a1.62,1.62,0,0,0,.46-1.14V38.06a1.58,1.58,0,0,1,.48-1.14l8.81-8.83a1.62,1.62,0,0,0-1.15-2.77H33.22A1.62,1.62,0,0,1,31.6,23.7V16.37a1.63,1.63,0,0,1,1.62-1.62H53.55A1.63,1.63,0,0,0,54.69,14.28Z" />
<path
d="M1.62,14.75H12.36A1.62,1.62,0,0,0,14,13.13V4.3a1.63,1.63,0,0,0-1.62-1.62H7.47a1.6,1.6,0,0,0-1.35.73L.27,12.24A1.62,1.62,0,0,0,1.62,14.75Z" />
</svg>
<h4 class="app-name">FLO Torrent</h4>
<section class="search-container">
<sm-input id="search_torrent" class="search-torrent" type="search" placeholder="Search">
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z" />
</svg>
</sm-input>
<section id="search_suggestions" class="search-suggestions-container"></section>
</section>
</section>
<section class="page-layout">
<div class="flex align-center space-between">
<h4>Recent</h4>
<a class="button" href="#/browse">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
width="24px">
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z" />
</svg>
Browse
</a>
</div>
<section id="torrent_container" class="torrent-container"></section>
</section>
</section>
<section id="torrent" class="page hidden page-layout">
<section class="torrent-preview">
<div id="torrent_type_icon" class="torrent-type-icon"></div>
<div class="torrent-preview__info-section">
<div id="torrent_tags"></div>
<h1 id="torrent_name"></h1>
<p id="torrent_description"></p>
<h5 id="torrent_uploader"></h5>
<a id="torrent_index_tx" target="_blank" class="flex align-center">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8z" />
</svg>
See the index entry in blockchain
</a>
<button id="torrent_download_button" class="button button--primary"
onclick="downloadTorrent(currentTorrent.filename, currentTorrent.startTx, currentTorrent.chunks, currentTorrent.id, true)">
Get torrent
</button>
</div>
</section>
</section>
<section id="search" class="page hidden page-layout">
<div class="grid gap-1">
<h1 class="page__title">Search</h1>
<section class="search-container">
<sm-input id="advance_torrent_search" class="search-torrent" type="search">
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z" />
</svg>
</sm-input>
<section id="advance_search_suggestions" class="search-suggestions-container"></section>
</section>
</div>
<div class="flex align-center space-between">
<p id="result_for"></p>
<button id="filter_button" class="button" onclick="getRef('filter_popup').show()">
<svg class="icon button__icon button__icon--left" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0H24V24H0z" />
<path d="M21 4L21 6 20 6 14 15 14 22 10 22 10 15 4 6 3 6 3 4z" />
</svg>
Filter
</button>
</div>
<section id="filters_bar" class="hidden">
<div id="selected_filters_container"></div>
<button class="button" onclick="resetFilter()">
<svg class="icon button__icon button__icon--left" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-11.414L9.172 7.757 7.757 9.172 10.586 12l-2.829 2.828 1.415 1.415L12 13.414l2.828 2.829 1.415-1.415L13.414 12l2.829-2.828-1.415-1.415L12 10.586z" />
</svg>
Clear all
</button>
</section>
<section id="search_result" class="torrent-container"></section>
<sm-chips id="search_page_selector"></sm-chips>
</section>
<section id="browse" class="hidden page page-layout">
<section class="flex direction-column">
<h1 class="page__title">Browse</h1>
<sm-chips id="browse_category_selector">
<sm-chip value="movie" selected>Movie</sm-chip>
</sm-chips>
</section>
<section id="browser_category_torrents" class="torrent-container"></section>
<sm-chips id="page_selector"></sm-chips>
</section>
<section id="how_it_works" class="page hidden page-layout">
<h1 class="page__title">How it works?</h1>
<p>FLO Torrent stores torrent files directly on the FLO blockchain making it completely decentralized. But
this approach came with some challenges.</p>
<section class="info-section">
<object class="info__image" data="assets/torrent-vs-flo-tx.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">The problem</h2>
<p>
The maximum number of free characters that can be stored in a FLO blockchain transaction is
1040.
But torrents are typically 20,000 characters each.
</p>
<p>
So, we need to first design a way to split the torrent into smaller segments and find a way to
link them inside the blockchain.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/split-torrent.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">The solution</h2>
<p>
Splitting can easily be achieved by reading only 900 characters from the torrent file one at a
time and using the remaining characters for linking purposes.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/put-in-blockchain.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">Next step</h2>
<p>
We put the first segment in the FLO Blockchain and get its unique transaction ID.
Then we put the second segment in the blockchain and link it with the previous transaction ID.
</p>
<p>
This process continues until all segments are put on the blockchain.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/chunk-linking.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">Linking the chunks</h2>
<p>
The transaction ID of the last segment is the entry point to the full data stream and is
published as a torrent ID.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/global.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">Discoverability</h2>
<p>
Transactions from a global FLO Address will list all Torrent IDs, and other details like the
name of the torrent, description, etc.
</p>
<p>
This global FLO Address will be a trusted address and will list only trusted and good quality
torrents.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/client-side.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">The FLO torrent client</h2>
<p>
FLO torrent will first read the global FLO Address, find all the torrent details and, list it.
</p>
<p>
When you select a torrent to download. Its entry transaction ID is retrieved, and the last
segment of the torrent file is downloaded. The previous transaction ID is also retrieved, and
data in that ID is downloaded.
</p>
<p>
Finally, all segments are downloaded until we reach the first segment which has no further
linkages.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/torrent-assembly.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">Putting everything back</h2>
<p>
Thus, all segments of the torrent can be downloaded from the blockchain.
</p>
<p>
Now, all the browser has to do is reassemble them in the correct order, and we have our torrent
file.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/missing-piece.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">But why?</h2>
<p>
This solution decentralizes the storage of Torrent files which was the last missing piece in the
decentralization of the torrent ecosystem. After this the entire chain of the torrent ecosystem
is decentralized.
</p>
</div>
</section>
<section class="info-section">
<object class="info__image" data="assets/context-menu.svg" type="image/svg+xml"></object>
<div class="textual-info">
<h2 class="info__title h2">What if we get blocked?</h2>
<p>
No worries, this app is completely contained within a single HTML file so even if the site URL
is blocked if you saved this webpage as an HTML file it will work seamlessly.
</p>
</div>
</section>
</section>
</main>
<footer id="main_footer" class="page-layout">
<p>Powered by FLO blockchain</p>
</footer>
<template id="torrent_card_template">
<li class="torrent-card">
<a class="torrent-info grid">
<div class="torrent-card__icon torrent-type-icon"></div>
<h3 class="torrent-card__title"></h3>
<span class="torrent-card__uploader"></span>
</a>
<button class="torrent-card__download-button">
Get torrent
</button>
</li>
</template>
<script src="components.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6"></script>
<script>
const cryptocoin = "FLO";
const mainnet = `https://flosight.duckdns.org/`;
const testnet = `https://testnet-flosight.duckdns.org`;
const adminID = "FDG64XNjdsA4rAgfm4ABEs2RcTgqn8Jecv";
const kudosID = "FKAEdnPfjXLHSYwrXQu377ugN4tXU7VGdf";
if (cryptocoin == "FLO")
var server = mainnet;
else if (cryptocoin == "FLO_TEST")
var server = testnet;
var torrents = [], searchResults = [], trustedIDs = [], torrentSearchIndex;
</script>
<script id="userNonPresentationData">
function clearLocalData() {
if (!confirm("Do you want to clear all local data?!"))
return;
var DBDeleteRequest = window.indexedDB.deleteDatabase("FLO_Torrent");
DBDeleteRequest.onsuccess = function (event) {
console.log("Database deleted successfully");
};
}
async function getResponce(uri) {
try {
const url = `${server}/${uri}`
const res = await fetch(url)
return res.json()
}
catch (err) {
console.error(err)
}
}
function storeTrustedIDs(data) {
return new Promise(
function (resolve, reject) {
var idb = indexedDB.open("FLO_Kudos");
idb.onerror = function (event) {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = function (event) {
var db = event.target.result;
var obs = db.transaction('trustedID', "readwrite").objectStore('trustedID');
for (id in data)
obs.put(data[id], id);
db.close();
resolve("Successfully stored Trusted IDs");
};
}
);
}
function getTrustedIDsfromAPI() {
return new Promise(
function (resolve, reject) {
var idb = indexedDB.open("FLO_Kudos");
idb.onerror = function (event) {
reject("Error in opening IndexedDB!");
};
idb.onupgradeneeded = function (event) {
var objectStore1 = event.target.result.createObjectStore("trustedID");
var objectStore2 = event.target.result.createObjectStore("lastTx");
};
idb.onsuccess = function (event) {
var db = event.target.result;
var lastTx = db.transaction('lastTx', "readwrite").objectStore('lastTx');
var addr = kudosID;
var lastTxReq = lastTx.get(addr);
lastTxReq.onsuccess = async function (event) {
var lasttx = event.target.result
if (lasttx === undefined) {
lasttx = 0;
}
try {
var response = await getResponce(`api/addrs/${addr}/txs`);
var nRequired = response.totalItems - lasttx;
console.log(nRequired);
while (true && nRequired) {
var response = await getResponce(`api/addrs/${addr}/txs?from=0&to=${nRequired}`);
if (nRequired + lasttx != response.totalItems) {
nRequired = response.totalItems - lasttx;
continue;
}
var errorTxCount = 0; //Count of txs that didnot go into any blocks
response.items.reverse().forEach(function (tx) {
try {
if (!tx.blockhash) { //ignore error txs that did not go into any blocks
errorTxCount += 1;
return;
}
if (addr != tx.vin[0].addr) //ignore if the tx is not from kudos ID
return
var kudosData = JSON.parse(tx.floData).FLO_Kudos;
if (kudosData === undefined)
return;
storeTrustedIDs(kudosData).then(function (response) {
}).catch(function (error) {
});
} catch (e) {
//console.log(e)
}
});
var idb2 = indexedDB.open("FLO_Kudos");
idb2.onerror = function (event) {
console.log("Error in opening IndexedDB!");
};
idb2.onsuccess = function (event) {
var dbt = event.target.result;
var obs = dbt.transaction('lastTx', "readwrite").objectStore('lastTx');
obs.put(response.totalItems - errorTxCount, addr);
dbt.close();
};
break;
}
resolve('retrieved data from API');
} catch (e) {
notify("Error in retrieving data from API", 'error');
console.error(e);
}
};
db.close();
};
});
}
function getTrustedIDsFromIDB() {
return new Promise(
function (resolve, reject) {
var idb = indexedDB.open("FLO_Kudos");
idb.onerror = function (event) {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = function (event) {
var db = event.target.result;
var obs = db.transaction('trustedID', "readwrite").objectStore('trustedID');
var resultArray = [];
var cursorReq = obs.openCursor();
cursorReq.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
if (cursor.value >= 2)
resultArray.push(cursor.key);
cursor.continue();
} else {
resolve(resultArray)
}
};
db.close();
};
}
);
}
function storeTorrentData(torrentdata) {
return new Promise(
function (resolve, reject) {
var idb = indexedDB.open("FLO_Torrent");
idb.onerror = function (event) {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = function (event) {
var db = event.target.result;
var obs = db.transaction('torrents', "readwrite").objectStore('torrents');
objectRequest = obs.put(torrentdata);
objectRequest.onerror = function (event) {
reject(Error('Error occured: Unable to store data'));
};
objectRequest.onsuccess = function (event) {
resolve('Data saved OK');
db.close();
};
};
}
);
}
function getDatafromAPI() {
return new Promise(
function (resolve, reject) {
var idb = indexedDB.open("FLO_Torrent");
idb.onerror = function (event) {
reject("Error in opening IndexedDB!");
};
idb.onupgradeneeded = function (event) {
var objectStore = event.target.result.createObjectStore("torrents", { keyPath: "id", autoIncrement: true });
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("filename", "filename", { unique: false });
objectStore.createIndex("type", "type", { unique: false });
objectStore.createIndex("size", "size", { unique: false });
objectStore.createIndex("description", "description", { unique: false });
objectStore.createIndex("tags", "tags", { unique: false });
objectStore.createIndex("chunks", "chunks", { unique: false });
objectStore.createIndex("startTx", "startTx", { unique: false });
objectStore.createIndex("uploader", "uploader", { unique: false });
var objectStore2 = event.target.result.createObjectStore("lastTx");
};
idb.onsuccess = function (event) {
var db = event.target.result;
var lastTx = db.transaction('lastTx', "readwrite").objectStore('lastTx');
var addr = adminID;
var lastTxReq = lastTx.get(addr);
lastTxReq.onsuccess = async function (event) {
var lasttx = event.target.result
if (lasttx === undefined) {
lasttx = 0;
}
var response = await getResponce(`api/addrs/${addr}/txs`);
var nRequired = response.totalItems - lasttx;
console.log(nRequired);
while (true && nRequired) {
var response = await getResponce(`api/addrs/${addr}/txs?from=0&to=${nRequired}`);
if (nRequired + lasttx != response.totalItems) {
nRequired = response.totalItems - lasttx;
continue;
}
var errorTxCount = 0; //Count of txs that didnot go into any blocks
response.items.reverse().forEach(function (tx) {
try {
//console.log(tx.floData)
if (!tx.blockhash) { //ignore error txs that did not go into any blocks
errorTxCount += 1;
return;
}
if (!trustedIDs.includes(tx.vin[0].addr)) //ignore if torrent is not from trusted ID
return
var torrentdata = JSON.parse(tx.floData).FLO_Torrent;
if (torrentdata === undefined)
return;
//console.log(torrentdata);
var data = { name: torrentdata.name, filename: torrentdata.filename, type: torrentdata.type, uploader: tx.vin[0].addr, description: torrentdata.description, size: torrentdata.size, tags: torrentdata.tags, chunks: torrentdata.chunks, startTx: torrentdata.startTx };
storeTorrentData(data).then(function (response) {
}).catch(function (error) {
//console.log(error.message);
});
} catch (e) {
//console.log(e)
}
});
var idb2 = indexedDB.open("FLO_Torrent");
idb2.onerror = function (event) {
console.log("Error in opening IndexedDB!");
};
idb2.onsuccess = function (event) {
var dbt = event.target.result;
var obs = dbt.transaction('lastTx', "readwrite").objectStore('lastTx');
obs.put(response.totalItems - errorTxCount, addr);
dbt.close();
};
break;
}
resolve('retrived data from API');
};
db.close();
};
});
}
function getDataFromIDB(torrentId) {
return new Promise(
function (resolve, reject) {
var idb = indexedDB.open("FLO_Torrent");
idb.onerror = function (event) {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = function (event) {
var db = event.target.result;
var obs = db.transaction('torrents', "readwrite").objectStore('torrents');
torrentdetails = [];
if (torrentId) {
obs.get(torrentId).onsuccess = e => {
resolve(e.target.result)
}
}
else {
var getReq = obs.getAll();
getReq.onsuccess = function (event) {
resolve(event.target.result);
};
}
db.close();
};
}
);
}
function getNewestDatafromAPI() {
return new Promise(
async function (resolve, reject) {
var addr = adminID;
var response = await getResponce(`api/addrs/${addr}/txs?from=0&to=100`);
var tmpData = [];
response.items.forEach(function (tx) {
try {
if (!tx.blockhash) { //ignore error txs that did not go into any blocks
errorTxCount += 1;
return;
}
if (!trustedIDs.includes(tx.vin[0].addr)) //ignore if torrent is not from trusted ID
return
var torrentdata = JSON.parse(tx.floData).FLO_Torrent;
if (torrentdata === undefined)
return;
var data = { name: torrentdata.name, filename: torrentdata.filename, type: torrentdata.type, uploader: tx.vin[0].addr, description: torrentdata.description, size: torrentdata.size, tags: torrentdata.tags, chunks: torrentdata.chunks, startTx: torrentdata.startTx };
tmpData.push(data);
} catch (e) {
//console.log(e)
}
});
resolve(tmpData);
}
);
}
function getTorrentMetafromAPI(txid, i, N, torrentId, fromTorrentPage = false) {
return new Promise(async (resolve, reject) => {
try {
let response = await getResponce(`/api/tx/${txid}`)
let floData = JSON.parse(response.floData);
let percent = Math.round((i / N) * 100);
console.log(torrentId)
if (fromTorrentPage) {
getRef('torrent_download_button').style.setProperty('--progress', `${percent}%`)
getRef('torrent_download_button').textContent = `Collecting... ${percent}%`
}
else {
getRef(torrentId).querySelector('.torrent-card__download-button').style.setProperty('--progress', `${percent}%`)
getRef(torrentId).querySelector('.torrent-card__download-button').textContent = `Collecting... ${percent}%`
}
// console.log(i, N, percent, floData.next);
if (!floData.next)
resolve([floData.data]);
else {
getTorrentMetafromAPI(floData.next, i + 1, N, torrentId, fromTorrentPage).then(chunks => {
resolve([floData.data].concat(chunks))
}).catch(error => {
reject(error);
});
}
}
catch (error) {
reject(error);
}
});
}
async function downloadTorrent(filename, txid, totalChunks, torrentId, fromTorrentPage = false) {
console.log(txid);
if (fromTorrentPage) {
getRef('torrent_download_button').setAttribute('data-collecting', '')
getRef('torrent_download_button').textContent = 'Collecting...'
getRef('torrent_download_button').disabled = true;
}
else {
getRef(torrentId).querySelector('.torrent-card__download-button').setAttribute('data-collecting', '')
getRef(torrentId).querySelector('.torrent-card__download-button').textContent = 'Collecting...'
getRef(torrentId).querySelector('.torrent-card__download-button').disabled = true;
}
getTorrentMetafromAPI(txid, 1, totalChunks, torrentId, fromTorrentPage).then(chunks => {
let filedata = chunks.join("");
// console.log(filedata);
download(filename, filedata);
setTimeout(() => {
if (fromTorrentPage) {
getRef('torrent_download_button').removeAttribute('data-collecting')
getRef('torrent_download_button').textContent = 'Get Torrent'
getRef('torrent_download_button').disabled = false;
getRef('torrent_download_button').style.removeProperty('--progress')
}
else {
getRef(torrentId).querySelector('.torrent-card__download-button').removeAttribute('data-collecting')
getRef(torrentId).querySelector('.torrent-card__download-button').textContent = 'Get Torrent';
getRef(torrentId).querySelector('.torrent-card__download-button').disabled = false;
getRef(torrentId).querySelector('.torrent-card__download-button').style.removeProperty('--progress')
}
}, 300);
}).catch(error => {
console.error(error);
})
}
function download(filename, data) {
var element = document.createElement('a');
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8;base64,' + data);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
//This function will be called by framework for you
async function getTorrents() {
console.log("StartUp");
try {
await getTrustedIDsfromAPI()
trustedIDs = await getTrustedIDsFromIDB()
await Promise.all([
getNewestDatafromAPI(),
getDatafromAPI()
])
return getDataFromIDB()
}
catch (err) {
console.error(err)
}
}
</script>
<script id="default_ui_library">
const domRefs = {};
const uiGlobals = {
connectionErrorNotifications: []
}
//Checks for internet connection status
if (!navigator.onLine)
uiGlobals.connectionErrorNotifications.push(notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error'))
window.addEventListener('offline', () => {
uiGlobals.connectionErrorNotifications.push(notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error'))
})
window.addEventListener('online', () => {
uiGlobals.connectionErrorNotifications.forEach(notificationId => getRef('notification_drawer').remove(notificationId))
uiGlobals.connectionErrorNotifications = []
notify('We are back online.', 'success')
})
function getRef(elementId) {
if (!domRefs.hasOwnProperty(elementId)) {
domRefs[elementId] = {
count: 1,
ref: null,
};
return document.getElementById(elementId);
} else {
if (domRefs[elementId].count < 3) {
domRefs[elementId].count = domRefs[elementId].count + 1;
return document.getElementById(elementId);
} else {
if (!domRefs[elementId].ref)
domRefs[elementId].ref = document.getElementById(elementId);
return domRefs[elementId].ref;
}
}
}
function create(tagName, obj) {
const { className, textContent, innerHTML } = obj
const elem = document.createElement(tagName)
if (className)
elem.className = className
elem.textContent = textContent
if (innerHTML)
elem.innerHTML = innerHTML
return elem
}
function randomHsl(saturation = 80, lightness = 80) {
let hue = Math.random() * 360;
let color = {
primary: `hsla( ${hue}, ${saturation}%, ${lightness}%, 1)`,
light: `hsla( ${hue}, ${saturation}%, 90%, 0.6)`,
};
return color;
}
const selectedColors = [
"#FF1744",
"#F50057",
"#8E24AA",
"#5E35B1",
"#3F51B5",
"#3D5AFE",
"#00B0FF",
"#00BCD4",
"#16c79a",
"#66BB6A",
"#8BC34A",
"#11698e",
"#FF6F00",
"#FF9100",
"#FF3D00",
];
function randomColor() {
return selectedColors[Math.floor(Math.random() * selectedColors.length)];
}
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
function notify(message, mode, options = {}) {
let icon
switch (mode) {
case 'success':
icon = `<svg class="icon icon--success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
break;
case 'error':
icon = `<svg class="icon icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
options.pinned = true
break;
}
getRef("notification_drawer").push(message, { icon, ...options });
if (mode === 'error') {
console.error(message)
}
}
const currentYear = new Date().getFullYear();
function getFormatedTime(time, relative) {
try {
if (String(time).indexOf("_")) time = String(time).split("_")[0];
const intTime = parseInt(time);
if (String(intTime).length < 13) time *= 1000;
let timeFrag = new Date(intTime).toString().split(" "),
day = timeFrag[0],
month = timeFrag[1],
date = timeFrag[2],
year = timeFrag[3],
minutes = new Date(intTime).getMinutes(),
hours = new Date(intTime).getHours(),
currentTime = new Date().toString().split(" ");
minutes = minutes < 10 ? `0${minutes}` : minutes;
let finalHours = ``;
if (hours > 12) finalHours = `${hours - 12}:${minutes}`;
else if (hours === 0) finalHours = `12:${minutes}`;
else finalHours = `${hours}:${minutes}`;
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`;
if (relative) {
return `${date} ${month} ${year}`;
} else return `${finalHours} ${month} ${date} ${year}`;
} catch (e) {
console.error(e);
return time;
}
}
window.addEventListener("load", () => {
document.addEventListener("keyup", (e) => {
});
document.addEventListener("pointerdown", (e) => {
if (e.target.closest("button, .interact")) {
createRipple(e, e.target.closest("button, .interact"));
}
});
});
function createRipple(event, target) {
const circle = document.createElement("span");
const diameter = Math.max(target.clientWidth, target.clientHeight);
const radius = diameter / 2;
const targetDimensions = target.getBoundingClientRect();
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`;
circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`;
circle.classList.add("ripple");
const rippleAnimation = circle.animate(
[
{
transform: "scale(3)",
opacity: 0,
},
],
{
duration: 1000,
fill: "forwards",
easing: "ease-out",
}
);
target.append(circle);
rippleAnimation.onfinish = () => {
circle.remove();
};
}
// Use when a function needs to be executed after user finishes changes
const debounce = (callback, wait) => {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback.apply(null, args);
}, wait);
};
}
</script>
<script>
const render = {
torrentCard(obj) {
const { id, name, description, tags, type = 'misc', size, uploader, startTx, filename, chunks } = obj
const card = getRef('torrent_card_template').content.cloneNode(true).firstElementChild
card.id = id
card.querySelector('.torrent-info').href = `#/torrent/${id}`
card.querySelector('.torrent-card__title').textContent = name
card.querySelector('.torrent-card__uploader').textContent = `by ${uploader}`
card.querySelector('.torrent-card__icon').innerHTML = getIcon(type)
return card
},
filterOption(obj) {
const { content, groupName, value } = obj
const option = create('label', {
className: 'filter-option interact'
})
option.innerHTML = `
<input type="${groupName ? 'radio' : 'checkbox'}" name="${groupName ? groupName : ''}" value="${value}">
<div class="option-text">${content}</div>
`
return option
},
selectedFilter(obj) {
const { content, type } = obj
const option = create('div', {
className: 'selected-filter interact'
})
option.dataset.type = type
option.dataset.value = content
option.innerHTML = `
<div class="option-text">${content}</div>
<svg class="icon button__icon--right" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-11.414L9.172 7.757 7.757 9.172 10.586 12l-2.829 2.828 1.415 1.415L12 13.414l2.828 2.829 1.415-1.415L13.414 12l2.829-2.828-1.415-1.415L12 10.586z"/></svg>
`
return option
}
}
function getIcon(type) {
let icon
switch (type.toLowerCase()) {
case 'movie':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M17.998 7l2.31-4h.7c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h3.006l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31z"/></svg>`
break
case 'tv series':
case 'tv_series':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zm8.622 4.422a.4.4 0 0 0-.622.332v6.506a.4.4 0 0 0 .622.332l4.879-3.252a.4.4 0 0 0 0-.666l-4.88-3.252z"/></svg>`
break
case 'video':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16 4a1 1 0 0 1 1 1v4.2l5.213-3.65a.5.5 0 0 1 .787.41v12.08a.5.5 0 0 1-.787.41L17 14.8V19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h14zM7.4 8.829a.4.4 0 0 0-.392.32L7 9.228v5.542a.4.4 0 0 0 .542.374l.073-.036 4.355-2.772a.4.4 0 0 0 .063-.624l-.063-.05L7.615 8.89A.4.4 0 0 0 7.4 8.83z"/></svg>`
break
case 'music':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M20 3v14a4 4 0 1 1-2-3.465V6H9v11a4 4 0 1 1-2-3.465V3h13z"/></svg>`
break
case 'software':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm17 7H4v9h16v-9zm-5-4v2h4V6h-4z"/></svg>`
break
case 'game':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M17 4a6 6 0 0 1 6 6v4a6 6 0 0 1-6 6H7a6 6 0 0 1-6-6v-4a6 6 0 0 1 6-6h10zm-7 5H8v2H6v2h1.999L8 15h2l-.001-2H12v-2h-2V9zm8 4h-2v2h2v-2zm-2-4h-2v2h2V9z"/></svg>`
break
case 'image':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11.1l2-2 5.5 5.5 3.5-3.5 3 3V5H5v6.1zM4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm11.5 7a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/></svg>`
break
case 'audio':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16 2l5 5v14.008a.993.993 0 0 1-.993.992H3.993A1 1 0 0 1 3 21.008V2.992C3 2.444 3.445 2 3.993 2H16zm-5 10.05a2.5 2.5 0 1 0 2 2.45V10h3V8h-5v4.05z"/></svg>`
break
default:
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16 2l5 5v14.008a.993.993 0 0 1-.993.992H3.993A1 1 0 0 1 3 21.008V2.992C3 2.444 3.445 2 3.993 2H16zm-5 13v2h2v-2h-2zm2-1.645A3.502 3.502 0 0 0 12 6.5a3.501 3.501 0 0 0-3.433 2.813l1.962.393A1.5 1.5 0 1 1 12 11.5a1 1 0 0 0-1 1V14h2v-.645z"/></svg>`
break
}
return icon
}
function renderTorrents(torrents) {
const torrentsFrag = document.createDocumentFragment()
torrents.forEach(torrent => {
torrentsFrag.append(render.torrentCard(torrent))
})
return torrentsFrag
}
let filteredSearch = {
active: false,
query: '',
category: '',
tags: new Set()
}
let lastQuery = ''
async function renderSearchResult(searchKey, shouldFilter = false) {
if (shouldFilter) {
searchKey = getRef('advance_torrent_search').value.trim() !== '' ? getRef('advance_torrent_search').value.trim() : lastQuery
}
filteredSearch['query'] = searchKey
let result
if (filteredSearch.isActive) {
if (filteredSearch.category && filteredSearch.tags.size) {
result = await getFilteredTorrents(['type'], filteredSearch.category)
result = await getFilteredTorrents(['tags'], [...filteredSearch.tags].join('/'), { torrents: result })
}
else if (filteredSearch.category) {
result = await getFilteredTorrents(['type'], filteredSearch.category)
}
else if (filteredSearch.tags.size) {
result = await getFilteredTorrents(['tags'], [...filteredSearch.tags].join('/'))
}
result = await getFilteredTorrents(['name', 'filename', 'tags'], searchKey, { torrents: result })
}
else {
result = await getFilteredTorrents(['name', 'filename', 'tags'], searchKey)
}
if (result.length) {
getRef('result_for').innerHTML = `Showing results for <strong>${searchKey}</strong>`
}
else {
getRef('result_for').innerHTML = `No results for <strong>${searchKey}</strong>`
}
getRef('search_result').innerHTML = ``
getRef('search_page_selector').innerHTML = ``
if (result.length > 20) {
const pages = Math.round(result.length / 20)
getRef('search_page_selector').append(createPageButtons(pages))
getRef('search_result').append(renderTorrents(result.slice(0, 20)))
}
else {
getRef('search_result').append(renderTorrents(result))
}
lastQuery = searchKey
}
const categories = ['movie', 'tv series', 'video', 'music', 'software', 'game', 'image', 'audio', 'misc']
const allTags = ['action', 'adventure', , 'anime', 'comedy', 'crime', 'drama', 'family', 'fantacy', 'horror', 'korean', 'mystery', 'romance', 'sci-fi', 'sport', 'thriller', 'western']
function renderOptions(list, options = {}) {
const { groupName } = options
const frag = document.createDocumentFragment()
list.forEach(item => {
const optionObj = {
content: item,
groupName,
value: item
}
frag.append(render.filterOption(optionObj))
})
return frag
}
const appState = {
params: {},
openedPages: new Set(),
}
const generalPages = ['sign_up', 'sign_in', 'loading', 'landing']
async function routeTo(targetPage, options = {}) {
const { firstLoad, hashChange } = options
let pageId
let params = {}
let searchParams
if (targetPage === '') {
pageId = 'home'
} else {
if (targetPage.includes('/')) {
if (targetPage.includes('?')) {
const splitAddress = targetPage.split('?')
searchParams = splitAddress.pop()
const pages = splitAddress.pop().split('/')
pageId = pages[1]
subPageId = pages[2]
} else {
const pages = targetPage.split('/')
pageId = pages[1]
subPageId = pages[2]
}
} else {
pageId = targetPage
}
}
appState.currentPage = pageId
if (searchParams) {
const urlSearchParams = new URLSearchParams('?' + searchParams);
params = Object.fromEntries(urlSearchParams.entries());
}
switch (pageId) {
case 'home':
history.replaceState(null, null, '#/home')
let allTorrents = await getDataFromIDB()
allTorrents = allTorrents.reverse().slice(0, 8)
getRef('torrent_container').innerHTML = ``
getRef('torrent_container').append(renderTorrents(allTorrents))
break;
case 'search':
if (appState.lastPage !== pageId) {
getRef('category_selector').innerHTML = ''
getRef('category_selector').append(renderOptions(categories, { groupName: 'category-selector' }))
getRef('tags_selector').innerHTML = ''
getRef('tags_selector').append(renderOptions(allTags))
}
if (subPageId)
renderSearchResult(subPageId)
break;
case 'torrent':
currentTorrent = await getDataFromIDB(parseInt(subPageId))
const { id, name, description, tags, type = 'misc', size, uploader, startTx, filename, chunks } = currentTorrent
getRef('torrent_type_icon').innerHTML = getIcon(type)
getRef('torrent_name').textContent = name
getRef('torrent_description').textContent = description
getRef('torrent_tags').textContent = tags.split('/').join('•')
getRef('torrent_uploader').textContent = `Uploaded by ${uploader}`
getRef('torrent_index_tx').href = `https://flosight.duckdns.org/tx/${startTx}`
break;
case 'browse':
const category = getRef('browse_category_selector').value
showCategoryTorrents(category)
break;
}
if (appState.lastPage !== pageId) {
const animOptions = {
duration: 100,
fill: 'forwards',
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
}
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'))
getRef(pageId).closest('.page').classList.remove('hidden')
// getRef('main_card').style.overflowY = "hidden";
getRef(pageId).animate([
{
opacity: 0,
transform: 'translateY(1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
],
{
duration: 300,
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
}).onfinish = () => {
// getRef('main_card').style.overflowY = "";
}
appState.lastPage = pageId
}
if (params)
appState.params = params
appState.openedPages.add(pageId)
}
function alphaIncSort(arr, prop) {
return arr.sort((a, b) => a[prop].localeCompare(b[prop]))
}
function alphaDecSort(arr, prop) {
return arr.sort((a, b) => b[prop].localeCompare(a[prop]))
}
async function getFilteredTorrents(keys, searchKey, options = {}) {
let { limit, torrents } = options
if (!torrents) {
torrents = await getDataFromIDB()
}
const config = {
keys,
threshold: 0.2,
}
const fuseSearch = new Fuse(torrents, config)
return fuseSearch.search(searchKey, { limit }).map(elem => elem.item)
}
function createPageButtons(pages) {
const paginationFrag = document.createDocumentFragment()
for (let i = 0; i < pages; i++) {
const pageButton = create('sm-chip', {
textContent: (i + 1)
})
pageButton.setAttribute('value', i)
if (i === 0) {
pageButton.setAttribute('selected', '')
}
paginationFrag.append(pageButton)
}
return paginationFrag
}
async function showCategoryTorrents(category) {
const result = await getFilteredTorrents(['type'], category)
getRef('browser_category_torrents').innerHTML = ``
getRef('page_selector').innerHTML = ''
if (result.length) {
getRef('browser_category_torrents').append(renderTorrents(result.reverse().slice(0, 20)))
const pages = Math.round(result.length / 20)
getRef('page_selector').append(createPageButtons(pages))
}
else {
getRef('browser_category_torrents').innerHTML = `<h4 class="justify-center text-center">No ${category} torrents 😔</h4>`
}
}
async function renderSearchSuggestions(searchKey, advance = false) {
const result = await getFilteredTorrents(['name', 'filename'], searchKey, { limit: 6 })
if (advance) {
getRef('advance_search_suggestions').innerHTML = ``
}
else {
getRef('search_suggestions').innerHTML = ``
}
if (result.length) {
const suggestionsFrag = document.createDocumentFragment()
result.forEach(elem => {
const regEx = new RegExp(`(${searchKey})`, 'gi')
const suggestion = create('a', {
className: 'search-suggestion',
})
const pre = create('pre', {
innerHTML: elem.name.replace(regEx, `<span>$&</span>`),
})
suggestion.append(pre)
suggestion.href = `#/torrent/${elem.id}`
suggestionsFrag.append(suggestion)
})
if (advance) {
getRef('advance_search_suggestions').append(suggestionsFrag)
}
else {
getRef('search_suggestions').append(suggestionsFrag)
}
}
}
function handleEnter(e) {
if (e.target.value.trim() !== '') {
switch (e.key) {
case 'Enter':
const searchKey = e.target.value.trim()
renderSearchResult(searchKey)
location.hash = `#/search/${filteredSearch.query}`;
e.target.value = ''
break;
case 'ArrowDown':
e.preventDefault()
e.target.nextElementSibling.firstElementChild.focus()
break;
}
}
}
function handleKeyboardNav(e) {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
if (e.target.previousElementSibling)
e.target.previousElementSibling.focus()
else
e.target.parentNode.previousElementSibling.focusIn()
break;
case 'ArrowDown':
e.preventDefault()
if (e.target.nextElementSibling)
e.target.nextElementSibling.focus()
break;
}
}
// Event listeners
window.addEventListener('load', e => {
routeTo('loading_page')
getTorrents().then(allTorrents => {
routeTo(window.location.hash)
})
.catch(err => {
console.log(err)
})
})
window.addEventListener('hashchange', e => {
routeTo(window.location.hash)
})
let currentTorrent
document.addEventListener('click', async e => {
if (e.target.closest('.torrent-card__download-button')) {
const _target = e.target.closest('.torrent-card__download-button')
const card = _target.closest('.torrent-card')
const cardId = parseInt(card.id)
const { id, name, description, tags, type = 'misc', size, uploader, startTx, filename, chunks } = await getDataFromIDB(cardId)
downloadTorrent(filename, startTx, chunks, cardId)
}
})
getRef('search_torrent').addEventListener('keydown', handleEnter)
getRef('advance_torrent_search').addEventListener('keydown', handleEnter)
getRef('search_suggestions').addEventListener('keydown', handleKeyboardNav)
getRef('advance_search_suggestions').addEventListener('keydown', handleKeyboardNav)
getRef('search_torrent').addEventListener('input', debounce((e) => {
const searchKey = e.target.value.trim()
renderSearchSuggestions(searchKey)
}, 100))
getRef('advance_torrent_search').addEventListener('input', debounce((e) => {
const searchKey = getRef('advance_torrent_search').value.trim()
renderSearchSuggestions(searchKey, true)
}, 100))
getRef('browse_category_selector').addEventListener('change', e => {
showCategoryTorrents(e.detail.value)
})
getRef('page_selector').addEventListener('change', async e => {
const category = getRef('browse_category_selector').value
const result = await getFilteredTorrents(['type'], category)
const startIndex = parseInt(e.detail.value) * 20
const endIndex = ((parseInt(e.detail.value) * 20) + 30) < result.length ? (parseInt(e.detail.value) * 20) + 20 : result.length
setTimeout(() => {
getRef('browser_category_torrents').scrollIntoView({ behavior: 'smooth', block: 'start' })
getRef('browser_category_torrents').innerHTML = ``
getRef('browser_category_torrents').append(renderTorrents(result.reverse().slice(startIndex, endIndex)))
}, 200);
})
getRef('search_page_selector').addEventListener('change', async e => {
const result = await getFilteredTorrents(['name', 'filename', 'tags'], lastQuery)
const startIndex = parseInt(e.detail.value) * 20
const endIndex = ((parseInt(e.detail.value) * 20) + 30) < result.length ? (parseInt(e.detail.value) * 20) + 20 : result.length
setTimeout(() => {
getRef('search_result').scrollIntoView({ behavior: 'smooth', block: 'start' })
getRef('search_result').innerHTML = ``
getRef('search_result').append(renderTorrents(result.slice(startIndex, endIndex)))
}, 200);
})
function addFilter() {
const selectedCategory = getRef('category_selector').querySelector('input:checked')
if (selectedCategory) {
filteredSearch.category = selectedCategory.value
}
filteredSearch['tags'].clear()
getRef('tags_selector').querySelectorAll('input:checked').forEach(tag => {
filteredSearch['tags'].add(tag.value)
})
if (filteredSearch.category || filteredSearch.tags.size) {
filteredSearch.isActive = true
getRef('filters_bar').classList.remove('hidden')
}
else {
getRef('filters_bar').classList.add('hidden')
}
renderSelectedFilters()
renderSearchResult('', true);
getRef('filter_popup').hide()
}
function resetFilter() {
filteredSearch.isActive = false
filteredSearch.category = undefined
getRef('filter_form').reset()
const selectedCategory = getRef('category_selector').querySelector('input:checked')
if (selectedCategory) {
selectedCategory.checked = false
}
filteredSearch['tags'].clear()
getRef('tags_selector').querySelectorAll('input:checked').forEach(tag => {
tag.checked = false
})
getRef('selected_filters_container').innerHTML = ''
getRef('filters_bar').classList.add('hidden')
renderSearchResult('', true);
}
function renderSelectedFilters() {
getRef('selected_filters_container').innerHTML = ''
const frag = document.createDocumentFragment()
if (filteredSearch.category) {
frag.append(render.selectedFilter({ content: filteredSearch.category, type: 'category' }))
}
filteredSearch.tags.forEach(tag => {
frag.append(render.selectedFilter({ content: tag, type: 'tag' }))
})
getRef('selected_filters_container').append(frag)
}
getRef('selected_filters_container').addEventListener('click', e => {
if (e.target.closest('.selected-filter')) {
const target = e.target.closest('.selected-filter')
if (target.dataset.type === 'category') {
filteredSearch.category = undefined
getRef('category_selector').querySelector('input:checked').checked = false
}
else {
filteredSearch.tags.delete(target.dataset.value)
getRef('tags_selector').querySelector(`input[value="${target.dataset.value}"]`).checked = false
}
target.remove()
if (!filteredSearch.category && !filteredSearch.tags.size) {
filteredSearch.isActive = false
getRef('filters_bar').classList.add('hidden')
}
}
renderSearchResult('', true);
})
</script>
</body>
</html>