diff --git a/src/renderer/App.js b/src/renderer/App.js
index 0e4e2922..efe9cc85 100644
--- a/src/renderer/App.js
+++ b/src/renderer/App.js
@@ -3,6 +3,7 @@ import { ObserveVisibility } from 'vue-observe-visibility'
import TopNav from './components/top-nav/top-nav.vue'
import SideNav from './components/side-nav/side-nav.vue'
import FtToast from './components/ft-toast/ft-toast.vue'
+import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue'
import $ from 'jquery'
let useElectron
@@ -23,11 +24,15 @@ export default Vue.extend({
components: {
TopNav,
SideNav,
- FtToast
+ FtToast,
+ FtProgressBar
},
computed: {
isOpen: function () {
return this.$store.getters.getIsSideNavOpen
+ },
+ showProgressBar: function () {
+ return this.$store.getters.getShowProgressBar
}
},
mounted: function () {
diff --git a/src/renderer/App.vue b/src/renderer/App.vue
index 2bb16c04..67ab34f4 100644
--- a/src/renderer/App.vue
+++ b/src/renderer/App.vue
@@ -15,6 +15,9 @@
+
diff --git a/src/renderer/components/ft-profile-selector/ft-profile-selector.css b/src/renderer/components/ft-profile-selector/ft-profile-selector.css
new file mode 100644
index 00000000..f630dc0c
--- /dev/null
+++ b/src/renderer/components/ft-profile-selector/ft-profile-selector.css
@@ -0,0 +1,65 @@
+.colorOption {
+ width: 50px;
+ height: 50px;
+ margin: 10px;
+ cursor: pointer;
+ border-radius: 200px 200px 200px 200px;
+ -webkit-border-radius: 200px 200px 200px 200px;
+}
+
+.initial {
+ font-size: 25px;
+ text-align: center;
+ position: relative;
+ bottom: 27px;
+}
+
+#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;
+}
+
+.profile:hover {
+ background-color: var(--side-nav-hover-color);
+}
+
+.profile .colorOption {
+ float: left;
+ position: relative;
+ bottom: 5px;
+}
+
+.profileListTitle {
+ position: absolute;
+ top: -15px;
+ left: 10px;
+}
+
+.profileSettings {
+ float: right;
+ position: absolute;
+ top: 10px;
+ right: 5px;
+}
diff --git a/src/renderer/components/ft-profile-selector/ft-profile-selector.js b/src/renderer/components/ft-profile-selector/ft-profile-selector.js
new file mode 100644
index 00000000..f63d5aa0
--- /dev/null
+++ b/src/renderer/components/ft-profile-selector/ft-profile-selector.js
@@ -0,0 +1,79 @@
+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 () {
+ $('#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)
+ this.showToast({
+ message: `${profile.name} is now the active profile`
+ })
+ $('#profileList').focusout()
+ },
+
+ ...mapActions([
+ 'showToast',
+ 'updateActiveProfile'
+ ])
+ }
+})
diff --git a/src/renderer/components/ft-profile-selector/ft-profile-selector.vue b/src/renderer/components/ft-profile-selector/ft-profile-selector.vue
new file mode 100644
index 00000000..e8c864d3
--- /dev/null
+++ b/src/renderer/components/ft-profile-selector/ft-profile-selector.vue
@@ -0,0 +1,57 @@
+
+
+
+
+ {{ profileInitials[activeProfile] }}
+
+
+
+
+ Profile Select
+
+
+
+
+
+
+ {{ profileInitials[index] }}
+
+
+
+ {{ profile.name }}
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/ft-progress-bar/ft-progress-bar.css b/src/renderer/components/ft-progress-bar/ft-progress-bar.css
new file mode 100644
index 00000000..43816bd2
--- /dev/null
+++ b/src/renderer/components/ft-progress-bar/ft-progress-bar.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;
+}
diff --git a/src/renderer/components/ft-progress-bar/ft-progress-bar.js b/src/renderer/components/ft-progress-bar/ft-progress-bar.js
new file mode 100644
index 00000000..c5e7fcbc
--- /dev/null
+++ b/src/renderer/components/ft-progress-bar/ft-progress-bar.js
@@ -0,0 +1,10 @@
+import Vue from 'vue'
+
+export default Vue.extend({
+ name: 'FtProgressBar',
+ computed: {
+ progressBarPercentage: function () {
+ return this.$store.getters.getProgressBarPercentage
+ }
+ }
+})
diff --git a/src/renderer/components/ft-progress-bar/ft-progress-bar.vue b/src/renderer/components/ft-progress-bar/ft-progress-bar.vue
new file mode 100644
index 00000000..dbbe9a3f
--- /dev/null
+++ b/src/renderer/components/ft-progress-bar/ft-progress-bar.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/src/renderer/components/side-nav/side-nav.css b/src/renderer/components/side-nav/side-nav.css
index c5e748ad..fda641e5 100644
--- a/src/renderer/components/side-nav/side-nav.css
+++ b/src/renderer/components/side-nav/side-nav.css
@@ -30,7 +30,7 @@
margin-top: 10px;
}
-.navOption {
+.navOption, .navChannel {
position: relative;
padding: 5px;
cursor: pointer;
@@ -40,14 +40,14 @@
display: none;
}
-.navOption:hover {
+.navOption:hover, .navChannel: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 {
+.navOption:active, .navChannel:active {
background-color: var(--side-nav-active-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
@@ -63,6 +63,26 @@
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 {
width: 80px;
}
@@ -107,6 +127,24 @@
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) {
.inner {
display: contents; /* sunglasses emoji */
diff --git a/src/renderer/components/side-nav/side-nav.js b/src/renderer/components/side-nav/side-nav.js
index 8c969441..41ec34d0 100644
--- a/src/renderer/components/side-nav/side-nav.js
+++ b/src/renderer/components/side-nav/side-nav.js
@@ -12,11 +12,35 @@ export default Vue.extend({
computed: {
isOpen: function () {
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: {
navigate: function (route) {
router.push('/' + route)
+ },
+
+ goToChannel: function (id) {
+ this.$router.push({ path: `/channel/${id}` })
}
}
})
diff --git a/src/renderer/components/side-nav/side-nav.vue b/src/renderer/components/side-nav/side-nav.vue
index f2e911a0..248e71b1 100644
--- a/src/renderer/components/side-nav/side-nav.vue
+++ b/src/renderer/components/side-nav/side-nav.vue
@@ -16,10 +16,6 @@
{{ $t("Subscriptions.Subscriptions") }}
-
+
+
+
+
+
+ {{ channel.name }}
+
+
diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js
index e95cd834..3eb03949 100644
--- a/src/renderer/components/top-nav/top-nav.js
+++ b/src/renderer/components/top-nav/top-nav.js
@@ -1,6 +1,7 @@
import Vue from 'vue'
import FtInput from '../ft-input/ft-input.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 router from '../../router/index.js'
import debounce from 'lodash.debounce'
@@ -10,7 +11,8 @@ export default Vue.extend({
name: 'TopNav',
components: {
FtInput,
- FtSearchFilters
+ FtSearchFilters,
+ FtProfileSelector
},
data: () => {
return {
diff --git a/src/renderer/components/top-nav/top-nav.sass b/src/renderer/components/top-nav/top-nav.sass
index 4040bcb3..3197cbce 100644
--- a/src/renderer/components/top-nav/top-nav.sass
+++ b/src/renderer/components/top-nav/top-nav.sass
@@ -59,6 +59,9 @@
display: flex
align-items: center
+ &.profiles
+ justify-content: flex-end
+
.navSearchIcon
@media only screen and (min-width: 681px)
display: none
diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue
index e04cf8c4..9a4ed144 100644
--- a/src/renderer/components/top-nav/top-nav.vue
+++ b/src/renderer/components/top-nav/top-nav.vue
@@ -55,7 +55,7 @@
:class="{ expand: !isSideNavOpen }"
/>
-
+
diff --git a/src/renderer/components/watch-video-info/watch-video-info.js b/src/renderer/components/watch-video-info/watch-video-info.js
index 2ccb2e43..2a9d1d4a 100644
--- a/src/renderer/components/watch-video-info/watch-video-info.js
+++ b/src/renderer/components/watch-video-info/watch-video-info.js
@@ -79,6 +79,14 @@ export default Vue.extend({
return this.$store.getters.getUsingElectron
},
+ profileList: function () {
+ return this.$store.getters.getProfileList
+ },
+
+ activeProfile: function () {
+ return this.$store.getters.getActiveProfile
+ },
+
formatTypeNames: function () {
return [
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()}`
},
+ 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 () {
- 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() {
@@ -116,9 +140,74 @@ export default Vue.extend({
},
handleSubscription: function () {
- this.showToast({
- message: 'Subscriptions have not yet been implemented'
- })
+ 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.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) {
@@ -136,7 +225,8 @@ export default Vue.extend({
},
...mapActions([
- 'showToast'
+ 'showToast',
+ 'updateProfile'
])
}
})
diff --git a/src/renderer/store/modules/profile.js b/src/renderer/store/modules/profile.js
index 562b27b2..984ab5ec 100644
--- a/src/renderer/store/modules/profile.js
+++ b/src/renderer/store/modules/profile.js
@@ -25,13 +25,23 @@ const profileDb = new Datastore({
})
const state = {
- profileList: [],
- activeProfile: 'allChannels'
+ profileList: [{
+ _id: 'allChannels',
+ name: 'All Channels',
+ bgColor: '#000000',
+ textColor: '#FFFFFF',
+ subscriptions: []
+ }],
+ activeProfile: 0
}
const getters = {
getProfileList: () => {
return state.profileList
+ },
+
+ getActiveProfile: () => {
+ return state.activeProfile
}
}
@@ -39,11 +49,23 @@ const actions = {
grabAllProfiles ({ dispatch, commit }, defaultName = null) {
profileDb.find({}, (err, results) => {
if (!err) {
- console.log(results)
if (results.length === 0) {
dispatch('createDefaultProfile', defaultName)
} else {
- commit('setProfileList', results)
+ // 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)
}
}
})
@@ -94,18 +116,25 @@ const actions = {
})
},
- removeProfile ({ dispatch }, videoId) {
- profileDb.remove({ videoId: videoId }, (err, numReplaced) => {
+ removeProfile ({ dispatch }, profileId) {
+ profileDb.remove({ _id: profileId }, (err, numReplaced) => {
if (!err) {
- dispatch('grabHistory')
+ dispatch('grabAllProfiles')
}
})
+ },
+
+ updateActiveProfile ({ commit }, index) {
+ commit('setActiveProfile', index)
}
}
const mutations = {
setProfileList (state, profileList) {
state.profileList = profileList
+ },
+ setActiveProfile (state, activeProfile) {
+ state.activeProfile = activeProfile
}
}
diff --git a/src/renderer/store/modules/subscriptions.js b/src/renderer/store/modules/subscriptions.js
index 2d48dbfc..84e36201 100644
--- a/src/renderer/store/modules/subscriptions.js
+++ b/src/renderer/store/modules/subscriptions.js
@@ -1,61 +1,40 @@
-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
-})
+import ytch from 'yt-channel-info'
const state = {
- subscriptions: []
+ subscriptions: [],
+ profileSubscriptions: {
+ activeProfile: 0,
+ videoList: []
+ }
}
-const mutations = {
- addSubscription (state, payload) {
- state.subscriptions.push(payload)
+const getters = {
+ getSubscriptions: () => {
+ return state.subscriptions
},
- setSubscriptions (state, payload) {
- state.subscriptions = payload
+ getProfileSubscriptions: () => {
+ return state.profileSubscriptions
}
}
const actions = {
- addSubscriptions ({ commit }, payload) {
- subDb.insert(payload, (err, payload) => {
- if (!err) {
- commit('addSubscription', payload)
- }
- })
+ updateSubscriptions ({ commit }, subscriptions) {
+ commit('setSubscriptions', subscriptions)
},
- getSubscriptions ({ commit }, payload) {
- subDb.find({}, (err, payload) => {
- if (!err) {
- commit('setSubscriptions', payload)
- }
- })
- },
- removeSubscription ({ commit }, channelId) {
- subDb.remove({ channelId: channelId }, {}, () => {
- commit('setSubscriptions', this.state.subscriptions.filter(sub => sub.channelId !== channelId))
- })
+ updateProfileSubscriptions ({ commit }, subscriptions) {
+ commit('setProfileSubscriptions', subscriptions)
}
}
-const getters = {}
+
+const mutations = {
+ setSubscriptions (state, subscriptions) {
+ state.subscriptions = subscriptions
+ },
+ setProfileSubscriptions (state, profileSubscriptions) {
+ state.profileSubscriptions = profileSubscriptions
+ }
+}
+
export default {
state,
getters,
diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js
index 8729d665..2ecd99d4 100644
--- a/src/renderer/store/modules/utils.js
+++ b/src/renderer/store/modules/utils.js
@@ -5,6 +5,8 @@ const state = {
sessionSearchHistory: [],
popularCache: null,
trendingCache: null,
+ showProgressBar: false,
+ progressBarPercentage: 0,
searchSettings: {
sortBy: 'relevance',
time: '',
@@ -76,10 +78,22 @@ const getters = {
getColorValues () {
return state.colorValues
+ },
+
+ getShowProgressBar () {
+ return state.showProgressBar
+ },
+
+ getProgressBarPercentage () {
+ return state.progressBarPercentage
}
}
const actions = {
+ updateShowProgressBar ({ commit }, value) {
+ commit('setShowProgressBar', value)
+ },
+
getRandomColorClass () {
const randomInt = Math.floor(Math.random() * state.colorClasses.length)
return state.colorClasses[randomInt]
@@ -105,6 +119,33 @@ const actions = {
}
},
+ 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) {
/** @type {URL} */
let urlObject
@@ -296,6 +337,14 @@ const mutations = {
state.isSideNavOpen = !state.isSideNavOpen
},
+ setShowProgressBar (state, value) {
+ state.showProgressBar = value
+ },
+
+ setProgressBarPercentage (state, value) {
+ state.progressBarPercentage = value
+ },
+
setSessionSearchHistory (state, history) {
state.sessionSearchHistory = history
},
diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js
index b44326ae..e0754ecd 100644
--- a/src/renderer/views/Channel/Channel.js
+++ b/src/renderer/views/Channel/Channel.js
@@ -78,6 +78,34 @@ export default Vue.extend({
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 () {
return [
this.$t('Channel.Videos.Sort Types.Newest'),
@@ -408,7 +436,74 @@ export default Vue.extend({
},
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 () {
@@ -549,7 +644,8 @@ export default Vue.extend({
},
...mapActions([
- 'showToast'
+ 'showToast',
+ 'updateProfile'
])
}
})
diff --git a/src/renderer/views/Channel/Channel.vue b/src/renderer/views/Channel/Channel.vue
index 5781b469..b59f0249 100644
--- a/src/renderer/views/Channel/Channel.vue
+++ b/src/renderer/views/Channel/Channel.vue
@@ -40,7 +40,7 @@
{
console.log(err)
diff --git a/src/renderer/views/ProfileEdit/ProfileEdit.css b/src/renderer/views/ProfileEdit/ProfileEdit.css
index e2935899..07f8f622 100644
--- a/src/renderer/views/ProfileEdit/ProfileEdit.css
+++ b/src/renderer/views/ProfileEdit/ProfileEdit.css
@@ -16,6 +16,12 @@
margin-bottom: 30px;
}
+.colorOptions {
+ max-width: 1000px;
+ margin: 0 auto;
+ margin-bottom: 30px;
+}
+
.colorOption {
width: 100px;
height: 100px;
diff --git a/src/renderer/views/ProfileEdit/ProfileEdit.js b/src/renderer/views/ProfileEdit/ProfileEdit.js
index a142dfb2..28fdbf05 100644
--- a/src/renderer/views/ProfileEdit/ProfileEdit.js
+++ b/src/renderer/views/ProfileEdit/ProfileEdit.js
@@ -2,6 +2,7 @@ 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'
@@ -11,6 +12,7 @@ export default Vue.extend({
components: {
'ft-loader': FtLoader,
'ft-card': FtCard,
+ 'ft-prompt': FtPrompt,
'ft-flex-box': FtFlexBox,
'ft-input': FtInput,
'ft-button': FtButton
@@ -18,12 +20,18 @@ export default Vue.extend({
data: function () {
return {
isLoading: false,
+ showDeletePrompt: false,
+ deletePromptLabel: '',
isNew: false,
profileId: '',
profileName: '',
profileBgColor: '',
profileTextColor: '',
- profileSubscriptions: []
+ profileSubscriptions: [],
+ deletePromptValues: [
+ 'yes',
+ 'no'
+ ]
}
},
computed: {
@@ -32,6 +40,18 @@ export default Vue.extend({
},
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: {
@@ -39,12 +59,16 @@ export default Vue.extend({
this.profileTextColor = await this.calculateColorLuminance(val)
}
},
- mounted: function () {
+ mounted: async function () {
this.isLoading = true
const profileType = this.$route.name
+ this.deletePromptLabel = 'Are you sure you want to delete this profile? All subscriptions in this profile 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
@@ -52,7 +76,14 @@ export default Vue.extend({
console.log(this.$route.name)
this.grabProfileInfo(this.profileId).then((profile) => {
- console.log(profile)
+ if (profile === null) {
+ this.showToast({
+ message: 'Profile could not be found'
+ })
+ this.$router.push({
+ path: '/settings/profile/'
+ })
+ }
this.profileName = profile.name
this.profileBgColor = profile.bgColor
this.profileTextColor = profile.textColor
@@ -62,7 +93,25 @@ export default Vue.extend({
}
},
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: 'Your profile name cannot be empty'
+ })
+ return
+ }
const profile = {
name: this.profileName,
bgColor: this.profileBgColor,
@@ -77,8 +126,44 @@ export default Vue.extend({
console.log(profile)
this.updateProfile(profile)
+
+ if (this.isNew) {
+ this.showToast({
+ message: 'Profile has been created'
+ })
+ this.$router.push({
+ path: '/settings/profile/'
+ })
+ } else {
+ this.showToast({
+ message: 'Profile has been updated'
+ })
+ }
+ },
+
+ setDefaultProfile: function () {
+ this.updateDefaultProfile(this.profileId)
this.showToast({
- message: 'Profile has been updated'
+ message: `Your default profile has been set to ${this.profileName}`
+ })
+ },
+
+ deleteProfile: function () {
+ this.removeProfile(this.profileId)
+ this.showToast({
+ message: `Removed ${this.profileName} from your profiles`
+ })
+ if (this.defaultProfile === this.profileId) {
+ this.updateDefaultProfile('allChannels')
+ this.showToast({
+ message: 'Your default profile has been set your Primary profile'
+ })
+ }
+ if (this.activeProfile._id === this.profileId) {
+ this.updateActiveProfile('allChannels')
+ }
+ this.$router.push({
+ path: '/settings/profile/'
})
},
@@ -86,7 +171,11 @@ export default Vue.extend({
'showToast',
'grabProfileInfo',
'updateProfile',
- 'calculateColorLuminance'
+ 'removeProfile',
+ 'updateDefaultProfile',
+ 'updateActiveProfile',
+ 'calculateColorLuminance',
+ 'getRandomColor'
])
}
})
diff --git a/src/renderer/views/ProfileEdit/ProfileEdit.vue b/src/renderer/views/ProfileEdit/ProfileEdit.vue
index f001b403..a51f6648 100644
--- a/src/renderer/views/ProfileEdit/ProfileEdit.vue
+++ b/src/renderer/views/ProfileEdit/ProfileEdit.vue
@@ -20,7 +20,7 @@
{{ $t("Profile.Color Picker") }}
+
+
diff --git a/src/renderer/views/ProfileSettings/ProfileSettings.css b/src/renderer/views/ProfileSettings/ProfileSettings.css
index b6db9095..670ae2a9 100644
--- a/src/renderer/views/ProfileSettings/ProfileSettings.css
+++ b/src/renderer/views/ProfileSettings/ProfileSettings.css
@@ -8,6 +8,12 @@
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%;
diff --git a/src/renderer/views/ProfileSettings/ProfileSettings.js b/src/renderer/views/ProfileSettings/ProfileSettings.js
index 3dd645a0..ba90e5e5 100644
--- a/src/renderer/views/ProfileSettings/ProfileSettings.js
+++ b/src/renderer/views/ProfileSettings/ProfileSettings.js
@@ -2,13 +2,15 @@ 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-profile-bubble': FtProfileBubble,
+ 'ft-button': FtButton
},
computed: {
profileList: function () {
@@ -17,5 +19,12 @@ export default Vue.extend({
},
mounted: function () {
console.log(this.profileList)
+ },
+ methods: {
+ newProfile: function () {
+ this.$router.push({
+ path: `/settings/profile/new/`
+ })
+ }
}
})
diff --git a/src/renderer/views/ProfileSettings/ProfileSettings.vue b/src/renderer/views/ProfileSettings/ProfileSettings.vue
index 5a85991f..b03cd6c4 100644
--- a/src/renderer/views/ProfileSettings/ProfileSettings.vue
+++ b/src/renderer/views/ProfileSettings/ProfileSettings.vue
@@ -2,7 +2,9 @@
{{ $t("Profile.Profile Manager") }}
-
+
+
+
+
diff --git a/src/renderer/views/Subscriptions/Subscriptions.js b/src/renderer/views/Subscriptions/Subscriptions.js
index a76e0a78..278a1d10 100644
--- a/src/renderer/views/Subscriptions/Subscriptions.js
+++ b/src/renderer/views/Subscriptions/Subscriptions.js
@@ -1,15 +1,164 @@
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 FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
+import ytch from 'yt-channel-info'
+
export default Vue.extend({
name: 'Subscriptions',
components: {
+ 'ft-loader': FtLoader,
'ft-card': FtCard,
'ft-flex-box': FtFlexBox,
'ft-element-list': FtElementList
},
+ data: function () {
+ return {
+ isLoading: false,
+ dataLimit: 100,
+ videoList: []
+ }
+ },
+ computed: {
+ backendPreference: function () {
+ return this.$store.getters.getBackendPreference
+ },
+
+ backendFallback: function () {
+ return this.$store.getters.getBackendFallback
+ },
+
+ profileList: function () {
+ return this.$store.getters.getProfileList
+ },
+
+ activeProfile: function () {
+ return this.$store.getters.getActiveProfile
+ },
+
+ profileSubscriptions: function () {
+ return this.$store.getters.getProfileSubscriptions
+ },
+
+ activeSubscriptionList: function () {
+ return this.profileList[this.activeProfile].subscriptions
+ },
+
+ allSubscriptionsList: function () {
+ return this.profileList[0].subscriptions
+ },
+
+ sortedVideoList: function () {
+ const profileSubscriptions = JSON.parse(JSON.stringify(this.profileSubscriptions))
+ return profileSubscriptions.videoList.sort((a, b) => {
+ if (a.title.toLowerCase() > b.title.toLowerCase()) {
+ return -1
+ }
+
+ if (a.title.toLowerCase() < b.title.toLowerCase()) {
+ return 1
+ }
+
+ console.log(a.title)
+
+ return 0
+ })
+ }
+ },
mounted: function () {
+ setTimeout(() => {
+ this.fetchActiveSubscriptionsLocal()
+ }, 1000)
+ },
+ methods: {
+ fetchActiveSubscriptionsLocal: function () {
+ if (this.activeSubscriptionList.length === 0) {
+ return
+ }
+ this.isLoading = true
+ this.updateShowProgressBar(true)
+
+ let videoList = []
+ let channelCount = 0
+
+ this.activeSubscriptionList.forEach(async (channel) => {
+ const videos = await this.getChannelVideosLocalScraper(channel.id)
+ console.log(videos)
+
+ 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.updateProfileSubscriptions(profileSubscriptions)
+ this.isLoading = false
+ this.updateShowProgressBar(false)
+ }
+ })
+ },
+
+ 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) {
+ console.log('TODO')
+ },
+
+ fetchActiveSubscriptionsInvidious: function () {
+ console.log('TODO')
+ },
+
+ getChannelVideosInvidiousScraper: function (channelId) {
+ console.log('TODO')
+ },
+
+ getChannelVideosInvidiousRSS: function (channelId) {
+ console.log('TODO')
+ },
+
+ ...mapActions([
+ 'showToast',
+ 'updateShowProgressBar',
+ 'updateProfileSubscriptions',
+ 'calculatePublishedDate'
+ ]),
+
+ ...mapMutations([
+ 'setProgressBarPercentage'
+ ])
}
})
diff --git a/src/renderer/views/Subscriptions/Subscriptions.vue b/src/renderer/views/Subscriptions/Subscriptions.vue
index ad990d1f..4f31f6ce 100644
--- a/src/renderer/views/Subscriptions/Subscriptions.vue
+++ b/src/renderer/views/Subscriptions/Subscriptions.vue
@@ -1,12 +1,35 @@
-
+
+
{{ $t("Subscriptions.Subscriptions") }}
-
+
- {{ $t("This part of the app is not ready yet. Come back later when progress has been made.") }}
+ {{ $t("History['Your history list is currently empty.']") }}
+
+
+
+
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index 3b53813f..0769a239 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -335,6 +335,8 @@ Video:
Dec: Dec
Second: Second
Seconds: Seconds
+ Minute: Minute
+ Minutes: Minutes
Hour: Hour
Hours: Hours
Day: Day