1347 lines
70 KiB
HTML
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> |