diff --git a/src/renderer/components/ft-settings-menu/ft-settings-menu.css b/src/renderer/components/ft-settings-menu/ft-settings-menu.css
new file mode 100644
index 000000000000..b6d613a3e072
--- /dev/null
+++ b/src/renderer/components/ft-settings-menu/ft-settings-menu.css
@@ -0,0 +1,125 @@
+.settingsMenu {
+ /* top nav + margin */
+ inset-block-start: 96px;
+ position: sticky;
+ display: flex;
+ flex-direction: column;
+ padding-inline-start: 0;
+ block-size: calc(85vh - 96px);
+ max-block-size: 600px;
+}
+
+.header {
+ inline-size: fit-content;
+ margin-block: 0 10px;
+}
+
+.title {
+ text-decoration: none;
+ color: var(--tertiary-text-color);
+ inline-size: 220px;
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+}
+
+/* prevent hover styling from showing on title click for mobile */
+@media (hover: hover) {
+ .title:hover {
+ color: var(--primary-text-color);
+ }
+}
+
+.titleContent {
+ inline-size: fit-content;
+ max-inline-size: 100%;
+}
+
+
+.iconAndTitleText {
+ overflow-x: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ /* needed to have underline poke out */
+ margin-inline-start: 3px;
+}
+
+.titleUnderline {
+ /* have underline poke out */
+ inline-size: calc(100% + 6px);
+
+ /* prevent "active" border from visibly pushing the content up */
+ border-block-end: 4px solid transparent;
+}
+
+@media only screen and (width >= 1015px) {
+ .settingsMenu {
+ margin-block: 0;
+ font-size: 16px;
+ }
+
+ .header {
+ margin-block-start: 10px;
+ font-size: 26px;
+ }
+
+ .titleIcon {
+ inline-size: 16px;
+ block-size: 16px;
+ }
+
+ .title.active {
+ color: var(--primary-text-color);
+ font-weight: 600;
+ }
+
+ .title.active .titleUnderline {
+ border-block-end: 4px solid var(--primary-color);
+ }
+}
+
+/* overall mobile breakpoint; large text */
+@media only screen and (width <= 1015px) {
+ .settingsMenu {
+ inline-size: fit-content;
+ margin-inline: auto;
+ position: relative;
+ inset-block-start: 0;
+ font-size: 30px;
+ max-block-size: 1100px;
+ }
+
+ .titleIcon {
+ inline-size: 30px;
+ block-size: 30px;
+ margin-inline-end: 10px;
+ }
+
+ .title {
+ inline-size: fit-content;
+ max-inline-size: 90vw;
+ }
+
+ .header {
+ font-size: 32px;
+ }
+}
+
+/* small height or width mobile breakpoint; intermediary text */
+@media only screen and (width <= 1015px) and (height <= 830px),
+ only screen and (width <= 500px) {
+ .settingsMenu {
+ font-size: 25px;
+ }
+
+ .titleIcon {
+ inline-size: 25px;
+ block-size: 25px;
+ margin-inline-end: 5px;
+ }
+
+ .header {
+ font-size: 25px;
+ }
+}
diff --git a/src/renderer/components/ft-settings-menu/ft-settings-menu.js b/src/renderer/components/ft-settings-menu/ft-settings-menu.js
new file mode 100644
index 000000000000..5378063a9803
--- /dev/null
+++ b/src/renderer/components/ft-settings-menu/ft-settings-menu.js
@@ -0,0 +1,17 @@
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+ name: 'FtSettingsMenu',
+ props: {
+ settingsSections: {
+ type: Array,
+ required: true
+ },
+ },
+ emits: ['navigate-to-section'],
+ methods: {
+ goToSettingsSection: function (sectionType) {
+ this.$emit('navigate-to-section', sectionType)
+ }
+ }
+})
diff --git a/src/renderer/components/ft-settings-menu/ft-settings-menu.vue b/src/renderer/components/ft-settings-menu/ft-settings-menu.vue
new file mode 100644
index 000000000000..71762363cb23
--- /dev/null
+++ b/src/renderer/components/ft-settings-menu/ft-settings-menu.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/src/renderer/components/ft-settings-section/ft-settings-section.js b/src/renderer/components/ft-settings-section/ft-settings-section.js
index b161a0806699..42715cbd1303 100644
--- a/src/renderer/components/ft-settings-section/ft-settings-section.js
+++ b/src/renderer/components/ft-settings-section/ft-settings-section.js
@@ -7,10 +7,5 @@ export default defineComponent({
type: String,
required: true
},
- },
- computed: {
- allSettingsSectionsExpandedByDefault: function () {
- return this.$store.getters.getAllSettingsSectionsExpandedByDefault
- }
}
})
diff --git a/src/renderer/components/ft-settings-section/ft-settings-section.scss b/src/renderer/components/ft-settings-section/ft-settings-section.scss
index cdf50ffd3296..50764ae34891 100644
--- a/src/renderer/components/ft-settings-section/ft-settings-section.scss
+++ b/src/renderer/components/ft-settings-section/ft-settings-section.scss
@@ -1,16 +1,20 @@
.settingsSection {
- background-color: var(--card-bg-color);
margin-block: 0;
- margin-inline: auto;
- inline-size: 85%;
@media only screen and (width <= 800px) {
inline-size: 100%;
}
+}
- &[open] {
- padding-block-end: 15px;
- }
+.sectionHeader {
+ cursor: pointer;
+ display: block;
+ padding: 1px;
+}
+
+.sectionBody {
+ background-color: var(--card-bg-color);
+ padding-block: 10px;
> div {
box-sizing: border-box;
@@ -26,22 +30,9 @@
}
}
-.sectionLine {
- border: 0;
- border-block-start: 2px solid var(--primary-color);
- margin-block-start: -1px;
- inline-size: 100%;
-}
-
-.sectionHeader {
- cursor: pointer;
- display: block;
- padding: 1px;
-}
-
.sectionTitle {
user-select: none;
- margin-inline-start: 2%;
+ margin-inline-start: 20px;
margin-block: 0.5em;
}
diff --git a/src/renderer/components/ft-settings-section/ft-settings-section.vue b/src/renderer/components/ft-settings-section/ft-settings-section.vue
index 5bcae70f1a1d..c748d931a32f 100644
--- a/src/renderer/components/ft-settings-section/ft-settings-section.vue
+++ b/src/renderer/components/ft-settings-section/ft-settings-section.vue
@@ -1,16 +1,14 @@
-
-
diff --git a/src/renderer/components/proxy-settings/proxy-settings.vue b/src/renderer/components/proxy-settings/proxy-settings.vue
index 3245e0f3c5f1..f4ac586403f5 100644
--- a/src/renderer/components/proxy-settings/proxy-settings.vue
+++ b/src/renderer/components/proxy-settings/proxy-settings.vue
@@ -19,7 +19,7 @@
:select-names="protocolNames"
:select-values="protocolValues"
class="protocol-dropdown"
- :icon="['fas', 'microchip']"
+ :icon="['fas', 'network-wired']"
@change="handleUpdateProxyProtocol"
/>
diff --git a/src/renderer/main.js b/src/renderer/main.js
index cfd35f888fa4..0725d4b0a4b2 100644
--- a/src/renderer/main.js
+++ b/src/renderer/main.js
@@ -15,6 +15,7 @@ import { ObserveVisibility } from 'vue-observe-visibility'
// to avoid code conflict and duplicate entries
import {
faAngleDown,
+ faAngleLeft,
faAngleUp,
faArrowDown,
faArrowDownShortWide,
@@ -23,15 +24,20 @@ import {
faArrowRight,
faArrowUp,
faBars,
+ faBorderAll,
faBookmark,
faCheck,
faChevronRight,
+ faCirclePlay,
faCircleUser,
+ faClapperboard,
faClock,
faClone,
faComment,
faCommentDots,
faCopy,
+ faDatabase,
+ faDisplay,
faDownload,
faEdit,
faEllipsisH,
@@ -46,6 +52,7 @@ import {
faFileImage,
faFileVideo,
faFilter,
+ faFlask,
faFire,
faForward,
faGauge,
@@ -56,12 +63,14 @@ import {
faHistory,
faImages,
faInfoCircle,
+ faKey,
faLanguage,
faLink,
faLinkSlash,
faList,
faLocationDot,
- faMicrochip,
+ faLock,
+ faNetworkWired,
faNewspaper,
faPalette,
faPause,
@@ -77,6 +86,7 @@ import {
faSearch,
faServer,
faShareAlt,
+ faShield,
faSlash,
faSlidersH,
faSortAlphaDown,
@@ -92,9 +102,10 @@ import {
faTimesCircle,
faTrash,
faUserCheck,
+ faUserLock,
faUsers,
faUsersSlash,
- faWifi,
+ faWifi
} from '@fortawesome/free-solid-svg-icons'
import {
faBookmark as farBookmark
@@ -116,6 +127,7 @@ Vue.config.productionTip = process.env.NODE_ENV === 'development'
library.add(
// solid icons
faAngleDown,
+ faAngleLeft,
faAngleUp,
faArrowDown,
faArrowDownShortWide,
@@ -124,15 +136,20 @@ library.add(
faArrowRight,
faArrowUp,
faBars,
+ faBorderAll,
faBookmark,
faCheck,
faChevronRight,
+ faCirclePlay,
faCircleUser,
+ faClapperboard,
faClock,
faClone,
faComment,
faCommentDots,
faCopy,
+ faDatabase,
+ faDisplay,
faDownload,
faEdit,
faEllipsisH,
@@ -147,6 +164,7 @@ library.add(
faFileImage,
faFileVideo,
faFilter,
+ faFlask,
faFire,
faForward,
faGauge,
@@ -157,19 +175,20 @@ library.add(
faHistory,
faImages,
faInfoCircle,
+ faKey,
faLanguage,
faLink,
faLinkSlash,
faList,
faLocationDot,
- faMicrochip,
+ faLock,
+ faNetworkWired,
faNewspaper,
faPalette,
faPause,
faPhotoFilm,
faPlay,
faPlus,
- faPhotoFilm,
faQuestionCircle,
faRandom,
faRetweet,
@@ -179,6 +198,7 @@ library.add(
faSearch,
faServer,
faShareAlt,
+ faShield,
faSlash,
faSlidersH,
faSortAlphaDown,
@@ -194,6 +214,7 @@ library.add(
faTimesCircle,
faTrash,
faUserCheck,
+ faUserLock,
faUsers,
faUsersSlash,
faWifi,
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index 1c657507afa0..5d46235782d7 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -162,7 +162,6 @@ const defaultSideEffectsTriggerId = settingId =>
/*****/
const state = {
- allSettingsSectionsExpandedByDefault: false,
autoplayPlaylists: true,
autoplayVideos: true,
backendFallback: process.env.SUPPORTS_LOCAL_API,
diff --git a/src/renderer/views/Settings/Settings.css b/src/renderer/views/Settings/Settings.css
index a796ede90747..52eeea4b52a1 100644
--- a/src/renderer/views/Settings/Settings.css
+++ b/src/renderer/views/Settings/Settings.css
@@ -1,24 +1,74 @@
-hr {
- inline-size: 85%;
- margin-block: 0;
- margin-inline: auto;
- border: 0;
- border-block-start: 2px solid var(--scrollbar-color-hover);
+.settingsPage {
+ display: flex;
+ gap: 20px;
}
-@media only screen and (width <= 800px) {
- hr {
- inline-size: 100%;
- }
+.settingsSections {
+ overflow-x: hidden;
+ max-inline-size: 90%;
}
.switchRow {
- inline-size: 85%;
- margin-inline: auto;
display: flex;
+ margin-inline-end: auto;
}
.settingsToggle {
padding-block: 0;
- margin-block: 10px 5px;
+ margin-block: 0;
+}
+
+.section {
+ /* enables anchor link clicks to land with the section title in the top quarter of the page */
+ scroll-margin-top: 24vh;
+}
+
+@media only screen and (width >= 1015px) {
+ .section + .section {
+ padding-block-start: 20px;
+ }
+
+ .settingsSections:last-child {
+ /* Add enough blank space to have the last setting section be active on the FtSettingsMenu when scrolled to */
+ margin-block-end: calc(76vh - 150px);
+ }
+}
+
+@media only screen and (width <= 1200px) {
+ .settingsSections {
+ max-inline-size: 100%;
+ }
+}
+
+@media only screen and (width <= 1015px) {
+ /* Needed to overcome routerView styling for margin-block */
+ .settingsPage.settingsPage {
+ margin-block: 0;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .switchRow,
+ .settingsSections {
+ margin-inline: auto;
+ }
+
+ .switchRow {
+ display: none;
+ }
+
+ .settingsSections {
+ overflow-y: hidden;
+ margin-block-end: 80px;
+ }
+
+ .hideOnMobile {
+ display: none;
+ }
+
+ .returnToMenuMobileIcon {
+ margin-block-start: 20px;
+ block-size: 30px;
+ font-size: 30px;
+ }
}
diff --git a/src/renderer/views/Settings/Settings.js b/src/renderer/views/Settings/Settings.js
index 722e37bba22d..d9568df05bb9 100644
--- a/src/renderer/views/Settings/Settings.js
+++ b/src/renderer/views/Settings/Settings.js
@@ -1,4 +1,4 @@
-import { defineComponent } from 'vue'
+import { defineComponent, nextTick } from 'vue'
import { mapActions } from 'vuex'
import GeneralSettings from '../../components/general-settings/general-settings.vue'
import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
@@ -16,6 +16,10 @@ import ExperimentalSettings from '../../components/experimental-settings/experim
import PasswordSettings from '../../components/password-settings/password-settings.vue'
import PasswordDialog from '../../components/password-dialog/password-dialog.vue'
import FtToggleSwitch from '../../components/ft-toggle-switch/ft-toggle-switch.vue'
+import FtSettingsMenu from '../../components/ft-settings-menu/ft-settings-menu.vue'
+
+const ACTIVE_CLASS_NAME = 'active'
+const SETTINGS_MOBILE_WIDTH_THRESHOLD = 1015
export default defineComponent({
name: 'Settings',
@@ -32,7 +36,7 @@ export default defineComponent({
'password-settings': PasswordSettings,
'password-dialog': PasswordDialog,
'ft-toggle-switch': FtToggleSwitch,
-
+ 'ft-settings-menu': FtSettingsMenu,
...(process.env.IS_ELECTRON
? {
'proxy-settings': ProxySettings,
@@ -44,104 +48,120 @@ export default defineComponent({
},
data: function () {
return {
- unlocked: false,
- settingsComponentsData: [
- {
- type: 'general-settings',
- title: this.$t('Settings.General Settings.General Settings')
- },
+ isInDesktopView: true,
+ settingsSectionTypeOpenInMobile: null,
+ unlocked: false
+ }
+ },
+ computed: {
+ locale: function() {
+ return this.$i18n.locale
+ },
+
+ settingsPassword: function () {
+ return this.$store.getters.getSettingsPassword
+ },
+
+ settingsSectionSortEnabled: function () {
+ return this.$store.getters.getSettingsSectionSortEnabled
+ },
+
+ settingsComponentsData: function () {
+ const settingsComponentsData = [
{
type: 'theme-settings',
- title: this.$t('Settings.Theme Settings.Theme Settings')
+ title: this.$t('Settings.Theme Settings.Theme Settings'),
+ icon: 'display'
},
{
type: 'player-settings',
- title: this.$t('Settings.Player Settings.Player Settings')
+ title: this.$t('Settings.Player Settings.Player Settings'),
+ icon: 'circle-play'
},
...(process.env.IS_ELECTRON
- ? [
- {
- type: 'external-player-settings',
- title: this.$t('Settings.External Player Settings.External Player Settings')
- }
- ]
+ ? [{
+ type: 'external-player-settings',
+ title: this.$t('Settings.External Player Settings.External Player Settings'),
+ icon: 'clapperboard'
+ }]
: []),
{
type: 'subscription-settings',
- title: this.$t('Settings.Subscription Settings.Subscription Settings')
+ title: this.$t('Settings.Subscription Settings.Subscription Settings'),
+ icon: 'play'
},
{
type: 'distraction-settings',
- title: this.$t('Settings.Distraction Free Settings.Distraction Free Settings')
+ title: this.$t('Settings.Distraction Free Settings.Distraction Free Settings'),
+ icon: 'eye-slash'
},
{
type: 'privacy-settings',
- title: this.$t('Settings.Privacy Settings.Privacy Settings')
+ title: this.$t('Settings.Privacy Settings.Privacy Settings'),
+ icon: 'lock'
},
{
type: 'data-settings',
- title: this.$t('Settings.Data Settings.Data Settings')
+ title: this.$t('Settings.Data Settings.Data Settings'),
+ icon: 'database'
},
...(process.env.IS_ELECTRON
? [
{
type: 'proxy-settings',
- title: this.$t('Settings.Proxy Settings.Proxy Settings')
+ title: this.$t('Settings.Proxy Settings.Proxy Settings'),
+ icon: 'network-wired',
},
{
type: 'download-settings',
- title: this.$t('Settings.Download Settings.Download Settings')
+ title: this.$t('Settings.Download Settings.Download Settings'),
+ icon: 'download',
}
]
: []),
{
type: 'parental-control-settings',
- title: this.$t('Settings.Parental Control Settings.Parental Control Settings')
+ title: this.$t('Settings.Parental Control Settings.Parental Control Settings'),
+ icon: 'user-lock'
},
{
type: 'sponsor-block-settings',
title: this.$t('Settings.SponsorBlock Settings.SponsorBlock Settings'),
+ // TODO: replace with SponsorBlock icon
+ icon: 'shield'
},
...(process.env.IS_ELECTRON
- ? [
- {
- type: 'experimental-settings',
- title: this.$t('Settings.Experimental Settings.Experimental Settings')
- },
- ]
+ ? [{
+ type: 'experimental-settings',
+ title: this.$t('Settings.Experimental Settings.Experimental Settings'),
+ icon: 'flask'
+ }]
: []),
{
type: 'password-settings',
- title: this.$t('Settings.Password Settings.Password Settings')
+ title: this.$t('Settings.Password Settings.Password Settings'),
+ icon: 'key'
},
]
- }
- },
- computed: {
- locale: function() {
- return this.$i18n.locale
- },
-
- settingsPassword: function () {
- return this.$store.getters.getSettingsPassword
- },
-
- allSettingsSectionsExpandedByDefault: function () {
- return this.$store.getters.getAllSettingsSectionsExpandedByDefault
- },
-
- settingsSectionSortEnabled: function () {
- return this.$store.getters.getSettingsSectionSortEnabled
+ return settingsComponentsData
},
settingsSectionComponents: function () {
+ let settingsSections = this.settingsComponentsData
if (this.settingsSectionSortEnabled) {
- return this.settingsComponentsData.toSorted((a, b) =>
- a.title.toLowerCase().localeCompare(b.title.toLowerCase(), this.locale)
- )
+ settingsSections = settingsSections.toSorted((a, b) => {
+ return a.title.toLowerCase().localeCompare(b.title.toLowerCase(), this.locale)
+ })
+ }
+
+ // ensure General Settings is placed first regardless of sorting
+ const generalSettingsEntry = {
+ type: 'general-settings',
+ title: this.$t('Settings.General Settings.General Settings'),
+ icon: 'border-all'
}
- return this.settingsComponentsData
+ return [generalSettingsEntry, ...settingsSections]
},
},
created: function () {
@@ -149,9 +169,84 @@ export default defineComponent({
this.unlocked = true
}
},
+ mounted: function () {
+ this.handleResize()
+ window.addEventListener('resize', this.handleResize)
+ document.addEventListener('scroll', this.markScrolledToSectionAsActive)
+
+ // mark first section as active before any scrolling has taken place
+ if (this.settingsSectionComponents.length > 0) {
+ const firstSection = document.getElementById(this.settingsSectionComponents[0].type)
+ firstSection.classList.add(ACTIVE_CLASS_NAME)
+ }
+ },
+ beforeDestroy: function () {
+ document.removeEventListener('scroll', this.markScrolledToSectionAsActive)
+ window.removeEventListener('resize', this.handleResize)
+ },
methods: {
+ navigateToSection: function(sectionType) {
+ if (this.isInDesktopView) {
+ nextTick(() => {
+ const sectionElement = this.$refs[sectionType][0].$el
+ sectionElement.scrollIntoView()
+
+ const sectionHeading = sectionElement.firstChild.firstChild
+ sectionHeading.tabIndex = 0
+ sectionHeading.focus()
+ sectionHeading.tabIndex = -1
+ })
+ } else {
+ this.settingsSectionTypeOpenInMobile = sectionType
+ }
+ },
+
+ returnToSettingsMenu: function () {
+ const openSection = this.settingsSectionTypeOpenInMobile
+ this.settingsSectionTypeOpenInMobile = null
+
+ // focus the corresponding Settings Menu title
+ nextTick(() => document.getElementById(openSection)?.focus())
+ },
+
+ /* Set the current section to be shown as active in the Settings Menu
+ * if it is the lowest section within the top quarter of the viewport (25vh) */
+ markScrolledToSectionAsActive: function() {
+ const scrollY = window.scrollY + innerHeight / 4
+ this.settingsSectionComponents.forEach((section) => {
+ const sectionElement = this.$refs[section.type][0].$el
+ const sectionHeight = sectionElement.offsetHeight
+ const sectionTop = sectionElement.offsetTop
+ const correspondingMenuLink = document.getElementById(section.type)
+
+ if (this.isInDesktopView && scrollY > sectionTop && scrollY <= sectionTop + sectionHeight) {
+ correspondingMenuLink.classList.add(ACTIVE_CLASS_NAME)
+ } else {
+ correspondingMenuLink.classList.remove(ACTIVE_CLASS_NAME)
+ }
+ })
+ },
+
+ handleResize: function () {
+ const wasNotInDesktopView = !this.isInDesktopView
+ this.isInDesktopView = window.innerWidth > SETTINGS_MOBILE_WIDTH_THRESHOLD
+
+ // navigate to section that was open in mobile or desktop view, if any
+ if (this.isInDesktopView && wasNotInDesktopView && this.settingsSectionTypeOpenInMobile != null) {
+ this.navigateToSection(this.settingsSectionTypeOpenInMobile)
+ this.settingsSectionTypeOpenInMobile = null
+ } else if (!this.isInDesktopView && !wasNotInDesktopView) {
+ const activeMenuLink = document.querySelector(`.settingsMenu .title.${ACTIVE_CLASS_NAME}`)
+ if (!activeMenuLink) {
+ return
+ }
+
+ const sectionType = activeMenuLink.id
+ this.navigateToSection(sectionType)
+ }
+ },
+
...mapActions([
- 'updateAllSettingsSectionsExpandedByDefault',
'updateSettingsSectionSortEnabled'
])
}
diff --git a/src/renderer/views/Settings/Settings.vue b/src/renderer/views/Settings/Settings.vue
index fd4931d03829..dee56f5ef516 100644
--- a/src/renderer/views/Settings/Settings.vue
+++ b/src/renderer/views/Settings/Settings.vue
@@ -1,34 +1,49 @@
-