Add subscription manager within profile settings. Add Upcoming video information. Other changes

This commit is contained in:
Preston 2020-09-15 22:07:54 -04:00
parent 20b379c269
commit 2a0c062915
25 changed files with 977 additions and 312 deletions

View File

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

View File

@ -1,4 +1,5 @@
.bubblePadding { .bubblePadding {
position: relative;
width: 100px; width: 100px;
height: 115px; height: 115px;
padding: 10px; padding: 10px;
@ -25,6 +26,20 @@
-webkit-border-radius: 200px 200px 200px 200px; -webkit-border-radius: 200px 200px 200px 200px;
} }
.selected {
position: absolute;
top: 10px;
background-color: rgba(0, 0, 0, 0.5);
}
.icon {
color: #EEEEEE;
font-size: 25px;
position: absolute;
top: 12px;
left: 12px;
}
.channelName { .channelName {
font-size: 13px; font-size: 13px;
height: 60px; height: 60px;

View File

@ -7,18 +7,26 @@ export default Vue.extend({
type: String, type: String,
required: true required: true
}, },
channelId: {
type: String,
required: true
},
channelThumbnail: { channelThumbnail: {
type: String, type: String,
required: true required: true
},
showSelected: {
type: Boolean,
default: false
}
},
data: function () {
return {
selected: false
} }
}, },
methods: { methods: {
goToChannel: function () { handleClick: function () {
console.log('Go to channel') if (this.showSelected) {
this.selected = !this.selected
}
this.$emit('click')
} }
} }
}) })

View File

@ -1,12 +1,21 @@
<template> <template>
<div <div
class="bubblePadding" class="bubblePadding"
@click="goToChannel(channelId)" @click="handleClick"
> >
<img <img
class="bubble" class="bubble"
:src="channelThumbnail" :src="channelThumbnail"
> >
<div
v-if="selected"
class="bubble selected"
>
<font-awesome-icon
icon="check"
class="icon"
/>
</div>
<div class="channelName"> <div class="channelName">
{{ channelName }} {{ channelName }}
</div> </div>

View File

@ -24,7 +24,7 @@
} }
.forceTextColor .ft-input { .forceTextColor .ft-input {
color: var(--text-with-main-color); color: #EEEEEE;
background-color: var(--primary-input-color); background-color: var(--primary-input-color);
} }
@ -36,7 +36,7 @@
} }
.forceTextColor ::-webkit-input-placeholder { .forceTextColor ::-webkit-input-placeholder {
color: var(--text-with-main-color); color: #EEEEEE;
} }
.inputAction { .inputAction {
@ -58,7 +58,7 @@
} }
.forceTextColor .inputAction { .forceTextColor .inputAction {
color: var(--text-with-main-color); color: #EEEEEE;
} }
.inputAction:hover { .inputAction:hover {

View File

@ -0,0 +1,5 @@
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 30px;
}

View File

@ -0,0 +1,155 @@
import Vue from 'vue'
import { mapActions } from 'vuex'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubble.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
export default Vue.extend({
name: 'FtProfileAllChannelsList',
components: {
'ft-card': FtCard,
'ft-flex-box': FtFlexBox,
'ft-channel-bubble': FtChannelBubble,
'ft-button': FtButton,
'ft-prompt': FtPrompt
},
props: {
profile: {
type: Object,
required: true
}
},
data: function () {
return {
channels: [],
selectedLength: 0
}
},
computed: {
profileList: function () {
return this.$store.getters.getProfileList
},
selectedText: function () {
const localeText = this.$t('Profile.$ selected')
return localeText.replace('$', this.selectedLength)
}
},
watch: {
profile: function () {
this.channels = [].concat(this.profileList[0].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
}).filter((channel) => {
const index = this.profile.subscriptions.findIndex((sub) => {
return sub.id === channel.id
})
return index === -1
}).map((channel) => {
channel.selected = false
return channel
})
}
},
mounted: function () {
if (typeof this.profile.subscriptions !== 'undefined') {
this.channels = [].concat(this.profileList[0].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
}).filter((channel) => {
const index = this.profile.subscriptions.findIndex((sub) => {
return sub.id === channel.id
})
return index === -1
}).map((channel) => {
channel.selected = false
return channel
})
}
},
methods: {
handleChannelClick: function (index) {
this.channels[index].selected = !this.channels[index].selected
this.selectedLength = this.channels.filter((channel) => {
return channel.selected
}).length
},
addChannelToProfile: function () {
if (this.selectedLength === 0) {
this.showToast({
message: this.$t('Profile.No channel(s) have been selected')
})
} else {
const subscriptions = this.channels.filter((channel) => {
return channel.selected
})
const profile = JSON.parse(JSON.stringify(this.profile))
profile.subscriptions = profile.subscriptions.concat(subscriptions)
this.updateProfile(profile)
this.showToast({
message: this.$t('Profile.Profile has been updated')
})
this.selectNone()
}
},
selectAll: function () {
Object.keys(this.$refs).forEach((ref) => {
if (typeof this.$refs[ref][0] !== 'undefined') {
this.$refs[ref][0].selected = true
}
})
this.channels = this.channels.map((channel) => {
channel.selected = true
return channel
})
this.selectedLength = this.channels.filter((channel) => {
return channel.selected
}).length
},
selectNone: function () {
Object.keys(this.$refs).forEach((ref) => {
if (typeof this.$refs[ref][0] !== 'undefined') {
this.$refs[ref][0].selected = false
}
})
this.channels = this.channels.map((channel) => {
channel.selected = false
return channel
})
this.selectedLength = this.channels.filter((channel) => {
return channel.selected
}).length
},
...mapActions([
'showToast',
'updateProfile'
])
}
})

View File

@ -0,0 +1,42 @@
<template>
<div>
<ft-card class="card">
<h2>
{{ $t("Profile.Other Channels") }}
</h2>
<p>
{{ selectedText }}
</p>
<ft-flex-box>
<ft-channel-bubble
v-for="(channel, index) in channels"
:key="index"
:ref="`all-channels-${index}`"
:channel-name="channel.name"
:channel-thumbnail="channel.thumbnail"
:show-selected="true"
@click="handleChannelClick(index)"
/>
</ft-flex-box>
<ft-flex-box>
<ft-button
:label="$t('Profile.Select All')"
@click="selectAll"
/>
<ft-button
:label="$t('Profile.Select None')"
@click="selectNone"
/>
<ft-button
:label="$t('Profile.Add Selected To Profile')"
text-color="var(--text-with-main-color)"
background-color="var(--primary-color)"
@click="addChannelToProfile"
/>
</ft-flex-box>
</ft-card>
</div>
</template>
<script src="./ft-profile-all-channels-list.js" />
<style scoped src="./ft-profile-all-channels-list.css" />

View File

@ -0,0 +1,5 @@
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 15px;
}

View File

@ -0,0 +1,204 @@
import Vue from 'vue'
import { mapActions } from 'vuex'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubble.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
export default Vue.extend({
name: 'FtProfileChannelList',
components: {
'ft-card': FtCard,
'ft-flex-box': FtFlexBox,
'ft-channel-bubble': FtChannelBubble,
'ft-button': FtButton,
'ft-prompt': FtPrompt
},
props: {
profile: {
type: Object,
required: true
},
isMainProfile: {
type: Boolean,
required: true
}
},
data: function () {
return {
showDeletePrompt: false,
subscriptions: [],
selectedLength: 0,
componentKey: 0,
deletePromptValues: [
'yes',
'no'
]
}
},
computed: {
profileList: function () {
return this.$store.getters.getProfileList
},
selectedText: function () {
const localeText = this.$t('Profile.$ selected')
return localeText.replace('$', this.selectedLength)
},
deletePromptMessage: function () {
if (this.isMainProfile) {
return this.$t('Profile["This is your primary profile. Are you sure you want to delete the selected channels? The same channels will be deleted in any profile they are found in."]')
} else {
return this.$t('Profile["Are you sure you want to delete the selected channels? This will not delete the channel from any other profile."]')
}
},
deletePromptNames: function () {
return [
this.$t('Yes'),
this.$t('No')
]
}
},
watch: {
profile: function () {
this.subscriptions = [].concat(this.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
}).map((channel) => {
channel.selected = false
return channel
}))
}
},
mounted: function () {
if (typeof this.profile.subscriptions !== 'undefined') {
this.subscriptions = [].concat(this.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
}).map((channel) => {
channel.selected = false
return channel
}))
}
},
methods: {
displayDeletePrompt: function () {
if (this.selectedLength === 0) {
this.showToast({
message: this.$t('Profile.No channel(s) have been selected')
})
} else {
this.showDeletePrompt = true
}
},
handleDeletePromptClick: function (value) {
if (value !== 'no' && value !== null) {
if (this.isMainProfile) {
const channelsToRemove = this.subscriptions.filter((channel) => {
return channel.selected
})
this.subscriptions = this.subscriptions.filter((channel) => {
return !channel.selected
})
this.profileList.forEach((x) => {
const profile = JSON.parse(JSON.stringify(x))
profile.subscriptions = profile.subscriptions.filter((channel) => {
const index = channelsToRemove.findIndex((y) => {
return y.id === channel.id
})
return index === -1
})
this.updateProfile(profile)
this.showToast({
message: this.$t('Profile.Profile has been updated')
})
this.selectNone()
})
} else {
const profile = JSON.parse(JSON.stringify(this.profile))
this.subscriptions = this.subscriptions.filter((channel) => {
return !channel.selected
})
profile.subscriptions = this.subscriptions
this.selectedLength = 0
this.updateProfile(profile)
this.showToast({
message: this.$t('Profile.Profile has been updated')
})
this.selectNone()
}
}
this.showDeletePrompt = false
},
handleChannelClick: function (index) {
this.subscriptions[index].selected = !this.subscriptions[index].selected
this.selectedLength = this.subscriptions.filter((channel) => {
return channel.selected
}).length
},
selectAll: function () {
Object.keys(this.$refs).forEach((ref) => {
if (typeof this.$refs[ref][0] !== 'undefined') {
this.$refs[ref][0].selected = true
}
})
this.subscriptions = this.subscriptions.map((channel) => {
channel.selected = true
return channel
})
this.selectedLength = this.subscriptions.filter((channel) => {
return channel.selected
}).length
},
selectNone: function () {
Object.keys(this.$refs).forEach((ref) => {
if (typeof this.$refs[ref][0] !== 'undefined') {
this.$refs[ref][0].selected = false
}
})
this.subscriptions = this.subscriptions.map((channel) => {
channel.selected = false
return channel
})
this.selectedLength = this.subscriptions.filter((channel) => {
return channel.selected
}).length
},
...mapActions([
'showToast',
'updateProfile'
])
}
})

View File

@ -0,0 +1,49 @@
<template>
<div>
<ft-card class="card">
<h2>
{{ $t("Profile.Subscription List") }}
</h2>
<p>
{{ selectedText }}
</p>
<ft-flex-box>
<ft-channel-bubble
v-for="(channel, index) in subscriptions"
:key="index"
:ref="`channel-${index}`"
:channel-name="channel.name"
:channel-thumbnail="channel.thumbnail"
:show-selected="true"
@click="handleChannelClick(index)"
/>
</ft-flex-box>
<ft-flex-box>
<ft-button
:label="$t('Profile.Select All')"
@click="selectAll"
/>
<ft-button
:label="$t('Profile.Select None')"
@click="selectNone"
/>
<ft-button
:label="$t('Profile.Delete Selected')"
text-color="var(--text-with-main-color)"
background-color="var(--primary-color)"
@click="displayDeletePrompt"
/>
</ft-flex-box>
</ft-card>
<ft-prompt
v-if="showDeletePrompt"
:label="deletePromptMessage"
:option-names="deletePromptNames"
:option-values="deletePromptValues"
@click="handleDeletePromptClick"
/>
</div>
</template>
<script src="./ft-profile-channel-list.js" />
<style scoped src="./ft-profile-channel-list.css" />

View File

@ -0,0 +1,45 @@
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 15px;
}
.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%;
}
}

View File

@ -0,0 +1,163 @@
import Vue from 'vue'
import { mapActions } from 'vuex'
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: 'FtProfileEdit',
components: {
'ft-card': FtCard,
'ft-prompt': FtPrompt,
'ft-flex-box': FtFlexBox,
'ft-input': FtInput,
'ft-button': FtButton
},
props: {
profile: {
type: Object,
required: true
},
isNew: {
type: Boolean,
required: true
}
},
data: function () {
return {
showDeletePrompt: 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
},
deletePromptLabel: function () {
return `${this.$t('Profile.Are you sure you want to delete this profile?')} ${this.$t('Profile["All subscriptions will also be deleted."]')}`
},
deletePromptNames: function () {
return [
this.$t('Yes'),
this.$t('No')
]
}
},
watch: {
profileBgColor: async function (val) {
this.profileTextColor = await this.calculateColorLuminance(val)
}
},
mounted: async function () {
this.profileId = this.$route.params.id
this.profileName = this.profile.name
this.profileBgColor = this.profile.bgColor
this.profileTextColor = this.profile.textColor
this.profileSubscriptions = this.profile.subscriptions
},
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',
'updateProfile',
'removeProfile',
'updateDefaultProfile',
'updateActiveProfile',
'calculateColorLuminance'
])
}
})

View File

@ -0,0 +1,99 @@
<template>
<div>
<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"
v-model="profileBgColor"
type="color"
>
</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>
<ft-prompt
v-if="showDeletePrompt"
:label="deletePromptLabel"
:option-names="deletePromptNames"
:option-values="deletePromptValues"
@click="handleDeletePrompt"
/>
</div>
</template>
<script src="./ft-profile-edit.js" />
<style scoped src="./ft-profile-edit.css" />

View File

@ -62,6 +62,10 @@ export default Vue.extend({
getTimestamp: { getTimestamp: {
type: Function, type: Function,
required: true required: true
},
isUpcoming: {
type: Boolean,
required: true
} }
}, },
data: function () { data: function () {

View File

@ -67,6 +67,7 @@
@click="$emit('theatreMode')" @click="$emit('theatreMode')"
/> />
<ft-icon-button <ft-icon-button
v-if="!isUpcoming"
:title="$t('Change Format.Change Video Formats')" :title="$t('Change Format.Change Video Formats')"
class="option" class="option"
theme="secondary" theme="secondary"

View File

@ -152,6 +152,7 @@ export default Vue.extend({
$route() { $route() {
// react to route changes... // react to route changes...
this.id = this.$route.params.id this.id = this.$route.params.id
this.currentTab = 'videos'
this.isLoading = true this.isLoading = true
if (!this.usingElectron) { if (!this.usingElectron) {
@ -224,6 +225,10 @@ export default Vue.extend({
} }
}, },
methods: { methods: {
goToChannel: function (id) {
this.$router.push({ path: `/channel/${id}` })
},
getChannelInfoLocal: function () { getChannelInfoLocal: function () {
this.apiUsed = 'local' this.apiUsed = 'local'
ytch.getChannelInfo(this.id).then((response) => { ytch.getChannelInfo(this.id).then((response) => {

View File

@ -123,6 +123,7 @@
:channel-name="channel.author" :channel-name="channel.author"
:channel-id="channel.authorId" :channel-id="channel.authorId"
:channel-thumbnail="channel.authorThumbnails[channel.authorThumbnails.length - 1].url" :channel-thumbnail="channel.authorThumbnails[channel.authorThumbnails.length - 1].url"
@click="goToChannel(channel.authorId)"
/> />
</ft-flex-box> </ft-flex-box>
</div> </div>

View File

@ -1,45 +0,0 @@
.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%;
}
}

View File

@ -1,62 +1,50 @@
import Vue from 'vue' import Vue from 'vue'
import { mapActions } from 'vuex' import { mapActions } from 'vuex'
import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue' import FtProfileEdit from '../../components/ft-profile-edit/ft-profile-edit.vue'
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue' import FtProfileChannelList from '../../components/ft-profile-channel-list/ft-profile-channel-list.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtProfileAllChannelsList from '../../components/ft-profile-all-channels-list/ft-profile-all-channels-list.vue'
import FtInput from '../../components/ft-input/ft-input.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
export default Vue.extend({ export default Vue.extend({
name: 'ProfileEdit', name: 'ProfileEdit',
components: { components: {
'ft-loader': FtLoader, 'ft-loader': FtLoader,
'ft-card': FtCard, 'ft-profile-edit': FtProfileEdit,
'ft-prompt': FtPrompt, 'ft-profile-channel-list': FtProfileChannelList,
'ft-flex-box': FtFlexBox, 'ft-profile-all-channels-list': FtProfileAllChannelsList
'ft-input': FtInput,
'ft-button': FtButton
}, },
data: function () { data: function () {
return { return {
isLoading: false, isLoading: false,
showDeletePrompt: false,
deletePromptLabel: '',
isNew: false, isNew: false,
profileId: '', profileId: '',
profileName: '', profile: {}
profileBgColor: '',
profileTextColor: '',
profileSubscriptions: [],
deletePromptValues: [
'yes',
'no'
]
} }
}, },
computed: { computed: {
colorValues: function () { profileList: function () {
return this.$store.getters.getColorValues return this.$store.getters.getProfileList
}, },
profileInitial: function () { isMainProfile: function () {
return this.profileName.slice(0, 1).toUpperCase() return this.profileId === 'allChannels'
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
defaultProfile: function () {
return this.$store.getters.getDefaultProfile
},
deletePromptNames: function () {
return [
this.$t('Yes'),
this.$t('No')
]
} }
}, },
watch: { watch: {
profileBgColor: async function (val) { profileList: {
this.profileTextColor = await this.calculateColorLuminance(val) handler: function () {
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.profile = profile
})
},
deep: true
} }
}, },
mounted: async function () { mounted: async function () {
@ -67,13 +55,18 @@ export default Vue.extend({
if (profileType === 'newProfile') { if (profileType === 'newProfile') {
this.isNew = true this.isNew = true
this.profileBgColor = await this.getRandomColor() const bgColor = await this.getRandomColor()
const textColor = await this.calculateColorLuminance(bgColor)
this.profile = {
name: '',
bgColor: bgColor,
textColor: textColor,
subscriptions: []
}
this.isLoading = false this.isLoading = false
} else { } else {
this.isNew = false this.isNew = false
this.profileId = this.$route.params.id this.profileId = this.$route.params.id
console.log(this.profileId)
console.log(this.$route.name)
this.grabProfileInfo(this.profileId).then((profile) => { this.grabProfileInfo(this.profileId).then((profile) => {
if (profile === null) { if (profile === null) {
@ -84,100 +77,17 @@ export default Vue.extend({
path: '/settings/profile/' path: '/settings/profile/'
}) })
} }
this.profileName = profile.name this.profile = profile
this.profileBgColor = profile.bgColor
this.profileTextColor = profile.textColor
this.profileSubscriptions = profile.subscriptions
this.isLoading = false this.isLoading = false
}) })
} }
}, },
methods: { 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([ ...mapActions([
'showToast', 'showToast',
'grabProfileInfo', 'grabProfileInfo',
'updateProfile', 'getRandomColor',
'removeProfile', 'calculateColorLuminance'
'updateDefaultProfile',
'updateActiveProfile',
'calculateColorLuminance',
'getRandomColor'
]) ])
} }
}) })

View File

@ -7,99 +7,20 @@
<div <div
v-else v-else
> >
<ft-card class="card"> <ft-profile-edit
<h2>{{ $t("Profile.Edit Profile") }}</h2> :profile="profile"
<ft-flex-box> :is-new="isNew"
<ft-input />
class="profileName" <ft-profile-channel-list
placeholder="Profile Name" v-if="!isNew"
:value="profileName" :profile="profile"
:show-arrow="false" :is-main-profile="isMainProfile"
@input="e => profileName = e" />
/> <ft-profile-all-channels-list
</ft-flex-box> v-if="!isNew && !isMainProfile"
<h3>{{ $t("Profile.Color Picker") }}</h3> :profile="profile"
<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"
v-model="profileBgColor"
type="color"
>
</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> </div>
<ft-prompt
v-if="showDeletePrompt"
:label="deletePromptLabel"
:option-names="deletePromptNames"
:option-values="deletePromptValues"
@click="handleDeletePrompt"
/>
</div> </div>
</template> </template>

View File

@ -40,6 +40,8 @@ export default Vue.extend({
showYouTubeNoCookieEmbed: false, showYouTubeNoCookieEmbed: false,
hidePlayer: false, hidePlayer: false,
isLive: false, isLive: false,
isUpcoming: false,
upcomingTimestamp: null,
activeFormat: 'legacy', activeFormat: 'legacy',
videoId: '', videoId: '',
videoTitle: '', videoTitle: '',
@ -215,8 +217,9 @@ export default Vue.extend({
this.videoLikeCount = result.videoDetails.likes this.videoLikeCount = result.videoDetails.likes
this.videoDislikeCount = result.videoDetails.dislikes this.videoDislikeCount = result.videoDetails.dislikes
this.isLive = result.player_response.videoDetails.isLiveContent this.isLive = result.player_response.videoDetails.isLiveContent
this.isUpcoming = result.player_response.videoDetails.isUpcoming
if (!this.isLive) { if (!this.isLive && !this.isUpcoming) {
const captionTracks = const captionTracks =
result.player_response.captions && result.player_response.captions &&
result.player_response.captions.playerCaptionsTracklistRenderer result.player_response.captions.playerCaptionsTracklistRenderer
@ -243,7 +246,7 @@ export default Vue.extend({
} }
} }
if (this.isLive) { if (this.isLive && !this.isUpcoming) {
this.enableLegacyFormat() this.enableLegacyFormat()
this.videoSourceList = result.formats.filter((format) => { this.videoSourceList = result.formats.filter((format) => {
@ -279,28 +282,39 @@ export default Vue.extend({
} else { } else {
this.activeSourceList = this.videoSourceList this.activeSourceList = this.videoSourceList
} }
} else if (this.isUpcoming) {
const upcomingTimestamp = new Date(result.videoDetails.liveBroadcastDetails.startTimestamp)
this.upcomingTimestamp = upcomingTimestamp.toLocaleString()
} else { } else {
this.videoLengthSeconds = parseInt(result.videoDetails.lengthSeconds) this.videoLengthSeconds = parseInt(result.videoDetails.lengthSeconds)
this.videoSourceList = result.player_response.streamingData.formats this.videoSourceList = result.player_response.streamingData.formats
this.dashSrc = await this.createLocalDashManifest(result.player_response.streamingData.adaptiveFormats)
this.audioSourceList = result.player_response.streamingData.adaptiveFormats.filter((format) => { if (typeof result.player_response.streamingData.adaptiveFormats !== 'undefined') {
return format.mimeType.includes('audio') this.dashSrc = await this.createLocalDashManifest(result.player_response.streamingData.adaptiveFormats)
}).map((format) => {
return { this.audioSourceList = result.player_response.streamingData.adaptiveFormats.filter((format) => {
url: format.url, return format.mimeType.includes('audio')
type: format.mimeType, }).map((format) => {
label: 'Audio', return {
qualityLabel: format.bitrate url: format.url,
type: format.mimeType,
label: 'Audio',
qualityLabel: format.bitrate
}
}).sort((a, b) => {
return a.qualityLabel - b.qualityLabel
})
if (this.activeFormat === 'audio') {
this.activeSourceList = this.audioSourceList
} else {
this.activeSourceList = this.videoSourceList
} }
}).sort((a, b) => {
return a.qualityLabel - b.qualityLabel
})
if (this.activeFormat === 'audio') {
this.activeSourceList = this.audioSourceList
} else { } else {
this.activeSourceList = this.videoSourceList this.activeSourceList = this.videoSourceList
this.audioSourceList = null
this.dashSrc = null
this.enableLegacyFormat()
} }
if (typeof result.player_response.storyboards !== 'undefined') { if (typeof result.player_response.storyboards !== 'undefined') {
@ -528,6 +542,13 @@ export default Vue.extend({
return return
} }
if (this.dashSrc === null) {
this.showToast({
message: this.$t('Change Format.Dash formats are not available for this video')
})
return
}
this.activeFormat = 'dash' this.activeFormat = 'dash'
this.hidePlayer = true this.hidePlayer = true
@ -555,6 +576,13 @@ export default Vue.extend({
return return
} }
if (this.audioSourceList === null) {
this.showToast({
message: this.$t('Change Format.Audio formats are not available for this video')
})
return
}
this.activeFormat = 'audio' this.activeFormat = 'audio'
this.activeSourceList = this.audioSourceList this.activeSourceList = this.audioSourceList
this.hidePlayer = true this.hidePlayer = true

View File

@ -35,6 +35,30 @@
grid-column: 1 grid-column: 1
max-width: calc(80vh * 1.78) max-width: calc(80vh * 1.78)
margin: 0 auto margin: 0 auto
position: relative
.upcomingThumbnail
width: 100%
.premiereDate
color: #FFFFFF
background-color: rgba(0, 0, 0, 0.7)
width: 400px
height: 60px
border-radius: 5%
position: absolute
bottom: 5px
.premiereIcon
float: left
font-size: 25px
margin-top: 12px
margin-left: 8px
padding: 5px
.premiereText
margin-left: 50px
margin-top: 10px
.watchVideo .watchVideo
margin: 0px 8px 16px margin: 0px 8px 16px

View File

@ -13,7 +13,7 @@
<div class="videoArea"> <div class="videoArea">
<div class="videoAreaMargin"> <div class="videoAreaMargin">
<ft-video-player <ft-video-player
v-if="!isLoading && !hidePlayer" v-if="!isLoading && !hidePlayer && !isUpcoming"
ref="videoPlayer" ref="videoPlayer"
:dash-src="dashSrc" :dash-src="dashSrc"
:source-list="activeSourceList" :source-list="activeSourceList"
@ -27,6 +27,30 @@
@ended="handleVideoEnded" @ended="handleVideoEnded"
@error="handleVideoError" @error="handleVideoError"
/> />
<div
v-if="!isLoading && isUpcoming"
class="videoPlayer"
>
<img
:src="thumbnail"
class="upcomingThumbnail"
/>
<div
class='premiereDate'
>
<font-awesome-icon
icon="satellite-dish"
class="premiereIcon"
/>
<p
class="premiereText"
>
Premieres on:
<br />
{{ upcomingTimestamp }}
</p>
</div>
</div>
</div> </div>
</div> </div>
<div class="infoArea"> <div class="infoArea">
@ -43,6 +67,7 @@
:dislike-count="videoDislikeCount" :dislike-count="videoDislikeCount"
:view-count="videoViewCount" :view-count="videoViewCount"
:get-timestamp="getTimestamp" :get-timestamp="getTimestamp"
:is-upcoming="isUpcoming"
class="watchVideo" class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }" :class="{ theatreWatchVideo: useTheatreMode }"
@theatreMode="toggleTheatreMode" @theatreMode="toggleTheatreMode"

View File

@ -306,6 +306,16 @@ Profile:
Your default profile has been changed to your primary profile: Your default profile Your default profile has been changed to your primary profile: Your default profile
has been changed to your primary profile has been changed to your primary profile
$ is now the active profile: $ is now the active profile $ is now the active profile: $ is now the active profile
Subscription List: Subscription List
Other Channels: Other Channels
$ selected: $ selected
Select All: Select All
Select None: Select None
Delete Selected: Delete Selected
Add Selected To Profile: Add Selected To Profile
No channel(s) have been selected: No channel(s) have been selected
This is your primary profile. Are you sure you want to delete the selected channels? The same channels will be deleted in any profile they are found in.: This is your primary profile. Are you sure you want to delete the selected channels? The same channels will be deleted in any profile they are found in.
Are you sure you want to delete the selected channels? This will not delete the channel from any other profile.: Are you sure you want to delete the selected channels? This will not delete the channel from any other profile.
#On Channel Page #On Channel Page
Channel: Channel:
Subscriber: Subscriber Subscriber: Subscriber
@ -435,6 +445,8 @@ Change Format:
Use Dash Formats: Use Dash Formats Use Dash Formats: Use Dash Formats
Use Legacy Formats: Use Legacy Formats Use Legacy Formats: Use Legacy Formats
Use Audio Formats: Use Audio Formats Use Audio Formats: Use Audio Formats
Dash formats are not available for this video: Dash formats are not available for this video
Audio formats are not available for this video: Audio formats are not available for this video
Share: Share:
Share Video: Share Video Share Video: Share Video
Include Timestamp: Include Timestamp Include Timestamp: Include Timestamp