Push Latest Code to Repository

This commit is contained in:
Preston 2020-02-16 13:30:00 -05:00
parent 89e8a506d4
commit ded6534d16
162 changed files with 24876 additions and 100 deletions

18
.babelrc Normal file
View File

@ -0,0 +1,18 @@
{
"presets": [
[
"@babel/env",
{
"targets": {
"chrome": "73",
"node": 12
}
}
],
"@babel/typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
_scripts
dist

42
.eslintrc.js Normal file
View File

@ -0,0 +1,42 @@
module.exports = {
// https://eslint.org/docs/user-guide/configuring#using-configuration-files-1
root: true,
// https://eslint.org/docs/user-guide/configuring#specifying-environments
env: {
browser: true,
node: true,
},
// https://eslint.org/docs/user-guide/configuring#specifying-parser
parser: 'vue-eslint-parser',
// https://vuejs.github.io/eslint-plugin-vue/user-guide/#faq
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2018,
sourceType: 'module',
},
// https://eslint.org/docs/user-guide/configuring#extending-configuration-files
// order matters: from least important to most important in terms of overriding
// Prettier + Vue: https://medium.com/@gogl.alex/how-to-properly-set-up-eslint-with-prettier-for-vue-or-nuxt-in-vscode-e42532099a9c
extends: [
'prettier/vue',
'prettier',
'eslint:recommended',
'plugin:vue/recommended',
'standard',
],
// https://eslint.org/docs/user-guide/configuring#configuring-plugins
plugins: ['vue'],
rules: {
'vue/no-v-html': "off",
'no-console': 0,
'no-unused-vars': 1,
'no-undef': 1,
'vue/no-template-key': 1
},
}

114
.gitignore vendored
View File

@ -1,104 +1,18 @@
# Logs .DS_Store
logs dist/electron/*
*.log dist/web/*
npm-debug.log* build/*
yarn-debug.log* !build/icons
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/ node_modules/
jspm_packages/ npm-debug.log
npm-debug.log.*
# TypeScript v1 declaration files thumbs.db
typings/ !.gitkeep
data/tmp/
# TypeScript cache .tmp/
*.tsbuildinfo tmp/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache .cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist dist
coverage
# Gatsby files __coverage__
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
}

51
.travis.yml Normal file
View File

@ -0,0 +1,51 @@
matrix:
include:
- os: osx
osx_image: xcode9.3
language: node_js
node_js: '12'
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- os: linux
services: docker
language: node_js
node_js: '12'
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
before_install:
- |
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v2.3.1/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-2.3.1.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1
export PATH="/tmp/git-lfs:$PATH"
fi
before_script:
- git lfs pull
script:
- |
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
docker run --rm \
--env-file <(env | grep -v '\r' | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine
fi
- npm run build
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
branches:
except:
- "/^v\\d+\\.\\d+\\.\\d+$/"

8
.whitesource Normal file
View File

@ -0,0 +1,8 @@
##########################################################
#### WhiteSource "Bolt for Github" configuration file ####
##########################################################
# Configuration #
#---------------#
ws.repo.scan=true
vulnerable.check.run.conclusion.level=failure

BIN
_icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
_icons/icon.icns Normal file

Binary file not shown.

BIN
_icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
_icons/iconBlack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
_icons/iconBlackSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
_icons/iconColor.icns Normal file

Binary file not shown.

BIN
_icons/iconColor.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
_icons/iconColor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
_icons/iconColorSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

BIN
_icons/iconWhite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
_icons/logoBlack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
_icons/logoColor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
_icons/logoWhite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

1
_icons/mejs-controls.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="120" viewBox="0 0 400 120"><style>.st0{fill:#FFFFFF;width:16px;height:16px} .st1{fill:none;stroke:#FFFFFF;stroke-width:1.5;stroke-linecap:round;} .st2{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-linecap:round;} .st3{fill:none;stroke:#FFFFFF;} .st4{fill:#231F20;} .st5{opacity:0.75;fill:none;stroke:#FFFFFF;stroke-width:5;enable-background:new;} .st6{fill:none;stroke:#FFFFFF;stroke-width:5;} .st7{opacity:0.4;fill:#FFFFFF;enable-background:new;} .st8{opacity:0.6;fill:#FFFFFF;enable-background:new;} .st9{opacity:0.8;fill:#FFFFFF;enable-background:new;} .st10{opacity:0.9;fill:#FFFFFF;enable-background:new;} .st11{opacity:0.3;fill:#FFFFFF;enable-background:new;} .st12{opacity:0.5;fill:#FFFFFF;enable-background:new;} .st13{opacity:0.7;fill:#FFFFFF;enable-background:new;}</style><path class="st0" d="M16.5 8.5c.3.1.4.5.2.8-.1.1-.1.2-.2.2l-11.4 7c-.5.3-.8.1-.8-.5V2c0-.5.4-.8.8-.5l11.4 7z"/><path class="st0" d="M24 1h2.2c.6 0 1 .4 1 1v14c0 .6-.4 1-1 1H24c-.6 0-1-.4-1-1V2c0-.5.4-1 1-1zm9.8 0H36c.6 0 1 .4 1 1v14c0 .6-.4 1-1 1h-2.2c-.6 0-1-.4-1-1V2c0-.5.4-1 1-1z"/><path class="st0" d="M81 1.4c0-.6.4-1 1-1h5.4c.6 0 .7.3.3.7l-6 6c-.4.4-.7.3-.7-.3V1.4zm0 15.8c0 .6.4 1 1 1h5.4c.6 0 .7-.3.3-.7l-6-6c-.4-.4-.7-.3-.7.3v5.4zM98.8 1.4c0-.6-.4-1-1-1h-5.4c-.6 0-.7.3-.3.7l6 6c.4.4.7.3.7-.3V1.4zm0 15.8c0 .6-.4 1-1 1h-5.4c-.6 0-.7-.3-.3-.7l6-6c.4-.4.7-.3.7.3v5.4z"/><path class="st0" d="M112.7 5c0 .6.4 1 1 1h4.1c.6 0 .7-.3.3-.7L113.4.6c-.4-.4-.7-.3-.7.3V5zm-7.1 1c.6 0 1-.4 1-1V.9c0-.6-.3-.7-.7-.3l-4.7 4.7c-.4.4-.3.7.3.7h4.1zm1 7.1c0-.6-.4-1-1-1h-4.1c-.6 0-.7.3-.3.7l4.7 4.7c.4.4.7.3.7-.3v-4.1zm7.1-1c-.6 0-1 .4-1 1v4.1c0 .5.3.7.7.3l4.7-4.7c.4-.4.3-.7-.3-.7h-4.1z"/><path class="st0" d="M67 5.8c-.5.4-1.2.6-1.8.6H62c-.6 0-1 .4-1 1v5.7c0 .6.4 1 1 1h4.2c.3.2.5.4.8.6l3.5 2.6c.4.3.8.1.8-.4V3.5c0-.5-.4-.7-.8-.4L67 5.8z"/><path class="st1" d="M73.9 2.5s3.9-.8 3.9 7.7-3.9 7.8-3.9 7.8"/><path class="st1" d="M72.6 6.4s2.6-.4 2.6 3.8-2.6 3.9-2.6 3.9"/><path class="st0" d="M47 5.8c-.5.4-1.2.6-1.8.6H42c-.6 0-1 .4-1 1v5.7c0 .6.4 1 1 1h4.2c.3.2.5.4.8.6l3.5 2.6c.4.3.8.1.8-.4V3.5c0-.5-.4-.7-.8-.4L47 5.8z"/><path class="st2" d="M52.8 7l5.4 5.4m-5.4 0L58.2 7"/><path class="st3" d="M128.7 8.6c-6.2-4.2-6.5 7.8 0 3.9m6.5-3.9c-6.2-4.2-6.5 7.8 0 3.9"/><path class="st0" d="M122.2 3.4h15.7v13.1h-15.7V3.4zM120.8 2v15.7h18.3V2h-18.3z"/><path class="st0" d="M143.2 3h14c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2h-14c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2z"/><path class="st4" d="M146.4 13.8c-.8 0-1.6-.4-2.1-1-1.1-1.4-1-3.4.1-4.8.5-.6 2-1.7 4.6.2l-.6.8c-1.4-1-2.6-1.1-3.3-.3-.8 1-.8 2.4-.1 3.5.7.9 1.9.8 3.4-.1l.5.9c-.7.5-1.6.7-2.5.8zm7.5 0c-.8 0-1.6-.4-2.1-1-1.1-1.4-1-3.4.1-4.8.5-.6 2-1.7 4.6.2l-.5.8c-1.4-1-2.6-1.1-3.3-.3-.8 1-.8 2.4-.1 3.5.7.9 1.9.8 3.4-.1l.5.9c-.8.5-1.7.7-2.6.8z"/><path class="st0" d="M60.3 77c.6.2.8.8.6 1.4-.1.3-.3.5-.6.6L30 96.5c-1 .6-1.7.1-1.7-1v-35c0-1.1.8-1.5 1.7-1L60.3 77z"/><path class="st5" d="M2.5 79c0-20.7 16.8-37.5 37.5-37.5S77.5 58.3 77.5 79 60.7 116.5 40 116.5 2.5 99.7 2.5 79z"/><path class="st0" d="M140.3 77c.6.2.8.8.6 1.4-.1.3-.3.5-.6.6L110 96.5c-1 .6-1.7.1-1.7-1v-35c0-1.1.8-1.5 1.7-1L140.3 77z"/><path class="st6" d="M82.5 79c0-20.7 16.8-37.5 37.5-37.5s37.5 16.8 37.5 37.5-16.8 37.5-37.5 37.5S82.5 99.7 82.5 79z"/><circle class="st0" cx="201.9" cy="47.1" r="8.1"/><circle class="st7" cx="233.9" cy="79" r="5"/><circle class="st8" cx="201.9" cy="110.9" r="6"/><circle class="st9" cx="170.1" cy="79" r="7"/><circle class="st10" cx="178.2" cy="56.3" r="7.5"/><circle class="st11" cx="226.3" cy="56.1" r="4.5"/><circle class="st12" cx="225.8" cy="102.8" r="5.5"/><circle class="st13" cx="178.2" cy="102.8" r="6.5"/><path class="st0" d="M178 9.4c0 .4-.4.7-.9.7-.1 0-.2 0-.2-.1L172 8.2c-.5-.2-.6-.6-.1-.8l6.2-3.6c.5-.3.8-.1.7.5l-.8 5.1z"/><path class="st0" d="M169.4 15.9c-1 0-2-.2-2.9-.7-2-1-3.2-3-3.2-5.2.1-3.4 2.9-6 6.3-6 2.5.1 4.8 1.7 5.6 4.1l.1-.1 2.1 1.1c-.6-4.4-4.7-7.5-9.1-6.9-3.9.6-6.9 3.9-7 7.9 0 2.9 1.7 5.6 4.3 7 1.2.6 2.5.9 3.8 1 2.6 0 5-1.2 6.6-3.3l-1.8-.9c-1.2 1.2-3 2-4.8 2z"/><path class="st0" d="M183.4 3.2c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5-1.5-.7-1.5-1.5c0-.9.7-1.5 1.5-1.5zm5.1 0h8.5c.9 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5h-8.5c-.9 0-1.5-.7-1.5-1.5-.1-.9.6-1.5 1.5-1.5zm-5.1 5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5-1.5-.7-1.5-1.5c0-.9.7-1.5 1.5-1.5zm5.1 0h8.5c.9 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5h-8.5c-.9 0-1.5-.7-1.5-1.5-.1-.9.6-1.5 1.5-1.5zm-5.1 5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5-1.5-.7-1.5-1.5c0-.9.7-1.5 1.5-1.5zm5.1 0h8.5c.9 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5h-8.5c-.9 0-1.5-.7-1.5-1.5-.1-.9.6-1.5 1.5-1.5z"/></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
_icons/textBlack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
_icons/textBlackSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
_icons/textColor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
_icons/textColorSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
_icons/textWhite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

77
_scripts/build.js Normal file
View File

@ -0,0 +1,77 @@
const os = require('os')
const builder = require('electron-builder')
const Platform = builder.Platform
const { name, productName } = require('../package.json')
let targets
var platform = os.platform()
if (platform == 'darwin') {
targets = Platform.MAC.createTarget()
} else if (platform == 'win32') {
targets = Platform.WINDOWS.createTarget()
} else if (platform == 'linux') {
targets = Platform.LINUX.createTarget()
}
const config = {
appId: `io.freetubeapp.${name}`,
copyright: 'Copyleft ©2020 freetubeapp@protonmail.com',
// asar: false,
// compression: 'store',
// productName,
directories: {
output: './build/',
},
files: ['_icons/icon.*', './dist/**/*'],
dmg: {
contents: [
{
path: '/Applications',
type: 'link',
x: 410,
y: 230,
},
{
type: 'file',
x: 130,
y: 230,
},
],
window: {
height: 380,
width: 540,
},
},
linux: {
icon: '_icons/icon.png',
target: ['deb', 'snap', 'AppImage'],
},
mac: {
category: 'public.app-category.utilities',
icon: '_icons/icon.icns',
target: ['dmg', 'zip'],
type: 'distribution',
},
win: {
icon: '_icons/icon.ico',
target: ['nsis', 'zip', 'portable'],
},
nsis: {
allowToChangeInstallationDirectory: true,
oneClick: false,
},
}
builder
.build({
targets,
config,
})
.then(m => {
console.log(m)
})
.catch(e => {
console.error(e)
})

120
_scripts/dev-runner.js Normal file
View File

@ -0,0 +1,120 @@
process.env.NODE_ENV = 'development'
const electron = require('electron')
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const kill = require('tree-kill')
const path = require('path')
const { spawn } = require('child_process')
const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
const workersConfig = require('./webpack.workers.config')
let electronProcess = null
let manualRestart = null
const remoteDebugging = !!(
process.argv[2] && process.argv[2] === '--remote-debug'
)
if (remoteDebugging) {
// disable dvtools open in electron
process.env.RENDERER_REMOTE_DEBUGGING = true
}
async function killElectron(pid) {
return new Promise((resolve, reject) => {
if (pid) {
kill(pid, err => {
if (err) reject(err)
resolve()
})
} else {
resolve()
}
})
}
async function restartElectron() {
console.log('\nStarting electron...')
const { pid } = electronProcess || {}
await killElectron(pid)
electronProcess = spawn(electron, [
path.join(__dirname, '../dist/main.js'),
// '--enable-logging', Enable to show logs from all electron processes
remoteDebugging ? '--inspect=9222' : '',
remoteDebugging ? '--remote-debugging-port=9223' : '',
])
electronProcess.on('exit', (code, signal) => {
if (!manualRestart) process.exit(0)
})
}
function startMain() {
const webpackSetup = webpack([mainConfig, workersConfig])
webpackSetup.compilers.forEach(compiler => {
const { name } = compiler
switch (name) {
case 'workers':
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(`\nCompiled ${name} script!`)
console.log(`\nWatching file changes for ${name} script...`)
})
break
case 'main':
default:
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(`\nCompiled ${name} script!`)
manualRestart = true
await restartElectron()
setTimeout(() => {
manualRestart = false
}, 2500)
console.log(`\nWatching file changes for ${name} script...`)
})
break
}
})
webpackSetup.watch({
aggregateTimeout: 500,
},
err => {
if (err) console.error(err)
})
}
function startRenderer(callback) {
const compiler = webpack(rendererConfig)
const { name } = compiler
compiler.hooks.afterEmit.tap('afterEmit', () => {
console.log(`\nCompiled ${name} script!`)
console.log(`\nWatching file changes for ${name} script...`)
})
const server = new WebpackDevServer(compiler, {
contentBase: path.join(__dirname, '../'),
hot: true,
overlay: true,
clientLogLevel: 'warning'
})
server.listen(9080, '', err => {
if (err) console.error(err)
callback()
})
}
startRenderer(startMain)

View File

@ -0,0 +1,75 @@
const path = require('path')
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const {
dependencies,
devDependencies,
productName,
} = require('../package.json')
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies))
const isDevMode = process.env.NODE_ENV === 'development'
const whiteListedModules = []
const config = {
name: 'main',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval' : false,
entry: {
main: path.join(__dirname, '../src/main/index.js'),
},
externals: externals.filter(d => !whiteListedModules.includes(d)),
module: {
rules: [
{
test: /\.(j|t)s$/,
loader: ['babel-loader'],
exclude: /node_modules/,
},
{
test: /\.node$/,
use: 'node-loader',
},
],
},
node: {
__dirname: isDevMode,
__filename: isDevMode,
},
plugins: [
new webpack.DefinePlugin({
'process.env.PRODUCT_NAME': JSON.stringify(productName),
}),
],
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist'),
},
resolve: {
extensions: ['.ts', '.js', '.json'],
alias: {
'@': path.join(__dirname, '../src/'),
src: path.join(__dirname, '../src/'),
},
},
target: 'electron-main',
}
if (!isDevMode) {
config.plugins.push(
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../src/data'),
to: path.join(__dirname, '../dist/data'),
},
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/static'),
},
])
)
}
module.exports = config

View File

@ -0,0 +1,165 @@
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const {
dependencies,
devDependencies,
productName,
} = require('../package.json')
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies))
const isDevMode = process.env.NODE_ENV === 'development'
const whiteListedModules = ['vue']
const config = {
name: 'renderer',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval' : false,
entry: {
renderer: path.join(__dirname, '../src/renderer/main.js'),
},
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist'),
filename: '[name].js',
},
externals: externals.filter(d => !whiteListedModules.includes(d)),
module: {
rules: [
{
test: /\.(j|t)s$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.node$/,
use: 'node-loader',
},
{
test: /\.vue$/,
loader: 'vue-loader',
// use: {
// loader: 'vue-loader',
// options: {
// loaders: {
// sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
// },
// },
// },
},
{
test: /\.s(c|a)ss$/,
use: [
// {
// loader: 'vue-style-loader',
// },
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: isDevMode,
},
},
{
loader: 'css-loader',
},
{
loader: 'sass-loader',
options: {
// eslint-disable-next-line
implementation: require('sass'),
},
},
],
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: isDevMode,
},
},
// 'style-loader',
'css-loader',
],
},
{
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]',
},
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]',
},
},
},
],
},
node: {
__dirname: isDevMode,
__filename: isDevMode,
},
plugins: [
// new WriteFilePlugin(),
new HtmlWebpackPlugin({
excludeChunks: ['processTaskWorker'],
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
nodeModules: isDevMode
? path.resolve(__dirname, '../node_modules')
: false,
}),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.PRODUCT_NAME': JSON.stringify(productName),
}),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
resolve: {
alias: {
vue$: 'vue/dist/vue.common.js',
'@': path.join(__dirname, '../src/'),
src: path.join(__dirname, '../src/'),
icons: path.join(__dirname, '../_icons/'),
},
extensions: ['.ts', '.js', '.vue', '.json'],
},
target: 'electron-renderer',
}
/**
* Adjust rendererConfig for production settings
*/
if (isDevMode) {
// any dev only config
config.plugins.push(new webpack.HotModuleReplacementPlugin())
} else {
config.plugins.push(
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/static'),
},
])
)
}
module.exports = config

View File

@ -0,0 +1,67 @@
const path = require('path')
const webpack = require('webpack')
const {
dependencies,
devDependencies,
productName,
} = require('../package.json')
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies))
const isDevMode = process.env.NODE_ENV === 'development'
const config = {
name: 'workers',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval' : false,
entry: {
workerSample: path.join(__dirname, '../src/utilities/workerSample.ts'),
},
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist'),
filename: '[name].js',
},
externals: externals,
module: {
rules: [
{
test: /\.(j|t)s$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.node$/,
use: 'node-loader',
},
],
},
node: {
__dirname: isDevMode,
__filename: isDevMode,
},
plugins: [
// new WriteFilePlugin(),
new webpack.DefinePlugin({
'process.env.PRODUCT_NAME': JSON.stringify(productName),
}),
],
resolve: {
alias: {
'@': path.join(__dirname, '../src/'),
src: path.join(__dirname, '../src/'),
},
extensions: ['.ts', '.js', '.json'],
},
target: 'node',
}
/**
* Adjust rendererConfig for production settings
*/
if (isDevMode) {
// any dev only config
config.plugins.push(new webpack.HotModuleReplacementPlugin())
}
module.exports = config

20
appveyor.yml Normal file
View File

@ -0,0 +1,20 @@
image: Visual Studio 2017
platform:
- x64
cache:
- node_modules
- '%USERPROFILE%\.electron'
init:
- git config --global core.autocrlf input
install:
- ps: Install-Product node 12 x64
- npm install
build_script:
- npm run build
test: off

15692
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

124
package.json Normal file
View File

@ -0,0 +1,124 @@
{
"author": {
"name": "PrestonN",
"email": "FreeTubeApp@protonmail.com",
"url": "https://github.com/FreeTubeApp/FreeTube"
},
"bugs": {
"url": "https://github.com/FreeTubeApp/FreeTube/issues"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/vue-fontawesome": "^0.1.9",
"autolinker": "^3.11.1",
"bulma-pro": "^0.1.8",
"dateformat": "^3.0.3",
"electron-context-menu": "^0.16.0",
"jquery": "^3.4.1",
"lodash.isequal": "^4.5.0",
"material-design-icons": "^3.0.1",
"mediaelement": "^4.2.14",
"nedb": "^1.8.0",
"opml-to-json": "0.0.3",
"video.js": "^7.6.6",
"videojs-replay": "^1.1.0",
"vue": "^2.6.11",
"vue-electron": "^1.0.6",
"vue-router": "^3.1.5",
"vuex": "^3.1.2",
"xml2json": "^0.12.0",
"youtube-chat": "^1.0.2",
"youtube-comments-fetch": "^1.0.1",
"youtube-comments-task": "^1.3.14",
"youtube-suggest": "^1.1.0",
"ytdl-core": "^1.0.7",
"ytpl": "^0.1.20",
"ytsr": "^0.1.10"
},
"description": "A private YouTube client",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-typescript": "^7.8.3",
"@typescript-eslint/eslint-plugin": "^2.19.0",
"@typescript-eslint/parser": "^2.19.0",
"acorn": "^7.1.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"devtron": "^1.4.0",
"electron": "^8.0.0",
"electron-builder": "^22.3.2",
"electron-debug": "^3.0.1",
"electron-rebuild": "^1.10.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.1.2",
"fast-glob": "^3.1.1",
"file-loader": "^5.0.2",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"node-loader": "^0.6.0",
"npm-run-all": "^4.1.5",
"prettier": "^1.19.1",
"sass": "^1.25.0",
"sass-loader": "^8.0.2",
"style-loader": "^1.1.3",
"tree-kill": "1.2.2",
"typescript": "^3.7.5",
"url-loader": "^3.0.0",
"vue-devtools": "^5.1.3",
"vue-eslint-parser": "^7.0.0",
"vue-loader": "^15.8.3",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.3"
},
"license": "GPL-3.0-or-later",
"main": "./dist/main.js",
"name": "freetube",
"private": true,
"productName": "FreeTube",
"repository": {
"type": "git",
"url": "git+https://github.com/mubaidr/vue-electron-template.git"
},
"scripts": {
"build": "run-s rebuild:electron pack build-release",
"build-release": "node _scripts/build.js",
"debug": "run-s rebuild:electron debug-runner",
"debug-runner": "node _scripts/dev-runner.js --remote-debug",
"dev": "run-s rebuild:electron dev-runner",
"dev-runner": "node _scripts/dev-runner.js",
"electron-builder-install": "electron-builder install-app-deps",
"electron-rebuild": "electron-rebuild",
"jest": "jest",
"jest:coverage": "jest --collect-coverage",
"jest:watch": "jest --watch",
"lint": "eslint --fix --ext .js,.ts,.vue ./",
"pack": "run-p pack:main pack:renderer pack:workers",
"pack:main": "webpack --mode=production --env.NODE_ENV=production --hide-modules --config _scripts/webpack.main.config.js",
"pack:renderer": "webpack --mode=production --env.NODE_ENV=production --hide-modules --config _scripts/webpack.renderer.config.js",
"pack:workers": "webpack --mode=production --env.NODE_ENV=production --hide-modules --config _scripts/webpack.workers.config.js",
"postinstall": "electron-rebuild",
"prettier": "prettier --write \"{src,_scripts}/**/*.{js,ts,vue}\"",
"rebuild:electron": "run-s electron-builder-install electron-rebuild",
"rebuild:node": "npm rebuild",
"release": "run-s test build",
"test": "run-s rebuild:node pack:workers jest",
"test:watch": "run-s rebuild:node pack:workers jest:watch"
},
"version": "0.8.0"
}

0
src/data/.gitkeep Normal file
View File

30
src/index.ejs Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0" />
<title></title>
<% if (htmlWebpackPlugin.options.nodeModules) { %>
<script>
require('module').globalPaths.push(
`<%= htmlWebpackPlugin.options.nodeModules.replace(/\\/g, '\\\\') %>`
)
</script>
<% } %>
</head>
<body class="redLight">
<div id="app"></div>
<!-- Set `__static` path to static files in production -->
<script>
if (process.env.NODE_ENV !== 'development')
window.__static = require('path')
.join(__dirname, '/static')
.replace(/\\/g, '\\\\')
</script>
<!-- webpack builds are automatically injected -->
</body>
</html>

247
src/main/index.js Normal file
View File

@ -0,0 +1,247 @@
import { app, BrowserWindow, Menu } from 'electron'
import { productName } from '../../package.json'
// set app name
app.setName(productName)
// disable electron warning
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
const gotTheLock = app.requestSingleInstanceLock()
const isDev = process.env.NODE_ENV === 'development'
const isDebug = process.argv.includes('--debug')
let mainWindow
// only allow single instance of application
if (!isDev) {
if (gotTheLock) {
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow && mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.focus()
})
} else {
app.quit()
process.exit(0)
}
} else {
require('electron-debug')({
showDevTools: !(process.env.RENDERER_REMOTE_DEBUGGING === 'true')
})
}
async function installDevTools () {
try {
/* eslint-disable */
require('devtron').install()
require('vue-devtools').install()
/* eslint-enable */
} catch (err) {
console.log(err)
}
}
function createWindow () {
/**
* Initial window options
*/
mainWindow = new BrowserWindow({
backgroundColor: '#fff',
width: 960,
height: 540,
minWidth: 960,
minHeight: 540,
// useContentSize: true,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: false,
webSecurity: false
},
show: false
})
// eslint-disable-next-line
setMenu()
// load root file/url
if (isDev) {
mainWindow.loadURL('http://localhost:9080')
} else {
mainWindow.loadFile(`${__dirname}/index.html`)
global.__static = require('path')
.join(__dirname, '/static')
.replace(/\\/g, '\\\\')
}
// Show when loaded
mainWindow.on('ready-to-show', () => {
mainWindow.show()
mainWindow.focus()
})
mainWindow.on('closed', () => {
console.log('closed')
})
}
app.on('ready', () => {
createWindow()
if (isDev) {
installDevTools()
}
if (isDebug) {
mainWindow.webContents.openDevTools()
}
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
/**
* Auto Updater
*
* Uncomment the following code below and install `electron-updater` to
* support auto updating. Code Signing with a valid certificate is required.
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
*/
/*
import { autoUpdater } from 'electron-updater'
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall()
})
app.on('ready', () => {
if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
})
*/
/* eslint-disable-next-line */
const sendMenuEvent = async data => {
mainWindow.webContents.send('change-view', data)
}
const template = [{
label: 'File',
submenu: [
{
role: 'quit'
}
]
},
{
label: 'Edit',
submenu: [{
role: 'cut'
},
{
role: 'copy',
accelerator: 'CmdOrCtrl+C',
selector: 'copy:'
},
{
role: 'paste',
accelerator: 'CmdOrCtrl+V',
selector: 'paste:'
},
{
role: 'pasteandmatchstyle'
},
{
role: 'delete'
},
{
role: 'selectall'
}
]
},
{
label: 'View',
submenu: [{
role: 'reload'
},
{
role: 'forcereload',
accelerator: 'CmdOrCtrl+Shift+R'
},
{
role: 'toggledevtools'
},
{
type: 'separator'
},
{
role: 'resetzoom'
},
{
role: 'zoomin'
},
{
role: 'zoomout'
},
{
type: 'separator'
},
{
role: 'togglefullscreen'
}
]
},
{
role: 'window',
submenu: [{
role: 'minimize'
},
{
role: 'close'
}
]
}
]
function setMenu () {
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
})
template.push({
role: 'window'
})
template.push({
role: 'help'
})
template.push({ role: 'services' })
}
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}

24
src/renderer/App.css Normal file
View File

@ -0,0 +1,24 @@
@font-face {
font-family: Roboto;
src: url(assets/font/Roboto-Regular.ttf);
}
body {
min-height: 100vh;
}
#app {
font-family: 'Roboto', sans-serif;
}
.routerView {
margin-left: 200px;
margin-top: 80px;
transition-property: margin;
transition-duration: 150ms;
transition-timing-function: ease-in-out;
}
.expand {
margin-left: 80px;
}

29
src/renderer/App.js Normal file
View File

@ -0,0 +1,29 @@
import Vue from 'vue'
import TopNav from './components/top-nav/top-nav.vue'
import SideNav from './components/side-nav/side-nav.vue'
import $ from 'jquery'
import { shell } from 'electron'
export default Vue.extend({
name: 'App',
components: {
TopNav,
SideNav
},
computed: {
isOpen: function () {
return this.$store.getters.getIsSideNavOpen
}
},
mounted: function () {
// Open links externally by default
$(document).on('click', 'a[href^="http"]', (event) => {
const el = event.currentTarget
console.log(el)
if (typeof (shell) !== 'undefined') {
event.preventDefault()
shell.openExternal(el.href)
}
})
}
})

24
src/renderer/App.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<div id="app">
<top-nav ref="topNav" />
<side-nav ref="sideNav" />
<Transition
mode="out-in"
name="slide-up"
>
<!-- <keep-alive> -->
<RouterView
ref="router"
class="routerView"
:class="{ expand: !isOpen }"
/>
<!-- </keep-alive> -->
</Transition>
</div>
</template>
<script src="./App.js" />
<style src="./themes.css" />
<style src="./videoJS.css" />
<style scoped src="./App.css" />

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,50 @@
.btn {
font-family: 'Roboto', sans-serif;
min-width: 100px;
font-size: 0.9rem;
padding: 10px 20px;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
align-items: center;
justify-content: center;
text-align: center;
text-decoration: none;
transition: 0.3s;
border-radius: 4px;
outline: none;
white-space: nowrap;
font-weight: 500;
vertical-align: middle;
margin: 5px;
box-shadow: 0 0 2px -2px rgba(29, 39, 231, .1), 0 0 3px 0 rgba(29, 39, 231, .1), 0 0 5px 0 rgba(29, 39, 231, .1), 0 2px 2px -4px rgba(29, 39, 231, .1), 0 4px 8px 0 rgba(29, 39, 231, .1), 0 2px 15px 0 rgba(29, 39, 231, .1);
}
.ripple {
position: relative;
overflow: hidden;
transform: translate3d(0, 0, 0);
}
.ripple:after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
background-image: radial-gradient(circle, #fff 10%, transparent 10.01%);
background-repeat: no-repeat;
background-position: 50%;
transform: scale(10, 10);
opacity: 0;
transition: transform .5s, opacity 1s;
}
.ripple:active:after {
transform: scale(0, 0);
opacity: .3;
transition: 0s;
}

View File

@ -0,0 +1,29 @@
import Vue from 'vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListChannel from '../ft-list-channel/ft-list-channel.vue'
import FtListPlaylist from '../ft-list-playlist/ft-list-playlist.vue'
export default Vue.extend({
name: 'FtElementList',
components: {
'ft-flex-box': FtFlexBox,
'ft-list-video': FtListVideo,
'ft-list-channel': FtListChannel,
'ft-list-playlist': FtListPlaylist
},
props: {
label: {
type: String,
required: true
},
textColor: {
type: String,
default: '#FFFFFF'
},
backgroundColor: {
type: String,
default: '#2196F3'
}
}
})

View File

@ -0,0 +1,16 @@
<template>
<button
class="btn ripple"
:style="{
color: textColor,
backgroundColor: backgroundColor,
border: `2px solid ${backgroundColor}`
}"
@click="$emit('click')"
>
{{ label }}
</button>
</template>
<script src="./ft-button.js" />
<style scoped src="./ft-button.css" />

View File

@ -0,0 +1,8 @@
.ft-card {
background-color: var(--card-bg-color);
margin: 8px;
padding: 16px;
padding-top: 3px;
padding-bottom: 16px;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
}

View File

@ -0,0 +1,5 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtCard'
})

View File

@ -0,0 +1,8 @@
<template>
<div class="ft-card">
<slot />
</div>
</template>
<script src="./ft-card.js" />
<style scoped src="./ft-card.css" />

View File

@ -0,0 +1,33 @@
.bubblePadding {
width: 100px;
height: 115px;
padding: 10px;
cursor: pointer;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.bubblePadding:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.bubble {
width: 50px;
height: 50px;
margin-bottom: 5px;
margin-left: 25px;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.channelName {
font-size: 13px;
height: 60px;
overflow: hidden;
text-align: center;
}

View File

@ -0,0 +1,24 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtChannelBubble',
props: {
channelName: {
type: String,
required: true
},
channelId: {
type: String,
required: true
},
channelThumbnail: {
type: String,
required: true
}
},
methods: {
goToChannel: function () {
console.log('Go to channel')
}
}
})

View File

@ -0,0 +1,17 @@
<template>
<div
class="bubblePadding"
@click="goToChannel(channelId)"
>
<img
class="bubble"
:src="channelThumbnail"
>
<div class="channelName">
{{ channelName }}
</div>
</div>
</template>
<script src="./ft-channel-bubble.js" />
<style scoped src="./ft-channel-bubble.css" />

View File

@ -0,0 +1,3 @@
.maxWidth {
width: 100%;
}

View File

@ -0,0 +1,33 @@
import Vue from 'vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtGrid from '../ft-grid/ft-grid.vue'
import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListChannel from '../ft-list-channel/ft-list-channel.vue'
import FtListPlaylist from '../ft-list-playlist/ft-list-playlist.vue'
export default Vue.extend({
name: 'FtElementList',
components: {
'ft-flex-box': FtFlexBox,
'ft-grid': FtGrid,
'ft-list-video': FtListVideo,
'ft-list-channel': FtListChannel,
'ft-list-playlist': FtListPlaylist
},
props: {
data: {
type: Array,
required: true
}
},
data: function () {
return {
test: 'hello'
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
}
}
})

View File

@ -0,0 +1,50 @@
<template>
<span>
<ft-flex-box
v-if="listType === 'list'"
>
<span
v-for="(result, index) in data"
:key="index"
class="maxWidth"
>
<ft-list-channel
v-if="result.type === 'channel'"
:data="result"
/>
<ft-list-video
v-if="result.type === 'video' || result.type === 'shortVideo'"
:data="result"
/>
<ft-list-playlist
v-if="result.type === 'playlist'"
:data="result"
/>
</span>
</ft-flex-box>
<ft-grid
v-else
>
<span
v-for="(result, index) in data"
:key="index"
>
<ft-list-channel
v-if="result.type === 'channel'"
:data="result"
/>
<ft-list-video
v-if="result.type === 'video' || result.type === 'shortVideo'"
:data="result"
/>
<ft-list-playlist
v-if="result.type === 'playlist'"
:data="result"
/>
</span>
</ft-grid>
</span>
</template>
<script src="./ft-element-list.js" />
<style scoped src="./ft-element-list.css" />

View File

@ -0,0 +1,5 @@
.ft-flex-box {
display: flex;
flex-flow: row wrap;
justify-content: space-evenly;
}

View File

@ -0,0 +1,5 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtFlexBox'
})

View File

@ -0,0 +1,8 @@
<template>
<div class="ft-flex-box">
<slot class="flex-container" />
</div>
</template>
<script src="./ft-flex-box.js" />
<style scoped src="./ft-flex-box.css" />

View File

@ -0,0 +1,6 @@
.ft-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 240px);
justify-content: space-evenly;
grid-gap: 5px;
}

View File

@ -0,0 +1,5 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtGrid'
})

View File

@ -0,0 +1,8 @@
<template>
<div class="ft-grid">
<slot />
</div>
</template>
<script src="./ft-grid.js" />
<style scoped src="./ft-grid.css" />

View File

@ -0,0 +1,46 @@
.ft-input-component {
position: relative;
}
.ft-input {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
outline: none;
width: 100%;
padding: 7px;
border: none;
background: transparent;
margin-bottom: 10px;
font: 16px;
height: 45px;
border-bottom: 1px solid var(--primary-input-color);
}
.ft-input-component ::-webkit-input-placeholder {
color: var(--primary-input-color);
}
.inputAction {
position: absolute;
padding: 10px;
top: 10px;
right: 0px;
cursor: pointer;
border-radius: 200px 200px 200px 200px;
color: var(--primary-text-color);
}
.inputAction:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.inputAction:active {
background-color: var(--teritary-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -0,0 +1,44 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtInput',
props: {
placeholder: {
type: String,
required: true
},
showArrow: {
type: Boolean,
default: true
}
},
data: function () {
return {
id: '',
inputData: '',
component: this
}
},
mounted: function () {
this.id = this._uid
setTimeout(this.addListener, 200)
},
methods: {
handleClick: function () {
this.$emit('click', this.inputData)
},
addListener: function () {
const inputElement = document.getElementById(this.id)
if (inputElement !== null) {
inputElement.addEventListener('keydown', (event) => {
if (event.keyCode === 13) {
this.handleClick()
}
})
}
}
}
})

View File

@ -0,0 +1,20 @@
<template>
<div class="ft-input-component">
<input
:id="id"
v-model="inputData"
class="ft-input"
type="text"
:placeholder="placeholder"
>
<font-awesome-icon
v-if="showArrow"
icon="arrow-right"
class="inputAction"
@click="handleClick(inputData, component)"
/>
</div>
</template>
<script src="./ft-input.js" />
<style scoped src="./ft-input.css" />

View File

@ -0,0 +1,93 @@
.channelThumbnail {
position: relative;
cursor: pointer;
}
.channelThumbnail img {
width: 140px;
height: 140px;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.channelName {
font-weight: bold;
color: var(--title-color);
cursor: pointer;
margin-bottom: 0px;
}
.subscriberCount {
cursor: pointer;
font-size: 13px;
color: var(--secondary-text-color);
}
.videoCount {
cursor: pointer;
font-size: 13px;
color: var(--secondary-text-color);
}
.grid {
width: 240px;
height: 250px;
padding: 2px;
overflow: hidden;
}
.grid .channelThumbnail {
width: 100%;
height: 140px;
margin-bottom: 5px;
}
.grid .channelThumbnail img {
position: relative;
left: 50px;
}
.grid .channelName {
width: 240px;
}
.list {
height: 140px;
width: 100%;
margin-left: 5px;
margin-top: 15px;
border-bottom: 1px solid var(--secondary-text-color);
}
.list .channelThumbnail {
float: left;
width: 240px;
}
.list .channelThumbnail img {
position: relative;
width: 120px;
height: 120px;
left: 60px;
}
.list .channelName {
margin-left: 250px;
width: 275px;
}
.list .subscriberCount {
margin-left: 10px;
}
.list .videoCount {
margin-left: 1px;
}
.list .description {
margin-left: 250px;
font-size: 13px;
color: var(--secondary-text-color);
height: 35px;
overflow: hidden;
}

View File

@ -0,0 +1,57 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtListChannel',
props: {
data: {
type: Object,
required: true
}
},
data: function () {
return {
id: '',
thumbnail: '',
channelName: '',
subscriberCount: 0,
videoCount: '',
uploadedTime: '',
description: ''
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
}
},
mounted: function () {
if (typeof (this.data.avatar) !== 'undefined') {
this.parseLocalData()
} else {
this.parseInvidiousData()
}
},
methods: {
goToChannel: function () {
this.$router.push({ path: `/channel/${this.id}` })
},
parseLocalData: function () {
this.thumbnail = this.data.avatar
this.channelName = this.data.name
this.id = this.data.channel_id
this.subscriberCount = this.data.followers.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.videoCount = this.data.videos.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.description = this.data.description_short
},
parseInvidiousData: function () {
this.thumbnail = this.data.authorThumbnails[2].url
this.channelName = this.data.author
this.id = this.data.authorId
this.subscriberCount = this.data.subCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.videoCount = this.data.videoCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.description = this.data.description
}
}
})

View File

@ -0,0 +1,40 @@
<template>
<div
class="ft-list-channel"
:class="{ list: listType === 'list', grid: listType === 'grid' }"
>
<div class="channelThumbnail">
<img
:src="thumbnail"
@click="goToChannel(id)"
>
</div>
<p
class="channelName"
@click="goToChannel(id)"
>
{{ channelName }}
</p>
<span
class="subscriberCount"
@click="goToChannel(id)"
>
{{ subscriberCount }} subscribers
</span>
<span
class="videoCount"
@click="goToChannel(id)"
>
- {{ videoCount }} videos
</span>
<p
v-if="listType !== 'grid'"
class="description"
>
{{ description }}
</p>
</div>
</template>
<script src="./ft-list-channel.js" />
<style scoped src="./ft-list-channel.css" />

View File

@ -0,0 +1,68 @@
.dropDown {
position: relative;
text-align: center;
text-transform: uppercase;
margin: 0 5px;
}
.dropDown ul {
display: none;
position: absolute;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
z-index: 100;
}
.dropDown ul li {
width: 100%;
float: none;
box-sizing: border-box;
}
.dropDown:hover ul {
display: block;
}
.dropDown ul li a {
text-decoration: none;
color: inherit;
}
.buttonTitle {
height: 30px;
font-size: 10px;
line-height: 30px;
text-align: center;
padding-left: 15px;
padding-right: 15px;
cursor: pointer;
color: var(--teritary-text-color);
background-color: var(--secondary-card-bg-color);
}
.buttonOption {
float: right;
width: 100%;
height: 30px;
font-size: 10px;
line-height: 30px;
text-align: center;
padding-left: 15px;
padding-right: 15px;
cursor: pointer;
color: var(--teritary-text-color);
background-color: var(--secondary-card-bg-color);
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.buttonOption:hover {
background-color: var(--card-bg-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -0,0 +1,42 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtListDropdown',
props: {
title: {
type: String,
required: true
},
labelNames: {
type: Array,
required: true
},
labelValues: {
type: Array,
required: true
}
},
data: function () {
return {
id: '',
thumbnail: '',
channelName: '',
subscriberCount: 0,
videoCount: '',
uploadedTime: '',
description: ''
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
}
},
mounted: function () {
},
methods: {
goToChannel: function () {
console.log('TODO: ft-list-channel method goToChannel')
}
}
})

View File

@ -0,0 +1,24 @@
<template>
<div class="dropDown">
<div class="buttonTitle">
{{ title }}
<font-awesome-icon
class="angleDownIcon"
icon="angle-down"
/>
</div>
<ul>
<li
v-for="(label, index) in labelNames"
:key="index"
class="buttonOption"
@click="$emit('click', labelValues[index])"
>
{{ label }}
</li>
</ul>
</div>
</template>
<script src="./ft-list-dropdown.js" />
<style scoped src="./ft-list-dropdown.css" />

View File

@ -0,0 +1,106 @@
.videoThumbnail {
position: relative;
cursor: pointer;
}
.videoCountContainer {
position: absolute;
top: 0px;
right: 0px;
width: 120px;
background-color: rgba(0,0,0,0.6);
color: #FFFFFF;
text-align: center;
font-size: 20px;
}
.videoCountContainer span {
position: absolute;
top: 40px;
left: 45px;
}
.playlistTitle {
font-weight: bold;
color: var(--title-color);
cursor: pointer;
}
.channelName {
color: var(--secondary-text-color);
cursor: pointer;
font-size: 14px;
}
.grid {
width: 240px;
height: 250px;
padding: 2px;
overflow: hidden;
}
.grid .videoThumbnail {
width: 100%;
height: 130px;
margin-bottom: -5px;
}
.grid .videoThumbnail img {
width: 100%;
height: 130px;
}
.grid .videoCountContainer {
height: 130px;
}
.grid .playlistTitle {
max-height: 75px;
overflow-y: hidden;
}
.grid .channelName {
width: 275px;
}
.list {
height: 140px;
width: 100%;
margin-left: 5px;
margin-top: 15px;
border-bottom: 1px solid var(--secondary-text-color);
}
.list .videoThumbnail {
float: left;
width: 240px;
height: 130px;
}
.list .videoThumbnail img {
width: 100%;
height: 130px;
}
.list .videoCountContainer {
height: 130px;
}
.list .playlistTitle {
margin-left: 250px;
margin-top: 5px;
margin-bottom: -10px;
}
.list .channelName {
margin-left: 250px;
width: 275px;
}
.list .description {
margin-left: 285px;
font-size: 13px;
color: var(--secondary-text-color);
height: 35px;
overflow: hidden;
}

View File

@ -0,0 +1,71 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtListVideo',
props: {
data: {
type: Object,
required: true
}
},
data: function () {
return {
playlistLink: '',
channelLink: '',
title: 'Pop Music Playlist - Timeless Pop Songs (Updated Weekly 2020)',
thumbnail: 'https://i.ytimg.com/vi/JGwWNGJdvx8/mqdefault.jpg',
channelName: '#RedMusic: Just Hits',
videoCount: 200,
description: ''
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
},
playlistId: function () {
return this.playlistLink.replace('https://www.youtube.com/playlist?list=', '')
},
channelId: function () {
let id = this.channelLink.replace('https://www.youtube.com/user/', '')
id = id.replace('https://www.youtube.com/channel/', '')
return id
}
},
mounted: function () {
if (typeof (this.data.author) === 'object') {
this.parseLocalData()
} else {
this.parseInvidiousData()
}
},
methods: {
parseInvidiousData: function () {
this.title = this.data.title
this.thumbnail = this.data.playlistThumbnail
this.channelName = this.data.author
this.channelLink = this.data.authorUrl
this.playlistLink = this.data.playlistId
this.videoCount = this.data.videoCount
},
parseLocalData: function () {
this.title = this.data.title
this.thumbnail = this.data.thumbnail
this.channelName = this.data.author.name
this.channelLink = this.data.author.ref
this.playlistLink = this.data.link
this.videoCount = parseInt(this.data.length.split(' ')[0])
},
goToPlaylist: function (id) {
this.$router.push({ path: `/playlist/${id}` })
},
goToChannel: function (id) {
console.log(id)
}
}
})

View File

@ -0,0 +1,38 @@
<template>
<div
class="ft-list-video"
:class="{ list: listType === 'list', grid: listType === 'grid' }"
>
<div class="videoThumbnail">
<img
:src="thumbnail"
@click="goToPlaylist(playlistId)"
>
<div
class="videoCountContainer"
@click="goToPlaylist(playlistId)"
>
<span>
{{ videoCount }}
<br>
<font-awesome-icon icon="list" />
</span>
</div>
</div>
<p
class="playlistTitle"
@click="goToPlaylist(playlistId)"
>
{{ title }}
</p>
<p
class="channelName"
@click="goToChannel(channelId)"
>
{{ channelName }}
</p>
</div>
</template>
<script src="./ft-list-playlist.js" />
<style scoped src="./ft-list-playlist.css" />

View File

@ -0,0 +1,172 @@
.videoThumbnail {
position: relative;
cursor: pointer;
}
.videoThumbnail:hover .videoWatched {
visibility: hidden;
}
.favoritesIcon {
position: absolute;
font-size: 15px;
top: 0px;
right: 0px;
color: #FFFFFF;
padding: 5px;
background-color: #000000;
opacity: 0.7;
cursor: pointer;
}
.favorited {
color: yellow;
}
.videoDuration {
position: absolute;
font-size: 13px;
bottom: -7px;
right: 0px;
color: #FFFFFF;
padding: 2px;
background-color: #000000;
opacity: 0.7;
cursor: pointer;
}
.videoWatched {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
background-color: rgba(0,0,0,0.4);
color: #FFFFFF;
pointer-events: none;
}
.watchedProgressBar {
background-color: var(--red-500);
opacity: 0.8;
height: 3px;
position: absolute;
bottom: 0px;
left: 0px;
cursor: pointer;
}
.videoTitle {
color: var(--title-color);
font-weight: bold;
cursor: pointer;
}
.channelName {
color: var(--secondary-text-color);
cursor: pointer;
font-size: 14px;
}
.viewCount {
cursor: pointer;
font-size: 13px;
}
.uploadedTime {
cursor: pointer;
font-size: 13px;
}
.liveText {
color: var(--red-500);
font-size: 13px;
cursor: pointer;
}
.grid {
width: 240px;
height: 250px;
padding: 2px;
overflow: hidden;
}
.grid .videoThumbnail {
width: 100%;
height: 130px;
margin-bottom: -5px;
}
.grid .videoThumbnail img {
width: 100%;
height: 130px;
}
.grid .videoWatched {
height: 110px;
}
.grid .videoTitle {
max-height: 55px;
overflow-y: hidden;
margin-bottom: -15px;
}
.grid .channelName {
width: 220px;
height: 17px;
margin-bottom: 10px;
overflow-y: hidden;
}
.grid .liveText {
float: right;
}
.list {
height: 140px;
width: 100%;
margin-left: 5px;
margin-top: 15px;
}
.list .videoThumbnail {
float: left;
width: 240px;
height: 130px;
}
.list .videoThumbnail img {
width: 100%;
height: 130px;
}
.list .videoWatched {
height: 130px;
}
.list .videoTitle {
margin-left: 250px;
margin-top: 5px;
margin-bottom: -10px;
}
.list .channelName {
margin-left: 250px;
max-width: 275px;
}
.list .viewCount {
margin-left: 10px;
}
.list .liveText {
margin-left: 10px;
}
.list .description {
margin-left: 250px;
font-size: 13px;
color: var(--secondary-text-color);
height: 35px;
overflow: hidden;
}

View File

@ -0,0 +1,199 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtListVideo',
props: {
data: {
type: Object,
required: true
},
forceListType: {
type: String,
default: null
}
},
data: function () {
return {
id: '',
title: '',
thumbnail: '',
channelName: '',
channelId: '',
viewCount: 0,
uploadedTime: '',
duration: '',
description: '',
watched: false,
progressPercentage: 0,
isLive: false,
isFavorited: false,
hideViews: false
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
},
clickBaitRemoverPreference: function () {
return this.$store.getters.getClickBaitRemoverPreference
}
},
mounted: function () {
// Check if data came from Invidious or from local backend
if (typeof (this.data.descriptionHtml) !== 'undefined' ||
typeof (this.data.index) !== 'undefined' ||
typeof (this.data.publishedText) !== 'undefined' ||
typeof (this.data.authorThumbnails) === 'object'
) {
this.parseInvidiousData()
} else {
console.log('parsing local data')
this.parseLocalData()
}
},
methods: {
play: function () {
this.$router.push({ path: `/watch/${this.id}` })
},
goToChannel: function () {
console.log(this.data)
this.$router.push({ path: `/channel/${this.channelId}` })
},
toggleSave: function () {
console.log('TODO: ft-list-video method toggleSave')
},
// For Invidious data, as duration is sent in seconds
calculateVideoDuration: function (lengthSeconds) {
let durationText = ''
let time = lengthSeconds
let hours = 0
if (time >= 3600) {
hours = Math.floor(time / 3600)
time = time - hours * 3600
}
let minutes = Math.floor(time / 60)
let seconds = time - minutes * 60
if (seconds < 10) {
seconds = '0' + seconds
}
if (minutes < 10 && hours > 0) {
minutes = '0' + minutes
}
if (hours > 0) {
durationText = hours + ':' + minutes + ':' + seconds
} else {
durationText = minutes + ':' + seconds
}
return durationText
},
parseInvidiousData: function () {
this.id = this.data.videoId
this.title = this.data.title
// this.thumbnail = this.data.videoThumbnails[4].url
switch (this.clickBaitRemoverPreference) {
case 'start':
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq1.jpg`
break
case 'middle':
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq2.jpg`
break
case 'end':
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq3.jpg`
break
default:
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mqdefault.jpg`
break
}
this.channelName = this.data.author
this.channelId = this.data.authorId
this.duration = this.calculateVideoDuration(this.data.lengthSeconds)
this.description = this.data.description
this.isLive = this.data.liveNow
if (typeof (this.data.publishedText) !== 'undefined') {
this.uploadedTime = this.data.publishedText
}
if (typeof (this.data.viewCount) !== 'undefined' && this.data.viewCount !== null) {
this.viewCount = this.data.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} else if (typeof (this.data.viewCountText) !== 'undefined') {
this.viewCount = this.data.viewCountText.replace(' views', '')
} else {
this.hideViews = true
}
},
parseLocalData: function () {
if (typeof (this.data.id) !== 'undefined') {
this.id = this.data.id
} else {
this.id = this.data.link.replace('https://www.youtube.com/watch?v=', '')
}
this.title = this.data.title
// this.thumbnail = this.data.thumbnail
switch (this.clickBaitRemoverPreference) {
case 'start':
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq1.jpg`
break
case 'middle':
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq2.jpg`
break
case 'end':
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq3.jpg`
break
default:
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mqdefault.jpg`
break
}
if (typeof (this.data.author) === 'string') {
this.channelName = this.data.author
this.channelId = this.data.ucid
// Data is returned as a literal string names 'undefined'
if (this.data.length_seconds !== 'undefined') {
this.duration = this.calculateVideoDuration(parseInt(this.data.length_seconds))
}
} else {
this.channelName = this.data.author.name
this.duration = this.data.duration
this.description = this.data.description
this.channelId = this.data.author.ref.replace('https://www.youtube.com/user/', '')
this.channelId = this.channelId.replace('https://www.youtube.com/channel/', '')
}
if (typeof (this.data.uploaded_at) !== 'undefined') {
this.uploadedTime = this.data.uploaded_at
}
if (this.data.views !== null && typeof (this.data.views) !== 'undefined') {
this.viewCount = this.data.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} else if (typeof (this.data.view_count) !== 'undefined') {
const viewCount = this.data.view_count.replace(',', '')
this.viewCount = viewCount.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} else {
this.hideViews = true
}
if (typeof (this.data.uploaded_at) !== 'undefined' && this.data.uploaded_at.includes('watching')) {
const uploadSplit = this.data.uploaded_at.split(' ')
this.viewCount = parseInt(uploadSplit[0])
this.isLive = true
}
}
}
})

View File

@ -0,0 +1,83 @@
<template>
<div
class="ft-list-video"
:class="{
list: (listType === 'list' || forceListType === 'list') && forceListType !== 'grid',
grid: (listType === 'grid' || forceListType === 'list') && forceListType !== 'list'
}"
>
<div class="videoThumbnail">
<img
:src="thumbnail"
@click="play(id)"
>
<p
v-if="!isLive"
class="videoDuration"
@click="play(id)"
>
{{ duration }}
</p>
<font-awesome-icon
v-if="!isLive"
icon="star"
class="favoritesIcon"
:class="{ favorited: isFavorited }"
@click="toggleSave(id)"
/>
<div
v-if="watched"
class="videoWatched"
>
WATCHED
</div>
<div
v-if="watched"
class="watchedProgressBar"
:style="{width: progressPercentage + '%'}"
/>
</div>
<p class="videoTitle">
{{ title }}
</p>
<p
class="channelName"
@click="goToChannel"
>
{{ channelName }}
</p>
<span
v-if="!isLive && !hideViews"
class="viewCount"
>
{{ viewCount }} views
</span>
<span
v-if="uploadedTime !== '' && !isLive"
class="uploadedTime"
>
- {{ uploadedTime }}
</span>
<span
v-if="isLive"
class="viewCount"
>
{{ viewCount }} watching
</span>
<p
v-if="listType !== 'grid'"
class="description"
>
{{ description }}
</p>
<span
v-if="isLive"
class="liveText"
>
LIVE NOW
</span>
</div>
</template>
<script src="./ft-list-video.js" />
<style scoped src="./ft-list-video.css" />

View File

@ -0,0 +1,85 @@
/*
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/>.
*/
.container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.fullscreen {
height: 85vh;
}
/*
* Thanks to @tobiasahlin for the loading animation.
* Find it here: http://tobiasahlin.com/spinkit/
* Twitter: https://twitter.com/tobiasahlin
*/
.spinner {
width: 40px;
height: 40px;
position: relative;
margin: 100px auto;
}
.double-bounce1,
.double-bounce2 {
width: 100%;
height: 100%;
border-radius: 50%;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
background-color: var(--primary-color);
-webkit-animation: sk-bounce 2.0s infinite ease-in-out;
animation: sk-bounce 2.0s infinite ease-in-out;
}
.double-bounce2 {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0.0)
}
50% {
-webkit-transform: scale(1.0)
}
}
@keyframes sk-bounce {
0%,
100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
}
50% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}

View File

@ -0,0 +1,11 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtLoader',
props: {
fullscreen: {
type: Boolean,
default: false
}
}
})

View File

@ -0,0 +1,14 @@
<template>
<div
class="container"
:class="{ fullscreen: fullscreen }"
>
<div class="spinner">
<div class="double-bounce1" />
<div class="double-bounce2" />
</div>
</div>
</template>
<script src="./ft-loader.js" />
<style scoped src="./ft-loader.css" />

View File

@ -0,0 +1,106 @@
pure-checkbox input[type="checkbox"], .pure-radiobutton input[type="checkbox"], .pure-checkbox input[type="radio"], .pure-radiobutton input[type="radio"] {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.pure-checkbox input[type="checkbox"]:focus + label:before, .pure-radiobutton input[type="checkbox"]:focus + label:before, .pure-checkbox input[type="radio"]:focus + label:before, .pure-radiobutton input[type="radio"]:focus + label:before, .pure-checkbox input[type="checkbox"]:hover + label:before, .pure-radiobutton input[type="checkbox"]:hover + label:before, .pure-checkbox input[type="radio"]:hover + label:before, .pure-radiobutton input[type="radio"]:hover + label:before {
border-color: var(--primary-color);
}
.pure-checkbox input[type="checkbox"]:active + label:before, .pure-radiobutton input[type="checkbox"]:active + label:before, .pure-checkbox input[type="radio"]:active + label:before, .pure-radiobutton input[type="radio"]:active + label:before { transition-duration: 0s; }
.pure-checkbox input[type="checkbox"] + label, .pure-radiobutton input[type="checkbox"] + label, .pure-checkbox input[type="radio"] + label, .pure-radiobutton input[type="radio"] + label {
position: relative;
padding-left: 2em;
vertical-align: middle;
user-select: none;
cursor: pointer;
display: block;
margin-bottom: -20px;
}
.pure-checkbox input[type="checkbox"] + label:before, .pure-radiobutton input[type="checkbox"] + label:before, .pure-checkbox input[type="radio"] + label:before, .pure-radiobutton input[type="radio"] + label:before {
box-sizing: content-box;
content: '';
color: var(--primary-color);
position: absolute;
top: 50%;
left: 0;
width: 14px;
height: 14px;
margin-top: -9px;
border: 2px solid var(--primary-color);
text-align: center;
transition: all 0.4s ease;
}
.pure-checkbox input[type="checkbox"] + label:after, .pure-radiobutton input[type="checkbox"] + label:after, .pure-checkbox input[type="radio"] + label:after, .pure-radiobutton input[type="radio"] + label:after {
box-sizing: content-box;
content: '';
background-color: var(--primary-color);
position: absolute;
top: 50%;
left: 4px;
width: 10px;
height: 10px;
margin-top: -5px;
transform: scale(0);
transform-origin: 50%;
transition: transform 200ms ease-out;
}
.pure-checkbox input[type="checkbox"]:disabled + label:before, .pure-radiobutton input[type="checkbox"]:disabled + label:before, .pure-checkbox input[type="radio"]:disabled + label:before, .pure-radiobutton input[type="radio"]:disabled + label:before { border-color: #cccccc; }
.pure-checkbox input[type="checkbox"]:disabled:focus + label:before, .pure-radiobutton input[type="checkbox"]:disabled:focus + label:before, .pure-checkbox input[type="radio"]:disabled:focus + label:before, .pure-radiobutton input[type="radio"]:disabled:focus + label:before, .pure-checkbox input[type="checkbox"]:disabled:hover + label:before, .pure-radiobutton input[type="checkbox"]:disabled:hover + label:before, .pure-checkbox input[type="radio"]:disabled:hover + label:before, .pure-radiobutton input[type="radio"]:disabled:hover + label:before { background-color: inherit; }
.pure-checkbox input[type="checkbox"]:disabled:checked + label:before, .pure-radiobutton input[type="checkbox"]:disabled:checked + label:before, .pure-checkbox input[type="radio"]:disabled:checked + label:before, .pure-radiobutton input[type="radio"]:disabled:checked + label:before { background-color: #cccccc; }
.pure-checkbox input[type="checkbox"] + label:after, .pure-radiobutton input[type="checkbox"] + label:after {
background-color: transparent;
top: 50%;
left: 4px;
width: 8px;
height: 3px;
margin-top: -4px;
border-style: solid;
border-width: 0 0 3px 3px;
border-image: none;
transform: rotate(-45deg) scale(0);
}
.pure-checkbox input[type="checkbox"]:checked + label:after, .pure-radiobutton input[type="checkbox"]:checked + label:after {
content: '';
transform: rotate(-45deg) scale(1);
transition: transform 200ms ease-out;
}
.pure-checkbox input[type="radio"]:checked + label:before, .pure-radiobutton input[type="radio"]:checked + label:before {
animation: borderscale 300ms ease-in;
}
.pure-checkbox input[type="radio"]:checked + label:after, .pure-radiobutton input[type="radio"]:checked + label:after { transform: scale(1); }
.pure-checkbox input[type="radio"] + label:before, .pure-radiobutton input[type="radio"] + label:before, .pure-checkbox input[type="radio"] + label:after, .pure-radiobutton input[type="radio"] + label:after { border-radius: 50%; }
.pure-checkbox input[type="checkbox"]:checked + label:before, .pure-radiobutton input[type="checkbox"]:checked + label:before {
animation: borderscale 200ms ease-in;
background: var(--primary-color);
}
.pure-checkbox input[type="checkbox"]:checked + label:after, .pure-radiobutton input[type="checkbox"]:checked + label:after { transform: rotate(-45deg) scale(1); }
@keyframes
borderscale { 50% {
box-shadow: 0 0 0 2px var(--primary-color);
}
}
.radioTitle {
margin-bottom: -20px;
}

View File

@ -0,0 +1,35 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtElementList',
props: {
title: {
type: String,
required: true
},
labels: {
type: Array,
required: true
},
values: {
type: Array,
required: true
}
},
data: function () {
return {
id: '',
selectedValue: ''
}
},
computed: {
inputName: function () {
const name = this.title.replace(' ', '')
return name.toLowerCase() + this.id
}
},
mounted: function () {
this.id = this._uid
this.selectedValue = this.values[0]
}
})

View File

@ -0,0 +1,33 @@
<template>
<div class="pure-radiobutton filter">
<h3 class="radioTitle">
{{ title }}
</h3>
<!-- eslint-disable vue/no-template-key -->
<template
v-for="(label, index) in labels"
class="radioButtonContainer"
>
<input
:id="values[index] + id"
:key="index"
v-model="selectedValue"
:name="inputName"
:value="values[index]"
:checked="index === 0"
class="radio"
type="radio"
@change="$emit('change', values[index])"
>
<label
:key="label"
:for="values[index] + id"
>
{{ label }}
</label>
</template>
</div>
</template>
<script src="./ft-radio-button.js" />
<style scoped src="./ft-radio-button.css" />

View File

@ -0,0 +1,20 @@
.searchFilter {
background-color: var(--card-bg-color);
padding: 20px;
padding-bottom: 70px;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
}
.center {
text-align: center;
}
.searchRadio {
width: 170px;
border-right: 1px solid var(--primary-color);
}
.radioFlexBox {
max-width: 1000px;
margin: 0 auto;
}

View File

@ -0,0 +1,91 @@
import Vue from 'vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtRadioButton from '../ft-radio-button/ft-radio-button.vue'
export default Vue.extend({
name: 'FtSearchFilters',
components: {
'ft-flex-box': FtFlexBox,
'ft-radio-button': FtRadioButton
},
data: function () {
return {
sortByTitle: 'Sort By',
sortByLabels: [
'Most Relevant',
'Rating',
'Upload Date',
'View Count'
],
sortByValues: [
'relevance',
'rating',
'upload_date',
'view_count'
],
timeTitle: 'Time',
timeLabels: [
'Any Time',
'Last Hour',
'Today',
'This Week',
'This Month',
'This Year'
],
timeValues: [
'',
'hour',
'today',
'week',
'month',
'year'
],
typeTitle: 'Type',
typeLabels: [
'All Types',
'Videos',
'Channels',
'Playlists'
],
typeValues: [
'all',
'video',
'channel',
'playlist'
],
durationTitle: 'Duration',
durationLabels: [
'All Durations',
'Short (< 4 minutes)',
'Long (> 20 minutes)'
],
durationValues: [
'',
'short',
'long'
]
}
},
computed: {
searchSettings: function () {
return this.$store.getters.getSearchSettings
}
},
methods: {
updateSortBy: function (value) {
this.$store.commit('setSearchSortBy', value)
},
updateTime: function (value) {
this.$store.commit('setSearchTime', value)
},
updateType: function (value) {
this.$store.commit('setSearchType', value)
},
updateDuration: function (value) {
this.$store.commit('setSearchDuration', value)
}
}
})

View File

@ -0,0 +1,40 @@
<template>
<div class="searchFilter">
<h2 class="center">
Search Filters
</h2>
<ft-flex-box class="radioFlexBox">
<ft-radio-button
:title="sortByTitle"
:labels="sortByLabels"
:values="sortByValues"
class="searchRadio"
@change="updateSortBy"
/>
<ft-radio-button
:title="timeTitle"
:labels="timeLabels"
:values="timeValues"
class="searchRadio radioMargin"
@change="updateTime"
/>
<ft-radio-button
:title="typeTitle"
:labels="typeLabels"
:values="typeValues"
class="searchRadio radioMargin"
@change="updateType"
/>
<ft-radio-button
:title="durationTitle"
:labels="durationLabels"
:values="durationValues"
class="radioMargin"
@change="updateDuration"
/>
</ft-flex-box>
</div>
</template>
<script src="./ft-search-filters.js" />
<style scoped src="./ft-search-filters.css" />

View File

@ -0,0 +1,127 @@
/*
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/>.
*/
/*
* Credit goes to pavelvaravko for making this css.
* https://codepen.io/pavelvaravko/pen/qjojOr
*/
/* select starting stylings ------------------------------*/
.select {
position: relative;
width: 200px;
padding: 10px 10px 10px 0;
margin-top: 10px;
}
.select-text {
position: relative;
font-family: inherit;
background-color: transparent;
color: var(--primary-text-color);
width: 200px;
padding: 10px 10px 10px 0;
font-size: 18px;
border-radius: 0;
border: none;
}
/* Remove focus */
.select-text:focus {
outline: none;
}
/* Use custom arrow */
.select .select-text {
appearance: none;
-webkit-appearance:none
}
.iconSelect {
position: absolute;
top: 18px;
right: 10px;
/* Styling the down arrow */
padding: 0;
content: '';
border-left: 6px solid transparent;
border-right: 6px solid transparent;
pointer-events: none;
color: var(--teritary-text-color);
}
/* LABEL ======================================= */
.select-label {
font-size: 18px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 0;
top: 10px;
transition: 0.2s ease all;
color: var(--teritary-text-color);
}
/* active state */
.select-text:focus ~ .select-label, .select-text:valid ~ .select-label {
color: var(--accent-color);
top: -20px;
transition: 0.2s ease all;
font-size: 14px;
}
/* BOTTOM BARS ================================= */
.select-bar {
position: relative;
display: block;
width: 200px;
}
.select-bar:before, .select-bar:after {
content: '';
height: 2px;
width: 0;
bottom: 1px;
position: absolute;
background: var(--accent-color);
transition: 0.2s ease all;
}
.select-bar:before {
left: 50%;
}
.select-bar:after {
right: 50%;
}
/* active state */
.select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after {
width: 50%;
}
/* HIGHLIGHTER ================================== */
.select-highlight {
position: absolute;
height: 60%;
width: 100px;
top: 25%;
left: 0;
pointer-events: none;
opacity: 0.5;
}

View File

@ -0,0 +1,23 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtSelect',
props: {
placeholder: {
type: String,
required: true
},
value: {
type: [String, Number],
required: true
},
selectNames: {
type: Array,
required: true
},
selectValues: {
type: Array,
required: true
}
}
})

View File

@ -0,0 +1,29 @@
<template>
<div class="select">
<select
class="select-text"
:value="value"
@change="$emit('change', $event.target.value)"
>
<option
v-for="(name, index) in selectNames"
:key="index"
:value="selectValues[index]"
>
{{ name }}
</option>
</select>
<font-awesome-icon
icon="sort-down"
class="iconSelect"
/>
<span class="select-highlight" />
<span class="select-bar" />
<label class="select-label">
{{ placeholder }}
</label>
</div>
</template>
<script src="./ft-select.js" />
<style scoped src="./ft-select.css" />

View File

@ -0,0 +1,8 @@
.relative {
position: relative;
width: 85%;
}
.ftVideoPlayer {
width: 85%;
}

View File

@ -0,0 +1,67 @@
import Vue from 'vue'
import FtCard from '../ft-card/ft-card.vue'
// I haven't decided which video player I want to use
// Need to expirement with both of them to see which one will work best.
import videojs from 'video.js'
// import mediaelement from 'mediaelement'
export default Vue.extend({
name: 'FtVideoPlayer',
components: {
'ft-card': FtCard
},
props: {
data: {
type: Array,
default: () => { return [] }
},
src: {
type: String,
required: true
}
},
data: function () {
return {
id: '',
player: null,
dataSetup: {
aspectRatio: '16:9',
playbackRates: [
0.25,
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2,
2.25,
2.5,
2.75,
3
]
}
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
}
},
mounted: function () {
this.id = this._uid
setTimeout(this.initializePlayer, 100)
},
methods: {
initializePlayer: function () {
console.log(this.id)
const videoPlayer = document.getElementById(this.id)
console.log(videoPlayer)
if (videoPlayer !== null) {
this.player = videojs(videoPlayer)
console.log(videojs.players)
}
}
}
})

View File

@ -0,0 +1,21 @@
<template>
<div class="relative">
<video
:id="id"
class="ftVideoPlayer video-js vjs-default-skin"
width="800"
height="600"
controls
preload="auto"
:data-setup="JSON.stringify(dataSetup)"
>
<source
:src="src"
type="video/mp4"
>
</video>
</div>
</template>
<script src="./ft-video-player.js" />
<style scoped src="./ft-video-player.css" />

View File

@ -0,0 +1,29 @@
.playListThumbnail {
width: 100%;
}
.playlistThumbnail img {
width: 100%;
}
.playlistChannel {
height: 70px;
}
.playlistChannel img {
width: 70px;
float: left;
cursor: pointer;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.playlistChannel h3 {
float: left;
position: relative;
cursor: pointer;
width: 200px;
margin-left: 10px;
top: 5px;
font-size: 15px;
}

View File

@ -0,0 +1,89 @@
import Vue from 'vue'
import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue'
import { shell } from 'electron'
export default Vue.extend({
name: 'FtElementList',
components: {
'ft-list-dropdown': FtListDropdown
},
props: {
data: {
type: Object,
required: true
}
},
data: function () {
return {
id: '',
title: '',
thumbnail: '',
channelThumbnail: '',
channelName: '',
channelId: '',
videoCount: 0,
viewCount: 0,
lastUpdated: '',
description: '',
shareHeaders: [
'Copy YouTube Link',
'Open in YouTube',
'Copy Invidious Link',
'Open in Invidious'
],
shareValues: [
'copyYoutube',
'openYoutube',
'copyInvidious',
'openInvidious'
]
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
}
},
mounted: function () {
console.log(this.data)
this.id = this.data.id
this.title = this.data.title
this.thumbnail = this.data.thumbnail
this.channelName = this.data.channelName
this.channelThumbnail = this.data.channelThumbnail
this.uploadedTime = this.data.uploaded_at
this.description = this.data.description
// Causes errors if not put inside of a check
if (typeof (this.data.viewCount) !== 'undefined') {
this.viewCount = this.data.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
if (typeof (this.data.videoCount) !== 'undefined') {
this.videoCount = this.data.videoCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
this.lastUpdated = this.data.lastUpdated
},
methods: {
sharePlaylist: function (method) {
const youtubeUrl = `https://youtube.com/playlist?list=${this.id}`
const invidiousUrl = `https://invidio.us/playlist?list=${this.id}`
switch (method) {
case 'copyYoutube':
navigator.clipboard.writeText(youtubeUrl)
break
case 'openYoutube':
shell.openExternal(youtubeUrl)
break
case 'copyInvidious':
navigator.clipboard.writeText(invidiousUrl)
break
case 'openInvidious':
shell.openExternal(invidiousUrl)
break
}
}
}
})

View File

@ -0,0 +1,39 @@
<template>
<div class="playlistInfo">
<div
class="playlistThumbnail"
>
<img
:src="thumbnail"
>
</div>
<h2>
{{ title }}
</h2>
<p>
{{ videoCount }} videos - {{ viewCount }} views - Last updated on {{ lastUpdated }}
</p>
<p>
{{ description }}
</p>
<hr>
<div
class="playlistChannel"
>
<img :src="channelThumbnail">
<h3>
{{ channelName }}
</h3>
</div>
<br>
<ft-list-dropdown
title="SHARE PLAYLIST"
:label-names="shareHeaders"
:label-values="shareValues"
@click="sharePlaylist"
/>
</div>
</template>
<script src="./playlist-info.js" />
<style scoped src="./playlist-info.css" />

View File

@ -0,0 +1,92 @@
.sideNav {
height: calc(100vh - 60px);
width: 200px;
overflow-y: auto;
position: fixed;
left: 0px;
top: 0px;
z-index: 1;
margin-top: 60px;
-webkit-box-shadow: 1px -1px 1px -1px var(--primary-shadow-color);
background-color: var(--side-nav-color);
transition-property: width;
transition-duration: 150ms;
transition-timing-function: ease-in-out;
}
.topNavOption {
margin-top: 10px;
}
.navOption {
position: relative;
padding: 5px;
cursor: pointer;
}
.navOption:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navOption:active {
background-color: var(--side-nav-active-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navIcon {
margin-left: 10px;
}
.navLabel {
margin-left: 5px;
display: inline-block;
}
.closed {
width: 80px;
}
.sideNav hr {
width: 90%;
height: 1px;
border: 0;
background-color: var(--primary-color);
}
.refreshIcon {
position: absolute;
top: 20px;
right: 20px;
}
.closed .refreshIcon {
display: none;
}
.closed .navOption {
width: 100%;
height: 40px;
padding: 0px;
padding-top: 10px;
padding-bottom: 10px;
}
.closed .navIcon {
margin-left: 0px;
width: 100%;
display: block;
margin-bottom: 0px;
}
.closed .navLabel {
margin-left: 0px;
width: 100%;
text-align: center;
left: 0px;
font-size: 11px;
}

View File

@ -0,0 +1,16 @@
import Vue from 'vue'
import router from '../../router/index.js'
export default Vue.extend({
name: 'SideNav',
computed: {
isOpen: function () {
return this.$store.getters.getIsSideNavOpen
}
},
methods: {
navigate: function (route) {
router.push('/' + route)
}
}
})

View File

@ -0,0 +1,101 @@
<template>
<div
ref="sideNav"
class="sideNav"
:class="{closed: !isOpen}"
>
<div
class="navOption topNavOption"
@click="navigate('subscriptions')"
>
<font-awesome-icon
icon="rss"
class="navIcon"
/>
<p class="navLabel">
Subscriptions
</p>
<font-awesome-icon
class="refreshIcon"
icon="sync"
/>
</div>
<div
class="navOption"
@click="navigate('trending')"
>
<font-awesome-icon
icon="fire"
class="navIcon"
/>
<p class="navLabel">
Trending
</p>
</div>
<div
class="navOption"
@click="navigate('popular')"
>
<font-awesome-icon
icon="users"
class="navIcon"
/>
<p class="navLabel">
Most Popular
</p>
</div>
<div
class="navOption"
@click="navigate('userplaylists')"
>
<font-awesome-icon
icon="star"
class="navIcon"
/>
<p class="navLabel">
Playlists
</p>
</div>
<div
class="navOption"
@click="navigate('history')"
>
<font-awesome-icon
icon="history"
class="navIcon"
/>
<p class="navLabel">
History
</p>
</div>
<hr>
<div
class="navOption"
@click="navigate('settings')"
>
<font-awesome-icon
icon="sliders-h"
class="navIcon"
/>
<p class="navLabel">
Settings
</p>
</div>
<div
class="navOption"
@click="navigate('about')"
>
<font-awesome-icon
icon="info-circle"
class="navIcon"
/>
<p class="navLabel">
About
</p>
</div>
<hr>
</div>
</template>
<script src="./side-nav.js" />
<style scoped src="./side-nav.css" />

View File

@ -0,0 +1,173 @@
.topNav {
background-color: var(--card-bg-color);
width: 100%;
height: 60px;
line-height: 60px;
position: fixed;
top: 0;
left: 0;
z-index: 4;
-webkit-box-shadow: 0px 2px 1px 0px var(--primary-shadow-color);
}
.menuIcon {
position: absolute;
top: 10px;
left: 10px;
font-size: 20px;
padding: 10px;
cursor: pointer;
color: var(--primary-text-color);
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.menuIcon:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.menuIcon:active {
background-color: var(--teritary-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navBackIcon {
position: absolute;
top: 10px;
left: 50px;
font-size: 20px;
padding: 10px;
cursor: pointer;
color: var(--primary-text-color);
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.navBackIcon:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navBackIcon:active {
background-color: var(--teritary-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navForwardIcon {
position: absolute;
top: 10px;
left: 85px;
font-size: 20px;
padding: 10px;
cursor: pointer;
color: var(--primary-text-color);
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.navForwardIcon:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navForwardIcon:active {
background-color: var(--teritary-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.logoIcon {
background-image: url("/_icons/iconColorSmall.png");
background-repeat: no-repeat;
background-position: right top;
background-size: 25px;
position: absolute;
top: 20px;
left: 140px;
width: 25px;
height: 25px;
}
.logoText {
background-image: url("/_icons/textColorSmall.png");
background-repeat: no-repeat;
background-position: right top;
background-size: 100px;
position: absolute;
top: 9px;
left: 175px;
width: 100px;
height: 40px;
}
.searchContainer {
margin: 0 auto;
width: 500px;
position: relative;
}
.search {
width: 450px;
}
.navFilterIcon {
position: absolute;
padding: 10px;
top: 10px;
right: 10px;
cursor: pointer;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.navFilterIcon:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navFilterIcon:active {
background-color: var(--teritary-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.searchFilters {
margin-left: 270px;
margin-right: 20px;
margin-top: 10px;
margin-bottom: 20px;
transition-property: margin;
transition-duration: 150ms;
transition-timing-function: ease-in-out;
}
.expand {
margin-left: 100px;
}

View File

@ -0,0 +1,56 @@
import Vue from 'vue'
import FtInput from '../ft-input/ft-input.vue'
import FtSearchFilters from '../ft-search-filters/ft-search-filters.vue'
import router from '../../router/index.js'
export default Vue.extend({
name: 'TopNav',
components: {
FtInput,
FtSearchFilters
},
data: () => {
return {
component: this,
showFilters: false
}
},
computed: {
searchSettings: function () {
return this.$store.getters.getSearchSettings
},
isSideNavOpen: function () {
return this.$store.getters.getIsSideNavOpen
}
},
methods: {
goToSearch: function (query) {
console.log(this)
this.showFilters = false
router.push(
{
path: `/search/${query}`,
query: {
sortBy: this.searchSettings.sortBy,
time: this.searchSettings.time,
type: this.searchSettings.type,
duration: this.searchSettings.duration
}
}
)
},
historyBack: function () {
window.history.back()
},
historyForward: function () {
window.history.forward()
},
toggleSideNav: function () {
this.$store.commit('toggleSideNav')
}
}
})

View File

@ -0,0 +1,41 @@
<template>
<div class="topNav">
<font-awesome-icon
class="menuIcon"
icon="bars"
@click="toggleSideNav"
/>
<font-awesome-icon
class="navBackIcon"
icon="arrow-left"
@click="historyBack"
/>
<font-awesome-icon
class="navForwardIcon"
icon="arrow-right"
@click="historyForward"
/>
<div class="logoIcon" />
<div class="logoText" />
<div class="searchContainer">
<ft-input
placeholder="Search / Go to URL"
class="search"
@click="goToSearch"
/>
<font-awesome-icon
class="navFilterIcon"
icon="filter"
@click="showFilters = !showFilters"
/>
</div>
<ft-search-filters
v-show="showFilters"
class="searchFilters"
:class="{ expand: !isSideNavOpen }"
/>
</div>
</template>
<script src="./top-nav.js" />
<style scoped src="./top-nav.css" />

Some files were not shown because too many files have changed in this diff Show More