Merge branch 'Subscriptions'
This commit is contained in:
commit
52475877fe
|
@ -11136,6 +11136,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"javascript-time-ago": {
|
||||||
|
"version": "2.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.0.13.tgz",
|
||||||
|
"integrity": "sha512-zH+obXUQ4vlc9UlERFe637rNJQaVYLizwODUfGzYN/cNW/owkk5wzb327gAfEXFpI4yhFcStEaoqoJtMGAmrAg==",
|
||||||
|
"requires": {
|
||||||
|
"relative-time-format": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"version": "26.4.2",
|
"version": "26.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest/-/jest-26.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/jest/-/jest-26.4.2.tgz",
|
||||||
|
@ -13318,6 +13326,11 @@
|
||||||
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=",
|
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lodash.uniqwith": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz",
|
||||||
|
"integrity": "sha1-egy/ZfQ7WShiWp1NDcVLGMrcfvM="
|
||||||
|
},
|
||||||
"log-symbols": {
|
"log-symbols": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
|
||||||
|
@ -16235,6 +16248,11 @@
|
||||||
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"relative-time-format": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-0O6i4fKjsx8qhz57zorG+LrIDnF9pSvP5s7H9R1Nb5nSqih5dvRyKzNKs6MxhL3bv4iwsz4DuDwAyw+c47QFIA=="
|
||||||
|
},
|
||||||
"remove-trailing-separator": {
|
"remove-trailing-separator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
||||||
|
@ -16517,6 +16535,31 @@
|
||||||
"sprintf-js": "^1.1.2"
|
"sprintf-js": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rss-parser": {
|
||||||
|
"version": "3.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.9.0.tgz",
|
||||||
|
"integrity": "sha512-wlRSfGrotOXuWo19Dtl2KmQt7o9i5zzCExUrxpechE0O54BAx7JD+xhWyGumPPqiJj771ndflV3sE3bTHen0HQ==",
|
||||||
|
"requires": {
|
||||||
|
"entities": "^2.0.3",
|
||||||
|
"xml2js": "^0.4.19"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"entities": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rss-to-json": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rss-to-json/-/rss-to-json-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-d+TwrFI5wAHbZ/fTd3Pvty14tadBjKHAjfMcUam9FWoWrC9g5rHJN9Slw10OZwk6Mey+hqdXwdmymO7d8ebVmw==",
|
||||||
|
"requires": {
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"xml2json": "^0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rsvp": {
|
"rsvp": {
|
||||||
"version": "4.8.5",
|
"version": "4.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||||
|
@ -20244,6 +20287,15 @@
|
||||||
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
|
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"xml2js": {
|
||||||
|
"version": "0.4.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||||
|
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||||
|
"requires": {
|
||||||
|
"sax": ">=0.6.0",
|
||||||
|
"xmlbuilder": "~11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"xml2json": {
|
"xml2json": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.12.0.tgz",
|
||||||
|
@ -20254,6 +20306,11 @@
|
||||||
"node-expat": "^2.3.18"
|
"node-expat": "^2.3.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"xmlbuilder": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||||
|
},
|
||||||
"xmlchars": {
|
"xmlchars": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
|
|
@ -20,10 +20,12 @@
|
||||||
"js-yaml": "^3.14.0",
|
"js-yaml": "^3.14.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.uniqwith": "^4.5.0",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"mediaelement": "^4.2.16",
|
"mediaelement": "^4.2.16",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"opml-to-json": "0.0.3",
|
"opml-to-json": "0.0.3",
|
||||||
|
"rss-parser": "^3.9.0",
|
||||||
"video.js": "7.6.6",
|
"video.js": "7.6.6",
|
||||||
"videojs-abloop": "^1.1.2",
|
"videojs-abloop": "^1.1.2",
|
||||||
"videojs-contrib-quality-levels": "^2.0.9",
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ObserveVisibility } from 'vue-observe-visibility'
|
||||||
import TopNav from './components/top-nav/top-nav.vue'
|
import TopNav from './components/top-nav/top-nav.vue'
|
||||||
import SideNav from './components/side-nav/side-nav.vue'
|
import SideNav from './components/side-nav/side-nav.vue'
|
||||||
import FtToast from './components/ft-toast/ft-toast.vue'
|
import FtToast from './components/ft-toast/ft-toast.vue'
|
||||||
|
import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
let useElectron
|
let useElectron
|
||||||
|
@ -23,16 +24,21 @@ export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
TopNav,
|
TopNav,
|
||||||
SideNav,
|
SideNav,
|
||||||
FtToast
|
FtToast,
|
||||||
|
FtProgressBar
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isOpen: function () {
|
isOpen: function () {
|
||||||
return this.$store.getters.getIsSideNavOpen
|
return this.$store.getters.getIsSideNavOpen
|
||||||
|
},
|
||||||
|
showProgressBar: function () {
|
||||||
|
return this.$store.getters.getShowProgressBar
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.$store.dispatch('grabUserSettings')
|
this.$store.dispatch('grabUserSettings')
|
||||||
this.$store.dispatch('grabHistory')
|
this.$store.dispatch('grabHistory')
|
||||||
|
this.$store.dispatch('grabAllProfiles', this.$t('Profile.All Channels'))
|
||||||
this.$store.commit('setUsingElectron', useElectron)
|
this.$store.commit('setUsingElectron', useElectron)
|
||||||
this.checkThemeSettings()
|
this.checkThemeSettings()
|
||||||
this.checkLocale()
|
this.checkLocale()
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
<!-- </keep-alive> -->
|
<!-- </keep-alive> -->
|
||||||
</Transition>
|
</Transition>
|
||||||
<ft-toast />
|
<ft-toast />
|
||||||
|
<ft-progress-bar
|
||||||
|
v-if="showProgressBar"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,10 @@ export default Vue.extend({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
dataList: {
|
dataList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => { return [] }
|
default: () => { return [] }
|
||||||
|
@ -47,6 +51,11 @@ export default Vue.extend({
|
||||||
return `${this.id}_datalist`
|
return `${this.id}_datalist`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
value: function (val) {
|
||||||
|
this.inputData = val
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.id = this._uid
|
this.id = this._uid
|
||||||
this.inputData = this.value
|
this.inputData = this.value
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@input="e => handleInput(e.target.value)"
|
@input="e => handleInput(e.target.value)"
|
||||||
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-if="showArrow"
|
v-if="showArrow"
|
||||||
|
|
|
@ -255,7 +255,8 @@ export default Vue.extend({
|
||||||
liveStreamString: this.$t('Video.Watching'),
|
liveStreamString: this.$t('Video.Watching'),
|
||||||
upcomingString: this.$t('Video.Published.Upcoming'),
|
upcomingString: this.$t('Video.Published.Upcoming'),
|
||||||
isLive: this.isLive,
|
isLive: this.isLive,
|
||||||
isUpcoming: this.data.isUpcoming
|
isUpcoming: this.data.isUpcoming,
|
||||||
|
isRSS: this.data.isRSS
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
this.uploadedTime = data
|
this.uploadedTime = data
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
.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-top: 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-left: 25px;
|
||||||
|
border-radius: 200px 200px 200px 200px;
|
||||||
|
-webkit-border-radius: 200px 200px 200px 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial {
|
||||||
|
font-size: 25px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileName {
|
||||||
|
font-size: 13px;
|
||||||
|
height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'FtProfileBubble',
|
||||||
|
props: {
|
||||||
|
profileName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
profileId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
textColor: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
profileInitial: function () {
|
||||||
|
return this.profileName.slice(0, 1).toUpperCase()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goToProfile: function () {
|
||||||
|
this.$router.push({
|
||||||
|
path: `/settings/profile/edit/${this.profileId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="bubblePadding"
|
||||||
|
@click="goToProfile"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bubble"
|
||||||
|
:style="{ background: backgroundColor, color: textColor }"
|
||||||
|
>
|
||||||
|
<p class="initial">
|
||||||
|
{{ profileInitial }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="profileName">
|
||||||
|
{{ profileName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./ft-profile-bubble.js" />
|
||||||
|
<style scoped src="./ft-profile-bubble.css" />
|
|
@ -0,0 +1,77 @@
|
||||||
|
.colorOption {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 200px 200px 200px 200px;
|
||||||
|
-webkit-border-radius: 200px 200px 200px 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial {
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profileList {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
right: 10px;
|
||||||
|
min-width: 250px;
|
||||||
|
height: 300px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: var(--card-bg-color);
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#profileList:focus {
|
||||||
|
display: inline;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileWrapper {
|
||||||
|
margin-top: 60px;
|
||||||
|
height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 50px;
|
||||||
|
-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile: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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .colorOption {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileName {
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileListTitle {
|
||||||
|
position: absolute;
|
||||||
|
top: -15px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileSettings {
|
||||||
|
float: right;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import $ from 'jquery'
|
||||||
|
|
||||||
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
|
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'FtProfileSelector',
|
||||||
|
components: {
|
||||||
|
'ft-card': FtCard,
|
||||||
|
'ft-icon-button': FtIconButton
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
showProfileList: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
profileList: function () {
|
||||||
|
return this.$store.getters.getProfileList
|
||||||
|
},
|
||||||
|
activeProfile: function () {
|
||||||
|
return this.$store.getters.getActiveProfile
|
||||||
|
},
|
||||||
|
defaultProfile: function () {
|
||||||
|
return this.$store.getters.getDefaultProfile
|
||||||
|
},
|
||||||
|
activeProfileInitial: function () {
|
||||||
|
return this.activeProfile.name.slice(0, 1).toUpperCase()
|
||||||
|
},
|
||||||
|
profileInitials: function () {
|
||||||
|
return this.profileList.map((profile) => {
|
||||||
|
return profile.name.slice(0, 1).toUpperCase()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
setTimeout(() => {
|
||||||
|
const profileIndex = this.profileList.findIndex((profile) => {
|
||||||
|
return profile._id === this.defaultProfile
|
||||||
|
})
|
||||||
|
|
||||||
|
if (profileIndex !== -1) {
|
||||||
|
this.updateActiveProfile(profileIndex)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
$('#profileList').focusout(() => {
|
||||||
|
$('#profileList')[0].style.display = 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleProfileList: function () {
|
||||||
|
$('#profileList')[0].style.display = 'inline'
|
||||||
|
$('#profileList').focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
openProfileSettings: function () {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/settings/profile/'
|
||||||
|
})
|
||||||
|
$('#profileList').focusout()
|
||||||
|
},
|
||||||
|
|
||||||
|
setActiveProfile: function (profile) {
|
||||||
|
if (this.profileList[this.activeProfile]._id === profile._id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const index = this.profileList.findIndex((x) => {
|
||||||
|
return x._id === profile._id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateActiveProfile(index)
|
||||||
|
const message = this.$t('Profile.$ is now the active profile').replace('$', profile.name)
|
||||||
|
this.showToast({
|
||||||
|
message: message
|
||||||
|
})
|
||||||
|
$('#profileList').focusout()
|
||||||
|
},
|
||||||
|
|
||||||
|
...mapActions([
|
||||||
|
'showToast',
|
||||||
|
'updateActiveProfile'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="colorOption"
|
||||||
|
:style="{ background: profileList[activeProfile].bgColor, color: profileList[activeProfile].textColor }"
|
||||||
|
@click="toggleProfileList"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="initial"
|
||||||
|
>
|
||||||
|
{{ profileInitials[activeProfile] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ft-card
|
||||||
|
id="profileList"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="profileListTitle"
|
||||||
|
>
|
||||||
|
Profile Select
|
||||||
|
</h3>
|
||||||
|
<ft-icon-button
|
||||||
|
class="profileSettings"
|
||||||
|
icon="sliders-h"
|
||||||
|
@click="openProfileSettings"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="profileWrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="profile"
|
||||||
|
v-for="(profile, index) in profileList"
|
||||||
|
:key="index"
|
||||||
|
@click="setActiveProfile(profile)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="colorOption"
|
||||||
|
:style="{ background: profile.bgColor, color: profile.textColor }"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="initial"
|
||||||
|
>
|
||||||
|
{{ profileInitials[index] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="profileName"
|
||||||
|
>
|
||||||
|
{{ profile.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ft-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./ft-profile-selector.js" />
|
||||||
|
<style scoped src="./ft-profile-selector.css" />
|
|
@ -0,0 +1,9 @@
|
||||||
|
.progressBar {
|
||||||
|
position: fixed;
|
||||||
|
height: 3px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
z-index: 1;
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'FtProgressBar',
|
||||||
|
computed: {
|
||||||
|
progressBarPercentage: function () {
|
||||||
|
return this.$store.getters.getProgressBarPercentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="progressBar"
|
||||||
|
:style="{ width: progressBarPercentage + '%' }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./ft-progress-bar.js" />
|
||||||
|
<style scoped src="./ft-progress-bar.css" />
|
|
@ -30,7 +30,7 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navOption {
|
.navOption, .navChannel {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -40,14 +40,14 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navOption:hover {
|
.navOption:hover, .navChannel:hover {
|
||||||
background-color: var(--side-nav-hover-color);
|
background-color: var(--side-nav-hover-color);
|
||||||
-moz-transition: background 0.2s ease-in;
|
-moz-transition: background 0.2s ease-in;
|
||||||
-o-transition: background 0.2s ease-in;
|
-o-transition: background 0.2s ease-in;
|
||||||
transition: background 0.2s ease-in;
|
transition: background 0.2s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navOption:active {
|
.navOption:active, .navChannel:active {
|
||||||
background-color: var(--side-nav-active-color);
|
background-color: var(--side-nav-active-color);
|
||||||
-moz-transition: background 0.2s ease-in;
|
-moz-transition: background 0.2s ease-in;
|
||||||
-o-transition: background 0.2s ease-in;
|
-o-transition: background 0.2s ease-in;
|
||||||
|
@ -63,6 +63,26 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navChannel .navLabel {
|
||||||
|
font-size: 11px;
|
||||||
|
width: 150px;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailContainer {
|
||||||
|
width: 35px;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelThumbnail {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
.closed {
|
.closed {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
@ -107,6 +127,24 @@
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closed .navChannel {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closed .thumbnailContainer {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
float: none;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin: 0 auto;
|
||||||
|
top: 0px;
|
||||||
|
-ms-transform: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 680px) {
|
@media only screen and (max-width: 680px) {
|
||||||
.inner {
|
.inner {
|
||||||
display: contents; /* sunglasses emoji */
|
display: contents; /* sunglasses emoji */
|
||||||
|
|
|
@ -12,11 +12,35 @@ export default Vue.extend({
|
||||||
computed: {
|
computed: {
|
||||||
isOpen: function () {
|
isOpen: function () {
|
||||||
return this.$store.getters.getIsSideNavOpen
|
return this.$store.getters.getIsSideNavOpen
|
||||||
|
},
|
||||||
|
profileList: function () {
|
||||||
|
return this.$store.getters.getProfileList
|
||||||
|
},
|
||||||
|
activeProfile: function () {
|
||||||
|
return this.$store.getters.getActiveProfile
|
||||||
|
},
|
||||||
|
activeSubscriptions: function () {
|
||||||
|
const profile = JSON.parse(JSON.stringify(this.profileList[this.activeProfile]))
|
||||||
|
return profile.subscriptions.sort((a, b) => {
|
||||||
|
const nameA = a.name.toLowerCase()
|
||||||
|
const nameB = b.name.toLowerCase()
|
||||||
|
if (nameA < nameB) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (nameA > nameB) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
navigate: function (route) {
|
navigate: function (route) {
|
||||||
router.push('/' + route)
|
router.push('/' + route)
|
||||||
|
},
|
||||||
|
|
||||||
|
goToChannel: function (id) {
|
||||||
|
this.$router.push({ path: `/channel/${id}` })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,10 +16,6 @@
|
||||||
<p class="navLabel">
|
<p class="navLabel">
|
||||||
{{ $t("Subscriptions.Subscriptions") }}
|
{{ $t("Subscriptions.Subscriptions") }}
|
||||||
</p>
|
</p>
|
||||||
<font-awesome-icon
|
|
||||||
class="refreshIcon"
|
|
||||||
icon="sync"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="navOption mobileHidden"
|
class="navOption mobileHidden"
|
||||||
|
@ -98,6 +94,28 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
<div
|
||||||
|
v-for="(channel, index) in activeSubscriptions"
|
||||||
|
:key="index"
|
||||||
|
class="navChannel mobileHidden"
|
||||||
|
:title="channel.name"
|
||||||
|
@click="goToChannel(channel.id)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="thumbnailContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="channelThumbnail"
|
||||||
|
:src="channel.thumbnail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="navLabel"
|
||||||
|
v-if="isOpen"
|
||||||
|
>
|
||||||
|
{{ channel.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
import FtCard from '../ft-card/ft-card.vue'
|
import FtCard from '../ft-card/ft-card.vue'
|
||||||
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||||
import FtButton from '../ft-button/ft-button.vue'
|
import FtButton from '../ft-button/ft-button.vue'
|
||||||
|
@ -27,9 +28,18 @@ export default Vue.extend({
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
computed: {
|
||||||
goToChannel: function () {
|
hideWatchedSubs: function () {
|
||||||
console.log('TODO: Handle goToChannel')
|
return this.$store.getters.getHideWatchedSubs
|
||||||
|
},
|
||||||
|
useRssFeeds: function () {
|
||||||
|
return this.$store.getters.getUseRssFeeds
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'updateHideWatchedSubs',
|
||||||
|
'updateUseRssFeeds'
|
||||||
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,16 +5,24 @@
|
||||||
<h3
|
<h3
|
||||||
class="videoTitle"
|
class="videoTitle"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ $t("Settings.Subscription Settings.Subscription Settings") }}
|
||||||
</h3>
|
</h3>
|
||||||
<ft-flex-box class="subscriptionSettingsFlexBox">
|
<ft-flex-box class="subscriptionSettingsFlexBox">
|
||||||
<ft-toggle-switch
|
<ft-toggle-switch
|
||||||
label="Hide Videos When Watched"
|
:label="$t('Settings.Subscription Settings.Hide Videos on Watch')"
|
||||||
|
:default-value="hideWatchedSubs"
|
||||||
|
@change="updateHideWatchedSubs"
|
||||||
|
/>
|
||||||
|
<ft-toggle-switch
|
||||||
|
:label="$t('Settings.Subscription Settings.Fetch Feeds from RSS')"
|
||||||
|
:default-value="useRssFeeds"
|
||||||
|
@change="updateUseRssFeeds"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
<br>
|
<br>
|
||||||
<ft-flex-box>
|
<ft-flex-box>
|
||||||
<ft-select
|
<ft-select
|
||||||
|
v-if="false"
|
||||||
placeholder="Subscription View Type"
|
placeholder="Subscription View Type"
|
||||||
:value="viewValues[0]"
|
:value="viewValues[0]"
|
||||||
:select-names="viewNames"
|
:select-names="viewNames"
|
||||||
|
@ -24,6 +32,7 @@
|
||||||
<br>
|
<br>
|
||||||
<ft-flex-box>
|
<ft-flex-box>
|
||||||
<ft-button
|
<ft-button
|
||||||
|
v-if="false"
|
||||||
label="Manage My Subscriptions"
|
label="Manage My Subscriptions"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import FtInput from '../ft-input/ft-input.vue'
|
import FtInput from '../ft-input/ft-input.vue'
|
||||||
import FtSearchFilters from '../ft-search-filters/ft-search-filters.vue'
|
import FtSearchFilters from '../ft-search-filters/ft-search-filters.vue'
|
||||||
|
import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import router from '../../router/index.js'
|
import router from '../../router/index.js'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
@ -10,7 +11,8 @@ export default Vue.extend({
|
||||||
name: 'TopNav',
|
name: 'TopNav',
|
||||||
components: {
|
components: {
|
||||||
FtInput,
|
FtInput,
|
||||||
FtSearchFilters
|
FtSearchFilters,
|
||||||
|
FtProfileSelector
|
||||||
},
|
},
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -59,6 +59,9 @@
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
|
|
||||||
|
&.profiles
|
||||||
|
justify-content: flex-end
|
||||||
|
|
||||||
.navSearchIcon
|
.navSearchIcon
|
||||||
@media only screen and (min-width: 681px)
|
@media only screen and (min-width: 681px)
|
||||||
display: none
|
display: none
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
:class="{ expand: !isSideNavOpen }"
|
:class="{ expand: !isSideNavOpen }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="side" />
|
<ft-profile-selector class="side profiles" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,14 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getUsingElectron
|
return this.$store.getters.getUsingElectron
|
||||||
},
|
},
|
||||||
|
|
||||||
|
profileList: function () {
|
||||||
|
return this.$store.getters.getProfileList
|
||||||
|
},
|
||||||
|
|
||||||
|
activeProfile: function () {
|
||||||
|
return this.$store.getters.getActiveProfile
|
||||||
|
},
|
||||||
|
|
||||||
formatTypeNames: function () {
|
formatTypeNames: function () {
|
||||||
return [
|
return [
|
||||||
this.$t('Change Format.Use Dash Formats').toUpperCase(),
|
this.$t('Change Format.Use Dash Formats').toUpperCase(),
|
||||||
|
@ -99,8 +107,24 @@ export default Vue.extend({
|
||||||
return this.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ` ${this.$t('Video.Views').toLowerCase()}`
|
return this.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ` ${this.$t('Video.Views').toLowerCase()}`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isSubscribed: function () {
|
||||||
|
const subIndex = this.profileList[this.activeProfile].subscriptions.findIndex((channel) => {
|
||||||
|
return channel.id === this.channelId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (subIndex === -1) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
subscribedText: function () {
|
subscribedText: function () {
|
||||||
return `${this.$t('Channel.Subscribe').toUpperCase()} ${this.subscriptionCountText}`
|
if (this.isSubscribed) {
|
||||||
|
return `${this.$t('Channel.Unsubscribe').toUpperCase()} ${this.subscriptionCountText}`
|
||||||
|
} else {
|
||||||
|
return `${this.$t('Channel.Subscribe').toUpperCase()} ${this.subscriptionCountText}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
dateString() {
|
dateString() {
|
||||||
|
@ -116,9 +140,74 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSubscription: function () {
|
handleSubscription: function () {
|
||||||
this.showToast({
|
const currentProfile = JSON.parse(JSON.stringify(this.profileList[this.activeProfile]))
|
||||||
message: 'Subscriptions have not yet been implemented'
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
||||||
})
|
|
||||||
|
if (this.isSubscribed) {
|
||||||
|
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
|
||||||
|
return channel.id !== this.channelId
|
||||||
|
})
|
||||||
|
|
||||||
|
this.updateProfile(currentProfile)
|
||||||
|
this.showToast({
|
||||||
|
message: 'Channel has been removed from your subscriptions'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.activeProfile === 0) {
|
||||||
|
// Check if a subscription exists in a different profile.
|
||||||
|
// Remove from there as well.
|
||||||
|
let duplicateSubscriptions = 0
|
||||||
|
|
||||||
|
this.profileList.forEach((profile) => {
|
||||||
|
if (profile._id === 'allChannels') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parsedProfile = JSON.parse(JSON.stringify(profile))
|
||||||
|
const index = parsedProfile.subscriptions.findIndex((channel) => {
|
||||||
|
return channel.id === this.channelId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
duplicateSubscriptions++
|
||||||
|
|
||||||
|
parsedProfile.subscriptions = parsedProfile.subscriptions.filter((x) => {
|
||||||
|
return x.id !== this.channelId
|
||||||
|
})
|
||||||
|
|
||||||
|
this.updateProfile(parsedProfile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (duplicateSubscriptions > 0) {
|
||||||
|
this.showToast({
|
||||||
|
message: `Removed subscription from ${duplicateSubscriptions} other channel(s)`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const subscription = {
|
||||||
|
id: this.channelId,
|
||||||
|
name: this.channelName,
|
||||||
|
thumbnail: this.channelThumbnail
|
||||||
|
}
|
||||||
|
currentProfile.subscriptions.push(subscription)
|
||||||
|
|
||||||
|
this.updateProfile(currentProfile)
|
||||||
|
this.showToast({
|
||||||
|
message: 'Added channel to your subscriptions'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.activeProfile !== 0) {
|
||||||
|
const index = primaryProfile.subscriptions.findIndex((channel) => {
|
||||||
|
return channel.id === this.channelId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
primaryProfile.subscriptions.push(subscription)
|
||||||
|
this.updateProfile(primaryProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleFormatChange: function (format) {
|
handleFormatChange: function (format) {
|
||||||
|
@ -136,7 +225,8 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'showToast'
|
'showToast',
|
||||||
|
'updateProfile'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
:label="subscribedText"
|
:label="subscribedText"
|
||||||
class="subscribeButton"
|
class="subscribeButton"
|
||||||
background-color="var(--primary-color)"
|
background-color="var(--primary-color)"
|
||||||
|
text-color="var(--text-with-main-color)"
|
||||||
@click="handleSubscription"
|
@click="handleSubscription"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
|
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
|
||||||
|
import ProfileSettings from '../views/ProfileSettings/ProfileSettings.vue'
|
||||||
|
import ProfileEdit from '../views/ProfileEdit/ProfileEdit.vue'
|
||||||
import Trending from '../views/Trending/Trending.vue'
|
import Trending from '../views/Trending/Trending.vue'
|
||||||
import Popular from '../views/Popular/Popular.vue'
|
import Popular from '../views/Popular/Popular.vue'
|
||||||
import UserPlaylists from '../views/UserPlaylists/UserPlaylists.vue'
|
import UserPlaylists from '../views/UserPlaylists/UserPlaylists.vue'
|
||||||
|
@ -32,6 +34,32 @@ const router = new Router({
|
||||||
},
|
},
|
||||||
component: Subscriptions
|
component: Subscriptions
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/profile',
|
||||||
|
meta: {
|
||||||
|
title: 'Profile Settings',
|
||||||
|
icon: 'fa-home'
|
||||||
|
},
|
||||||
|
component: ProfileSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/profile/new',
|
||||||
|
name: 'newProfile',
|
||||||
|
meta: {
|
||||||
|
title: 'New Profile',
|
||||||
|
icon: 'fa-home'
|
||||||
|
},
|
||||||
|
component: ProfileEdit
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/profile/edit/:id',
|
||||||
|
name: 'editProfile',
|
||||||
|
meta: {
|
||||||
|
title: 'Edit Profile',
|
||||||
|
icon: 'fa-home'
|
||||||
|
},
|
||||||
|
component: ProfileEdit
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/trending',
|
path: '/trending',
|
||||||
meta: {
|
meta: {
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
import Datastore from 'nedb'
|
||||||
|
|
||||||
|
let dbLocation
|
||||||
|
|
||||||
|
if (window && window.process && window.process.type === 'renderer') {
|
||||||
|
// Electron is being used
|
||||||
|
/* let dbLocation = localStorage.getItem('dbLocation')
|
||||||
|
|
||||||
|
if (dbLocation === null) {
|
||||||
|
const electron = require('electron')
|
||||||
|
dbLocation = electron.remote.app.getPath('userData')
|
||||||
|
} */
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
dbLocation = electron.remote.app.getPath('userData')
|
||||||
|
|
||||||
|
dbLocation = dbLocation + '/profiles.db'
|
||||||
|
} else {
|
||||||
|
dbLocation = 'profiles.db'
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileDb = new Datastore({
|
||||||
|
filename: dbLocation,
|
||||||
|
autoload: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
profileList: [{
|
||||||
|
_id: 'allChannels',
|
||||||
|
name: 'All Channels',
|
||||||
|
bgColor: '#000000',
|
||||||
|
textColor: '#FFFFFF',
|
||||||
|
subscriptions: []
|
||||||
|
}],
|
||||||
|
activeProfile: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
getProfileList: () => {
|
||||||
|
return state.profileList
|
||||||
|
},
|
||||||
|
|
||||||
|
getActiveProfile: () => {
|
||||||
|
return state.activeProfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
grabAllProfiles ({ dispatch, commit }, defaultName = null) {
|
||||||
|
profileDb.find({}, (err, results) => {
|
||||||
|
if (!err) {
|
||||||
|
if (results.length === 0) {
|
||||||
|
dispatch('createDefaultProfile', defaultName)
|
||||||
|
} else {
|
||||||
|
// We want the primary profile to always be first
|
||||||
|
// So sort with that then sort alphabetically by profile name
|
||||||
|
const profiles = results.sort((a, b) => {
|
||||||
|
if (a._id === 'allChannels') {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b._id === 'allChannels') {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.name - a.name
|
||||||
|
})
|
||||||
|
commit('setProfileList', profiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
grabProfileInfo (_, profileId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log(profileId)
|
||||||
|
profileDb.findOne({ _id: profileId }, (err, results) => {
|
||||||
|
if (!err) {
|
||||||
|
resolve(results)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async createDefaultProfile ({ dispatch }, defaultName) {
|
||||||
|
const randomColor = await dispatch('getRandomColor')
|
||||||
|
const textColor = await dispatch('calculateColorLuminance', randomColor)
|
||||||
|
const defaultProfile = {
|
||||||
|
_id: 'allChannels',
|
||||||
|
name: defaultName,
|
||||||
|
bgColor: randomColor,
|
||||||
|
textColor: textColor,
|
||||||
|
subscriptions: []
|
||||||
|
}
|
||||||
|
console.log(defaultProfile)
|
||||||
|
profileDb.update({ _id: 'allChannels' }, defaultProfile, { upsert: true }, (err, numReplaced) => {
|
||||||
|
if (!err) {
|
||||||
|
dispatch('grabAllProfiles')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProfile ({ dispatch }, profile) {
|
||||||
|
profileDb.update({ _id: profile._id }, profile, { upsert: true }, (err, numReplaced) => {
|
||||||
|
if (!err) {
|
||||||
|
dispatch('grabAllProfiles')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
insertProfile ({ dispatch }, profile) {
|
||||||
|
profileDb.insert(profile, (err, newDocs) => {
|
||||||
|
if (!err) {
|
||||||
|
dispatch('grabAllProfiles')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
removeProfile ({ dispatch }, profileId) {
|
||||||
|
profileDb.remove({ _id: profileId }, (err, numReplaced) => {
|
||||||
|
if (!err) {
|
||||||
|
dispatch('grabAllProfiles')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateActiveProfile ({ commit }, index) {
|
||||||
|
commit('setActiveProfile', index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
setProfileList (state, profileList) {
|
||||||
|
state.profileList = profileList
|
||||||
|
},
|
||||||
|
setActiveProfile (state, activeProfile) {
|
||||||
|
state.activeProfile = activeProfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ const state = {
|
||||||
listType: 'grid',
|
listType: 'grid',
|
||||||
thumbnailPreference: '',
|
thumbnailPreference: '',
|
||||||
invidiousInstance: 'https://invidio.us',
|
invidiousInstance: 'https://invidio.us',
|
||||||
|
defaultProfile: 'allChannels',
|
||||||
barColor: false,
|
barColor: false,
|
||||||
enableSearchSuggestions: true,
|
enableSearchSuggestions: true,
|
||||||
rememberHistory: true,
|
rememberHistory: true,
|
||||||
|
@ -56,9 +57,8 @@ const state = {
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
disctractionFreeMode: false,
|
disctractionFreeMode: false,
|
||||||
hideWatchedSubs: false,
|
hideWatchedSubs: false,
|
||||||
usingElectron: true,
|
useRssFeeds: false,
|
||||||
profileList: [{ name: 'All Channels', color: '#304FFE' }],
|
usingElectron: true
|
||||||
defaultProfile: 'All Channels'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
|
@ -102,6 +102,10 @@ const getters = {
|
||||||
return state.invidiousInstance
|
return state.invidiousInstance
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDefaultProfile: () => {
|
||||||
|
return state.defaultProfile
|
||||||
|
},
|
||||||
|
|
||||||
getRememberHistory: () => {
|
getRememberHistory: () => {
|
||||||
return state.rememberHistory
|
return state.rememberHistory
|
||||||
},
|
},
|
||||||
|
@ -154,13 +158,21 @@ const getters = {
|
||||||
return state.defaultQuality
|
return state.defaultQuality
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getHideWatchedSubs: () => {
|
||||||
|
return state.hideWatchedSubs
|
||||||
|
},
|
||||||
|
|
||||||
|
getUseRssFeeds: () => {
|
||||||
|
return state.useRssFeeds
|
||||||
|
},
|
||||||
|
|
||||||
getUsingElectron: () => {
|
getUsingElectron: () => {
|
||||||
return state.usingElectron
|
return state.usingElectron
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
grabUserSettings ({ dispatch, commit }) {
|
grabUserSettings ({ dispatch, commit, rootState }) {
|
||||||
settingsDb.find({}, (err, results) => {
|
settingsDb.find({}, (err, results) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
console.log(results)
|
console.log(results)
|
||||||
|
@ -176,6 +188,9 @@ const actions = {
|
||||||
case 'backendFallback':
|
case 'backendFallback':
|
||||||
commit('setBackendFallback', result.value)
|
commit('setBackendFallback', result.value)
|
||||||
break
|
break
|
||||||
|
case 'defaultProfile':
|
||||||
|
commit('setDefaultProfile', result.value)
|
||||||
|
break
|
||||||
case 'checkForUpdates':
|
case 'checkForUpdates':
|
||||||
commit('setCheckForUpdates', result.value)
|
commit('setCheckForUpdates', result.value)
|
||||||
break
|
break
|
||||||
|
@ -200,6 +215,12 @@ const actions = {
|
||||||
case 'barColor':
|
case 'barColor':
|
||||||
commit('setBarColor', result.value)
|
commit('setBarColor', result.value)
|
||||||
break
|
break
|
||||||
|
case 'hideWatchedSubs':
|
||||||
|
commit('setHideWatchedSubs', result.value)
|
||||||
|
break
|
||||||
|
case 'useRssFeeds':
|
||||||
|
commit('setUseRssFeeds', result.value)
|
||||||
|
break
|
||||||
case 'rememberHistory':
|
case 'rememberHistory':
|
||||||
commit('setRememberHistory', result.value)
|
commit('setRememberHistory', result.value)
|
||||||
break
|
break
|
||||||
|
@ -255,6 +276,14 @@ const actions = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateDefaultProfile ({ commit }, defaultProfile) {
|
||||||
|
settingsDb.update({ _id: 'defaultProfile' }, { _id: 'defaultProfile', value: defaultProfile }, { upsert: true }, (err, numReplaced) => {
|
||||||
|
if (!err) {
|
||||||
|
commit('setDefaultProfile', defaultProfile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
updateBackendFallback ({ commit }, backendFallback) {
|
updateBackendFallback ({ commit }, backendFallback) {
|
||||||
settingsDb.update({ _id: 'backendFallback' }, { _id: 'backendFallback', value: backendFallback }, { upsert: true }, (err, numReplaced) => {
|
settingsDb.update({ _id: 'backendFallback' }, { _id: 'backendFallback', value: backendFallback }, { upsert: true }, (err, numReplaced) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
@ -327,6 +356,22 @@ const actions = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateHideWatchedSubs ({ commit }, hideWatchedSubs) {
|
||||||
|
settingsDb.update({ _id: 'hideWatchedSubs' }, { _id: 'hideWatchedSubs', value: hideWatchedSubs }, { upsert: true }, (err, numReplaced) => {
|
||||||
|
if (!err) {
|
||||||
|
commit('setHideWatchedSubs', hideWatchedSubs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUseRssFeeds ({ commit }, useRssFeeds) {
|
||||||
|
settingsDb.update({ _id: 'useRssFeeds' }, { _id: 'useRssFeeds', value: useRssFeeds }, { upsert: true }, (err, numReplaced) => {
|
||||||
|
if (!err) {
|
||||||
|
commit('setUseRssFeeds', useRssFeeds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
updateRememberHistory ({ commit }, history) {
|
updateRememberHistory ({ commit }, history) {
|
||||||
settingsDb.update({ _id: 'rememberHistory' }, { _id: 'rememberHistory', value: history }, { upsert: true }, (err, numReplaced) => {
|
settingsDb.update({ _id: 'rememberHistory' }, { _id: 'rememberHistory', value: history }, { upsert: true }, (err, numReplaced) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
@ -448,6 +493,9 @@ const mutations = {
|
||||||
setCurrentTheme (state, currentTheme) {
|
setCurrentTheme (state, currentTheme) {
|
||||||
state.barColor = currentTheme
|
state.barColor = currentTheme
|
||||||
},
|
},
|
||||||
|
setDefaultProfile (state, defaultProfile) {
|
||||||
|
state.defaultProfile = defaultProfile
|
||||||
|
},
|
||||||
setBackendFallback (state, backendFallback) {
|
setBackendFallback (state, backendFallback) {
|
||||||
state.backendFallback = backendFallback
|
state.backendFallback = backendFallback
|
||||||
},
|
},
|
||||||
|
@ -529,6 +577,9 @@ const mutations = {
|
||||||
setHideWatchedSubs (state, hideWatchedSubs) {
|
setHideWatchedSubs (state, hideWatchedSubs) {
|
||||||
state.hideWatchedSubs = hideWatchedSubs
|
state.hideWatchedSubs = hideWatchedSubs
|
||||||
},
|
},
|
||||||
|
setUseRssFeeds (state, useRssFeeds) {
|
||||||
|
state.useRssFeeds = useRssFeeds
|
||||||
|
},
|
||||||
setUsingElectron (state, usingElectron) {
|
setUsingElectron (state, usingElectron) {
|
||||||
state.usingElectron = usingElectron
|
state.usingElectron = usingElectron
|
||||||
},
|
},
|
||||||
|
@ -537,9 +588,6 @@ const mutations = {
|
||||||
},
|
},
|
||||||
setProfileList (state, profileList) {
|
setProfileList (state, profileList) {
|
||||||
state.profileList = profileList
|
state.profileList = profileList
|
||||||
},
|
|
||||||
setDefaultProfile (state, defaultProfile) {
|
|
||||||
state.defaultProfile = defaultProfile
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,61 +1,38 @@
|
||||||
import Datastore from 'nedb'
|
|
||||||
|
|
||||||
let dbLocation
|
|
||||||
|
|
||||||
if (window && window.process && window.process.type === 'renderer') {
|
|
||||||
// Electron is being used
|
|
||||||
let dbLocation = localStorage.getItem('dbLocation')
|
|
||||||
|
|
||||||
if (dbLocation === null) {
|
|
||||||
const electron = require('electron')
|
|
||||||
dbLocation = electron.remote.app.getPath('userData')
|
|
||||||
}
|
|
||||||
|
|
||||||
dbLocation += '/subscriptions.db'
|
|
||||||
} else {
|
|
||||||
dbLocation = 'subscriptions.db'
|
|
||||||
}
|
|
||||||
|
|
||||||
const subDb = new Datastore({
|
|
||||||
filename: dbLocation,
|
|
||||||
autoload: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
subscriptions: []
|
allSubscriptionsList: [],
|
||||||
|
profileSubscriptions: {
|
||||||
|
activeProfile: 0,
|
||||||
|
videoList: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const getters = {
|
||||||
addSubscription (state, payload) {
|
getAllSubscriptionsList: () => {
|
||||||
state.subscriptions.push(payload)
|
return state.allSubscriptionsList
|
||||||
},
|
},
|
||||||
setSubscriptions (state, payload) {
|
getProfileSubscriptions: () => {
|
||||||
state.subscriptions = payload
|
return state.profileSubscriptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
addSubscriptions ({ commit }, payload) {
|
updateAllSubscriptionsList ({ commit }, subscriptions) {
|
||||||
subDb.insert(payload, (err, payload) => {
|
commit('setAllSubscriptionsList', subscriptions)
|
||||||
if (!err) {
|
|
||||||
commit('addSubscription', payload)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
getSubscriptions ({ commit }, payload) {
|
updateProfileSubscriptions ({ commit }, subscriptions) {
|
||||||
subDb.find({}, (err, payload) => {
|
commit('setProfileSubscriptions', subscriptions)
|
||||||
if (!err) {
|
|
||||||
commit('setSubscriptions', payload)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
removeSubscription ({ commit }, channelId) {
|
|
||||||
subDb.remove({ channelId: channelId }, {}, () => {
|
|
||||||
commit('setSubscriptions', this.state.subscriptions.filter(sub => sub.channelId !== channelId))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const getters = {}
|
|
||||||
|
const mutations = {
|
||||||
|
setAllSubscriptionsList (state, allSubscriptionsList) {
|
||||||
|
state.allSubscriptionsList = allSubscriptionsList
|
||||||
|
},
|
||||||
|
setProfileSubscriptions (state, profileSubscriptions) {
|
||||||
|
state.profileSubscriptions = profileSubscriptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
state,
|
state,
|
||||||
getters,
|
getters,
|
||||||
|
|
|
@ -5,6 +5,8 @@ const state = {
|
||||||
sessionSearchHistory: [],
|
sessionSearchHistory: [],
|
||||||
popularCache: null,
|
popularCache: null,
|
||||||
trendingCache: null,
|
trendingCache: null,
|
||||||
|
showProgressBar: false,
|
||||||
|
progressBarPercentage: 0,
|
||||||
searchSettings: {
|
searchSettings: {
|
||||||
sortBy: 'relevance',
|
sortBy: 'relevance',
|
||||||
time: '',
|
time: '',
|
||||||
|
@ -28,6 +30,24 @@ const state = {
|
||||||
'mainAmber',
|
'mainAmber',
|
||||||
'mainOrange',
|
'mainOrange',
|
||||||
'mainDeepOrange'
|
'mainDeepOrange'
|
||||||
|
],
|
||||||
|
colorValues: [
|
||||||
|
'#d50000',
|
||||||
|
'#C51162',
|
||||||
|
'#AA00FF',
|
||||||
|
'#6200EA',
|
||||||
|
'#304FFE',
|
||||||
|
'#2962FF',
|
||||||
|
'#0091EA',
|
||||||
|
'#00B8D4',
|
||||||
|
'#00BFA5',
|
||||||
|
'#00C853',
|
||||||
|
'#64DD17',
|
||||||
|
'#AEEA00',
|
||||||
|
'#FFD600',
|
||||||
|
'#FFAB00',
|
||||||
|
'#FF6D00',
|
||||||
|
'#DD2C00'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,15 +74,78 @@ const getters = {
|
||||||
|
|
||||||
getSearchSettings () {
|
getSearchSettings () {
|
||||||
return state.searchSettings
|
return state.searchSettings
|
||||||
|
},
|
||||||
|
|
||||||
|
getColorValues () {
|
||||||
|
return state.colorValues
|
||||||
|
},
|
||||||
|
|
||||||
|
getShowProgressBar () {
|
||||||
|
return state.showProgressBar
|
||||||
|
},
|
||||||
|
|
||||||
|
getProgressBarPercentage () {
|
||||||
|
return state.progressBarPercentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
updateShowProgressBar ({ commit }, value) {
|
||||||
|
commit('setShowProgressBar', value)
|
||||||
|
},
|
||||||
|
|
||||||
getRandomColorClass () {
|
getRandomColorClass () {
|
||||||
const randomInt = Math.floor(Math.random() * state.colorClasses.length)
|
const randomInt = Math.floor(Math.random() * state.colorClasses.length)
|
||||||
return state.colorClasses[randomInt]
|
return state.colorClasses[randomInt]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getRandomColor () {
|
||||||
|
const randomInt = Math.floor(Math.random() * state.colorValues.length)
|
||||||
|
return state.colorValues[randomInt]
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateColorLuminance (_, colorValue) {
|
||||||
|
const cutHex = colorValue.substring(1, 7)
|
||||||
|
const colorValueR = parseInt(cutHex.substring(0, 2), 16)
|
||||||
|
const colorValueG = parseInt(cutHex.substring(2, 4), 16)
|
||||||
|
const colorValueB = parseInt(cutHex.substring(4, 6), 16)
|
||||||
|
|
||||||
|
const luminance = (0.299 * colorValueR + 0.587 * colorValueG + 0.114 * colorValueB) / 255
|
||||||
|
|
||||||
|
if (luminance > 0.5) {
|
||||||
|
return '#000000'
|
||||||
|
} else {
|
||||||
|
return '#FFFFFF'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
calculatePublishedDate(_, publishedText) {
|
||||||
|
const date = new Date()
|
||||||
|
|
||||||
|
const textSplit = publishedText.split(' ')
|
||||||
|
const timeFrame = textSplit[1]
|
||||||
|
const timeAmount = parseInt(textSplit[0])
|
||||||
|
let timeSpan = null
|
||||||
|
|
||||||
|
if (timeFrame.indexOf('second') > -1) {
|
||||||
|
timeSpan = timeAmount * 1000
|
||||||
|
} else if (timeFrame.indexOf('minute') > -1) {
|
||||||
|
timeSpan = timeAmount * 60000
|
||||||
|
} else if (timeFrame.indexOf('hour') > -1) {
|
||||||
|
timeSpan = timeAmount * 3600000
|
||||||
|
} else if (timeFrame.indexOf('day') > -1) {
|
||||||
|
timeSpan = timeAmount * 86400000
|
||||||
|
} else if (timeFrame.indexOf('week') > -1) {
|
||||||
|
timeSpan = timeAmount * 604800000
|
||||||
|
} else if (timeFrame.indexOf('month') > -1) {
|
||||||
|
timeSpan = timeAmount * 2592000000
|
||||||
|
} else if (timeFrame.indexOf('year') > -1) {
|
||||||
|
timeSpan = timeAmount * 31556952000
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.getTime() - timeSpan
|
||||||
|
},
|
||||||
|
|
||||||
getVideoIdFromUrl (_, url) {
|
getVideoIdFromUrl (_, url) {
|
||||||
/** @type {URL} */
|
/** @type {URL} */
|
||||||
let urlObject
|
let urlObject
|
||||||
|
@ -182,6 +265,8 @@ const actions = {
|
||||||
} else if (payload.isUpcoming || payload.publishText === null) {
|
} else if (payload.isUpcoming || payload.publishText === null) {
|
||||||
// the check for null is currently just an inferring of knowledge, because there is no other possibility left
|
// the check for null is currently just an inferring of knowledge, because there is no other possibility left
|
||||||
return payload.upcomingString
|
return payload.upcomingString
|
||||||
|
} else if (payload.isRSS) {
|
||||||
|
return payload.publishText
|
||||||
}
|
}
|
||||||
const strings = payload.publishText.split(' ')
|
const strings = payload.publishText.split(' ')
|
||||||
const singular = (strings[0] === '1')
|
const singular = (strings[0] === '1')
|
||||||
|
@ -254,6 +339,14 @@ const mutations = {
|
||||||
state.isSideNavOpen = !state.isSideNavOpen
|
state.isSideNavOpen = !state.isSideNavOpen
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setShowProgressBar (state, value) {
|
||||||
|
state.showProgressBar = value
|
||||||
|
},
|
||||||
|
|
||||||
|
setProgressBarPercentage (state, value) {
|
||||||
|
state.progressBarPercentage = value
|
||||||
|
},
|
||||||
|
|
||||||
setSessionSearchHistory (state, history) {
|
setSessionSearchHistory (state, history) {
|
||||||
state.sessionSearchHistory = history
|
state.sessionSearchHistory = history
|
||||||
},
|
},
|
||||||
|
|
|
@ -78,6 +78,34 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getSessionSearchHistory
|
return this.$store.getters.getSessionSearchHistory
|
||||||
},
|
},
|
||||||
|
|
||||||
|
profileList: function () {
|
||||||
|
return this.$store.getters.getProfileList
|
||||||
|
},
|
||||||
|
|
||||||
|
activeProfile: function () {
|
||||||
|
return this.$store.getters.getActiveProfile
|
||||||
|
},
|
||||||
|
|
||||||
|
isSubscribed: function () {
|
||||||
|
const subIndex = this.profileList[this.activeProfile].subscriptions.findIndex((channel) => {
|
||||||
|
return channel.id === this.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (subIndex === -1) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribedText: function () {
|
||||||
|
if (this.isSubscribed) {
|
||||||
|
return this.$t('Channel.Unsubscribe').toUpperCase()
|
||||||
|
} else {
|
||||||
|
return this.$t('Channel.Subscribe').toUpperCase()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
videoSelectNames: function () {
|
videoSelectNames: function () {
|
||||||
return [
|
return [
|
||||||
this.$t('Channel.Videos.Sort Types.Newest'),
|
this.$t('Channel.Videos.Sort Types.Newest'),
|
||||||
|
@ -408,7 +436,74 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSubscription: function () {
|
handleSubscription: function () {
|
||||||
console.log('TODO: Channel handleSubscription')
|
const currentProfile = JSON.parse(JSON.stringify(this.profileList[this.activeProfile]))
|
||||||
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
||||||
|
|
||||||
|
if (this.isSubscribed) {
|
||||||
|
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
|
||||||
|
return channel.id !== this.id
|
||||||
|
})
|
||||||
|
|
||||||
|
this.updateProfile(currentProfile)
|
||||||
|
this.showToast({
|
||||||
|
message: 'Channel has been removed from your subscriptions'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.activeProfile === 0) {
|
||||||
|
// Check if a subscription exists in a different profile.
|
||||||
|
// Remove from there as well.
|
||||||
|
let duplicateSubscriptions = 0
|
||||||
|
|
||||||
|
this.profileList.forEach((profile) => {
|
||||||
|
if (profile._id === 'allChannels') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parsedProfile = JSON.parse(JSON.stringify(profile))
|
||||||
|
const index = parsedProfile.subscriptions.findIndex((channel) => {
|
||||||
|
return channel.id === this.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
duplicateSubscriptions++
|
||||||
|
|
||||||
|
parsedProfile.subscriptions = parsedProfile.subscriptions.filter((x) => {
|
||||||
|
return x.id !== this.channelId
|
||||||
|
})
|
||||||
|
|
||||||
|
this.updateProfile(parsedProfile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (duplicateSubscriptions > 0) {
|
||||||
|
this.showToast({
|
||||||
|
message: `Removed subscription from ${duplicateSubscriptions} other channel(s)`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const subscription = {
|
||||||
|
id: this.id,
|
||||||
|
name: this.channelName,
|
||||||
|
thumbnail: this.thumbnailUrl
|
||||||
|
}
|
||||||
|
currentProfile.subscriptions.push(subscription)
|
||||||
|
|
||||||
|
this.updateProfile(currentProfile)
|
||||||
|
this.showToast({
|
||||||
|
message: 'Added channel to your subscriptions'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.activeProfile !== 0) {
|
||||||
|
const index = primaryProfile.subscriptions.findIndex((channel) => {
|
||||||
|
return channel.id === this.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
primaryProfile.subscriptions.push(subscription)
|
||||||
|
this.updateProfile(primaryProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleFetchMore: function () {
|
handleFetchMore: function () {
|
||||||
|
@ -549,7 +644,8 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'showToast'
|
'showToast',
|
||||||
|
'updateProfile'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ft-button
|
<ft-button
|
||||||
:label="$t('Channel.Subscribe')"
|
:label="subscribedText"
|
||||||
background-color="var(--primary-color)"
|
background-color="var(--primary-color)"
|
||||||
text-color="var(--text-with-main-color)"
|
text-color="var(--text-with-main-color)"
|
||||||
class="subscribeButton"
|
class="subscribeButton"
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
.card {
|
||||||
|
width: 85%;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: var(--tertiary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileName {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomMargin {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOptions {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOption {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
margin: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 200px 200px 200px 200px;
|
||||||
|
-webkit-border-radius: 200px 200px 200px 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial {
|
||||||
|
font-size: 50px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
bottom: 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 680px) {
|
||||||
|
.card {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||||
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
|
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
|
||||||
|
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||||
|
import FtInput from '../../components/ft-input/ft-input.vue'
|
||||||
|
import FtButton from '../../components/ft-button/ft-button.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'ProfileEdit',
|
||||||
|
components: {
|
||||||
|
'ft-loader': FtLoader,
|
||||||
|
'ft-card': FtCard,
|
||||||
|
'ft-prompt': FtPrompt,
|
||||||
|
'ft-flex-box': FtFlexBox,
|
||||||
|
'ft-input': FtInput,
|
||||||
|
'ft-button': FtButton
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
showDeletePrompt: false,
|
||||||
|
deletePromptLabel: '',
|
||||||
|
isNew: false,
|
||||||
|
profileId: '',
|
||||||
|
profileName: '',
|
||||||
|
profileBgColor: '',
|
||||||
|
profileTextColor: '',
|
||||||
|
profileSubscriptions: [],
|
||||||
|
deletePromptValues: [
|
||||||
|
'yes',
|
||||||
|
'no'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
colorValues: function () {
|
||||||
|
return this.$store.getters.getColorValues
|
||||||
|
},
|
||||||
|
profileInitial: function () {
|
||||||
|
return this.profileName.slice(0, 1).toUpperCase()
|
||||||
|
},
|
||||||
|
activeProfile: function () {
|
||||||
|
return this.$store.getters.getActiveProfile
|
||||||
|
},
|
||||||
|
defaultProfile: function () {
|
||||||
|
return this.$store.getters.getDefaultProfile
|
||||||
|
},
|
||||||
|
deletePromptNames: function () {
|
||||||
|
return [
|
||||||
|
this.$t('Yes'),
|
||||||
|
this.$t('No')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
profileBgColor: async function (val) {
|
||||||
|
this.profileTextColor = await this.calculateColorLuminance(val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: async function () {
|
||||||
|
this.isLoading = true
|
||||||
|
const profileType = this.$route.name
|
||||||
|
|
||||||
|
this.deletePromptLabel = `${this.$t('Profile.Are you sure you want to delete this profile?')} ${this.$t('Profile["All subscriptions will also be deleted."]')}`
|
||||||
|
|
||||||
|
if (profileType === 'newProfile') {
|
||||||
|
this.isNew = true
|
||||||
|
this.profileBgColor = await this.getRandomColor()
|
||||||
|
this.isLoading = false
|
||||||
|
} else {
|
||||||
|
this.isNew = false
|
||||||
|
this.profileId = this.$route.params.id
|
||||||
|
console.log(this.profileId)
|
||||||
|
console.log(this.$route.name)
|
||||||
|
|
||||||
|
this.grabProfileInfo(this.profileId).then((profile) => {
|
||||||
|
if (profile === null) {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Profile.Profile could not be found')
|
||||||
|
})
|
||||||
|
this.$router.push({
|
||||||
|
path: '/settings/profile/'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.profileName = profile.name
|
||||||
|
this.profileBgColor = profile.bgColor
|
||||||
|
this.profileTextColor = profile.textColor
|
||||||
|
this.profileSubscriptions = profile.subscriptions
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openDeletePrompt: function () {
|
||||||
|
this.showDeletePrompt = true
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDeletePrompt: function (response) {
|
||||||
|
if (response === 'yes') {
|
||||||
|
this.deleteProfile()
|
||||||
|
} else {
|
||||||
|
this.showDeletePrompt = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveProfile: function () {
|
||||||
|
if (this.profileName === '') {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Profile.Your profile name cannot be empty')
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const profile = {
|
||||||
|
name: this.profileName,
|
||||||
|
bgColor: this.profileBgColor,
|
||||||
|
textColor: this.profileTextColor,
|
||||||
|
subscriptions: this.profileSubscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isNew) {
|
||||||
|
profile._id = this.profileId
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(profile)
|
||||||
|
|
||||||
|
this.updateProfile(profile)
|
||||||
|
|
||||||
|
if (this.isNew) {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Profile.Profile has been created')
|
||||||
|
})
|
||||||
|
this.$router.push({
|
||||||
|
path: '/settings/profile/'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Profile.Profile has been updated')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setDefaultProfile: function () {
|
||||||
|
this.updateDefaultProfile(this.profileId)
|
||||||
|
const message = this.$t('Profile.Your default profile has been set to $').replace('$', this.profileName)
|
||||||
|
this.showToast({
|
||||||
|
message: message
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteProfile: function () {
|
||||||
|
this.removeProfile(this.profileId)
|
||||||
|
const message = this.$t('Profile.Removed $ from your profiles').replace('$', this.profileName)
|
||||||
|
this.showToast({
|
||||||
|
message: message
|
||||||
|
})
|
||||||
|
if (this.defaultProfile === this.profileId) {
|
||||||
|
this.updateDefaultProfile('allChannels')
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Profile.Your default profile has been changed to your primary profile')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.activeProfile._id === this.profileId) {
|
||||||
|
this.updateActiveProfile('allChannels')
|
||||||
|
}
|
||||||
|
this.$router.push({
|
||||||
|
path: '/settings/profile/'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
...mapActions([
|
||||||
|
'showToast',
|
||||||
|
'grabProfileInfo',
|
||||||
|
'updateProfile',
|
||||||
|
'removeProfile',
|
||||||
|
'updateDefaultProfile',
|
||||||
|
'updateActiveProfile',
|
||||||
|
'calculateColorLuminance',
|
||||||
|
'getRandomColor'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ft-loader
|
||||||
|
v-if="isLoading"
|
||||||
|
:fullscreen="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<ft-card class="card">
|
||||||
|
<h2>{{ $t("Profile.Edit Profile") }}</h2>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-input
|
||||||
|
class="profileName"
|
||||||
|
placeholder="Profile Name"
|
||||||
|
:value="profileName"
|
||||||
|
:show-arrow="false"
|
||||||
|
@input="e => profileName = e"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<h3>{{ $t("Profile.Color Picker") }}</h3>
|
||||||
|
<ft-flex-box
|
||||||
|
class="bottomMargin colorOptions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(color, index) in colorValues"
|
||||||
|
:key="index"
|
||||||
|
class="colorOption"
|
||||||
|
:style="{ background: color }"
|
||||||
|
@click="profileBgColor = color"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box
|
||||||
|
class="bottomMargin"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label for="colorPicker">{{ $t("Profile.Custom Color") }}</label>
|
||||||
|
<input
|
||||||
|
id="colorPicker"
|
||||||
|
type="color"
|
||||||
|
v-model="profileBgColor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-input
|
||||||
|
class="profileName"
|
||||||
|
placeholder=""
|
||||||
|
:value="profileBgColor"
|
||||||
|
:show-arrow="false"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<h3>{{ $t("Profile.Profile Preview") }}</h3>
|
||||||
|
<ft-flex-box
|
||||||
|
class="bottomMargin"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="colorOption"
|
||||||
|
:style="{ background: profileBgColor, color: profileTextColor }"
|
||||||
|
style="cursor: default"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="initial"
|
||||||
|
>
|
||||||
|
{{ profileInitial }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-button
|
||||||
|
v-if="isNew"
|
||||||
|
:label="$t('Profile.Create Profile')"
|
||||||
|
@click="saveProfile"
|
||||||
|
/>
|
||||||
|
<ft-button
|
||||||
|
v-if="!isNew"
|
||||||
|
:label="$t('Profile.Update Profile')"
|
||||||
|
@click="saveProfile"
|
||||||
|
/>
|
||||||
|
<ft-button
|
||||||
|
v-if="!isNew"
|
||||||
|
:label="$t('Profile.Make Default Profile')"
|
||||||
|
@click="setDefaultProfile"
|
||||||
|
/>
|
||||||
|
<ft-button
|
||||||
|
v-if="profileId !== 'allChannels' && !isNew"
|
||||||
|
:label="$t('Profile.Delete Profile')"
|
||||||
|
text-color="var(--text-with-main-color)"
|
||||||
|
background-color="var(--primary-color)"
|
||||||
|
@click="openDeletePrompt"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
</ft-card>
|
||||||
|
</div>
|
||||||
|
<ft-prompt
|
||||||
|
v-if="showDeletePrompt"
|
||||||
|
:label="deletePromptLabel"
|
||||||
|
:option-names="deletePromptNames"
|
||||||
|
:option-values="deletePromptValues"
|
||||||
|
@click="handleDeletePrompt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./ProfileEdit.js" />
|
||||||
|
<style scoped src="./ProfileEdit.css" />
|
|
@ -0,0 +1,21 @@
|
||||||
|
.card {
|
||||||
|
width: 85%;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: var(--tertiary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileList {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 680px) {
|
||||||
|
.card {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
|
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||||
|
import FtProfileBubble from '../../components/ft-profile-bubble/ft-profile-bubble.vue'
|
||||||
|
import FtButton from '../../components/ft-button/ft-button.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'ProfileSettings',
|
||||||
|
components: {
|
||||||
|
'ft-card': FtCard,
|
||||||
|
'ft-flex-box': FtFlexBox,
|
||||||
|
'ft-profile-bubble': FtProfileBubble,
|
||||||
|
'ft-button': FtButton
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
profileList: function () {
|
||||||
|
return this.$store.getters.getProfileList
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
console.log(this.profileList)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
newProfile: function () {
|
||||||
|
this.$router.push({
|
||||||
|
path: `/settings/profile/new/`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ft-card class="card">
|
||||||
|
<h3>{{ $t("Profile.Profile Manager") }}</h3>
|
||||||
|
<ft-flex-box
|
||||||
|
class="profileList"
|
||||||
|
>
|
||||||
|
<ft-profile-bubble
|
||||||
|
v-for="(profile, index) in profileList"
|
||||||
|
:key="index"
|
||||||
|
:profile-id="profile._id"
|
||||||
|
:profile-name="profile.name"
|
||||||
|
:background-color="profile.bgColor"
|
||||||
|
:text-color="profile.textColor"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-button
|
||||||
|
:label="$t('Profile.Create New Profile')"
|
||||||
|
@click="newProfile"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
</ft-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./ProfileSettings.js" />
|
||||||
|
<style scoped src="./ProfileSettings.css" />
|
|
@ -3,6 +3,7 @@
|
||||||
<general-settings />
|
<general-settings />
|
||||||
<theme-settings />
|
<theme-settings />
|
||||||
<player-settings />
|
<player-settings />
|
||||||
|
<subscription-settings />
|
||||||
<privacy-settings />
|
<privacy-settings />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
color: var(--tertiary-text-color);
|
color: var(--tertiary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floatingTopButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 70px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 680px) {
|
@media only screen and (max-width: 680px) {
|
||||||
.card {
|
.card {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
|
|
@ -1,15 +1,307 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import { mapActions, mapMutations } from 'vuex'
|
||||||
|
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
|
import FtButton from '../../components/ft-button/ft-button.vue'
|
||||||
|
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
|
||||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||||
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
|
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
|
||||||
|
|
||||||
|
import ytch from 'yt-channel-info'
|
||||||
|
import Parser from 'rss-parser'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'Subscriptions',
|
name: 'Subscriptions',
|
||||||
components: {
|
components: {
|
||||||
|
'ft-loader': FtLoader,
|
||||||
'ft-card': FtCard,
|
'ft-card': FtCard,
|
||||||
|
'ft-button': FtButton,
|
||||||
|
'ft-icon-button': FtIconButton,
|
||||||
'ft-flex-box': FtFlexBox,
|
'ft-flex-box': FtFlexBox,
|
||||||
'ft-element-list': FtElementList
|
'ft-element-list': FtElementList
|
||||||
},
|
},
|
||||||
mounted: function () {
|
data: function () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
dataLimit: 100,
|
||||||
|
videoList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
usingElectron: function () {
|
||||||
|
return this.$store.getters.getUsingElectron
|
||||||
|
},
|
||||||
|
|
||||||
|
backendPreference: function () {
|
||||||
|
return this.$store.getters.getBackendPreference
|
||||||
|
},
|
||||||
|
|
||||||
|
backendFallback: function () {
|
||||||
|
return this.$store.getters.getBackendFallback
|
||||||
|
},
|
||||||
|
|
||||||
|
invidiousInstance: function () {
|
||||||
|
return this.$store.getters.getInvidiousInstance
|
||||||
|
},
|
||||||
|
|
||||||
|
hideWatchedSubs: function () {
|
||||||
|
return this.$store.getters.getHideWatchedSubs
|
||||||
|
},
|
||||||
|
|
||||||
|
useRssFeeds: function () {
|
||||||
|
return this.$store.getters.getUseRssFeeds
|
||||||
|
},
|
||||||
|
|
||||||
|
profileList: function () {
|
||||||
|
return this.$store.getters.getProfileList
|
||||||
|
},
|
||||||
|
|
||||||
|
activeVideoList: function () {
|
||||||
|
if (this.videoList.length < this.dataLimit) {
|
||||||
|
return this.videoList
|
||||||
|
} else {
|
||||||
|
return this.videoList.slice(0, this.dataLimit)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
activeProfile: function () {
|
||||||
|
return this.$store.getters.getActiveProfile
|
||||||
|
},
|
||||||
|
|
||||||
|
profileSubscriptions: function () {
|
||||||
|
return this.$store.getters.getProfileSubscriptions
|
||||||
|
},
|
||||||
|
|
||||||
|
allSubscriptionsList: function () {
|
||||||
|
return this.$store.getters.getAllSubscriptionsList
|
||||||
|
},
|
||||||
|
|
||||||
|
historyCache: function () {
|
||||||
|
return this.$store.getters.getHistoryCache
|
||||||
|
},
|
||||||
|
|
||||||
|
activeSubscriptionList: function () {
|
||||||
|
return this.profileList[this.activeProfile].subscriptions
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeProfile: async function (val) {
|
||||||
|
if (this.allSubscriptionsList.length !== 0) {
|
||||||
|
this.isLoading = true
|
||||||
|
this.videoList = await Promise.all(this.allSubscriptionsList.filter((video) => {
|
||||||
|
const channelIndex = this.activeSubscriptionList.findIndex((x) => {
|
||||||
|
return x.id === video.authorId
|
||||||
|
})
|
||||||
|
|
||||||
|
const historyIndex = this.historyCache.findIndex((x) => {
|
||||||
|
return x.videoId === video.videoId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.hideWatchedSubs) {
|
||||||
|
return channelIndex !== -1 && historyIndex === -1
|
||||||
|
} else {
|
||||||
|
return channelIndex !== -1
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
this.isLoading = false
|
||||||
|
} else {
|
||||||
|
this.getSubscriptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: async function () {
|
||||||
|
this.isLoading = true
|
||||||
|
const dataLimit = sessionStorage.getItem('subscriptionLimit')
|
||||||
|
if (dataLimit !== null) {
|
||||||
|
this.dataLimit = dataLimit
|
||||||
|
}
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (this.profileSubscriptions.videoList.length === 0) {
|
||||||
|
this.getSubscriptions()
|
||||||
|
} else {
|
||||||
|
const subscriptionList = JSON.parse(JSON.stringify(this.profileSubscriptions))
|
||||||
|
if (this.hideWatchedSubs) {
|
||||||
|
this.videoList = await Promise.all(subscriptionList.videoList.filter((video) => {
|
||||||
|
const historyIndex = this.historyCache.findIndex((x) => {
|
||||||
|
return x.videoId === video.videoId
|
||||||
|
})
|
||||||
|
|
||||||
|
return historyIndex === -1
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
this.videoList = subscriptionList.videoList
|
||||||
|
}
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSubscriptions: function () {
|
||||||
|
if (this.activeSubscriptionList.length === 0) {
|
||||||
|
this.isLoading = false
|
||||||
|
this.videoList = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.isLoading = true
|
||||||
|
this.updateShowProgressBar(true)
|
||||||
|
this.setProgressBarPercentage(0)
|
||||||
|
|
||||||
|
let videoList = []
|
||||||
|
let channelCount = 0
|
||||||
|
|
||||||
|
this.activeSubscriptionList.forEach(async (channel) => {
|
||||||
|
let videos = []
|
||||||
|
|
||||||
|
if (!this.usingElectron || this.backendPreference === 'invidious') {
|
||||||
|
if (this.useRssFeeds) {
|
||||||
|
videos = await this.getChannelVideosInvidiousRSS(channel.id)
|
||||||
|
} else {
|
||||||
|
videos = await this.getChannelVideosInvidiousScraper(channel.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.useRssFeeds) {
|
||||||
|
videos = await this.getChannelVideosLocalRSS(channel.id)
|
||||||
|
} else {
|
||||||
|
videos = await this.getChannelVideosLocalScraper(channel.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videoList = videoList.concat(videos)
|
||||||
|
channelCount++
|
||||||
|
const percentageComplete = (channelCount / this.activeSubscriptionList.length) * 100
|
||||||
|
this.setProgressBarPercentage(percentageComplete)
|
||||||
|
|
||||||
|
if (channelCount === this.activeSubscriptionList.length) {
|
||||||
|
videoList = await Promise.all(videoList.sort((a, b) => {
|
||||||
|
return b.publishedDate - a.publishedDate
|
||||||
|
}))
|
||||||
|
|
||||||
|
const profileSubscriptions = {
|
||||||
|
activeProfile: this.activeProfile,
|
||||||
|
videoList: videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
this.videoList = await Promise.all(videoList.filter((video) => {
|
||||||
|
if (this.hideWatchedSubs) {
|
||||||
|
const historyIndex = this.historyCache.findIndex((x) => {
|
||||||
|
return x.videoId === video.videoId
|
||||||
|
})
|
||||||
|
|
||||||
|
return historyIndex === -1
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
this.updateProfileSubscriptions(profileSubscriptions)
|
||||||
|
this.isLoading = false
|
||||||
|
this.updateShowProgressBar(false)
|
||||||
|
|
||||||
|
if (this.activeProfile === 0) {
|
||||||
|
this.updateAllSubscriptionsList(profileSubscriptions.videoList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getChannelVideosLocalScraper: function (channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
ytch.getChannelVideos(channelId, 'latest').then(async (response) => {
|
||||||
|
const videos = await Promise.all(response.items.map(async (video) => {
|
||||||
|
video.publishedDate = await this.calculatePublishedDate(video.publishedText)
|
||||||
|
return video
|
||||||
|
}))
|
||||||
|
|
||||||
|
resolve(videos)
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||||
|
this.showToast({
|
||||||
|
message: `${errorMessage}: ${err}`,
|
||||||
|
time: 10000,
|
||||||
|
action: () => {
|
||||||
|
navigator.clipboard.writeText(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resolve([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getChannelVideosLocalRSS: function (channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parser = new Parser()
|
||||||
|
const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`
|
||||||
|
|
||||||
|
parser.parseURL(feedUrl).then(async (feed) => {
|
||||||
|
resolve(await Promise.all(feed.items.map((video) => {
|
||||||
|
video.authorId = channelId
|
||||||
|
video.videoId = video.id.replace('yt:video:', '')
|
||||||
|
video.type = 'video'
|
||||||
|
video.publishedDate = new Date(video.pubDate)
|
||||||
|
video.publishedText = video.publishedDate.toLocaleString()
|
||||||
|
video.lengthSeconds = '0:00'
|
||||||
|
video.isRSS = true
|
||||||
|
|
||||||
|
return video
|
||||||
|
})))
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
resolve([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getChannelVideosInvidiousScraper: function (channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const subscriptionsPayload = {
|
||||||
|
resource: 'channels/latest',
|
||||||
|
id: channelId,
|
||||||
|
params: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.invidiousAPICall(subscriptionsPayload).then((result) => {
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getChannelVideosInvidiousRSS: function (channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parser = new Parser()
|
||||||
|
const feedUrl = `${this.invidiousInstance}/feed/channel/${channelId}`
|
||||||
|
|
||||||
|
parser.parseURL(feedUrl).then(async (feed) => {
|
||||||
|
resolve(await Promise.all(feed.items.map((video) => {
|
||||||
|
video.authorId = channelId
|
||||||
|
video.videoId = video.id.replace('yt:video:', '')
|
||||||
|
video.type = 'video'
|
||||||
|
video.publishedDate = new Date(video.pubDate)
|
||||||
|
video.publishedText = video.publishedDate.toLocaleString()
|
||||||
|
video.lengthSeconds = '0:00'
|
||||||
|
video.isRSS = true
|
||||||
|
|
||||||
|
return video
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
increaseLimit: function () {
|
||||||
|
this.dataLimit += 100
|
||||||
|
sessionStorage.setItem('subscriptionLimit', this.dataLimit)
|
||||||
|
},
|
||||||
|
|
||||||
|
...mapActions([
|
||||||
|
'showToast',
|
||||||
|
'invidiousAPICall',
|
||||||
|
'updateShowProgressBar',
|
||||||
|
'updateProfileSubscriptions',
|
||||||
|
'updateAllSubscriptionsList',
|
||||||
|
'calculatePublishedDate'
|
||||||
|
]),
|
||||||
|
|
||||||
|
...mapMutations([
|
||||||
|
'setProgressBarPercentage'
|
||||||
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,13 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ft-card class="card">
|
<ft-loader
|
||||||
|
v-if="isLoading"
|
||||||
|
:fullscreen="true"
|
||||||
|
/>
|
||||||
|
<ft-card
|
||||||
|
v-else
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
<h3>{{ $t("Subscriptions.Subscriptions") }}</h3>
|
<h3>{{ $t("Subscriptions.Subscriptions") }}</h3>
|
||||||
<ft-flex-box>
|
<ft-flex-box
|
||||||
|
v-if="activeVideoList.length === 0"
|
||||||
|
>
|
||||||
<p class="message">
|
<p class="message">
|
||||||
{{ $t("This part of the app is not ready yet. Come back later when progress has been made.") }}
|
{{ $t("Subscriptions['Your Subscription list is currently empty. Start adding subscriptions to see them here.']") }}
|
||||||
</p>
|
</p>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
<ft-element-list
|
||||||
|
v-else
|
||||||
|
:data="activeVideoList"
|
||||||
|
/>
|
||||||
|
<ft-flex-box
|
||||||
|
>
|
||||||
|
<ft-button
|
||||||
|
v-if="videoList.length > dataLimit"
|
||||||
|
label="Load More"
|
||||||
|
background-color="var(--primary-color)"
|
||||||
|
text-color="var(--text-with-main-color)"
|
||||||
|
@click="increaseLimit"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
</ft-card>
|
</ft-card>
|
||||||
|
<ft-icon-button
|
||||||
|
v-if="!isLoading"
|
||||||
|
icon="sync"
|
||||||
|
class="floatingTopButton"
|
||||||
|
:title="$t('Subscriptions.Refresh Subscriptions')"
|
||||||
|
:size="12"
|
||||||
|
theme="primary"
|
||||||
|
@click="getSubscriptions"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,8 @@ Subscriptions:
|
||||||
Latest Subscriptions: Latest Subscriptions
|
Latest Subscriptions: Latest Subscriptions
|
||||||
'Your Subscription list is currently empty. Start adding subscriptions to see them here.': Your
|
'Your Subscription list is currently empty. Start adding subscriptions to see them here.': Your
|
||||||
Subscription list is currently empty. Start adding subscriptions to see them here.
|
Subscription list is currently empty. Start adding subscriptions to see them here.
|
||||||
'Getting Subscriptions. Please wait.': Getting Subscriptions. Please wait.
|
'Getting Subscriptions. Please wait.': Getting Subscriptions. Please wait.
|
||||||
|
Refresh Subscriptions: Refresh Subscriptions
|
||||||
Trending: Trending
|
Trending: Trending
|
||||||
Most Popular: Most Popular
|
Most Popular: Most Popular
|
||||||
Playlists: Playlists
|
Playlists: Playlists
|
||||||
|
@ -180,6 +181,7 @@ Settings:
|
||||||
Subscription Settings:
|
Subscription Settings:
|
||||||
Subscription Settings: Subscription Settings
|
Subscription Settings: Subscription Settings
|
||||||
Hide Videos on Watch: Hide Videos on Watch
|
Hide Videos on Watch: Hide Videos on Watch
|
||||||
|
Fetch Feeds from RSS: Fetch Feeds from RSS
|
||||||
Subscriptions Export Format:
|
Subscriptions Export Format:
|
||||||
Subscriptions Export Format: Subscriptions Export Format
|
Subscriptions Export Format: Subscriptions Export Format
|
||||||
#& Freetube
|
#& Freetube
|
||||||
|
@ -246,6 +248,28 @@ About:
|
||||||
|
|
||||||
Latest FreeTube News: Latest FreeTube News
|
Latest FreeTube News: Latest FreeTube News
|
||||||
|
|
||||||
|
Profile:
|
||||||
|
All Channels: All Channels
|
||||||
|
Profile Manager: Profile Manager
|
||||||
|
Create New Profile: Create New Profile
|
||||||
|
Edit Profile: Edit Profile
|
||||||
|
Color Picker: Color Picker
|
||||||
|
Custom Color: Custom Color
|
||||||
|
Profile Preview: Profile Preview
|
||||||
|
Create Profile: Create Profile
|
||||||
|
Update Profile: Update Profile
|
||||||
|
Make Default Profile: Make Default Profile
|
||||||
|
Delete Profile: Delete Profile
|
||||||
|
Are you sure you want to delete this profile?: Are you sure you want to delete this profile?
|
||||||
|
All subscriptions will also be deleted.: All subscriptions will also be deleted.
|
||||||
|
Profile could not be found: Profile could not be found
|
||||||
|
Your profile name cannot be empty: Your profile name cannot be empty
|
||||||
|
Profile has been created: Profile has been created
|
||||||
|
Profile has been updated: Profile has been updated
|
||||||
|
Your default profile has been set to $: Your default profile has been set to $
|
||||||
|
Removed $ from your profiles: Removed $ from your profiles
|
||||||
|
Your default profile has been changed to your primary profile: Your default profile has been changed to your primary profile
|
||||||
|
$ is now the active profile: $ is now the active profile
|
||||||
#On Channel Page
|
#On Channel Page
|
||||||
Channel:
|
Channel:
|
||||||
Subscriber: Subscriber
|
Subscriber: Subscriber
|
||||||
|
@ -321,6 +345,8 @@ Video:
|
||||||
Dec: Dec
|
Dec: Dec
|
||||||
Second: Second
|
Second: Second
|
||||||
Seconds: Seconds
|
Seconds: Seconds
|
||||||
|
Minute: Minute
|
||||||
|
Minutes: Minutes
|
||||||
Hour: Hour
|
Hour: Hour
|
||||||
Hours: Hours
|
Hours: Hours
|
||||||
Day: Day
|
Day: Day
|
||||||
|
|
Loading…
Reference in New Issue