Merge branch 'development'

This commit is contained in:
Preston 2018-06-02 12:04:43 -04:00
commit 6539b1db2a
46 changed files with 4047 additions and 7047 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@ subscriptions\.db
*.db
electron-packager/win32-x64/FreeTube-win32-x64/
dist/

View File

@ -15,7 +15,6 @@ Please follow these guidlines before sending your pull request and making contri
* Comment your code when necessary. Follow the [JavaScript Documentation and Comments Standard](https://www.drupal.org/docs/develop/standards/javascript/javascript-api-documentation-and-comment-standards) for functions.
* Please test your code. Make sure new features work as well as core features such as watching videos or loading subscriptions.
* Please limit the amount of Node Modules that you introduce into the project. Only include them when absolutely necessary for your code to work (Ex: Using nedb for databases) or if a module provides similar functionality to what you are trying to achieve (Ex: Using autolinker to create links to outside URLs instead of writing the functionality myself).
* If using a new Node Module, please include the `require` statement in `layout.js` to keep them together.
* Please try to stay involved with the community and maintain your code. I am only one person and I work on FreeTube only in my spare time. I do not have time to work on everything and it would be nice if you can maintain your code when necessary.
# Setting up Your Environment
@ -32,6 +31,11 @@ Install Dependencies:
npm install
```
Install Dev Dependencies:
```
npm install --only=dev
```
Run the application:
```
npm start
@ -41,12 +45,12 @@ Make / Package application:
Windows (Requires Wine on Linux):
```
npm run make:win32
npm run make:win
```
Mac:
```
npm run make:darwin
npm run make:mac
```
Linux (Requires deb and rpm to be installed):
@ -54,4 +58,6 @@ Linux (Requires deb and rpm to be installed):
npm run make:linux
```
Other commands are available in the `package.json` file for making specific packages. Refer to `package.json` for instructions.
I will update this document when necessary. Anyone who has questions or suggestions on this document are welcome to create an issue or pull request.

Binary file not shown.

View File

@ -1 +0,0 @@

View File

@ -1,13 +1,6 @@
- [ ] I have read and agree to the [Contribution Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/master/CONTRIBUTING.md)
- [ ] I can maintain / support my code if issues arise.
**Does your change relate to a current issue? Please list it here if applicable.**
**Notes and Comments about your pull request**
**Please list out the changes you've made**
**Does your change include any new Node Modules? If yes, what modules and what are they licensed under?**
**Other Comments**

View File

@ -1,9 +1,9 @@
<p align="center">
<img src="https://raw.githubusercontent.com/jbeguna04/FreeTube/master/LogoDesigns/logotype.png" width=500 align="center">
<img src="https://freetubeapp.github.io/images/logoColor.png" width=500 align="center">
</p>
FreeTube is an open source desktop YouTube player built with privacy in mind.
Use YouTube without advertisments and Google tracking you using cookies and JavaScript.
Use YouTube without advertisments and prevent Google tracking from you with their cookies and JavaScript.
Available for Windows, Mac & Linux thanks to Electron.
Please note that FreeTube is currently in Beta. While it should work well for
@ -20,7 +20,8 @@ hard coded API keys. Videos are resolved using
and played using the stock HTML5 video
player. While YouTube can still see your API and video requests it can no
longer track you using cookies or JavaScript. Your subscriptions, history, and
saved videos are stored locally on your computer and never sent out.
saved videos are stored locally on your computer and never sent out. Using a VPN or Tor is recommended
to hide your IP while using FreeTube.
## Screenshots
<img src="https://freetubeapp.github.io/images/FreeTube1.png" width=200> <img src="https://freetubeapp.github.io/images/FreeTube2.png" width=200> <img src="https://freetubeapp.github.io/images/FreeTube3.png" width=200> <img src="https://freetubeapp.github.io/images/FreeTube5.png" width=200>
@ -28,6 +29,7 @@ saved videos are stored locally on your computer and never sent out.
## Features
* Watch videos without ads
* Use YouTube without Google tracking you using cookies and JavaScript
* Make API calls through the Tor network
* Subscribe to channels without an account
* Local subscriptions, history, and saved videos
* Export & import subscriptions
@ -45,6 +47,8 @@ follow the [Contribution
Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/master/CONTRIBUTING.md)
before sending your pull request.
Thank you very much to the [People and Projects](https://github.com/FreeTubeApp/FreeTube/wiki/Credits) that make FreeTube possible!
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

95
locales/en-US.json Normal file
View File

@ -0,0 +1,95 @@
{
"File": "File",
"Quit": "Quit",
"Edit": "Edit",
"Undo": "Undo",
"Redo": "Redo",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Delete": "Delete",
"Select all": "Select all",
"View": "View",
"Reload": "Reload",
"Force Reload": "Force Reload",
"Toggle Developer Tools": "Toggle Developer Tools",
"Actual size": "Actual size",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Toggle fullscreen": "Toggle fullscreen",
"Window": "Window",
"Minimize": "Minimize",
"Close": "Close",
"FreeTube": "FreeTube",
"Subscriptions": "Subscriptions",
"Featured": "Featured",
"Most Popular": "Most Popular",
"Saved": "Saved",
"Playlists": "Playlists",
"History": "History",
"Settings": "Settings",
"About": "About",
"Search / Go to URL": "Search / Go to URL",
"Search Results": "Search Results",
"Subscriber": "Subscriber",
"Subscriber": "Subscribers",
"Video": "Video",
"Videos": "Videos",
"View Full Playlist": "View Full Playlist",
"Live Now": "Live Now",
"Fetch more results": "Fetch more results",
"Fetching results. Please wait": "Fetching results. Please wait",
"Latest Subscriptions": "Latest Subscriptions",
"Save Video": "Save Video",
"Remove Saved Video": "Remove Saved Video",
"Open in YouTube": "Open in YouTube",
"Copy YouTube Link": "Copy YouTube Link",
"Open in HookTube": "Open in HookTube",
"Copy HookTube Link": "Copy HookTube Link",
"URL has been copied to the clipboard": "URL has been copied to the clipboard",
"Found valid URL for 480p, but returned a 404. Video type might be available in the future.": "Found valid URL for 480p, but returned a 404. Video type might be available in the future.",
"Save": "Save",
"Mini Player": "Mini Player",
"View": "View",
"Views": "Views",
"Subscribe": "Subscribe",
"Unsubscribe": "Unsubscribe",
"Published on": "Published on",
"Jan": "Jan",
"Feb": "Feb",
"Mar": "Mar",
"Apr": "Apr",
"May": "May",
"Jun": "Jun",
"Jul": "Jul",
"Aug": "Aug",
"Sep": "Sep",
"Oct": "Oct",
"Nov": "Nov",
"Dec": "Dec",
"Show Comments": "Show Comments",
"Max of 100": "Max of 100",
"Recommendations": "Recommendations",
"Latest Subscriptions": "Latest Subscriptions",
"Getting Subscriptions. Please wait...": "Getting Subscriptions. Please wait...",
"Your Subscription list is currently empty. Start adding subscriptions to see them here.": "Your Subscription list is currently empty. Start adding subscriptions to see them here.",
"Saved Videos": "Saved Videos",
"Watch History": "Watch History",
"API Key": "API Key",
"Set API Key: Leave blank to use default": "Set API Key: Leave blank to use default",
"Use Dark Theme": "Use Dark Theme",
"Import Subscriptions": "Import Subscriptions",
"Export Subscriptions": "Export Subscriptions",
"Clear History": "Clear History",
"Are you sure you want to delete your history?": "Are you sure you want to delete your history?",
"Clear Saved Videos": "Clear Saved Videos",
"Are you sure you want to remove all saved videos?": "Are you sure you want to remove all saved videos?",
"Clear Subscriptions": "Clear Subscriptions",
"Are you sure you want to remove all subscriptions?": "Are you sure you want to remove all subscriptions?",
"Save Settings": "Save Settings",
"Yes": "Yes",
"No": "No",
"Beta": "Beta",
"This software is FOSS and released under the GNU Public License v3+.": "This software is FOSS and released under the GNU Public License v3+.",
"Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome.": "Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome."
}

95
locales/nl.json Normal file
View File

@ -0,0 +1,95 @@
{
"File": "Bestand",
"Quit": "Afluiten",
"Edit": "Bewerken",
"Undo": "Ongedaan maken",
"Redo": "Opnieuw",
"Cut": "Knippen",
"Copy": "Kopiëren",
"Paste": "Plakken",
"Delete": "Verwijderen",
"Select all": "Alles selecteren",
"View": "Weergave",
"Reload": "Herladen",
"Force Reload": "Herladen forceren",
"Toggle Developer Tools": "Hulpprogramma's voor ontwikkelaars",
"Actual size": "Daadwerkelijke grootte",
"Zoom in": "Zoom in",
"Zoom out": "Zoom uit",
"Toggle fullscreen": "Volledig scherm",
"Window": "Venster",
"Minimize": "Minimaliseren",
"Close": "Sluiten",
"FreeTube": "FreeTube",
"Subscriptions": "Abonnementen",
"Featured": "Uitgelicht",
"Most Popular": "Populair",
"Saved": "Opgeslagen",
"Playlists": "Afspeellijsten",
"History": "Geschiedenis",
"Settings": "Instellingen",
"About": "Over",
"Search / Go to URL": "Zoeken / Ga naar URL",
"Search Results": "Zoekresultaten",
"Subscriber": "Abonnee",
"Subscriber": "Abonnees",
"Video": "Video",
"Videos": "Videos",
"View Full Playlist": "Volledige afspeellijst weergeven",
"Live Now": "Nu Live",
"Fetch more results": "Meer resultaten ophalen",
"Fetching results. Please wait": "Resultaten ophalen. Een ogenblik geduld",
"Latest Subscriptions": "Laatste abonnementen",
"Save Video": "Video opslaan",
"Remove Saved Video": "Opgeslagen video verwijderen",
"Open in YouTube": "Open in YouTube",
"Copy YouTube Link": "Kopieer YouTube Link",
"Open in HookTube": "Open in HookTube",
"Copy HookTube Link": "Kopieer HookTube Link",
"URL has been copied to the clipboard": "URL is naar het klembord gekopieerd",
"Found valid URL for 480p, but returned a 404. Video type might be available in the future.": "Geldige URL voor 480p gevonden, echter een 404 foutmelding ontvangen. Video is wellicht in de toekomst beschikbaar.",
"Save": "Opslaan",
"Mini Player": "Mini Speler",
"View": "Weergave",
"Views": "Weergaven",
"Subscribe": "Abboneren",
"Unsubscribe": "Afmelden",
"Published on": "Gepubliceerd op",
"Jan": "Jan",
"Feb": "Feb",
"Mar": "Maa",
"Apr": "Apr",
"May": "Mei",
"Jun": "Jun",
"Jul": "Jul",
"Aug": "Aug",
"Sep": "Sep",
"Oct": "Okt",
"Nov": "Nov",
"Dec": "Dec",
"Show Comments": "Commentaren weergeven",
"Max of 100": "Maximaal 100",
"Recommendations": "Aanbevelingen",
"Latest Subscriptions": "Laatste abonnementen",
"Getting Subscriptions. Please wait...": "Abonnementen worden opgehaald. Een ogenblik geduld...",
"Your Subscription list is currently empty. Start adding subscriptions to see them here.": "Deze lijst is momenteel leeg. Voeg abonnementen toe om ze hier terug te zien.",
"Saved Videos": "Opgeslagen Videos",
"Watch History": "Geschiedenis",
"API Key": "API Sleutel",
"Set API Key: Leave blank to use default": "API sleutel instellen: Laat leeg om standaard te gebruiken",
"Use Dark Theme": "Donker thema gebruiken",
"Import Subscriptions": "Importeer Abonnementen",
"Export Subscriptions": "Exporteer Abonnementen",
"Clear History": "Geschiedenis wissen",
"Are you sure you want to delete your history?": "Weet u zeker dat u de geschiedenis wilt wissen?",
"Clear Saved Videos": "Opgeslagen videos verwijderen",
"Are you sure you want to remove all saved videos?": "Weet u zeker dat u alle opgeslagen videos wilt verwijderen?",
"Clear Subscriptions": "Abonnementen verwijderen",
"Are you sure you want to remove all subscriptions?": "Weet u zeker dat u alle abonnementen wilt verwijderen?",
"Save Settings": "Instellingen opslaan",
"Yes": "Ja",
"No": "Nee",
"Beta": "Beta",
"This software is FOSS and released under the GNU Public License v3+.": "Deze software is FOSS en gepubliceerd onder de GNU Public License v3+.",
"Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome.": "Een bug gevonden? Graag iets toegevoegd zien? Wilt u helpen bij de ontwikkeling? Check de GitHub pagina. Pull requests zijn welkom."
}

5673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "FreeTube",
"productName": "FreeTube",
"version": "0.2.1",
"version": "0.3.0",
"description": "An Open Source YouTube app for privacy.",
"main": "src/js/init.js",
"scripts": {
@ -9,9 +9,16 @@
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"make:darwin": "electron-forge make --platform=darwin",
"make:mac": "electron-forge make --platform=darwin",
"make:linux": "electron-forge make --platform=linux",
"make:win32": "electron-forge make --platform=win32"
"make:linux:zip": "electron-forge make --platform=linux --targets=zip",
"make:deb": "electron-forge make --platform=linux --targets=deb",
"make:rpm": "electron-forge make --platform=linux --targets=rpm",
"make:snap": "electron-forge package && electron-installer-snap --src=out/FreeTube-linux-x64",
"make:flatpak": "electron-installer-flatpak --src out/FreeTube-linux-x64/ --dest out/make --arch x64",
"make:appimage": "electron-forge make --platform=linux --targets=electron-forge-maker-appimage",
"make:win": "electron-forge make --platform=win32",
"make:win:zip": "electron-forge make --platform=win32 --targets=zip"
},
"keywords": [],
"author": {
@ -19,32 +26,44 @@
"email": "FreeTubeApp@protonmail.com",
"url": "https://github.com/FreeTubeApp/FreeTube"
},
"license": "GPL-3.0",
"license": "GPL-3.0-or-later",
"config": {
"forge": {
"make_targets": {
"win32": [
"squirrel"
"squirrel",
"zip"
],
"darwin": [
"zip"
],
"linux": [
"deb",
"rpm"
"rpm",
"electron-forge-maker-appimage",
"zip"
]
},
"protocols": [
{
"name": "freetube",
"role": "Viewer",
"schemes": [
"freetube"
]
}
],
"electronPackagerConfig": {
"packageManager": "yarn",
"icon": "./src/icons/icon.icns"
"icon": "./src/icons/iconColor.icns"
},
"electronWinstallerConfig": {
"name": "freetube",
"iconUrl": "https://raw.githubusercontent.com/FreeTubeApp/FreeTubeApp.github.io/master/images/icon.ico",
"setupIcon": "./src/icons/icon.ico"
"iconUrl": "https://raw.githubusercontent.com/FreeTubeApp/FreeTubeApp.github.io/master/images/iconColor.ico",
"setupIcon": "./src/icons/iconColor.ico"
},
"electronInstallerDebian": {
"icon": "src/icons/icon.svg"
"icon": "src/icons/iconColor.png"
},
"repository": {
"type": "git",
@ -53,18 +72,24 @@
}
},
"devDependencies": {
"electron-forge": "^5.1.1",
"electron-prebuilt-compile": "1.8.2",
"electron-forge": "^5.2.2",
"electron-forge-maker-appimage": "^20.14.4",
"electron-installer-flatpak": "^0.8.0",
"electron-installer-snap": "^2.0.1",
"electron-prebuilt-compile": "2.0.2",
"electron-winstaller": "^2.6.4"
},
"dependencies": {
"autolinker": "^1.6.2",
"dateformat": "^3.0.3",
"electron-compile": "^6.4.2",
"electron-compile": "6.4.2",
"electron-squirrel-startup": "^1.0.0",
"github-version-checker": "^2.0.1",
"jquery": "^3.3.1",
"mustache": "^2.3.0",
"nedb": "^1.8.0",
"opml-to-json": "0.0.3"
"opml-to-json": "0.0.3",
"tor-request": "^2.1.2",
"ytdl-core": "^0.20.4"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="222mm"
height="222mm"
viewBox="0 0 222 222"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="icon.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-100"
inkscape:cy="445.71429"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:pagecheckerboard="false"
inkscape:window-width="1720"
inkscape:window-height="1336"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-75)">
<circle
id="path3711"
cx="110.74702"
cy="186.25298"
r="102.43154"
style="fill:#eeeeee;fill-opacity:1;stroke:#f44336;stroke-width:15.02900028;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-linecap:round;stroke-linejoin:bevel" />
<path
sodipodi:type="star"
style="fill:#f44336;fill-opacity:1;stroke-width:0.26458332"
id="path4520"
sodipodi:sides="3"
sodipodi:cx="108.25239"
sodipodi:cy="186.70654"
sodipodi:r1="65.785233"
sodipodi:r2="32.892616"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 174.03762,186.70654 -49.33893,28.48585 -49.338918,28.48584 0,-56.97169 0,-56.97168 49.338928,28.48584 z"
inkscape:transform-center-x="-16.446307"
inkscape:transform-center-y="-1.0179227e-06" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

BIN
src/icons/iconBlack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
src/icons/iconColor.icns Normal file

Binary file not shown.

BIN
src/icons/iconColor.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
src/icons/iconColor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
src/icons/iconWhite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
src/icons/logoBlack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/icons/logoWhite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/icons/textBlack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
src/icons/textColor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
src/icons/textWhite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -13,6 +13,7 @@
<link rel="stylesheet" href="style/fontawesome-all.min.css">
<script src="js/youtubeApi.js"></script>
<script src="js/settings.js"></script>
<script src="js/updates.js"></script>
<script src="js/layout.js"></script>
<script src="js/videos.js"></script>
<script src="js/player.js"></script>
@ -41,26 +42,29 @@
<i onclick='hideToast()' class="closeToast fas fa-times"></i>
</div>
<div class="topNav">
<span class="logo"><i onclick='toggleSideNavigation()' class="menuButton fas fa-bars"></i>&nbsp;&nbsp;FreeTube <i class="far fa-play-circle"></i></span>
<i onclick='toggleSideNavigation()' class="fas fa-bars" id='menuButton'></i>
<div class="searchBar">
<input id='jumpToInput' class='jumpToInput' type='text' placeholder="Jump to Video Link / Id" /><i onclick='parseVideoLink()' class="fas fa-search searchButton" style='margin-right: 50px; cursor: pointer;'></i>
<input id='search' class="search" type="text" placeholder="Search">
<i onclick='search()' class="fas fa-search searchButton" style='margin-right: -10px; cursor: pointer'></i>
<input id='search' class="search" type="text" placeholder="Search / Go to URL">
<i onclick='parseSearchText()' class="fas fa-search searchButton" style='margin-right: -10px; cursor: pointer'></i>
</div>
<img src='icons/iconBlack.png' id='menuIcon'/>
&nbsp;
<img src='icons/textBlack.png' id='menuText'/>
</div>
<div id='sideNavDisabled'></div>
<div id="sideNav">
<div class="sideNavContainer">
<ul>
<li onclick='loadSubscriptions()'><i class="fas fa-users"></i>&nbsp;&nbsp;Subscriptions</li>
<li onclick='showMostPopular()'><i class="fas fa-fire"></i>&nbsp;&nbsp;Most Popular</li>
<li onclick='loadSubscriptions()'><i class="fas fa-rss"></i>&nbsp;&nbsp;Subscriptions</li>
<li onclick='showMostPopular()'><i class="fas fa-users"></i>&nbsp;&nbsp;Most Popular</li>
<li onclick='showSavedVideos()'><i class="fas fa-star"></i>&nbsp;&nbsp;Saved</li>
<li onclick='showHistory()'><i class="fas fa-history"></i>&nbsp;&nbsp;History</li>
</ul>
<hr />
<ul>
<li onclick='showSettings()'><i class="fas fa-cog"></i>&nbsp;&nbsp;Settings</li>
<li onclick='showAbout()'><i class="far fa-question-circle"></i>&nbsp;&nbsp;About</li>
<li onclick='showSettings()'><i class="fas fa-sliders-h"></i>&nbsp;&nbsp;Settings</li>
<li onclick='showAbout()'><i class="fas fa-info-circle"></i>&nbsp;&nbsp;About</li>
<a href="https://github.com/FreeTubeApp/FreeTube/wiki" style='text-decoration: none; color: inherit;'><li><i class="fas fa-question-circle"></i>&nbsp;&nbsp;Help</li></a>
</ul>
<hr />
<ul id='subscriptions'>

View File

@ -1,4 +1,4 @@
/*
/*
This file is part of FreeTube.
FreeTube is free software: you can redistribute it and/or modify
@ -115,8 +115,7 @@ let videoShortcutHandler = function(event) {
}
break;
case 67:
// F Key
event.preventDefault();
// C Key
let subtitleMode = $('.videoPlayer').get(0).textTracks[0].mode;
if (subtitleMode === 'hidden'){
$('.videoPlayer').get(0).textTracks[0].mode = 'showing'
@ -205,7 +204,7 @@ let fullscreenVideo = function(event){
}
else{
$('.videoPlayer').get(0).webkitRequestFullscreen();
}
}
}
/**

View File

@ -20,12 +20,26 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
/*
* File used to initializing the application
*/
const {app, BrowserWindow, dialog} = require('electron');
const {app, BrowserWindow, dialog, protocol} = require('electron');
const path = require('path');
const url = require('url');
let win;
if(require('electron-squirrel-startup')) app.quit();
protocol.registerStandardSchemes(['freetube']);
app.setAsDefaultProtocolClient('freetube');
const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (win) {
if (win.isMinimized()) win.restore()
win.focus()
win.webContents.send('ping', commandLine)
}
});
if(require('electron-squirrel-startup') || isSecondInstance) app.quit();
/**
* Initialize the Electron application
@ -34,6 +48,7 @@ if(require('electron-squirrel-startup')) app.quit();
*/
let init = function() {
const Menu = require('electron').Menu;
win = new BrowserWindow({width: 1200, height: 800, autoHideMenuBar: true});
win.loadURL(url.format({
@ -43,7 +58,7 @@ let init = function() {
}));
if (process.env = 'development') {
//win.webContents.openDevTools();
//win.webContents.openDevTools();ff
}
win.on('closed', () => {
@ -51,15 +66,18 @@ let init = function() {
});
const template = [
{
label: 'File',
submenu: [
{role: 'quit'}
]
},
{
label: 'Edit',
submenu: [
{role: 'undo'},
{role: 'redo'},
{type: 'separator'},
{role: 'cut'},
{role: 'copy'},
{role: 'paste'},
{role: 'copy', accelerator: "CmdOrCtrl+C", selector: "copy:" },
{role: 'paste', accelerator: "CmdOrCtrl+V", selector: "paste:" },
{role: 'pasteandmatchstyle'},
{role: 'delete'},
{role: 'selectall'}
@ -96,6 +114,8 @@ let init = function() {
* Quit the application
*/
let allWindowsClosed = function() {
win.webContents.session.clearStorageData([], (data) => {});
win.webContents.session.clearCache((data) => {});
app.quit();
};

View File

@ -18,9 +18,9 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
/*
* File for main layout manipulation and general variable configuration.
* There are some functions from other files that will probably need to be moved to here.
*/
* File for main layout manipulation and general variable configuration.
* There are some functions from other files that will probably need to be moved to here.
*/
// Add general variables. Please put all require statements here.
const Datastore = require('nedb'); // database logic
@ -28,28 +28,34 @@ window.$ = window.jQuery = require('jquery');
const mustache = require('mustache'); // templating
const dateFormat = require('dateformat'); // formatting dates
//const RxPlayer = require('rx-player'); // formatting dates
// Used for finding links within text and making them clickable. Used mostly for video descriptions.
const autolinker = require('autolinker');
const electron = require('electron');
const protocol = electron.remote.protocol;
// Used for getting the user's subscriptions. Can probably remove this when that function
// is rewritten.
//const asyncLoop = require('node-async-loop');
//const youtubedl = require('youtube-dl');
const ytdl = require('ytdl-core');
const shell = electron.shell; // Used to open external links into the user's native browser.
const localDataStorage = electron.remote.app.getPath('userData'); // Grabs the userdata directory based on the user's OS
const clipboard = electron.clipboard;
const getOpml = require('opml-to-json'); // Gets the file type for imported files.
const fs = require('fs'); // Used to read files. Specifically in the settings page.
const tor = require('tor-request');
let currentTheme = '';
let apiKey;
let dialog = require('electron').remote.dialog; // Used for opening file browser to export / import subscriptions.
let useTor = false;
let dialog = electron.remote.dialog; // Used for opening file browser to export / import subscriptions.
let toastTimeout; // Timeout for toast notifications.
let mouseTimeout; // Timeout for hiding the mouse cursor on video playback
require.extensions['.html'] = function (module, filename) {
module.exports = fs.readFileSync(filename, 'utf8');
require.extensions['.html'] = function(module, filename) {
module.exports = fs.readFileSync(filename, 'utf8');
};
const subDb = new Datastore({
@ -68,7 +74,7 @@ const savedVidsDb = new Datastore({
});
const settingsDb = new Datastore({
filename: localDataStorage + 'settings.db',
filename: localDataStorage + '/settings.db',
autoload: true
});
@ -76,15 +82,21 @@ const settingsDb = new Datastore({
// none are found.
checkDefaultSettings();
// Ppen links externally by default
$(document).on('click', 'a[href^="http"]', (event) =>{
require('electron').ipcRenderer.on('ping', function(event, message) {
let url = message[1].replace('freetube://', '');
parseSearchText(url);
console.log(message); // Prints "whoooooooh!"
});
// Open links externally by default
$(document).on('click', 'a[href^="http"]', (event) => {
let el = event.currentTarget;
event.preventDefault();
shell.openExternal(el.href);
});
// Open links externally on middle click.
$(document).on('auxclick', 'a[href^="http"]', (event) =>{
$(document).on('auxclick', 'a[href^="http"]', (event) => {
let el = event.currentTarget;
event.preventDefault();
shell.openExternal(el.href);
@ -101,14 +113,7 @@ $(document).ready(() => {
// Allow user to use the 'enter' key to search for a video.
searchBar.onkeypress = (e) => {
if (e.keyCode === 13) {
search();
}
};
// Allow user to use the 'enter' key to open a video link.
jumpToInput.onkeypress = (e) => {
if (e.keyCode === 13) {
parseVideoLink();
parseSearchText();
}
};
@ -118,10 +123,10 @@ $(document).ready(() => {
});
/**
* Toggle the ability to view the side navigation bar.
*
* @return {Void}
*/
* Toggle the ability to view the side navigation bar.
*
* @return {Void}
*/
function toggleSideNavigation() {
const sideNav = document.getElementById('sideNav');
const mainContainer = document.getElementById('main');
@ -136,10 +141,10 @@ function toggleSideNavigation() {
}
/**
* Clears out the #main container to allow other information to be shown.
*
* @return {Void}
*/
* Clears out the #main container to allow other information to be shown.
*
* @return {Void}
*/
function clearMainContainer() {
const container = document.getElementById('main');
container.innerHTML = '';
@ -150,32 +155,35 @@ function startLoadingAnimation() {
const loading = document.getElementById('loading');
const sideNavDisabled = document.getElementById('sideNavDisabled');
const searchBar = document.getElementById('search');
const goToVideoInput = document.getElementById('jumpToInput');
loading.style.display = 'inherit';
sideNavDisabled.style.display = 'inherit';
if(sideNavDisabled !== null){
sideNavDisabled.style.display = 'inherit';
}
searchBar.disabled = true;
goToVideoInput.disabled = true;
}
function stopLoadingAnimation() {
const loading = document.getElementById('loading');
const sideNavDisabled = document.getElementById('sideNavDisabled');
const searchBar = document.getElementById('search');
const goToVideoInput = document.getElementById('jumpToInput');
loading.style.display = 'none';
sideNavDisabled.style.display = 'none';
if(sideNavDisabled !== null){
sideNavDisabled.style.display = 'none';
}
searchBar.disabled = false;
goToVideoInput.disabled = false;
}
/**
* Creates a div container in #main meant to be a container for video lists.
*
* @param {string} headerLabel - The header of the container. Not used for showing video recommendations.
*
* @return {Void}
*/
* Creates a div container in #main meant to be a container for video lists.
*
* @param {string} headerLabel - The header of the container. Not used for showing video recommendations.
*
* @return {Void}
*/
function createVideoListContainer(headerLabel = '') {
const videoListContainer = document.createElement("div");
videoListContainer.id = 'videoListContainer';
@ -191,11 +199,11 @@ function createVideoListContainer(headerLabel = '') {
}
/**
* Displays the about page to #main
*
* @return {Void}
*/
function showAbout(){
* Displays the about page to #main
*
* @return {Void}
*/
function showAbout() {
// Remove current information and display loading animation
clearMainContainer();
startLoadingAnimation();
@ -211,14 +219,14 @@ function showAbout(){
}
/**
* Display a toast message in the bottom right corner of the page.
* The toast automatically disappears after a timeout.
*
* @param {string} message - The toast message.
*
* @return {Void}
*/
function showToast(message){
* Display a toast message in the bottom right corner of the page.
* The toast automatically disappears after a timeout.
*
* @param {string} message - The toast message.
*
* @return {Void}
*/
function showToast(message) {
let toast = document.getElementById('toast');
let toastMessage = document.getElementById('toastMessage');
@ -234,27 +242,27 @@ function showToast(message){
}
/**
* Hide the toast notification from the page.
*
* @return {Void}
*/
function hideToast(){
* Hide the toast notification from the page.
*
* @return {Void}
*/
function hideToast() {
let toast = document.getElementById('toast');
toast.style.opacity = 0;
toast.style.visibility = 'hidden';
}
/**
* Displays a confirmation box before performing an action. The action will be performed
* if the user clicks 'yes'.
*
* @param {string} message - The message to be displayed in the confirmation box
* @param {function} performFunction - The function to be performed upon confirmation
* @param {*} parameters - The parameters that will be sent to performFunction
*
* @return {Void}
*/
function confirmFunction(message, performFunction, parameters){
* Displays a confirmation box before performing an action. The action will be performed
* if the user clicks 'yes'.
*
* @param {string} message - The message to be displayed in the confirmation box
* @param {function} performFunction - The function to be performed upon confirmation
* @param {*} parameters - The parameters that will be sent to performFunction
*
* @return {Void}
*/
function confirmFunction(message, performFunction, parameters = '') {
let confirmContainer = document.getElementById('confirmFunction');
let confirmMessage = document.getElementById('confirmMessage');
@ -262,50 +270,54 @@ function confirmFunction(message, performFunction, parameters){
confirmContainer.style.visibility = 'visible';
$(document).on('click', '#confirmYes', (event) => {
performFunction(parameters);
if(parameters != ''){
performFunction(parameters);
}
else{
performFunction();
}
hideConfirmFunction();
});
}
/**
* Hides the confirmation box. Happens when the user clicks on 'no'.
*
* @return {Void}
*/
function hideConfirmFunction(){
* Hides the confirmation box. Happens when the user clicks on 'no'.
*
* @return {Void}
*/
function hideConfirmFunction() {
let confirmContainer = document.getElementById('confirmFunction');
confirmContainer.style.visibility = 'hidden';
}
/**
* Hide the mouse cursor after ~3 seconds. Used to hide the video when the user
* hovers the mouse over the video player.
*
* @return {Void}
*/
function hideMouseTimeout(){
* Hide the mouse cursor after ~3 seconds. Used to hide the video when the user
* hovers the mouse over the video player.
*
* @return {Void}
*/
function hideMouseTimeout() {
$('.videoPlayer')[0].style.cursor = 'default';
clearTimeout(mouseTimeout);
mouseTimeout = window.setTimeout(function(){
mouseTimeout = window.setTimeout(function() {
$('.videoPlayer')[0].style.cursor = 'none';
}, 3150);
}
/**
* Remove the timeout for the mouse cursor as a fallback.
*
* @return {Void}
*/
function removeMouseTimeout(){
* Remove the timeout for the mouse cursor as a fallback.
*
* @return {Void}
*/
function removeMouseTimeout() {
$('.videoPlayer')[0].style.cursor = 'default';
clearTimeout(mouseTimeout);
}
function showVideoOptions(element){
if (element.nextElementSibling.style.display == 'none' || element.nextElementSibling.style.display == ''){
element.nextElementSibling.style.display = 'inline-block'
}
else{
element.nextElementSibling.style.display = 'none'
}
function showVideoOptions(element) {
if (element.nextElementSibling.style.display == 'none' || element.nextElementSibling.style.display == '') {
element.nextElementSibling.style.display = 'inline-block'
} else {
element.nextElementSibling.style.display = 'none'
}
}

View File

@ -28,9 +28,14 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
*
* @return {Void}
*/
function playVideo(videoId) {
clearMainContainer();
startLoadingAnimation();
function playVideo(videoId, videoThumbnail = '', useWindowPlayer = false) {
if (useWindowPlayer === false){
clearMainContainer();
startLoadingAnimation();
}
else{
showToast('Getting video information. Please wait...')
}
let subscribeText = '';
let savedText = '';
@ -42,32 +47,20 @@ function playVideo(videoId) {
let subtitleHtml = '';
let subtitleLabel;
let subtitleLanguage;
let subtitleCode;
let subtitleUrl;
let defaultUrl;
let defaultQuality;
let channelId;
let videoHtml;
let videoThumbnail;
let videoType = 'video';
let embedPlayer;
let embedPlayer = "<iframe width='560' height='315' src='https://www.youtube-nocookie.com/embed/" + videoId + "?rel=0' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>";
let useEmbedPlayer = false;
let validUrl;
// Grab the embeded player. Used as fallback if the video URL cannot be found.
// Also grab the channel ID.
try {
let getInfoFunction = getChannelAndPlayer(videoId);
getInfoFunction.then((data) => {
console.log(data);
embedPlayer = data[0];
channelId = data[1];
});
} catch (ex) {
showToast('Video not found. ID may be invalid.');
stopLoadingAnimation();
return;
}
let videoLikes;
let videoDislikes;
let totalLikes;
let likePercentage;
const checkSavedVideo = videoIsSaved(videoId);
@ -82,58 +75,56 @@ function playVideo(videoId) {
}
});
youtubeAPI('videos', {
part: 'statistics',
id: videoId,
}, function(data) {
console.log(data);
// Figure out the width for the like/dislike bar.
videoLikes = data['items'][0]['statistics']['likeCount'];
videoDislikes = data['items'][0]['statistics']['dislikeCount'];
totalLikes = parseInt(videoLikes) + parseInt(videoDislikes);
likePercentage = parseInt((videoLikes / totalLikes) * 100);
});
/*
* FreeTube calls youtube-dl to grab the direct video URL.
*/
youtubedlGetInfo(videoId, (info) => {
console.log(info);
videoThumbnail = info['thumbnail'];
console.log(videoLikes);
channelId = info['author']['id'];
let channelThumbnail = info['author']['avatar'];
let videoUrls = info['formats'];
// Add commas to the video view count.
const videoViews = info['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// Format the date to a more readable format.
let dateString = info['upload_date'];
dateString = [dateString.slice(0, 4), '-', dateString.slice(4)].join('');
dateString = [dateString.slice(0, 7), '-', dateString.slice(7)].join('');
const publishedDate = dateFormat(dateString, "mmm dS, yyyy");
videoThumbnail = info['player_response']['videoDetails']['thumbnail']['thumbnails'][3]['url'];
// Figure out the width for the like/dislike bar.
const videoLikes = info['like_count'];
const videoDislikes = info['dislike_count'];
const totalLikes = videoLikes + videoDislikes;
const likePercentage = parseInt((videoLikes / totalLikes) * 100);
// Format the date to a more readable format.
let dateString = new Date(info['published']);
dateString.setDate(dateString.getDate() + 1);
const publishedDate = dateFormat(dateString, "mmm dS, yyyy");
let description = info['description'];
// Adds clickable links to the description.
description = autolinker.link(description);
if (info['subtitles'] !== null) {
videoSubtitles = info['subtitles'];
// Grab all subtitles
Object.keys(videoSubtitles).forEach((subtitle) => {
subtitleLabel = subtitle.toUpperCase();
subtitleUrl = videoSubtitles[subtitle][1]['url'];
if (subtitle === 'en') {
subtitleHtml = subtitleHtml + '<track label="' + subtitleLabel + '" kind="subtitles" srclang="' + subtitle + '" src="' + subtitleUrl + '" default>';
} else {
subtitleHtml = subtitleHtml + '<track label="' + subtitleLabel + '" kind="subtitles" srclang="' + subtitle + '" src="' + subtitleUrl + '">';
}
});
}
// Search through the returned object to get the 480p and 720p video URLs (If available)
Object.keys(videoUrls).forEach((key) => {
switch (videoUrls[key]['format_id']) {
switch (videoUrls[key]['itag']) {
case '18':
video480p = videoUrls[key]['url'];
console.log(video480p);
break;
case '22':
video720p = videoUrls[key]['url'];
console.log(video720p);
break;
}
});
@ -157,71 +148,119 @@ function playVideo(videoId) {
}
if (!useEmbedPlayer) {
videoHtml = '<video class="videoPlayer" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" src="' + defaultUrl + '" poster="' + videoThumbnail + '" autoplay>' + subtitleHtml + '</video>';
videoHtml = '<video class="videoPlayer" type="application/x-mpegURL" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" src="' + defaultUrl + '" poster="' + videoThumbnail + '" autoplay>';
if (typeof(info.player_response.captions) === 'object') {
if (typeof(info.player_response.captions.playerCaptionsTracklistRenderer.captionTracks) === 'object') {
const videoSubtitles = info.player_response.captions.playerCaptionsTracklistRenderer.captionTracks;
videoSubtitles.forEach((subtitle) => {
let subtitleUrl = 'https://www.youtube.com/api/timedtext?lang=' + subtitle.languageCode + '&fmt=vtt&name=&v=' + videoId;
if (subtitle.kind == 'asr') {
//subtitleUrl = subtitle.baseUrl;
return;
}
videoHtml = videoHtml + '<track kind="subtitles" src="' + subtitleUrl + '" srclang="' + subtitle.languageCode + '" label="' + subtitle.name.simpleText + '">';
});
}
}
videoHtml = videoHtml + '</video>';
}
const checkSubscription = isSubscribed(channelId);
// Change the subscribe button text depending on if the user has subscribed to the channel or not.
checkSubscription.then((results) => {
const subscribeButton = document.getElementById('subscribeButton');
if (results === false) {
subscribeText = 'SUBSCRIBE';
if (subscribeButton != null) {
subscribeButton.innerHTML = 'SUBSCRIBE';
}
} else {
subscribeText = 'UNSUBSCRIBE';
if (subscribeButton != null) {
subscribeButton.innerHTML = 'UNSUBSCRIBE';
}
}
});
// API Request
youtubeAPI('channels', {
'id': channelId,
'part': 'snippet'
}, function(data) {
const channelThumbnail = data['items'][0]['snippet']['thumbnails']['high']['url'];
const playerTemplate = require('./templates/player.html')
mustache.parse(playerTemplate);
const rendered = mustache.render(playerTemplate, {
videoHtml: videoHtml,
videoQuality: defaultQuality,
videoTitle: info['title'],
videoViews: videoViews,
videoThumbnail: videoThumbnail,
channelName: info['author']['name'],
videoLikes: videoLikes,
videoDislikes: videoDislikes,
likePercentage: likePercentage,
videoId: videoId,
channelId: channelId,
channelIcon: channelThumbnail,
publishedDate: publishedDate,
description: description,
isSubscribed: subscribeText,
savedText: savedText,
savedIconClass: savedIconClass,
savedIconColor: savedIconColor,
video480p: video480p,
video720p: video720p,
embedPlayer: embedPlayer,
});
const playerTemplate = require('./templates/player.html')
mustache.parse(playerTemplate);
const rendered = mustache.render(playerTemplate, {
videoHtml: videoHtml,
videoQuality: defaultQuality,
videoTitle: info['title'],
videoViews: videoViews,
videoThumbnail: videoThumbnail,
channelName: info['uploader'],
videoLikes: videoLikes,
videoDislikes: videoDislikes,
likePercentage: likePercentage,
videoId: videoId,
channelId: channelId,
channelIcon: channelThumbnail,
publishedDate: publishedDate,
description: description,
isSubscribed: subscribeText,
savedText: savedText,
savedIconClass: savedIconClass,
savedIconColor: savedIconColor,
video480p: video480p,
video720p: video720p,
embedPlayer: embedPlayer,
// Add the video to the user's history
addToHistory(videoId);
if (useWindowPlayer){
// Create a new browser window.
const BrowserWindow = electron.remote.BrowserWindow;
let newWindow = new BrowserWindow({
width: 1200,
height: 700
});
let playerWindowHeader = require('./templates/playerWindow.html');
mustache.parse(playerWindowHeader);
const playerHeaderRender = mustache.render(playerWindowHeader, {
videoId: videoId,
channelId: channelId
});
newWindow.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(playerHeaderRender + rendered), {
baseURLForDataURL: `file://${__dirname}/src`
});
}
else{
$('#main').html(rendered);
stopLoadingAnimation();
if (Object.keys(info['subtitles']).length > 0) {
showVideoRecommendations(videoId);
// Sometimes a video URL is found, but the video will not play. I believe the issue is
// that the video has yet to render for that quality, as the video will be available at a later time.
// This will check the URLs and switch video sources if there is an error.
checkVideoUrls(video480p, video720p);
// Hide subtitles by default
if (typeof(info['subtitles']) !== 'undefined' && Object.keys(info['subtitles']).length > 0) {
let textTracks = $('.videoPlayer').get(0).textTracks;
Object.keys(textTracks).forEach((track) => {
textTracks[track].mode = 'hidden';
});
}
}
showVideoRecommendations(videoId);
console.log('done');
});
// Sometimes a video URL is found, but the video will not play. I believe the issue is
// that the video has yet to render for that quality, as the video will be available at a later time.
// This will check the URLs and switch video sources if there is an error.
checkVideoUrls(video480p, video720p);
// Add the video to the user's history
addToHistory(videoId);
});
}

View File

@ -51,11 +51,11 @@ function addSavedVideo(videoId){
/**
* Removes a video from the user's saved video database.
*
* @param {string} {videoId} - The video ID of the video that will be removed.
* @param {string} videoId - The video ID of the video that will be removed.
*
* @return {Void}
*/
function removeSavedVideo(videoId: string){
function removeSavedVideo(videoId, string){
savedVidsDb.remove({
videoId: videoId
}, {}, (err, numRemoved) => {

View File

@ -72,6 +72,12 @@ function showSettings() {
} else {
document.getElementById('themeSwitch').checked = true;
}
if (useTor) {
document.getElementById('torSwitch').checked = true;
} else {
document.getElementById('torSwitch').checked = false;
}
});
}
@ -84,46 +90,49 @@ function checkDefaultSettings() {
// Grab a random API Key.
apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)];
let newSetting;
// Check settings database
settingsDb.find({}, (err, docs) => {
if (jQuery.isEmptyObject(docs)) {
let settingDefaults = {
'theme': 'light',
'apiKey': apiKey,
'useTor': false
};
// Set User Defaults
let themeDefault = {
_id: 'theme',
value: 'light',
};
console.log(settingDefaults);
let apiDefault = {
_id: 'apiKey',
value: apiKey,
};
for (let key in settingDefaults){
settingsDb.find({_id: key}, (err, docs) => {
if (jQuery.isEmptyObject(docs)) {
newSetting = {
_id: key,
value: settingDefaults[key]
};
// Set default theme
setTheme('light');
settingsDb.insert(newSetting);
// Inset default settings into the settings database.
settingsDb.insert(themeDefault);
settingsDb.insert(apiDefault);
} else {
// Use user current defaults
docs.forEach((setting) => {
switch (setting['_id']) {
if (key == 'theme'){
setTheme('light');
}
}
else{
switch (docs[0]['_id']) {
case 'theme':
setTheme(setting['value']);
setTheme(docs[0]['value']);
break;
case 'apiKey':
if (apiKeyBank.indexOf(setting['value']) == -1) {
apiKey = setting['value'];
if (apiKeyBank.indexOf(docs[0]['value']) == -1) {
apiKey = docs[0]['value'];
}
break;
case 'useTor':
useTor = docs[0]['value'];
break;
default:
break;
}
});
}
});
}
});
}
}
/**
@ -132,17 +141,21 @@ function checkDefaultSettings() {
* @return {Void}
*/
function updateSettings() {
var themeSwitch = document.getElementById('themeSwitch').checked;
var key = document.getElementById('api-key').value;
let themeSwitch = document.getElementById('themeSwitch').checked;
let torSwitch = document.getElementById('torSwitch').checked;
let key = document.getElementById('api-key').value;
let theme = 'light';
apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)];
if (themeSwitch == true) {
var theme = 'dark';
} else {
var theme = 'light';
console.log(themeSwitch);
if (themeSwitch === true) {
theme = 'dark';
}
console.log(theme);
// Update default theme
settingsDb.update({
_id: 'theme'
@ -153,6 +166,17 @@ function updateSettings() {
console.log(numReplaced);
});
// Update tor usage.
settingsDb.update({
_id: 'useTor'
}, {
value: torSwitch
}, {}, function(err, numReplaced) {
console.log(err);
console.log(numReplaced);
useTor = torSwitch;
});
if (key != '') {
settingsDb.update({
_id: 'apiKey'
@ -207,14 +231,20 @@ function setTheme(option) {
// Grab the css file to be used.
switch (option) {
case 'light':
cssFile = 'style/lightTheme.css';
cssFile = './style/lightTheme.css';
document.getElementById('menuText').src = 'icons/textBlack.png';
document.getElementById('menuIcon').src = 'icons/iconBlack.png';
document.getElementById('menuButton').style.color = 'black';
break;
case 'dark':
cssFile = 'style/darkTheme.css';
cssFile = './style/darkTheme.css';
document.getElementById('menuText').src = 'icons/textColor.png';
document.getElementById('menuIcon').src = 'icons/iconColor.png';
document.getElementById('menuButton').style.color = 'white';
break;
default:
// Default to the light theme
cssFile = 'style/lightTheme.css';
cssFile = './style/lightTheme.css';
break;
}
newTheme.setAttribute("href", cssFile);

View File

@ -79,6 +79,7 @@ function removeSubscription(channelId) {
*/
function loadSubscriptions() {
clearMainContainer();
showToast('Getting Subscriptions. Please wait...');
const loading = document.getElementById('loading');
startLoadingAnimation()
@ -162,90 +163,6 @@ function loadSubscriptions() {
container.innerHTML = `<h2 class="message">Your Subscription list is currently empty. Start adding subscriptions
to see them here.<br /><br /><i class="far fa-frown" style="font-size: 200px"></i></h2>`;
}
// Yes, This function is the thing that needs to most improvment
/*
if (results.length > 0) {
showToast('Getting Subscriptions. This may take a while...');
/*
* If this loop gets rewritten, we can remove the asyncLoop dependency from the project.
*
* I wasn't able to figure out a way to loop through the list of channels and grab the recent uploads
* while then sorting them afterwards, this was my best solution at the time. I'm sure someone more
* experienced in Node can help out with this.
*/
/*
asyncLoop(results, (sub, next) => {
const channelId = sub['channelId'];
/*
* Grab the channels 15 most recent uploads. Typically this should be enough.
* This number can be changed if we feel necessary.
*/
/*
youtubeAPI('search', {
part: 'snippet',
channelId: channelId,
type: 'video',
maxResults: 15,
order: 'date',
}, (data) => {
videoList = videoList.concat(data.items);
next();
});
}, (err) => {
// Sort the videos by date
videoList.sort((a, b) => {
const date1 = Date.parse(a.snippet.publishedAt);
const date2 = Date.parse(b.snippet.publishedAt);
return date2.valueOf() - date1.valueOf();
});
// Render the videos to the application.
createVideoListContainer('Latest Subscriptions:');
// The YouTube website limits the subscriptions to 100 before grabbing more so we only show 100
// to keep the app running at a good speed.
if(videoList.length < 50){
let grabDuration = getDuration(videoList.slice(0,49));
grabDuration.then((list) => {
list.items.forEach((video) => {
displayVideo(video);
});
});
}
else{
let finishedList = []
let firstBatchDuration = getDuration(videoList.slice(0, 49));
firstBatchDuration.then((list1) => {
finishedList = finishedList.concat(list1.items);
let secondBatchDuration = getDuration(videoList.slice(50, 99));
secondBatchDuration.then((list2) => {
finishedList = finishedList.concat(list2.items);
finishedList.forEach((video) => {
displayVideo(video);
});
});
});
}
stopLoadingAnimation()
});
} else {
// User has no subscriptions. Display message.
const container = document.getElementById('main');
stopLoadingAnimation()
container.innerHTML = `<h2 class="message">Your Subscription list is currently empty. Start adding subscriptions
to see them here.<br /><br /><i class="far fa-frown" style="font-size: 200px"></i></h2>`;
}
});*/
});
}

58
src/js/updates.js Normal file
View File

@ -0,0 +1,58 @@
/*
This file is part of FreeTube.
FreeTube is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FreeTube is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* A file for checking / managing updates
*/
const updateChecker = require('github-version-checker');
const options = {
token: 'USERACCESSTOKEN', // personal access token. Github will not allow commiting the access token, which is why this is blank.
repo: 'freetube', // repository name
owner: 'freetubeapp', // repository owner
currentVersion: require('electron').remote.app.getVersion(), // your app's current version
fetchTags: false // whether to fetch releases or tags
};
const openReleasePage = function(){
shell.openExternal('https://github.com/FreeTubeApp/FreeTube/releases');
}
/*function checkForUpdates() {
updateChecker(options, function(error, update) { // callback function
if (error){
showToast('There was a problem with checking for updates');
console.log(error);
}
if (update) { // print some update info if an update is available
confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage);
}
else{
showToast('No update is currently available.');
}
});
}*/
updateChecker(options, function(error, update) { // callback function
if (error) throw error;
if (update) { // print some update info if an update is available
confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage);
}
});

View File

@ -18,12 +18,12 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
/**
* Perform a search using the YouTube API. The search query is grabbed from the #search element.
*
* @param {string} nextPageToken - Optional: The page token to be inlcuded in the search.
*
* @return {Void}
*/
* Perform a search using the YouTube API. The search query is grabbed from the #search element.
*
* @param {string} nextPageToken - Optional: The page token to be inlcuded in the search.
*
* @return {Void}
*/
function search(nextPageToken = '') {
const query = document.getElementById('search').value;
@ -42,13 +42,45 @@ function search(nextPageToken = '') {
youtubeAPI('search', {
q: query,
part: 'id',
type: 'video',
pageToken: nextPageToken,
maxResults: 25,
}, function (data){
let grabDuration = getDuration(data.items);
}, function(data) {
console.log(data);
let channels = data.items.filter((item) => {
if (item.id.kind === 'youtube#channel') {
return true;
}
});
let playlists = data.items.filter((item) => {
if (item.id.kind === 'youtube#playlist') {
return true;
}
});
let videos = data.items.filter((item) => {
if (item.id.kind === 'youtube#video') {
return true;
}
});
console.log(channels);
console.log(typeof(channels));
console.log(playlists);
if(playlists.length > 0){
//displayPlaylists(playlists);
}
if(channels.length > 0){
displayChannels(channels);
}
let grabDuration = getDuration(videos);
grabDuration.then((videoList) => {
console.log(videoList);
videoList.items.forEach(displayVideo);
});
@ -61,30 +93,27 @@ function search(nextPageToken = '') {
}
/**
* Grab the duration of the videos
*
* @param {array} data - An array of videos to get the duration from
*
* @return {promise} - The list of videos with the duration included.
*/
function getDuration(data){
* Grab the duration of the videos
*
* @param {array} data - An array of videos to get the duration from
*
* @return {promise} - The list of videos with the duration included.
*/
function getDuration(data) {
return new Promise((resolve, reject) => {
let videoIdList = '';
for(let i = 0; i < data.length; i++){
if (videoIdList === ''){
if(typeof(data[i]['id']) === 'string'){
for (let i = 0; i < data.length; i++) {
if (videoIdList === '') {
if (typeof(data[i]['id']) === 'string') {
videoIdList = data[i]['id'];
}
else{
} else {
videoIdList = data[i]['id']['videoId'];
}
}
else{
if(typeof(data[i]['id']) === 'string'){
} else {
if (typeof(data[i]['id']) === 'string') {
videoIdList = videoIdList + ', ' + data[i]['id'];
}
else{
} else {
videoIdList = videoIdList + ', ' + data[i]['id']['videoId'];
}
}
@ -100,14 +129,14 @@ function getDuration(data){
}
/**
* Display a video on the page. Function is typically contained in a loop.
*
* @param {video} video - The video ID of the video to be displayed.
* @param {string} listType - Optional: Specifies the list type of the video
* Used for displaying the remove icon for history and saved videos.
*
* @return {Void}
*/
* Display a video on the page. Function is typically contained in a loop.
*
* @param {video} video - The video ID of the video to be displayed.
* @param {string} listType - Optional: Specifies the list type of the video
* Used for displaying the remove icon for history and saved videos.
*
* @return {Void}
*/
function displayVideo(video, listType = '') {
const videoSnippet = video.snippet;
@ -132,18 +161,18 @@ function displayVideo(video, listType = '') {
};
// Includes text if the video is live.
const liveText = (videoSnippet.liveBroadcastContent === 'live')? 'LIVE NOW' : '';
const liveText = (videoSnippet.liveBroadcastContent === 'live') ? 'LIVE NOW' : '';
const videoListTemplate = require('./templates/videoList.html');
mustache.parse(videoListTemplate);
const rendered = mustache.render(videoListTemplate, {
videoId: videoId,
videoThumbnail: videoSnippet.thumbnails.medium.url,
videoTitle: videoSnippet.title,
channelName: videoSnippet.channelTitle,
videoThumbnail: videoSnippet.thumbnails.medium.url,
videoTitle: videoSnippet.title,
channelName: videoSnippet.channelTitle,
videoDescription: videoSnippet.description,
channelId: videoSnippet.channelId,
videoDuration: videoDuration,
channelId: videoSnippet.channelId,
videoDuration: videoDuration,
publishedDate: publishedDate,
liveText: liveText,
deleteHtml: deleteHtml,
@ -158,13 +187,95 @@ function displayVideo(video, listType = '') {
}
}
function displayChannels(channels) {
let channelIds;
channels.forEach((channel) => {
if (typeof(channelIds) === 'undefined') {
channelIds = channel.id.channelId;
} else {
channelIds = channelIds + ',' + channel.id.channelId;
}
});
console.log(channelIds);
youtubeAPI('channels', {
part: 'snippet,statistics',
id: channelIds,
}, function(data) {
console.log(data);
let items = data['items'].reverse();
const videoListTemplate = require('./templates/channelList.html');
console.log(items);
items.forEach((item) => {
mustache.parse(videoListTemplate);
let rendered = mustache.render(videoListTemplate, {
channelId: item.id,
channelThumbnail: item.snippet.thumbnails.medium.url,
channelName: item.snippet.title,
channelDescription: item.snippet.description,
subscriberCount: item.statistics.subscriberCount,
videoCount: item.statistics.videoCount,
});
$(rendered).insertBefore('#getNextPage');
});
});
}
function displayPlaylists(playlists) {
let playlistIds;
playlists.forEach((playlist) => {
if (typeof(playlistIds) === 'undefined') {
playlistIds = playlist.id.playlistId;
} else {
playlistIds = playlistIds + ',' + playlist.id.playlistId;
}
});
console.log(playlistIds);
youtubeAPI('playlists', {
part: 'snippet,contentDetails',
id: playlistIds,
}, function(data) {
console.log(data);
let items = data['items'].reverse();
const playlistListTemplate = require('./templates/playlistList.html');
console.log(items);
items.forEach((item) => {
let dateString = new Date(item.snippet.publishedAt);
let publishedDate = dateFormat(dateString, "mmm dS, yyyy");
mustache.parse(playlistListTemplate);
let rendered = mustache.render(playlistListTemplate, {
channelId: item.snippet.channelId,
channelName: item.snippet.channelTitle,
playlistThumbnail: item.snippet.thumbnails.medium.url,
playlistTitle: item.snippet.title,
playlistDescription: item.snippet.description,
videoCount: item.contentDetails.itemCount,
publishedDate: publishedDate,
});
$(rendered).insertBefore('#getNextPage');
});
});
}
/**
* Changes the page token to the next page button during a video search.
*
* @param {string} nextPageToken - The page token to replace the button function.
*
* @return {Void}
*/
* Changes the page token to the next page button during a video search.
*
* @param {string} nextPageToken - The page token to replace the button function.
*
* @return {Void}
*/
function addNextPage(nextPageToken) {
let oldFetchButton = document.getElementById('getNextPage');
@ -185,19 +296,19 @@ function addNextPage(nextPageToken) {
}
/**
* Grab the video recommendations for a video. This does not get recommendations based on what you watch,
* as that would defeat the main purpose of using FreeTube. At any time you can check the video on HookTube
* and compare the recommendations there. They should be nearly identical.
*
* @param {string} videoId - The video ID of the video to get recommendations from.
*/
* Grab the video recommendations for a video. This does not get recommendations based on what you watch,
* as that would defeat the main purpose of using FreeTube. At any time you can check the video on HookTube
* and compare the recommendations there. They should be nearly identical.
*
* @param {string} videoId - The video ID of the video to get recommendations from.
*/
function showVideoRecommendations(videoId) {
youtubeAPI('search', {
part: 'id',
type: 'video',
relatedToVideoId: videoId,
maxResults: 15,
}, function (data){
}, function(data) {
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach((video) => {
@ -208,10 +319,10 @@ function showVideoRecommendations(videoId) {
mustache.parse(recommTemplate);
const rendered = mustache.render(recommTemplate, {
videoId: video.id,
videoTitle: snippet.title,
channelName: snippet.channelTitle,
videoTitle: snippet.title,
channelName: snippet.channelTitle,
videoThumbnail: snippet.thumbnails.medium.url,
videoDuration: videoDuration,
videoDuration: videoDuration,
publishedDate: dateFormat(snippet.publishedAt, "mmm dS, yyyy")
});
const recommendationHtml = $('#recommendations').html();
@ -222,13 +333,20 @@ function showVideoRecommendations(videoId) {
}
/**
* Check if a link is a valid YouTube/HookTube link and play that video. Gets input
* from the #jumpToInput element.
*
* @return {Void}
*/
function parseVideoLink() {
let input = document.getElementById('jumpToInput').value;
* Check if a link is a valid YouTube/HookTube link and play that video. Gets input
* from the #jumpToInput element.
*
* @return {Void}
*/
function parseSearchText(url = '') {
let input;
if (url === ''){
input = document.getElementById('search').value;
}
else{
input = url;
}
if (input === '') {
return;
@ -243,26 +361,28 @@ function parseVideoLink() {
// Play video if a match is found.
try {
console.log('Match Found');
playVideo(match[2]);
} catch (err) {
showToast('Video Not Found');
console.log('Video not found');
search();
}
}
/**
* Convert duration into a more readable format
*
* @param {string} durationString - The string containing the video duration. Formated as 'PT12H34M56S'
*
* @return {string} - The formated string. Ex: 12:34:56
*/
function parseVideoDuration(durationString){
* Convert duration into a more readable format
*
* @param {string} durationString - The string containing the video duration. Formated as 'PT12H34M56S'
*
* @return {string} - The formated string. Ex: 12:34:56
*/
function parseVideoDuration(durationString) {
let match = durationString.match(/PT(\d+H)?(\d+M)?(\d+S)?/);
let duration = '';
match = match.slice(1).map(function(x) {
if (x != null) {
return x.replace(/\D/, '');
return x.replace(/\D/, '');
}
});
@ -270,30 +390,25 @@ function parseVideoDuration(durationString){
let minutes = (parseInt(match[1]) || 0);
let seconds = (parseInt(match[2]) || 0);
if (hours != 0){
if (hours != 0) {
duration = hours + ':';
}
else{
} else {
duration = minutes + ':';
}
if (hours != 0 && minutes < 10){
if (hours != 0 && minutes < 10) {
duration = duration + '0' + minutes + ':';
}
else if (hours != 0 && minutes > 10){
} else if (hours != 0 && minutes > 10) {
duration = duration + minutes + ':';
}
else if (hours != 0 && minutes == 0){
} else if (hours != 0 && minutes == 0) {
duration = duration + '00:';
}
if (seconds == 0){
if (seconds == 0) {
duration = duration + '00';
}
else if (seconds < 10){
} else if (seconds < 10) {
duration = duration + '0' + seconds;
}
else{
} else {
duration = duration + seconds;
}
@ -301,10 +416,10 @@ function parseVideoDuration(durationString){
}
/**
* Grab the most popular videos over the last couple of days and display them.
*
* @return {Void}
*/
* Grab the most popular videos over the last couple of days and display them.
*
* @return {Void}
*/
function showMostPopular() {
clearMainContainer();
startLoadingAnimation();
@ -323,7 +438,7 @@ function showMostPopular() {
type: 'video',
publishedAfter: d.toISOString(),
maxResults: 50,
}, function (data){
}, function(data) {
createVideoListContainer('Most Popular:');
console.log(data);
let grabDuration = getDuration(data.items);
@ -337,13 +452,13 @@ function showMostPopular() {
}
/**
* Create a link of the video to HookTube or YouTube and copy it to the user's clipboard.
*
* @param {string} website - The website to watch the video on.
* @param {string} videoId - The video ID of the video to add to the URL
*
* @return {Void}
*/
* Create a link of the video to HookTube or YouTube and copy it to the user's clipboard.
*
* @param {string} website - The website to watch the video on.
* @param {string} videoId - The video ID of the video to add to the URL
*
* @return {Void}
*/
function copyLink(website, videoId) {
// Create the URL and copy to the clipboard.
const url = 'https://' + website + '.com/watch?v=' + videoId;
@ -352,19 +467,19 @@ function copyLink(website, videoId) {
}
/**
* Get the YouTube embeded player of a video as well as channel information..
*
* @param {string} videoId - The video ID of the video to get.
*
* @return {promise} - The HTML of the embeded player
*/
* Get the YouTube embeded player of a video as well as channel information..
*
* @param {string} videoId - The video ID of the video to get.
*
* @return {promise} - The HTML of the embeded player
*/
function getChannelAndPlayer(videoId) {
console.log(videoId);
return new Promise((resolve, reject) => {
youtubeAPI('videos', {
part: 'snippet,player',
id: videoId,
}, function (data){
}, function(data) {
let embedHtml = data.items[0].player.embedHtml;
embedHtml = embedHtml.replace('src="', 'src="https:');
embedHtml = embedHtml.replace('width="480px"', '');
@ -376,20 +491,20 @@ function getChannelAndPlayer(videoId) {
}
/**
* Check to see if the video URLs are valid. Change the video quality if one is not.
* The API will grab video URLs, but they will sometimes return a 404. This
* is why this check is needed. The video URL will typically be resolved over time.
*
* @param {string} video480p - The URL to the 480p video.
* @param {string} video720p - The URL to the 720p video.
*/
* Check to see if the video URLs are valid. Change the video quality if one is not.
* The API will grab video URLs, but they will sometimes return a 404. This
* is why this check is needed. The video URL will typically be resolved over time.
*
* @param {string} video480p - The URL to the 480p video.
* @param {string} video720p - The URL to the 720p video.
*/
function checkVideoUrls(video480p, video720p) {
const currentQuality = $('#currentQuality').html();
let buttonEmbed = document.getElementById('qualityEmbed');
let valid480 = false;
if (typeof(video480p) !== 'undefined'){
if (typeof(video480p) !== 'undefined') {
let get480pUrl = fetch(video480p);
get480pUrl.then((status) => {
switch (status.status) {
@ -412,7 +527,7 @@ function checkVideoUrls(video480p, video720p) {
break;
default:
console.log('480p is valid');
if (currentQuality === '720p' && typeof(video720p) === 'undefined'){
if (currentQuality === '720p' && typeof(video720p) === 'undefined') {
changeQuality(video480p);
}
break;
@ -420,7 +535,7 @@ function checkVideoUrls(video480p, video720p) {
});
}
if (typeof(video720p) !== 'undefined'){
if (typeof(video720p) !== 'undefined') {
let get720pUrl = fetch(video720p);
get720pUrl.then((status) => {
switch (status.status) {
@ -430,7 +545,7 @@ function checkVideoUrls(video480p, video720p) {
$(document).on('click', '#quality720p', (event) => {
changeQuality('');
});
if (typeof(valid480) !== 'undefined'){
if (typeof(valid480) !== 'undefined') {
changeQuality(video480p, '480p');
}
break;
@ -444,7 +559,7 @@ function checkVideoUrls(video480p, video720p) {
break;
default:
console.log('720p is valid');
if (currentQuality === '720p'){
if (currentQuality === '720p') {
return;
}
break;

View File

@ -7,48 +7,62 @@
*
* @return {Void}
*/
function youtubeAPI(resource, params, success) {
params.key = apiKey;
$.getJSON(
'https://www.googleapis.com/youtube/v3/' + resource,
params,
success
).fail((xhr, textStatus, error) => {
showToast('There was an error calling the YouTube API.');
console.log(error);
console.log(xhr);
console.log(textStatus);
stopLoadingAnimation();
});
if (useTor) {
tor.request('https://www.googleapis.com/youtube/v3/' + resource + '?' + $.param(params), function(err, res, body) {
if (!err && res.statusCode == 200) {
success(JSON.parse(body));
} else {
showToast('Unable to connect to the Tor network. Check the help page if you\'re having trouble setting up your node.');
console.log(err);
console.log(res);
console.log(body);
stopLoadingAnimation();
}
});
} else {
$.getJSON(
'https://www.googleapis.com/youtube/v3/' + resource,
params,
success
).fail((xhr, textStatus, error) => {
showToast('There was an error calling the YouTube API.');
console.log(error);
console.log(xhr);
console.log(textStatus);
stopLoadingAnimation();
});
}
}
/**
* Use youtube-dl to resolve a video.
*
* @param {string} videoId - The video Id to get info from.
* @param {function} callback - The function called on success with the info.
*
* @return {Void}
*/
* Use youtube-dl to resolve a video.
*
* @param {string} videoId - The video Id to get info from.
* @param {function} callback - The function called on success with the info.
*
* @return {Void}
*/
function youtubedlGetInfo(videoId, callback) {
let url = 'https://stormy-inlet-41826.herokuapp.com/api/info?url=https://www.youtube.com/watch?v=' + videoId + 'flatten=True&writesubtitles=True&geo_bypass=True';
$.getJSON(url, (response) => {
callback(response.info);
});
let url = 'https://youtube.com/watch?v=' + videoId;
let options = ['--all-subs', '--write-subs'];
/*let url = 'https://youtube.com/watch?v=' + videoId;
let options = ['--all-subs'];
youtubedl.getInfo(url, options, function(err, info) {
if (err){
showToast('There was an issue calling youtube-dl.');
ytdl.getInfo(url, options, function(err, info) {
if (err) {
showToast(err.message);
stopLoadingAnimation();
console.log(err);
console.log(info);
return;
}
console.log('Success');
callback(info);
});*/
});
}

View File

@ -46,35 +46,46 @@ a {
line-height: normal;
}
.menuButton {
#menuButton {
cursor: pointer;
}
.logo {
font-weight: bold;
font-size: 20px;
margin-left: 20px;
position: relative;
top: 3px;
}
#menuIcon {
width: 25px;
position: relative;
top: 15px;
right: 120px;
float: right;
}
#menuText {
width: 100px;
position: relative;
top: 5px;
left: 10px;
float: right;
}
.searchBar {
position: absolute;
right: 0px;
right: 135px;
top: 0;
width: 60%;
width: 50%;
}
.searchBar input {
width: 40%;
width: 90%;
}
.searchButton {
text-decoration: none;
}
.jumpToInput {
width: 80%;
}
#sideNav {
height: 100vh;
width: 250px;
@ -246,6 +257,7 @@ a {
font-weight: bold;
line-height: 65px;
visibility: hidden;
z-index: 1;
-webkit-box-shadow: 5px 5px 15px -5px rgba(0,0,0,0.75);
-moz-box-shadow: 5px 5px 15px -5px rgba(0,0,0,0.75);
box-shadow: 5px 5px 15px -5px rgba(0,0,0,0.75);

View File

@ -27,6 +27,21 @@ iframe {
width: 100%;
}
.channelThumbnail {
width: 140px;
height: 140px;
float: left;
cursor: pointer;
position: relative;
left: 60px;
}
.channelThumbnail img{
width: 100%;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.videoOptions {
float: right;
width: 50px;

View File

@ -1,5 +1,8 @@
<div class='center'>
<h1>FreeTube {{versionNumber}} Beta</h1>
<img src='icons/logoColor.png' width='500px;'/>
<h1>{{versionNumber}} Beta</h1>
<h2>This software is FOSS and released under the <a href='https://www.gnu.org/licenses/quick-guide-gplv3.html'>GNU Public License v3+</a>.</h2>
Found a bug? Want to suggest a feature? Want to help out? Check out our <a href='https://github.com/FreeTubeApp/FreeTube'>GitHub</a> page. Pull requests are welcome.
<p>Thank you very much to the <a href='https://github.com/FreeTubeApp/FreeTube/wiki/Credits'>People and Projects</a> that make FreeTube possible!</p>
<h3><i class='fas fa-envelope'></i>&nbsp;&nbsp;FreeTubeApp@protonmail.com</h3>
</div>

View File

@ -0,0 +1,9 @@
<div class='video'>
<div class='channelThumbnail'>
<img onclick='goToChannel("{{channelId}}")' src={{channelThumbnail}} />
</div>
<p onclick='goToChannel("{{channelId}}")' class='videoTitle'>{{channelName}}</p>
<p onclick='goToChannel("{{channelId}}")' class='channelName'>{{subscriberCount}} subscribers - {{videoCount}} videos</p>
<p onclick='goToChannel("{{channelId}}")' class='videoDescription'>{{channelDescription}}</p>
</div>
<hr />

View File

@ -43,7 +43,7 @@
COPY HOOKTUBE LINK
</div>
<a href='https://hooktube.com/watch?v={{videoId}}'>
<div class='smallButton' onclick='copyLink("hooktube", "{{videoId}}")'>
<div class='smallButton'>
OPEN IN HOOKTUBE
</div>
</a>

View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style/main.css">
<link rel="stylesheet" href="style/lightTheme.css">
<link rel="stylesheet" href="style/player.css">
<link rel="stylesheet" href="style/videoList.css">
<link rel="stylesheet" href="style/channel.css">
<link rel="stylesheet" href="style/loading.css">
<link rel="stylesheet" href="style/fa-solid.min.css">
<link rel="stylesheet" href="style/fontawesome-all.min.css">
<script src="js/youtubeApi.js"></script>
<script src="js/settings.js"></script>
<script src="js/layout.js"></script>
<script src="js/videos.js"></script>
<script src="js/player.js"></script>
<script src="js/subscriptions.js"></script>
<script src="js/channels.js"></script>
<script src="js/savedVideos.js"></script>
<script src="js/history.js"></script>
<script src="js/events.js"></script>
<script>
showVideoRecommendations("{{videoId}}");
const checkIfSubscribed = isSubscribed('{{channelId}}');
checkIfSubscribed.then((results) => {
const subscribeButton = document.getElementById('subscribeButton');
console.log(results);
console.log(subscribeButton);
if (results === false) {
if (subscribeButton != null) {
subscribeButton.innerHTML = 'UNSUBSCRIBE';
}
} else {
if (subscribeButton != null) {
subscribeButton.innerHTML = 'SUBSCRIBE';
}
}
});
</script>
<title>Freetube Player</title>
<style>
#main{
width: 100%;
margin-left: 0px;
}
.spinner {
width: 40px;
height: 40px;
position: relative;
margin: 0px auto;
}
#loading{
width: 100%;
height: 0%;
}
</style>
</head>
<body>
<div id='loading'>
<div class="spinner">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
</div>
<div id='confirmFunction'>
<span id='confirmMessage'>Would you like to perform the function?</span>
<div class='confirmButton' id='confirmYes'>Yes</div>
<div class='confirmButton' id='confirmNo'>No</div>
</div>
<div id='toast'>
<span id='toastMessage'></span>
<i onclick='hideToast()' class="closeToast fas fa-times"></i>
</div>
<div class="topNav">
<i onclick='toggleSideNavigation()' class="fas fa-bars" id='menuButton' style='display: none;'></i>
<div class="searchBar">
<input id='search' class="search" type="text" placeholder="Search / Go to URL">
<i onclick='parseSearchText()' class="fas fa-search searchButton" style='margin-right: -10px; cursor: pointer'></i>
</div>
<img src='icons/iconBlack.png' id='menuIcon'/>
&nbsp;
<img src='icons/textBlack.png' id='menuText'/>
</div>
<div id='main'>

View File

@ -0,0 +1,10 @@
<div class='video'>
<div class='videoThumbnail'>
<img onclick='console.log("Not Implemented")' src={{playlistThumbnail}} />
</div>
<p onclick='goToChannel("{{channelId}}")' class='videoTitle'>{{playlistTitle}}</p>
<p onclick='goToChannel("{{channelId}}")' class='channelName'>{{channelName}} - {{publishedDate}}</p>
<p onclick='goToChannel("{{channelId}}")' class='videoDescription'>{{playlistDescription}}</p>
<p>VIEW FULL PLAYLIST ({{videoCount}} videos)</p>
</div>
<hr />

View File

@ -6,6 +6,8 @@
<br />
<input type="checkbox" id="themeSwitch" name="set-name" class="switch-input" onchange='toggleTheme(this)' {{isChecked}}>
<label for="themeSwitch" class="switch-label">Use Dark Theme</label>
<input type="checkbox" id="torSwitch" name="set-name" class="switch-input" {{isChecked}}>
<label for="torSwitch" class="switch-label">Use TOR for API calls</label>
</div>
<div class='center'>
<div onclick='importSubscriptions()' class='settingsButton'>

4
src/templates/video.html Normal file
View File

@ -0,0 +1,4 @@
<video class="videoPlayer" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" poster="{{videoThumbnail}}" autoplay>
<source src="{{videoUrl}}" type="video/mp4">
{{subtitleElements}}
</video>

View File

@ -2,14 +2,15 @@
<div class='videoOptions'>
<i class="fas fa-ellipsis-v" onclick='showVideoOptions(this)'></i>
<ul>
<a href='https://youtube.com/watch?v={{videoId}}' ><li>Open in YouTube</li></a>
<a href='https://hooktube.com/watch?v={{videoId}}' ><li>Open in HookTube</li></a>
<li onclick='addSavedVideo("{{videoId}}")'>Save Video</li>
<li onclick='playVideo("{{videoId}}", "", true); showVideoOptions(this.parentNode.previousSibling);'>Open (New Window)</li>
<li onclick='addSavedVideo("{{videoId}}"); showVideoOptions(this.parentNode.previousSibling);'>Save Video</li>
<a href='https://youtube.com/watch?v={{videoId}}; showVideoOptions(this.parentNode.previousSibling);' ><li>Open in YouTube</li></a>
<a href='https://hooktube.com/watch?v={{videoId}}; showVideoOptions(this.parentNode.previousSibling);' ><li>Open in HookTube</li></a>
{{{deleteHtml}}}
</ul>
</div>
<div class='videoThumbnail'>
<img onclick='playVideo("{{videoId}}")' src={{videoThumbnail}} />
<img onclick='playVideo("{{videoId}}", "{{videoThumbnail}}")' src={{videoThumbnail}} />
<p onclick='playVideo("{{videoId}}")' class='videoDuration'>{{videoDuration}}</p>
</div>
<p onclick='playVideo("{{videoId}}")' class='videoTitle'>{{videoTitle}}</p>