Push Latest Code to Repository
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "73",
|
||||
"node": 12
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/proposal-class-properties",
|
||||
"@babel/proposal-object-rest-spread"
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
_scripts
|
||||
dist
|
|
@ -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
|
||||
},
|
||||
}
|
|
@ -1,104 +1,18 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
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
|
||||
.DS_Store
|
||||
dist/electron/*
|
||||
dist/web/*
|
||||
build/*
|
||||
!build/icons
|
||||
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/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# 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/)
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
thumbs.db
|
||||
!.gitkeep
|
||||
data/tmp/
|
||||
.tmp/
|
||||
tmp/
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.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
|
||||
coverage
|
||||
__coverage__
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
|
@ -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+$/"
|
|
@ -0,0 +1,8 @@
|
|||
##########################################################
|
||||
#### WhiteSource "Bolt for Github" configuration file ####
|
||||
##########################################################
|
||||
|
||||
# Configuration #
|
||||
#---------------#
|
||||
ws.repo.scan=true
|
||||
vulnerable.check.run.conclusion.level=failure
|
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 264 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 579 B |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 11 KiB |
|
@ -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 |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 1.7 KiB |
|
@ -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)
|
||||
})
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,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>
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FtCard'
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<div class="ft-card">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./ft-card.js" />
|
||||
<style scoped src="./ft-card.css" />
|
|
@ -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;
|
||||
}
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -0,0 +1,3 @@
|
|||
.maxWidth {
|
||||
width: 100%;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -0,0 +1,5 @@
|
|||
.ft-flex-box {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-evenly;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FtFlexBox'
|
||||
})
|
|
@ -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" />
|
|
@ -0,0 +1,6 @@
|
|||
.ft-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 240px);
|
||||
justify-content: space-evenly;
|
||||
grid-gap: 5px;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FtGrid'
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<div class="ft-grid">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./ft-grid.js" />
|
||||
<style scoped src="./ft-grid.css" />
|
|
@ -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;
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FtLoader',
|
||||
props: {
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -0,0 +1,8 @@
|
|||
.relative {
|
||||
position: relative;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.ftVideoPlayer {
|
||||
width: 85%;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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;
|
||||
}
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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" />
|