~cadence/cloudtube

0d23d66700a146643b3dc85b714d76c119da4fd8 — Cadence Ember 4 months ago 4e1f2b3
Add theme support, light theme, and edgeless light
44 files changed, 424 insertions(+), 203 deletions(-)

M api/channels.js
M api/filters.js
M api/pages.js
M api/search.js
M api/video.js
R html/static/images/{arrow-down-wide.svg => arrow-down-wide-dark.svg}
A html/static/images/arrow-down-wide-light.svg
M html/static/images/settings.svg
M html/static/images/subscriptions.svg
M pug/includes/layout.pug
M pug/settings.pug
M pug/subscriptions.pug
M pug/video.pug
A sass/dark.sass
A sass/edgeless-light.sass
R sass/includes/{base.sass => _base.sass}
R sass/includes/{buttons.sass => _buttons.sass}
R sass/includes/{cant-think-page.sass => _cant-think-page.sass}
R sass/includes/{channel-page.sass => _channel-page.sass}
R sass/includes/{filters-page.sass => _filters-page.sass}
R sass/includes/{footer.sass => _footer.sass}
R sass/includes/{forms.sass => _forms.sass}
R sass/includes/{home-page.sass => _home-page.sass}
R sass/includes/{licenses-page.sass => _licenses-page.sass}
A sass/includes/_main.sass
R sass/includes/{nav.sass => _nav.sass}
R sass/includes/{privacy-page.sass => _privacy-page.sass}
R sass/includes/{search-page.sass => _search-page.sass}
R sass/includes/{settings-page.sass => _settings-page.sass}
R sass/includes/{subscriptions-page.sass => _subscriptions-page.sass}
R sass/includes/{takedown-page.sass => _takedown-page.sass}
R sass/includes/{video-list-item.sass => _video-list-item.sass}
R sass/includes/{video-page.sass => _video-page.sass}
D sass/includes/colors.sass
A sass/light.sass
D sass/main.sass
A sass/theme-modules/_edgeless.sass
A sass/themes/_dark.scss
A sass/themes/_edgeless-light.scss
A sass/themes/_light.scss
M server.js
M utils/constants.js
A utils/icon-loader.js
M utils/upgradedb.js
M api/channels.js => api/channels.js +1 -1
@@ 24,7 24,7 @@ module.exports = [
					video.watched = watchedVideos.includes(video.videoId)
				})
			}
			return render(200, "pug/channel.pug", {url, data, subscribed, instanceOrigin})
			return render(200, "pug/channel.pug", {settings, url, data, subscribed, instanceOrigin})
		}
	}
]

M api/filters.js => api/filters.js +9 -6
@@ 9,8 9,7 @@ const {Matcher, PatternCompileError} = require("../utils/matcher")
const filterMaxLength = 160
const regexpEnabledText = constants.server_setup.allow_regexp_filters ? "" : "not"

function getCategories(req) {
	const user = getUser(req)
function getCategories(user) {
	const filters = user.getFilters()

	// Sort filters into categories for display. Titles are already sorted.


@@ 39,7 38,9 @@ function getCategories(req) {
module.exports = [
	{
		route: "/filters", methods: ["GET"], code: async ({req, url}) => {
			const categories = getCategories(req)
			const user = getUser(req)
			const categories = getCategories(user)
			const settings = user.getSettingsOrDefaults()
			let referrer = url.searchParams.get("referrer") || null

			let type = null


@@ 54,7 55,7 @@ module.exports = [
				label = url.searchParams.get("label")
			}

			return render(200, "pug/filters.pug", {categories, type, contents, label, referrer, filterMaxLength, regexpEnabledText})
			return render(200, "pug/filters.pug", {settings, categories, type, contents, label, referrer, filterMaxLength, regexpEnabledText})
		}
	},
	{


@@ 100,8 101,10 @@ module.exports = [
					return true
				}, state => {
					const {type, contents, label, compileError} = state
					const categories = getCategories(req)
					return render(400, "pug/filters.pug", {categories, type, contents, label, compileError, filterMaxLength, regexpEnabledText})
					const user = getUser(req)
					const categories = getCategories(user)
					const settings = user.getSettingsOrDefaults()
					return render(400, "pug/filters.pug", {settings, categories, type, contents, label, compileError, filterMaxLength, regexpEnabledText})
				})
				.last(state => {
					const {type, contents, label} = state

M api/pages.js => api/pages.js +22 -3
@@ 1,16 1,35 @@
const {render} = require("pinski/plugins")
const {getUser} = require("../utils/getuser")

module.exports = [
	{
		route: "/", methods: ["GET"], code: async ({req}) => {
			const userAgent = req.headers["user-agent"] || ""
			const mobile = userAgent.toLowerCase().includes("mobile")
			return render(200, "pug/home.pug", {mobile})
			const user = getUser(req)
			const settings = user.getSettingsOrDefaults()
			return render(200, "pug/home.pug", {settings, mobile})
		}
	},
	{
		route: "/js-licenses", methods: ["GET"], code: async () => {
			return render(200, "pug/js-licenses.pug")
		route: "/(?:js-)?licenses", methods: ["GET"], code: async ({req}) => {
			const user = getUser(req)
			const settings = user.getSettingsOrDefaults()
			return render(200, "pug/licenses.pug", {settings})
		}
	},
	{
		route: "/cant-think", methods: ["GET"], code: async ({req}) => {
			const user = getUser(req)
			const settings = user.getSettingsOrDefaults()
			return render(200, "pug/cant-think.pug", {settings})
		}
	},
	{
		route: "/privacy", methods: ["GET"], code: async ({req}) => {
			const user = getUser(req)
			const settings = user.getSettingsOrDefaults()
			return render(200, "pug/privacy.pug", {settings})
		}
	}
]

M api/search.js => api/search.js +1 -1
@@ 26,7 26,7 @@ module.exports = [
			const filters = user.getFilters()
			results = converters.applyVideoFilters(results, filters).videos

			return render(200, "pug/search.pug", {url, query, results, instanceOrigin})
			return render(200, "pug/search.pug", {settings, url, query, results, instanceOrigin})
		}
	}
]

M api/video.js => api/video.js +4 -4
@@ 111,7 111,7 @@ module.exports = [
			// Check if playback is allowed
			const videoTakedownInfo = db.prepare("SELECT id, org, url FROM TakedownVideos WHERE id = ?").get(id)
			if (videoTakedownInfo) {
				return render(451, "pug/takedown-video.pug", videoTakedownInfo)
				return render(451, "pug/takedown-video.pug", Object.assign({settings}, videoTakedownInfo))
			}

			// Media fragment


@@ 129,7 129,7 @@ module.exports = [
			// Work out how to fetch the video
			if (req.method === "GET") {
				if (settings.local) { // skip to the local fetching page, which will then POST video data in a moment
					return render(200, "pug/local-video.pug", {id})
					return render(200, "pug/local-video.pug", {settings, id})
				}
				var instanceOrigin = settings.instance
				var outURL = `${instanceOrigin}/api/v1/videos/${id}`


@@ 153,7 153,7 @@ module.exports = [
					// automatically add the entry to the videos list, so it won't be fetched again
					const args = {id, ...channelTakedownInfo}
					db.prepare("INSERT INTO TakedownVideos (id, org, url) VALUES (@id, @org, @url)").run(args)
					return render(451, "pug/takedown-video.pug", channelTakedownInfo)
					return render(451, "pug/takedown-video.pug", Object.assign({settings}, channelTakedownInfo))
				}

				// process stream list ordering


@@ 225,7 225,7 @@ module.exports = [
				// Create appropriate formatted message
				const message = render(0, `pug/errors/${errorType}.pug`, locals).content

				return render(500, "pug/video.pug", {video: {videoId: id}, error: true, message})
				return render(500, "pug/video.pug", {video: {videoId: id}, error: true, message, settings})
			}
		}
	}

R html/static/images/arrow-down-wide.svg => html/static/images/arrow-down-wide-dark.svg +0 -0
A html/static/images/arrow-down-wide-light.svg => html/static/images/arrow-down-wide-light.svg +1 -0
@@ 0,0 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="8" viewBox="0 0 5.821 2.117"><path d="M1.269.53l.767.793h.161L2.964.53h.211v.265L2.117 1.852 1.058.794V.529z" fill="#202020" paint-order="markers stroke fill"/></svg>

M html/static/images/settings.svg => html/static/images/settings.svg +1 -1
@@ 1,1 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 6.615 6.615"><path d="M2.64 0s-.03.94-.424 1.215C1.823 1.49.885.894.885.894L.112 2.232.109 2.23l.003.002c.01.006.797.499.838.974.042.478-.944.992-.944.992l.77 1.34s.83-.442 1.265-.24c.435.204.387 1.314.387 1.314l1.546.003s.032-.94.425-1.215c.393-.275 1.331.321 1.331.321l.775-1.338s-.798-.496-.84-.974c-.041-.478.944-.993.944-.993l-.77-1.34s-.83.443-1.265.24C4.14 1.113 4.187.002 4.187.002zm.688 2.25a1.106 1.106 0 110 2.211 1.106 1.106 0 010-2.21z" fill="#c4c4c4" paint-order="fill markers stroke"/></svg>
\ No newline at end of file
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 6.615 6.615"><title>Settings</title><path d="M2.64 0s-.03.94-.424 1.215C1.823 1.49.885.894.885.894L.112 2.232.109 2.23l.003.002c.01.006.797.499.838.974.042.478-.944.992-.944.992l.77 1.34s.83-.442 1.265-.24c.435.204.387 1.314.387 1.314l1.546.003s.032-.94.425-1.215c.393-.275 1.331.321 1.331.321l.775-1.338s-.798-.496-.84-.974c-.041-.478.944-.993.944-.993l-.77-1.34s-.83.443-1.265.24C4.14 1.113 4.187.002 4.187.002zm.688 2.25a1.106 1.106 0 110 2.211 1.106 1.106 0 010-2.21z" paint-order="fill markers stroke"/></svg>

M html/static/images/subscriptions.svg => html/static/images/subscriptions.svg +1 -1
@@ 1,1 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="25" viewBox="0 0 7.937 6.615"><path d="M2.373 0h3.191a.52.52 0 01.521.521H1.852A.52.52 0 012.373 0zm-.91.794h5.011c.371 0 .67.298.67.67v.123l-6.35-.016v-.108c0-.37.298-.67.67-.67zm-.405 1.058C.472 1.852 0 2.184 0 2.77v2.868c0 .586.472.977 1.058.977H6.88c.586 0 1.059-.39 1.059-.977V2.77c0-.586-.473-.918-1.059-.918zM5.3 4.017c.167.099.19.276.012.366l-2.098.985c-.131.077-.304-.002-.302-.19V3.29c0-.203.18-.333.34-.245z" fill="#c4c4c4" paint-order="fill markers stroke"/></svg>
\ No newline at end of file
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="30" height="25" viewBox="0 0 7.937 6.615"><title>Subscriptions</title><path d="M2.373 0h3.191a.52.52 0 01.521.521H1.852A.52.52 0 012.373 0zm-.91.794h5.011c.371 0 .67.298.67.67v.123l-6.35-.016v-.108c0-.37.298-.67.67-.67zm-.405 1.058C.472 1.852 0 2.184 0 2.77v2.868c0 .586.472.977 1.058.977H6.88c.586 0 1.059-.39 1.059-.977V2.77c0-.586-.473-.918-1.059-.918zM5.3 4.017c.167.099.19.276.012.366l-2.098.985c-.131.077-.304-.002-.302-.19V3.29c0-.203.18-.333.34-.245z" paint-order="fill markers stroke"/></svg>

M pug/includes/layout.pug => pug/includes/layout.pug +4 -7
@@ 3,7 3,8 @@ html
  head
    meta(charset="utf-8")
    meta(name="viewport" content="width=device-width, initial-scale=1")
    link(rel="stylesheet" type="text/css" href=getStaticURL("sass", "/main.sass"))
    - const theme = settings && ["dark", "light", "edgeless-light"][settings.theme] || "dark"
    link(rel="stylesheet" type="text/css" href=getStaticURL("sass", `/${theme}.sass`))
    script(type="module" src=getStaticURL("html", "/static/js/focus.js"))
    block head



@@ 15,13 16,9 @@ html
        .links
          a(href="/").link.home CloudTube
          a(href="/subscriptions" title="Subscriptions").link.icon-link
            svg(width=30 height=25)
              image(href=getStaticURL("html", "/static/images/subscriptions.svg") alt="Subscriptions.").icon
              title Subscriptions
            != icons.get("subscriptions")
          a(href="/settings" title="Settings").link.icon-link
            svg(width=25 height=25)
              image(href=getStaticURL("html", "/static/images/settings.svg") alt="Settings.").icon
              title Settings
            != icons.get("settings")
        form(method="get" action="/search").search-form
          input(type="text" placeholder="Search" aria-label="Search a video" name="q" autocomplete="off" value=query).search


M pug/settings.pug => pug/settings.pug +10 -0
@@ 36,6 36,16 @@ block content
    form(method="post" action="/settings")
      +fieldset("Settings")

        +select({
          id: "theme",
          label: "Theme",
          options: [
            {value: "0", text: "Standard dark"},
            {value: "1", text: "Standard light"},
            {value: "2", text: "Edgeless light"}
          ]
        })

        +input({
          id: "instance",
          label: "Instance",

M pug/subscriptions.pug => pug/subscriptions.pug +2 -2
@@ 31,8 31,8 @@ block content

      if settings.save_history
        input(type="checkbox" id="watched-videos-display")
        .watched-videos-display-container
          label(for="watched-videos-display").watched-videos-display-label Hide watched videos
        .checkbox-hider__container
          label(for="watched-videos-display").checkbox-hider__label Hide watched videos

      each video in videos
        +video_list_item("subscriptions-video", video, instanceOrigin, {showMarkWatched: settings.save_history && !video.watched})

M pug/video.pug => pug/video.pug +1 -0
@@ 21,6 21,7 @@ block content
        noscript
          meta(http-equiv="refresh" content=`${video.lengthSeconds+5};url=/watch?v=${first.videoId}&continuous=1&session-watched=${sessionWatchedNext}`)
    .video-page(class={
      "video-page--recommended-side": settings.recommended_mode === 0,
      "video-page--recommended-below": settings.recommended_mode === 1,
      "video-page--recommended-hidden": settings.recommended_mode === 2
    })

A sass/dark.sass => sass/dark.sass +9 -0
@@ 0,0 1,9 @@
@use "themes/dark" as *
@use "includes/main" with ($_theme: $theme)

@use "theme-modules/edgeless" with ($_theme: $theme)

// navigation shadow
.main-nav
  position: relative // needed for box shadow to overlap related videos section
  box-shadow: 0px 0px 20px 5px rgba(0, 0, 0, 0.1)

A sass/edgeless-light.sass => sass/edgeless-light.sass +4 -0
@@ 0,0 1,4 @@
@use "themes/edgeless-light" as *
@use "includes/main" with ($_theme: $theme)

@use "theme-modules/edgeless" with ($_theme: $theme)

R sass/includes/base.sass => sass/includes/_base.sass +19 -14
@@ 1,8 1,10 @@
@use "colors.sass" as c
$_theme: () !default

@use "sass:map"

body
  background-color: c.$bg-dark
  color: c.$fg-main
  background-color: map.get($_theme, "bg-2")
  color: map.get($_theme, "fg-main")
  font-family: "Bariol", sans-serif
  font-size: 18px
  margin: 0


@@ 13,13 15,13 @@ body
  flex-direction: column

a
  color: c.$link
  color: map.get($_theme, "link")

pre, code
  font-size: 0.88em

code
  background: c.$bg-darker
  background: map.get($_theme, "bg-1")
  padding: 3px 5px
  border-radius: 4px



@@ 32,7 34,7 @@ button
  cursor: pointer

::placeholder
  color: #c4c4c4
  color: map.get($_theme, "placeholder")
  opacity: 1

// focus section


@@ 48,19 50,20 @@ button

select:-moz-focusring
  color: transparent
  text-shadow: 0 0 0 c.$fg-bright
  text-shadow: 0 0 0 map.get($_theme, "fg-bright")

body.show-focus
  a, select, button, input, video, summary
    &:focus
      outline: 2px dotted #ddd
      outline: 2px dotted map.get($_theme, "fg-main")

video
  background-color: black

details
  background-color: c.$bg-accent-x
  background-color: map.get($_theme, "bg-3")
  padding: 12px
  border: 1px solid map.get($_theme, "edge-grey")
  border-radius: 8px

  summary


@@ 68,20 71,22 @@ details
    line-height: 1
    margin-bottom: 0
    user-select: none
    color: c.$fg-main
    color: map.get($_theme, "fg-main")

  &[open] summary
    margin-bottom: 16px
    padding-bottom: 12px
    border-bottom: 1px solid map.get($_theme, "edge-grey")
    margin-bottom: 8px

table
  background-color: c.$bg-darker
  background-color: map.get($_theme, "bg-1")

table, td, th
  border: 1px solid c.$edge-grey
  border: 1px solid map.get($_theme, "edge-grey")
  border-collapse: collapse

td, th
  padding: 4px 8px

thead, tr:nth-child(even)
 background-color: c.$bg-darkest
 background-color: map.get($_theme, "bg-0")

R sass/includes/buttons.sass => sass/includes/_buttons.sass +14 -12
@@ 1,10 1,12 @@
$_theme: () !default

@use "sass:selector"
@use "colors.sass" as c
@use "sass:map"

@mixin button-base
  -webkit-appearance: none
  -moz-appearance: none
  color: c.$fg-bright
  color: map.get($_theme, "fg-bright")
  border: none
  border-radius: 4px
  padding: 8px


@@ 14,7 16,7 @@

  @at-root #{selector.unify(&, "select")}
    padding: 8px 27px 8px 8px
    background: url(/static/images/arrow-down-wide.svg) right 53% no-repeat c.$bg-accent-x
    background: map.get($_theme, "image-dropdown") right 53% no-repeat map.get($_theme, "bg-4")

  @at-root #{selector.unify(&, "a")}
    padding: 7px 8px


@@ 31,12 33,12 @@
@mixin button-bg
  @include button-base

  background-color: c.$bg-accent-x
  background-color: map.get($_theme, "bg-4")

@mixin border-button
  @include button-bg

  border: 1px solid c.$edge-grey
  border: 1px solid map.get($_theme, "edge-grey")

@mixin button-size
  margin: 4px


@@ 44,10 46,10 @@

@mixin button-hover
  &:hover
    background-color: c.$bg-accent
    background-color: map.get($_theme, "bg-3")

  &:active
    background-color: c.$bg-dark
    background-color: map.get($_theme, "bg-2")

.base-border-look
  @include border-button


@@ 62,13 64,13 @@
  @include button-size
  -webkit-appearance: none
  -moz-appearance: none
  color: c.$fg-bright
  color: map.get($_theme, "fg-bright")
  text-decoration: none
  line-height: 1.25
  margin: 0
  padding: 8px 20px
  background: c.$bg-accent
  border: solid c.$bg-darker
  background: map.get($_theme, "bg-3")
  border: solid map.get($_theme, "edge-grey")
  border-width: 1px 0px 0px
  text-align: left



@@ 76,7 78,7 @@
    border-width: 1px 0px 1px

  &:hover
    background: c.$bg-accent-x
    background: map.get($_theme, "bg-4")

  &:active
    background: c.$bg-darker
    background: map.get($_theme, "bg-1")

R sass/includes/cant-think-page.sass => sass/includes/_cant-think-page.sass +6 -4
@@ 1,5 1,7 @@
$_theme: () !default

@use "sass:list"
@use "colors.sass" as c
@use "sass:map"

.cant-think-page
  .main-nav


@@ 13,7 15,7 @@
    box-sizing: border-box

  .page-narration
    background-color: c.$bg-accent
    background-color: map.get($_theme, "bg-3")
    border: 1px solid #aaa
    color: #fff
    border-radius: 0


@@ 28,7 30,7 @@

  .leave
    margin: 26px 32px !important
    color: #aaa
    color: map.get($_theme, "fg-dim")

    $sizes: 14px 16px 21px 30px 72px
    @each $size in $sizes


@@ 37,7 39,7 @@

    &.leave__final
      font-weight: bold
      color: #f2f2f2
      color: map.get($_theme, "fg-bright")
      text-align: center

      .leave__actions

R sass/includes/channel-page.sass => sass/includes/_channel-page.sass +10 -7
@@ 1,6 1,8 @@
@use "colors.sass" as c
@use "video-list-item.sass" as *
@use "_dimensions.sass" as dimensions
$_theme: () !default

@use "sass:map"
@use "_dimensions" as dimensions
@use "video-list-item" as *

.channel-page
  padding: 40px 20px 20px


@@ 17,7 19,7 @@
  align-self: flex-start

.channel-data
  background-color: c.$bg-darker
  background-color: map.get($_theme, "bg-1")
  padding: 24px
  margin: 12px 0px 24px
  border-radius: 8px


@@ 44,11 46,11 @@
      .name
        font-size: 30px
        font-weight: normal
        color: c.$fg-bright
        color: map.get($_theme, "fg-bright")
        margin: 0

      .subscribers
        color: c.$fg-main
        color: map.get($_theme, "fg-main")
        font-size: 18px

    .subscribe-form


@@ 61,7 63,8 @@
        line-height: 1
        border-radius: 8px
        font-size: 22px
        background-color: c.$power-deep
        background-color: map.get($_theme, "power-deep")
        color: map.get($_theme, "power-fg")
        border: none

  .description

R sass/includes/filters-page.sass => sass/includes/_filters-page.sass +11 -9
@@ 1,10 1,12 @@
@use "colors.sass" as c
$_theme: () !default

@use "sass:map"

@mixin filter-notice
  margin-top: 24px
  padding: 12px
  border-radius: 8px
  background-color: c.$bg-darker
  background-color: map.get($_theme, "bg-1")
  white-space: pre-line

.filters-page


@@ 20,23 22,23 @@

  .filter-confirmation-notice
    @include filter-notice
    color: c.$fg-warning
    color: map.get($_theme, "fg-warning")

  .filter-compile-error
    @include filter-notice

    &__header
      color: c.$fg-warning
      color: map.get($_theme, "fg-warning")

    &__trace
      background-color: c.$bg-darkest
      background-color: map.get($_theme, "bg-0")
      padding: 6px

  .save-filter
    margin-top: 12px

    .border-look
      background-color: c.$bg-darker
      background-color: map.get($_theme, "bg-1")
      font-size: 22px
      padding: 7px 16px 8px
      font-size: 24px


@@ 48,17 50,17 @@
  .filter
    display: flex
    padding: 5px 0
    border-top: 1px solid c.$edge-grey
    border-top: 1px solid map.get($_theme, "edge-grey")

    &:last-child
      border-bottom: 1px solid c.$edge-grey
      border-bottom: 1px solid map.get($_theme, "edge-grey")

    &__details
      flex: 1

    &__type
      font-size: 15px
      color: c.$fg-dim
      color: map.get($_theme, "fg-dim")

    &__remove
      flex-shrink: 0

R sass/includes/footer.sass => sass/includes/_footer.sass +5 -2
@@ 1,4 1,6 @@
@use "./colors.sass" as c
$_theme: () !default

@use "sass:map"

.footer__container
  flex: 1


@@ 10,9 12,10 @@
  display: flex
  flex-direction: column
  align-items: center
  background-color: c.$bg-darkest
  background-color: map.get($_theme, "bg-dim")
  margin: 40px 0 0
  padding: 10px 10px 30px
  border-top: 1px solid map.get($_theme, "edge-grey")

.footer__cols
  display: flex

R sass/includes/forms.sass => sass/includes/_forms.sass +22 -19
@@ 1,7 1,9 @@
@use "colors.sass" as c
$_theme: () !default

@use "sass:map"

@mixin disabled
  background-color: c.$bg-dark
  background-color: map.get($_theme, "bg-2")
  color: #808080

fieldset


@@ 20,7 22,7 @@ fieldset
    font-size: 28px
    font-weight: bold
    padding: 0
    border-bottom: 1px solid #333
    border-bottom: 1px solid map.get($_theme, "edge-grey") // TODO: originally contrasted more
    line-height: 1.56

    @media screen and (max-width: 400px)


@@ 36,7 38,7 @@ fieldset
  position: relative
  padding-bottom: 5px
  margin-bottom: 5px
  border-bottom: 1px solid #999
  border-bottom: 1px solid map.get($_theme, "edge-grey")

  @media screen and (max-width: 400px)
    flex-direction: column


@@ 52,7 54,7 @@ fieldset
  &__label
    grid-area: label
    padding: 8px 8px 8px 0px
    color: #fff
    color: map.get($_theme, "fg-main")

  &__input
    grid-area: input


@@ 63,7 65,7 @@ fieldset
    white-space: pre-line
    margin: 12px 0px 18px
    font-size: 16px
    color: #ccc
    color: map.get($_theme, "fg-dim")
    line-height: 1.2

//


@@ 79,7 81,7 @@ fieldset
      width: 16px
      height: 16px
      padding: 0px
      border: 1px solid #666
      border: 1px solid map.get($_theme, "edge-grey")
      border-radius: 3px
      margin-left: 8px
      position: relative


@@ 110,18 112,19 @@ fieldset
    height: 42px
    margin: 0

  .#{$base}-container
    position: relative
    display: grid // why does the default not work???
    top: -42px
    background: c.$bg-accent-x
    line-height: 1
    border-radius: 8px
    margin-bottom: -18px

    .#{$base}-label
      padding: 12px 0px 12px 32px
      cursor: pointer
.checkbox-hider__container
  position: relative
  display: grid // why does the default not work???
  top: -42px
  background: map.get($_theme, "bg-3")
  line-height: 1
  border: 1px solid map.get($_theme, "edge-grey")
  border-radius: 8px
  margin-bottom: -18px

  .checkbox-hider__label
    padding: 12px 0px 12px 32px
    cursor: pointer

@mixin single-button-form
  display: inline-block

R sass/includes/home-page.sass => sass/includes/_home-page.sass +5 -3
@@ 1,4 1,6 @@
@use "colors.sass" as c
$_theme: () !default

@use "sass:map"

.home-page
  padding: 40px


@@ 18,8 20,8 @@
  padding: 16px
  border-radius: 4px
  font-size: 20px
  background-color: c.$bg-darker
  color: c.$fg-main
  background-color: map.get($_theme, "bg-1")
  color: map.get($_theme, "fg-main")

  p
    margin: 0 32px

R sass/includes/licenses-page.sass => sass/includes/_licenses-page.sass +2 -0
@@ 1,3 1,5 @@
$_theme: () !default

.js-licenses-page
  max-width: 800px
  margin: 0 auto

A sass/includes/_main.sass => sass/includes/_main.sass +32 -0
@@ 0,0 1,32 @@
$_theme: () !default

@use "sass:selector"

// preload second-level includes with the theme (there will be conflicts due to reconfiguration they are loaded individually)
// this isn't _exactly_ what @forward is supposed to be used for, but it's the best option here
@forward "video-list-item" show _ with ($_theme: $_theme)
@forward "forms" show _ with ($_theme: $_theme)
@forward "buttons" show _ with ($_theme: $_theme)

@use "base" with ($_theme: $_theme)
@use "video-page" with ($_theme: $_theme)
@use "search-page" with ($_theme: $_theme)
@use "home-page" with ($_theme: $_theme)
@use "channel-page" with ($_theme: $_theme)
@use "subscriptions-page" with ($_theme: $_theme)
@use "settings-page" with ($_theme: $_theme)
@use "cant-think-page" with ($_theme: $_theme)
@use "privacy-page" with ($_theme: $_theme)
@use "licenses-page" with ($_theme: $_theme)
@use "filters-page" with ($_theme: $_theme)
@use "takedown-page" with ($_theme: $_theme)
@use "nav" with ($_theme: $_theme)
@use "footer" with ($_theme: $_theme)

@font-face
  font-family: "Bariol"
  src: url(/static/fonts/bariol.woff?statichash=1)

.button-container
  display: flex
  flex-wrap: wrap

R sass/includes/nav.sass => sass/includes/_nav.sass +17 -10
@@ 1,12 1,14 @@
@use "colors.sass" as c
@use "buttons.sass" as *
@use "_dimensions.sass" as dimensions
$_theme: () !default

@use "sass:map"
@use "buttons" as *
@use "_dimensions" as dimensions

.main-nav
  background-color: c.$bg-accent
  background-color: map.get($_theme, "bg-nav")
  display: flex
  padding: 8px
  box-shadow: 0px 0px 20px 5px rgba(0, 0, 0, 0.1)
  border-bottom: 1px solid map.get($_theme, "edge-grey")

  +dimensions.thin
    display: block


@@ 30,10 32,16 @@
      font-weight: bold

    &, &:visited
      color: #fff
      color: map.get($_theme, "fg-bright")

    &:focus, &:hover
      background-color: c.$bg-accent-x
      background-color: map.get($_theme, "bg-4")

    &.icon-link
      color: map.get($_theme, "fg-dim")

      &:hover, &:focus
        color: map.get($_theme, "fg-bright")

  .search-form
    display: flex


@@ 44,8 52,7 @@
      @include button-bg
      padding: 10px
      flex: 1
      margin: 1px
      border: 1px solid map.get($_theme, "bg-nav")

      &:hover, &:focus
        border: 1px solid c.$edge-grey
        margin: 0px
        border-color: map.get($_theme, "edge-grey")

R sass/includes/privacy-page.sass => sass/includes/_privacy-page.sass +2 -0
@@ 1,3 1,5 @@
$_theme: () !default

.privacy-page
  max-width: 600px
  margin: 0 auto

R sass/includes/search-page.sass => sass/includes/_search-page.sass +4 -2
@@ 1,5 1,7 @@
@use "video-list-item.sass" as *
@use "colors.sass" as c
$_theme: () !default

@use "sass:map"
@use "video-list-item" as *

.search-page
  padding: 40px 20px 20px

R sass/includes/settings-page.sass => sass/includes/_settings-page.sass +7 -4
@@ 1,5 1,7 @@
@use "forms.sass" as forms
@use "colors.sass" as c
$_theme: () !default

@use "sass:map"
@use "forms" as forms

.settings-page
  padding: 40px 20px 20px


@@ 19,8 21,9 @@
.more-settings
  margin-top: 24px
  padding: 12px
  border: 1px solid map.get($_theme, "edge-grey")
  border-radius: 8px
  background-color: c.$bg-accent-x
  background-color: map.get($_theme, "bg-3")

  &__list
    margin: 0


@@ 34,7 37,7 @@
  margin-top: 24px

  .delete-confirm-container
    background: c.$bg-darker
    background: map.get($_theme, "bg-1")
    margin-bottom: -36px

@include forms.checkbox-hider("delete-confirm")

R sass/includes/subscriptions-page.sass => sass/includes/_subscriptions-page.sass +6 -4
@@ 1,6 1,8 @@
@use "colors.sass" as c
@use "video-list-item.sass" as *
@use "forms.sass" as forms
$_theme: () !default

@use "sass:map"
@use "forms" as forms
@use "video-list-item" as *

.subscriptions-page
  padding: 40px 20px 20px


@@ 33,7 35,7 @@

  .name
    font-size: 22px
    color: c.$fg-main
    color: map.get($_theme, "fg-main")

@include forms.checkbox-hider("watched-videos-display")


R sass/includes/takedown-page.sass => sass/includes/_takedown-page.sass +6 -4
@@ 1,4 1,6 @@
@use "colors.sass" as c
$_theme: () !default

@use "sass:map"

.takedown-page
  max-width: 700px


@@ 9,6 11,6 @@

  .important-section
    padding: 4px 20px
    border: 1px solid c.$edge-grey
    color: c.$fg-bright
    background-color: c.$bg-darker
    border: 1px solid map.get($_theme, "edge-grey")
    color: map.get($_theme, "fg-bright")
    background-color: map.get($_theme, "bg-1")

R sass/includes/video-list-item.sass => sass/includes/_video-list-item.sass +18 -15
@@ 1,5 1,7 @@
@use "colors.sass" as c
@use "_dimensions.sass" as dimensions
$_theme: () !default

@use "sass:map"
@use "_dimensions" as dimensions

// navigator hacks
.thumbnail > .thumbnail__options-container


@@ 30,6 32,7 @@
  &__show-more
    display: block
    height: $more-size
    color: #fff
    line-height: 16px
    font-size: 25px
    text-align: center


@@ 52,7 55,7 @@
  &__options-list
    pointer-events: auto
    display: grid
    background-color: c.$bg-accent
    background-color: map.get($_theme, "bg-3")
    padding: 8px 0px
    border-radius: 8px
    box-shadow: 0 2px 6px 2px #000


@@ 67,7 70,7 @@
      right: 0
      transform: translate(-6px, -1px) rotate(-45deg)
      clip-path: polygon(-5% -20%, 120% -20%, 120% 125%)
      background-color: c.$bg-accent
      background-color: map.get($_theme, "bg-3")
      box-shadow: 0px 0px 4px 0px #000
      pointer-events: none



@@ 80,7 83,7 @@
  margin-bottom: 12px

  @at-root .video-list-item--watched#{&}
    background: c.$bg-darker
    background: map.get($_theme, "bg-dim")
    padding: 4px 4px 0px
    margin: -4px -4px 8px



@@ 93,7 96,7 @@
  .thumbnail
    position: relative
    display: flex
    background: c.$bg-darkest
    background: map.get($_theme, "bg-0")

    &__link
      font-size: 0 // remove whitespace around the image


@@ 106,7 109,7 @@
    position: absolute
    bottom: 3px
    right: 3px
    color: c.$fg-bright
    color: #fff
    font-size: 14px
    background: rgba(20, 20, 20, 0.85)
    line-height: 1


@@ 119,20 122,20 @@
    line-height: 1.2

  .title-link
    color: c.$fg-main
    color: map.get($_theme, "fg-main")
    text-decoration: none

  .author-line
    margin-top: 4px
    font-size: 15px
    color: c.$fg-dim
    color: map.get($_theme, "fg-dim")

  .author
    color: c.$fg-dim
    color: map.get($_theme, "fg-dim")
    text-decoration: none

    &:hover, &:active
      color: c.$fg-bright
      color: map.get($_theme, "fg-bright")
      text-decoration: underline

@mixin recommendation-item


@@ 176,15 179,15 @@

    .author-line
      font-size: 15px
      color: c.$fg-main
      color: map.get($_theme, "fg-main")

    .author
      color: c.$fg-main
      color: map.get($_theme, "fg-main")

    .description
      margin-top: 16px
      font-size: 15px
      color: c.$fg-dim
      color: map.get($_theme, "fg-dim")

  +dimensions.thin
    .description


@@ 195,7 198,7 @@

  .description b
    font-weight: normal
    color: c.$fg-main
    color: map.get($_theme, "fg-main")

@mixin channel-video
  @include large-item

R sass/includes/video-page.sass => sass/includes/_video-page.sass +30 -15
@@ 1,11 1,11 @@
@use "colors.sass" as c
@use "video-list-item.sass" as *
$_theme: () !default

@use "sass:map"
@use "video-list-item" as *

.video-page
  display: grid
  grid-auto-flow: row
  padding: 20px
  grid-gap: 16px

  @media screen and (min-width: 1000px)
    grid-template-columns: 1fr 400px


@@ 13,10 13,25 @@
  &--recommended-below, &--recommended-hidden
    grid-template-columns: none

  &--recommended-hidden .related-videos
    display: none
  &--recommended-side
    .related-videos
      border-left: 1px solid map.get($_theme, "edge-grey")
      padding-left: 12px
      padding-right: 20px
      background-color: map.get($_theme, "bg-4")
      padding-top: 12px

  &--recommended-below
    .related-videos
      padding: 20px

  &--recommended-hidden
    .related-videos
      display: none

.main-video-section
  padding: 20px

  .video-container
    text-align: center



@@ 27,7 42,7 @@
      max-height: 80vh

  .stream-notice
    background: c.$bg-darkest
    background: map.get($_theme, "bg-0")
    padding: 4px

  .info


@@ 45,15 60,15 @@
        margin: 0px 0px 4px
        font-size: 30px
        font-weight: normal
        color: c.$fg-bright
        color: map.get($_theme, "fg-bright")
        word-break: break-word

      .author-link
        color: c.$fg-main
        color: map.get($_theme, "fg-main")
        text-decoration: none

        &:hover, &:active
          color: c.$fg-bright
          color: map.get($_theme, "fg-bright")
          text-decoration: underline

    .info-secondary


@@ 75,7 90,7 @@
    margin: 16px 4px
    padding: 12px
    border-radius: 4px
    background-color: c.$bg-darkest
    background-color: map.get($_theme, "bg-0")

    &__description
      margin-left: 12px


@@ 85,7 100,7 @@

    &__script-warning
      font-size: 15px
      color: c.$fg-warning
      color: map.get($_theme, "fg-warning")

    &__buttons
      display: flex


@@ 97,12 112,12 @@
    line-height: 1.4
    word-break: break-word
    margin: 16px 4px 4px 4px
    background-color: c.$bg-accent-area
    background-color: map.get($_theme, "bg-5")
    padding: 12px
    border-radius: 4px

    --regular-background: #{c.$bg-accent-area}
    --highlight-background: #{c.$bg-darker}
    --regular-background: #{map.get($_theme, "bg-5")}
    --highlight-background: #{map.get($_theme, "bg-1")}

.subscribe-form
  display: inline-block

D sass/includes/colors.sass => sass/includes/colors.sass +0 -17
@@ 1,17 0,0 @@
$bg-darkest: #202123
$bg-darker: #303336
$bg-dark: #36393f
$bg-accent: #4f5359
$bg-accent-x: #3f4247
$bg-accent-area: #44474b

$fg-bright: #fff
$fg-main: #ddd
$fg-dim: #bbb
$fg-warning: #fdca6d

$edge-grey: #a0a0a0

$link: #8ac2f9

$power-deep: #c62727

A sass/light.sass => sass/light.sass +2 -0
@@ 0,0 1,2 @@
@use "themes/light" as *
@use "includes/main" with ($_theme: $theme)

D sass/main.sass => sass/main.sass +0 -30
@@ 1,30 0,0 @@
@use "includes/colors.sass" as c

@use "includes/base.sass"
@use "sass:selector"
@use "includes/video-page.sass"
@use "includes/search-page.sass"
@use "includes/home-page.sass"
@use "includes/channel-page.sass"
@use "includes/subscriptions-page.sass"
@use "includes/settings-page.sass"
@use "includes/cant-think-page.sass"
@use "includes/privacy-page.sass"
@use "includes/licenses-page.sass"
@use "includes/filters-page.sass"
@use "includes/takedown-page.sass"
@use "includes/forms.sass"
@use "includes/nav.sass"
@use "includes/footer.sass"

@font-face
  font-family: "Bariol"
  src: url(/static/fonts/bariol.woff?statichash=1)

.icon-link:hover, .icon-link:focus
  .icon
    filter: brightness(2)

.button-container
  display: flex
  flex-wrap: wrap

A sass/theme-modules/_edgeless.sass => sass/theme-modules/_edgeless.sass +27 -0
@@ 0,0 1,27 @@
$_theme: () !default

@use "sass:map"

// remove separating edges
.main-nav, .footer__center, .video-page--recommended-side .related-videos
  border: none

// no background change to recommended videos sidebar
.video-page--recommended-side .related-videos
  background-color: map.get($_theme, "bg-2")

// navigation shadow
.main-nav
  position: relative // needed for box shadow to overlap related videos section
  box-shadow: 0px 0px 20px 5px rgba(0, 0, 0, 0.1)

// thumbnail dropdown menu dividers
.menu-look
  border-color: map.get($_theme, "bg-0")

// details areas
details, .checkbox-hider__container, .more-settings
  border: none
details[open] summary
  border: none
  margin-bottom: 4px

A sass/themes/_dark.scss => sass/themes/_dark.scss +38 -0
@@ 0,0 1,38 @@
// Defined in scss file instead of sass because indented syntax does not have multiline maps
// https://github.com/sass/sass/issues/216

@use "sass:map";

// This section is for colour shades
$theme: (
  // darker
  "bg-0": #252628,
  "bg-1": #303336,
  // regular
  "bg-2": #36393f,
  // lighter
  "bg-3": #3f4247, // slightly
  "bg-4": #44474b, // noticably
  "bg-5": #4f5359, // brightly

  "fg-bright": #fff,
  "fg-main": #ddd,
  "fg-dim": #bbb,
  "fg-warning": #fdca6d,

  "edge-grey": #a0a0a0,
  "placeholder": #c4c4c4,

  "link": #8ac2f9,

  "power-deep": #c62727,
  "power-fg": "#fff",

  "image-dropdown": url(/static/images/arrow-down-wide-dark.svg)
);

// This section is for colour meanings
$theme: map.merge($theme, (
  "bg-dim": map.get($theme, "bg-0"),
  "bg-nav": map.get($theme, "bg-5"),
));

A sass/themes/_edgeless-light.scss => sass/themes/_edgeless-light.scss +8 -0
@@ 0,0 1,8 @@
// extend regular light theme to change a couple of shades
@use "light";
@use "sass:map";

// this section is for colour meanings
$theme: map.merge(light.$theme, (
  "edge-grey": #c0c0c0,
));

A sass/themes/_light.scss => sass/themes/_light.scss +38 -0
@@ 0,0 1,38 @@
// Defined in scss file instead of sass because indented syntax does not have multiline maps
// https://github.com/sass/sass/issues/216

@use "sass:map";

// this section is for colour shades
$theme: (
  // lighter
  "bg-0": #fff,
  "bg-1": #fff,
  // regular
  "bg-2": #f2f2f2,
  // darker
  "bg-3": #e8e8e8, // slightly
  "bg-4": #dadada, // noticably
  "bg-5": #d0d0d0, // brightly

  "fg-bright": #000,
  "fg-main": #202020,
  "fg-dim": #454545,
  "fg-warning": #ce8600,

  "edge-grey": #909090,
  "placeholder": #636363,

  "link": #0b51d4,

  "power-deep": #c62727,
  "power-fg": #fff,

  "image-dropdown": url(/static/images/arrow-down-wide-light.svg)
);

// this section is for colour meanings
$theme: map.merge($theme, (
  "bg-dim": map.get($theme, "bg-4"),
  "bg-nav": map.get($theme, "bg-0")
));

M server.js => server.js +7 -5
@@ 1,9 1,11 @@
const {Pinski} = require("pinski")
const {setInstance} = require("pinski/plugins")
const constants = require("./utils/constants")
const iconLoader = require("./utils/icon-loader").icons

;(async () => {
	await require("./utils/upgradedb")()
	const icons = await iconLoader

	const server = new Pinski({
		port: 10412,


@@ 13,19 15,19 @@ const constants = require("./utils/constants")

	setInstance(server)
	server.pugDefaultLocals.constants = constants
	server.pugDefaultLocals.icons = icons

	server.muteLogsStartingWith("/vi/")
	server.muteLogsStartingWith("/favicon")
	server.muteLogsStartingWith("/static")

	server.addSassDir("sass", ["sass/includes"])
	server.addRoute("/static/css/main.css", "sass/main.sass", "sass")
	server.addSassDir("sass", ["sass/includes", "sass/themes", "sass/theme-modules"])
	server.addRoute("/static/css/dark.css", "sass/dark.sass", "sass")
	server.addRoute("/static/css/light.css", "sass/light.sass", "sass")
	server.addRoute("/static/css/edgeless-light.css", "sass/edgeless-light.sass", "sass")

	server.addPugDir("pug", ["pug/includes"])
	server.addPugDir("pug/errors")
	server.addRoute("/cant-think", "pug/cant-think.pug", "pug")
	server.addRoute("/privacy", "pug/privacy.pug", "pug")
	server.addRoute("/licenses", "pug/licenses.pug", "pug")

	server.addStaticHashTableDir("html/static/js")
	server.addStaticHashTableDir("html/static/js/elemjs")

M utils/constants.js => utils/constants.js +4 -0
@@ 9,6 9,10 @@ let constants = {
			type: "string",
			default: "http://localhost:3000"
		},
		theme: {
			type: "integer",
			default: 0
		},
		save_history: {
			type: "boolean",
			default: false

A utils/icon-loader.js => utils/icon-loader.js +8 -0
@@ 0,0 1,8 @@
const fs = require("fs").promises

const names = ["subscriptions", "settings"]
const icons = names.map(name => fs.readFile(`html/static/images/${name}.svg`, "utf8"))

module.exports.icons = Promise.all(icons).then(resolvedIcons => {
	return new Map(names.map((name, index) => [name, resolvedIcons[index]]))
})

M utils/upgradedb.js => utils/upgradedb.js +6 -1
@@ 70,6 70,11 @@ const deltas = [
			.run()
		db.prepare("CREATE TABLE TakedownChannels (ucid TEXT NOT NULL, org TEXT, url TEXT, PRIMARY KEY (ucid))")
			.run()
	},
	// 11: Settings +theme
	function() {
		db.prepare("ALTER TABLE Settings ADD COLUMN theme INTEGER DEFAULT 0")
			.run()
	}
]



@@ 82,7 87,7 @@ async function createBackup(entry) {

/**
 * @param {number} entry
 * @param {boolean} log
 * @param {boolean} [log]
 */
function runDelta(entry, log) {
	process.stdout.write(`Upgrading database to version ${entry}... `)