flotorrent/index.html
2023-07-27 22:46:17 +05:30

1347 lines
70 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">
<script src="scripts/components.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6" defer></script>
<script src="scripts/lib.js" defer></script>
<script src="scripts/btcOperator.js" defer></script>
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
</head>
<body class="hidden">
<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="closePopup()">
<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">Filter</button>
</div>
</form>
</section>
</sm-popup>
<header id="main_header">
<a href="#/home" class="flex align-center header__company-name">
<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>
FLO Torrent
</a>
<section id="search_wrapper">
<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 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>
<button id="filter_button" class="button icon-only" slot="right"
onclick="getRef('filter_popup').show()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
fill="#000000">
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z" />
</svg>
</button>
</sm-input>
<section id="search_suggestions" class="search-suggestions-container"></section>
</section>
<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 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>
<p id="torrent_uploader" class="breakable"></p>
<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">
<p id="result_for"></p>
<section id="filters_bar" class="hidden">
<div id="selected_filters_container"></div>
<button class="button" onclick="resetFilter()">
<svg class="icon margin-right-0-3" 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-chip value="tv series">Series</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 does it work? </h1>
<p>FLO Torrent stores torrent files directly on the FLO blockchain making it completely decentralized. But
this approach comes 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>
Only 1040 free characters can be stored in a FLO blockchain transaction, but torrents are
typically 20,000 characters each.
</p>
<p>
Therefore, we had 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>
<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 trustedIDs = [];
</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 { html, render: renderElem, svg } = uhtml;
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', () => {
location.reload()
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.body.classList.remove('hidden')
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(4)",
opacity: 0,
},
],
{
duration: uiGlobals.prefersReducedMotion ? 0 : 600,
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);
};
}
let zIndex = 50
// function required for popups or modals to appear
function openPopup(popupId, pinned) {
zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
return getRef(popupId).show({ pinned })
}
// hides the popup or modal
function closePopup(options = {}) {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide(options)
}
document.addEventListener('popupopened', async e => {
document.querySelector('main').inert = true
switch (e.target.id) {
case 'filter_popup':
renderElem(getRef('category_selector'), html`${renderOptions(categories, { groupName: 'category-selector' })}`)
renderElem(getRef('tags_selector'), html`${renderOptions(allTags)}`)
break;
}
})
document.addEventListener('popupclosed', e => {
switch (e.target.id) {
case 'filter_popup':
renderElem(getRef('category_selector'), html``)
renderElem(getRef('tags_selector'), html``)
break;
}
if (popupStack.items.length === 0) {
document.querySelector('main').inert = false
}
zIndex--;
})
</script>
<script>
const render = {
torrentCard(obj) {
let { id, name, description, tags, type = 'misc', size, uploader, startTx, filename, chunks } = obj
// convert svg string to dom element
const icon = new DOMParser().parseFromString(getIcon(type), 'image/svg+xml').querySelector('svg')
// remove extension from filename
filename = filename.split('.').slice(0, -1).join('.')
const btcAddress = btcOperator.convert.legacy2bech(uploader)
return html`
<li id=${id} class="torrent-card">
<a class="torrent-info grid" href=${`#/torrent/${id}`}>
<div class="torrent-card__icon torrent-type-icon">${icon}</div>
<h3 class="torrent-card__title breakable">${filename}</h3>
<p class="torrent-card__tags">${tags.replace(/[\/,]/g, ' • ')}</p>
<div class="torrent-card__uploader grid breakable">
Uploaded by
<span>FLO address: ${uploader}</span>
<span>Equivalent BTC address: ${btcAddress}</span>
</div>
</a>
<button class="torrent-card__download-button">
Get Torrent
</button>
</li>
`;
},
filterOption(obj) {
const { content, groupName, value } = obj
return html`
<label class="filter-option interact">
<input type=${groupName ? 'radio' : 'checkbox'} name=${groupName || ''} value=${value} ?checked=${filteredSearch.category === value || filteredSearch.tags.has(value)}>
<div class="option-text">${content}</div>
</label>
`;
},
selectedFilter(obj) {
const { content, type } = obj
return html`
<button class="selected-filter interact" .dataset=${{ type, value: content }} title="Remove filter">
<div class="option-text">${content}</div>
<svg class="icon 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>
</button>
`;
}
}
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) {
return html`${torrents.map(torrent => render.torrentCard(torrent))}`
}
let filteredSearch = {
active: false,
query: '',
category: '',
tags: new Set()
}
async function renderSearchResult(searchKey = '', shouldFilter = false) {
searchKey = decodeURI(searchKey)
if (shouldFilter) {
searchKey = getRef('search_torrent').value.trim() !== '' ? getRef('search_torrent').value.trim() : filteredSearch['query']
}
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 && searchKey.trim() !== '') {
renderElem(getRef('result_for'), html`Showing results for <strong>${searchKey}</strong>`)
} else if (!result.length) {
renderElem(getRef('result_for'), html`No results for <strong>${searchKey}</strong>`)
}
if (result.length > 20) {
const pages = Math.round(result.length / 20)
renderElem(getRef('search_page_selector'), createPageButtons(pages))
renderElem(getRef('search_result'), renderTorrents(result.slice(0, 20)))
} else {
renderElem(getRef('search_page_selector'), html``)
renderElem(getRef('search_result'), renderTorrents(result))
}
}
const categories = ['movie', 'tv series', 'video', 'music', 'software', 'game', 'image', 'audio', 'misc']
const allTags = ['action', 'adventure', 'anime', 'comedy', 'crime', 'drama', 'family', 'fantasy', 'horror', 'korean', 'mystery', 'romance', 'sci-fi', 'sport', 'thriller', 'western']
function renderOptions(list, options = {}) {
return list.map(item => {
const optionObj = {
content: item,
groupName: options.groupName,
value: item
}
return render.filterOption(optionObj)
})
}
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)
renderElem(getRef('torrent_container'), renderTorrents(allTorrents))
break;
case 'search':
renderSearchResult(subPageId)
break;
case 'torrent':
currentTorrent = await getDataFromIDB(parseInt(subPageId))
const { id, name, description, tags, type = 'misc', size, uploader, startTx, filename, chunks } = currentTorrent
const btcAddress = btcOperator.convert.legacy2bech(uploader)
getRef('torrent_type_icon').innerHTML = getIcon(type)
getRef('torrent_name').textContent = name
getRef('torrent_description').textContent = description
getRef('torrent_tags').innerHTML = tags.replace(/[\/,]/g, ' • ')
getRef('torrent_uploader').innerHTML = `
Uploaded by
<span>FLO address: ${uploader}</span>
<span>Equivalent BTC address: ${btcAddress}</span>
`;
getRef('torrent_index_tx').href = `https://blockbook.ranchimall.net/tx/${startTx}`
getRef('search_torrent').value = ''
renderElem(getRef('search_suggestions'), html``)
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)
}
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)
if (searchKey.trim() !== '')
return fuseSearch.search(searchKey, { limit }).map(elem => elem.item)
else return torrents
}
function createPageButtons(pages) {
const pagination = []
for (let index = 0; index < pages; index++) {
pagination.push(html`<sm-chip value="${index}" ?selected=${index === 0}>${index + 1}</sm-chip>`)
}
return html`${pagination}`;
}
async function showCategoryTorrents(category) {
const result = await getFilteredTorrents(['type'], category)
if (result.length) {
renderElem(getRef('browser_category_torrents'), renderTorrents(result.reverse().slice(0, 20)))
const pages = Math.round(result.length / 20)
renderElem(getRef('page_selector'), createPageButtons(pages))
}
else {
renderElem(getRef('browser_category_torrents'), html`<h4 class="justify-center text-center">No ${category} torrents 😔</h4>`)
}
}
async function renderSearchSuggestions(searchKey, advance = false) {
if (searchKey.trim() === '') {
renderElem(getRef('search_suggestions'), html``)
return
}
const result = await getFilteredTorrents(['name', 'filename'], searchKey, { limit: 6 })
const suggestions = result.map(elem => {
const regEx = new RegExp(`(${searchKey})`, 'gi')
const pre = create('pre', { innerHTML: elem.name.replace(regEx, `<span>$&</span>`), })
return html`
<a class="search-suggestion" href=${`#/torrent/${elem.id}`}>
${pre}
</a>
`;
})
renderElem(getRef('search_suggestions'), html`${suggestions}`)
}
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 = ''
renderElem(getRef('search_suggestions'), html``)
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 => {
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('search_suggestions').addEventListener('keydown', handleKeyboardNav)
getRef('search_torrent').addEventListener('input', debounce((e) => {
const searchKey = e.target.value.trim()
renderSearchSuggestions(searchKey)
}, 100))
getRef('browse_category_selector').addEventListener('change', e => {
showCategoryTorrents(e.target.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.target.value) * 20
const endIndex = (parseInt(e.target.value) * 20) + 30
setTimeout(() => {
getRef('browser_category_torrents').scrollIntoView({ behavior: 'smooth', block: 'start' })
renderElem(getRef('browser_category_torrents'), renderTorrents(result.reverse().slice(startIndex, endIndex)))
}, 200);
})
getRef('search_page_selector').addEventListener('change', async e => {
const result = await getFilteredTorrents(['name', 'filename', 'tags'], filteredSearch['query'])
const startIndex = parseInt(e.target.value) * 20
const endIndex = (parseInt(e.target.value) * 20) + 30
setTimeout(() => {
getRef('search_result').scrollIntoView({ behavior: 'smooth', block: 'start' })
renderElem(getRef('search_result'), renderTorrents(result.slice(startIndex, endIndex)))
}, 100);
})
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 (appState.currentPage !== 'search')
location.hash = `#/search`;
renderSelectedFilters()
renderSearchResult('', true);
closePopup()
}
function resetFilter() {
filteredSearch.category = undefined
getRef('filter_form').reset()
filteredSearch['tags'].clear()
renderSelectedFilters()
renderSearchResult('', true);
closePopup()
}
function renderSelectedFilters() {
const appliedFilters = []
if (filteredSearch.category) {
appliedFilters.push(render.selectedFilter({ content: filteredSearch.category, type: 'category' }))
}
filteredSearch.tags.forEach(tag => {
appliedFilters.push(render.selectedFilter({ content: tag, type: 'tag' }))
})
renderElem(getRef('selected_filters_container'), html`${appliedFilters}`)
if (!filteredSearch.category && !filteredSearch.tags.size) {
filteredSearch.isActive = false
getRef('filters_bar').classList.add('hidden')
delete getRef('filter_button').dataset.active
} else {
filteredSearch.isActive = true
getRef('filters_bar').classList.remove('hidden')
getRef('filter_button').dataset.active = appliedFilters.length
}
}
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
} else {
filteredSearch.tags.delete(target.dataset.value)
}
renderSelectedFilters()
}
renderSearchResult('', true);
})
</script>
</body>
</html>