Compare commits

...

29 Commits

Author SHA1 Message Date
5d1ef1c364 Fix typo. music->audio 2020-08-25 13:13:02 +05:30
5e369994be Change subscribe page style. Fix typo. Fix spacing. Change anchor style 2020-04-04 17:46:13 +05:30
bc2ecc1cff Add metadata 2020-03-17 22:56:06 +05:30
329039541a [SSG] Move archives, subscribe, donate into directories.
Allow templates to be relative using {{ relative }} which can be set by
caller.
2020-03-10 21:21:54 +05:30
401e1b64f0 [SSG][Fix] Fix %M to %m. Add -v for logging. 2020-03-10 12:41:54 +05:30
f7582b1b2b [Fix] Resolve relative paths to absolute paths. 2020-03-09 23:43:22 +05:30
802acb5cc1 [SSG] Strip spaces in values in file headers. 2020-02-05 05:02:51 +05:30
11ede38d1b [SSG] Generate RSS file. Fix extension handling. URL in EpisodeList.
Episode.audio now takes path without extension. Parse url as part of
argument parsing.
2020-02-05 04:59:57 +05:30
0080597858 [Design][SSG] Add donate page. 2020-02-02 03:35:27 +05:30
1cbf2e9554 [Design] Change buttons on hover on subscribe 2020-02-02 02:42:57 +05:30
c1b3690df8 [SSG][Design] Add Subscribe page. 2020-02-01 15:29:36 +05:30
3d8a816b66 [SSG] Fix generate_site. parse_file for new file format.
Change sort order. Move argument parsing outside main. Better error
reporting. Better exit codes.
2020-01-30 05:43:13 +05:30
2729f74683 [Design] Set title in archives. 2020-01-29 23:43:19 +05:30
93bdd1885d [Design] Style Archives page. Make header a link. 2020-01-29 17:55:22 +05:30
917d3addb6 [SSG] Isolate argument parsing. 2020-01-27 17:15:03 +05:30
5b13ce2a2f [SSG] Allow overriding files and adding more files 2020-01-27 04:43:39 +05:30
5636815aaf [Design] Add metadata. 2020-01-27 04:23:30 +05:30
5041f4c221 [SSG] Move common parts from archives, index to base.html 2020-01-27 03:46:10 +05:30
f69d922f32 [Design] Update fonts. 2020-01-27 02:43:09 +05:30
6a33095635 [Breaking] Change audios/ to audio/ 2020-01-26 07:30:02 +05:30
c54b12e2b1 [Design] Fixed bottom spacing. 2020-01-26 06:43:02 +05:30
2507bc7596 [Fix] Fix Archive and Feed links to be relative 2020-01-26 06:25:59 +05:30
2aaea11e8d [SSG] Add generate_archives. Visual fixes to code. 2020-01-26 04:58:09 +05:30
1b4e1f7640 [Update] Update archives url to archives.html 2020-01-26 04:55:56 +05:30
32a1aa9712 [SSG] Add template for archives 2020-01-26 04:55:11 +05:30
8b9a367fb4 [SSG] Add generate_site to generate episodes, copy videos, audios. 2020-01-26 03:24:59 +05:30
97c898acdb [SSG] Parse markdown shownotes. 2020-01-26 00:40:29 +05:30
d0b7922c9b [SSG] Complete thumbnail gen. Refactor Episode, main. 2020-01-25 23:40:25 +05:30
e6139b4c33 [SSG} Add requirements.txt 2020-01-25 23:38:17 +05:30
19 changed files with 3456 additions and 116 deletions

7
archives.html Normal file
View File

@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block left_content %}
<ul class="archives">
{% for episode in episodes %}
<li><a href="../{{ episode.slug }}">{{ episode.title }}</a></li>
{% endfor %}
{% endblock %}

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

85
base.html Normal file
View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}{{ title }} | Redacted Life{% endblock %}</title>
<link href="{{ relative }}/assets/css/index.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="{{ relative }}/assets/plyr/plyr.css" />
<!-- Metadata -->
<meta name="description" content="An audiocast on Linux and libre software with a hard spin on personal privacy and security">
<meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett, Lloyd Barlowe" />
<link rel="canonical" href="https://redacted.life/">
<link rel="me" href="https://masto.nixnet.xyz/@amolith">
<link rel="me" href="https://masto.nixnet.xyz/@RedactedLife">
<link rel="me" href="https://social.nixnet.services/@RedactedLife">
{% block stylesheets %}{% endblock %}
<!-- Social: Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Redacted Life" />
<meta name="twitter:description" content="An audiocast on Linux and libre software with a hard spin on personal privacy and security" />
<meta name="twitter:image" content="/assets/media/cover-site.png" />
<!-- Social: OpenGraph -->
<meta property="og:locale" content="en_US">
<meta property="og:type" content="article">
<meta property="og:title" content="Redacted Life">
<meta property="og:description" content="An audiocast on Linux and libre software with a hard spin on personal privacy and security">
<meta property="og:url" content="https://redacted.life">
<meta property="og:site_name" content="Redacted Life">
<meta property="og:image" content="/assets/media/cover-site.png" />
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ relative }}/assets/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ relative }}/assets/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ relative }}/assets/favicons/favicon-16x16.png">
<link rel="manifest" href="{{ relative }}/assets/favicons/site.webmanifest">
<link rel="mask-icon" href="{{ relative }}/assets/favicons/safari-pinned-tab.svg" color="#46ad83">
<link rel="shortcut icon" href="{{ relative }}/assets/favicons/favicon.ico">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="msapplication-config" content="{{ relative }}/assets/favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<img src="{{ relative }}/assets/images/cloud.svg" class="clouds" id="cloud1" />
<img src="{{ relative }}/assets/images/cloud.svg" class="clouds" id="cloud2" />
<div class="top_bg"></div>
<div class="bottom_bg"></div>
<div class="top">
<div class="wrapper">
<div class="content">
<h1><a href="{{relative}}">Redacted Life</a></h1>
{% block left_content %}
{% endblock %}
</div>
</div>
</div>
<div class="bottom">
<div class="wrapper">
<div class="content">
<a class="button" href="{{relative}}/subscribe/">Subscribe</a>
<a class="button" href="mailto:hello@redacted.life">Contact</a>
<a class="button" href="{{relative}}/archives/">Archives</a>
</div>
<a class="donate-link" href="{{relative}}/donate/">Consider supporting these individuals and organisations</a>
<footer>
Designed by <a href="https://webionite.com">Ceda EI</a><br />
Source available on <a href="https://git.webionite.com/ceda_ei/redacted.life">Webionite</a><br />
Licensed under <a href="https://git.webionite.com/ceda_ei/redacted.life/src/branch/master/LICENSE">MIT</a>
</footer>
</div>
</div>
<script src="{{ relative }}/assets/plyr/plyr.js"></script>
<script>
const player = new Plyr("#player", {
iconUrl: "{{ relative }}/assets/plyr/plyr.svg",
});
</script>
</body>
</html>

9
donate.html Normal file
View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Donate | Redacted Life{% endblock %}
{% block left_content %}
<ul class="donate">
{% for donate in donations %}
<li><a href="{{ donate.link }}">{{ donate.text }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -1,23 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} | Redacted Life</title>
<link href="assets/css/index.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="assets/plyr/plyr.css" />
</head>
<body>
<img src="assets/images/cloud.svg" class="clouds" id="cloud1" />
<img src="assets/images/cloud.svg" class="clouds" id="cloud2" />
<div class="top_bg"></div>
<div class="bottom_bg"></div>
<div class="top">
<div class="wrapper">
<div class="content">
<h1>Redacted Life</h1>
{% extends "base.html" %}
{% block left_content %}
<div class="player">
<video id="player" poster="{{ thumbnail_src }}" controls>
<source src="{{ video_src }}" type="video/mp4" />
@@ -27,29 +9,4 @@
<summary>Show Notes</summary>
{{ show_notes }}
</details>
</div>
</div>
</div>
<div class="bottom">
<div class="wrapper">
<div class="content">
<a class="button" href="/feed.xml">Subscribe</a>
<a class="button" href="mailto:hello@redacted.life">Contact</a>
<a class="button" href="/archives/">Archives</a>
</div>
<footer>
Designed by <a href="https://webionite.com">Ceda EI</a><br />
Source available on <a href="https://git.webionite.com/ceda_ei/redacted.life">Webionite</a><br />
Licensed under <a href="https://git.webionite.com/ceda_ei/redacted.life/src/branch/master/LICENSE">MIT</a>
</footer>
</div>
</div>
<script src="assets/plyr/plyr.js"></script>
<script>
const player = new Plyr("#player", {
iconUrl: "assets/plyr/plyr.svg",
});
</script>
</body>
</html>
{% endblock %}

365
nova.py
View File

@@ -1,100 +1,393 @@
#!/usr/bin/env python3
# pylint: disable=logging-format-interpolation
"Creates a static site for redacted.life"
import argparse
from collections import UserList
from datetime import datetime
import json
import os
import os.path as path
import logging
import re
import subprocess
import shutil
import sys
import jinja2
import markdown
from mutagen.mp3 import MP3
def gen_name(date, slug):
"Returns to file name"
return date.strftime("%Y-%m-%d-") + slug
def seconds_to_str(seconds):
"Convert seconds to a string of hh:mm:ss"
seconds = int(seconds)
return f"{seconds // 3600:02}:{(seconds % 3600) // 60:02}:{seconds % 60:02}"
class EpisodeList(UserList):
"Represents list of episodes"
def __init__(self, data, output, template):
def __init__(self, url, data, output, template, archives):
super().__init__(data)
self.url = url
self.output = output
self.template = template
self.archives = archives
logging.info(f"New EpisodeList: {url=} {output=} {template=} {archives=}")
def sort(self):
def sort(self, *_args, **_kwargs):
"Sorts the EpisodeList"
super().sort(key=lambda x: x.date, reverse=False)
def generate_thumnails(self):
def generate_thumbnails(self):
"Generates thumbnails for all the videos"
logging.info(f"Creating missing directories")
if not path.isdir(self.output + "assets"):
os.mkdir(self.output + "assets")
if not path.isdir(self.output + "assets/thumbnails"):
os.mkdir(self.output + "assets/thumbnails")
for episode in self.data:
location = (self.output + "assets/thumbnails/" +
gen_name(episode.date, episode.slug) + ".jpg")
logging.info(f"Creating thumbnail for {episode=} at {location}")
episode.store_thumbnail(location)
def generate_atom(self):
"Generates the Atom feed"
def generate_rss(self, header):
"Generates the RSS Feed"
with open(self.output + "feed_mp3.rss", "w") as mp3, \
open(self.output + "feed_ogg.rss", "w") as ogg:
# pylint: disable = invalid-name
for s, ext in ((mp3, "mp3"), (ogg, "ogg")):
logging.info(f"Writing header for {ext}")
s.write(header)
for ep in self.data:
logging.info(f"Writing item for episode {ep} with {ext=}")
s.write("<item>")
s.write("\n")
# Title
s.write(f"<title><![CDATA[{ep.title}]]></title>")
s.write("\n")
# Description
s.write("<description><![CDATA["
f"{ep.config['description']}]]></description>")
s.write("\n")
# Date
datestring = ep.date.strftime(
'%a, %d %b, %Y %H:%M:%Sz GMT'
)
s.write(f"<pubDate>{datestring}</pubDate>")
s.write("\n")
# iTunes: explicit, author, subtitle, keywords
s.write(f"<itunes:explicit>{ep.config['explicit']}"
"</itunes:explicit>")
s.write("\n")
s.write(
f"<itunes:author><![CDATA[{ep.config['author']}]]>"
"</itunes:author>"
)
s.write("\n")
s.write(
"<itunes:subtitle><![CDATA["
f"{ep.config['subtitle']}]]></itunes:subtitle>"
)
s.write("\n")
s.write(
f"<itunes:keywords>{','.join(ep.config['tags'])}"
"</itunes:keywords>"
)
s.write("\n")
s.write(f"<itunes:duration>{seconds_to_str(len(ep))}"
"</itunes:duration>")
s.write("\n")
# Content (show_notes)
s.write(
f"<content:encoded><![CDATA[{ep.show_notes}]]>"
"</content:encoded>"
)
s.write("\n")
# GUID
s.write(
f"<guid isPermaLink=\"true\">{self.url}{ep.slug}"
".html</guid>"
)
s.write("\n")
# Enclosure
audio = f'{self.url}assets/audio/{ep.slug}.{ext}'
size = path.getsize(f"{ep.audio}.{ext}")
s.write(
f'<enclosure url="{audio}" type="audio/{ext}" '
f'length="{size}" />'
)
s.write("\n")
# Categories
for tag in ep.config["tags"]:
s.write(f"<category><![CDATA[{tag}]]></category>")
s.write("\n")
s.write("</item>")
s.write("\n")
logging.info(f"Writing end for {ext}")
s.write("</channel>")
s.write("\n")
s.write("</rss>")
def generate_site(self):
"Generates the entire "
def generate_archives(self):
"Generates archives page"
if not path.isdir(self.output + "archives"):
logging.info("Creating directory archives")
os.mkdir(self.output + "archives")
with open(self.output + "archives/index.html", "w") as file:
episodes = [{
"slug": gen_name(i.date, i.slug) + ".html",
"title": i.title
} for i in self.data[::-1]]
file.write(self.archives.render(episodes=episodes,
title="Archives",
relative=".."
))
def generate_site(self, root):
"Generates the entire site"
logging.info("Generating CSS from SCSS")
subprocess.run(["sass", "--update", f"{root}scss:{root}assets/css"],
check=True)
logging.info("Copy the existing assets")
shutil.copytree(root + "assets", self.output + "assets",
dirs_exist_ok=True)
logging.info("Create the required directories")
paths = [
"assets",
"assets/audio",
"assets/videos",
]
logging.info("Creating missing directories")
for directory in paths:
if not path.isdir(self.output + directory):
logging.info(f"Creating directory {directory}")
os.mkdir(self.output + directory)
logging.info("Render episodes and copy data")
for episode in self.data:
logging.info(f"Rendering episode {episode}")
html = f"{self.output}{gen_name(episode.date, episode.slug)}.html"
thumbnail = ("assets/thumbnails/" +
gen_name(episode.date, episode.slug) + ".jpg")
video = (self.output + "assets/videos/" +
gen_name(episode.date, episode.slug) + ".mp4")
audio = (self.output + "assets/audio/" +
gen_name(episode.date, episode.slug) + ".mp3")
logging.info(f"Copying {episode.video} to {video}")
shutil.copy2(episode.video, video)
logging.info(f"Copying {episode.audio}.mp3 to {audio}")
shutil.copy2(episode.audio + ".mp3", audio)
logging.info(f"Copying {episode.audio}.ogg to {audio}")
shutil.copy2(episode.audio + ".ogg", audio)
logging.info(f"Writing to {html}")
with open(html, "w") as file:
file.write(episode.render(self.template, thumbnail))
last = self.data[-1]
last_name = f"{self.output}{gen_name(last.date, last.slug)}.html"
logging.info(f"Copying last one ({last}) to index.html")
shutil.copy2(last_name, self.output + "index.html")
class Episode:
"Represents one episode of podcast"
def __init__(self, date, slug, title, show_notes):
def __init__(self, date, slug, title, show_notes, video_src, audio_src, config):
self.date = date
self.slug = slug
self.title = title
self.show_notes = show_notes
self.show_notes = markdown.markdown(show_notes)
self.video = video_src
self.audio = audio_src
self.config = config
self.length = MP3(audio_src + ".mp3").info.length
logging.info(f"New episode: {date=} {slug=} {title=} {self.video=} "
f"{self.audio=} {config=} {self.length=} {self.show_notes=}")
def render(self, template, thumbnail_src, video_src):
def render(self, template, thumbnail_src, relative="."):
"Renders the Episode with the given template"
return template.render(
title=self.title,
show_notes=jinja2.Markup(self.show_notes),
thumbnail_src=thumbnail_src,
video_src=video_src
relative=relative,
video_src=f"assets/videos/{path.basename(self.video)}"
)
def store_thumbnail(self, location):
"Stores the thumbnail for given image at path"
args = ["ffmpeg", "-i", self.video, "-ss", "00:00:01.000", "-vframes",
"1", location]
logging.info(f"Running {' '.join(args)}")
subprocess.run(args, check=False)
def __len__(self):
return int(self.length)
def main():
"Main method"
def __str__(self):
return f"{self.slug}: {self.title}"
def __repr__(self):
return str(self)
def parse_args():
"Parses arguments"
parser = argparse.ArgumentParser()
parser.add_argument("input_dir", help="Input directory")
parser.add_argument("output_dir", help="Output directory")
parser.add_argument("url", help="Base URL of website")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose Logging")
args = parser.parse_args()
input_dir = args.input_dir.rstrip("/") + "/"
output_dir = args.output_dir.rstrip("/") + "/"
input_dir = path.abspath(args.input_dir.rstrip("/")) + "/"
output_dir = path.abspath(args.output_dir.rstrip("/")) + "/"
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig()
url = args.url.rstrip("/") + "/"
return input_dir, output_dir, url
class ParseError(ValueError):
"Error raised while parsing a file"
def parse_file(file, array_keys=("tags")):
"Parses a file"
config = {}
kv_re = re.compile(r"(?P<key>\w+):\s*(?P<value>.*)")
while line := file.readline():
if line.rstrip("\n") == "---":
break
if line.strip() == "":
continue
if match := kv_re.match(line):
if match.group("key").strip().lower() in array_keys:
config[match.group("key")] = [i.strip() for i in
match.group("value").split(",")]
else:
config[match.group("key")] = match.group("value").strip()
else:
raise ParseError(f"Invalid line {line}")
return (config, file.read())
def main(args):
"Main method"
root = path.dirname(sys.argv[0]).rstrip("/") + "/"
input_dir, output_dir, url = args
logging.info(f"Input directory: {input_dir}")
logging.info(f"Output directory: {output_dir}")
logging.info(f"URL: {url}")
# Input validation
if not all(path.isdir(i) for i in (input_dir, input_dir + "md",
input_dir + "videos")):
print("Invalid Input", file=sys.stderr)
return
paths = [
input_dir,
input_dir + "md",
input_dir + "videos",
input_dir + "audio",
]
logging.info("Checking if all paths exist.")
if not all(path.isdir((fail := i)) for i in paths):
logging.error(f"Invalid Input. {fail} is not a directory.")
return 1
if not path.isdir(args.output_dir):
os.mkdir(args.output_dir)
logging.info("Creating output directory if it doesn't exist")
if not path.isdir(output_dir):
os.mkdir(output_dir)
template = jinja2.Environment(
loader=jinja2.FileSystemLoader(input_dir),
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(root),
autoescape=jinja2.select_autoescape("html")
).get_template("index.html")
)
podcast = EpisodeList([], output_dir, template)
logging.info("Creating EpisodeList")
podcast = EpisodeList(
url,
[],
output_dir,
env.get_template("index.html"),
env.get_template("archives.html")
)
split = re.compile(r"((?P<date>\d{4}-[01]?\d-[0123]?\d)-(?P<slug>.*))")
split = re.compile(r"((?P<date>\d{4}-[01]?\d-[0123]?\d)-(?P<slug>.*).md)")
logging.info(f"Parsing all files in {input_dir}md")
for file in os.listdir(input_dir + "md"):
logging.info(f"File: {file}")
match = split.match(file)
logging.info(f"Match: {match}")
if not match:
print(f"Invalid filename: {file}", file=sys.stderr)
logging.error(f"Invalid filename: {file}")
continue
date = match.group("date")
date = datetime.strptime(match.group("date"), "%Y-%m-%d")
logging.info(f"Date: {date}")
slug = match.group("slug")
with open(file) as f: # pylint: disable=invalid-name
title = f.readline()
show_notes = f.read()
podcast.append(Episode(date, slug, title, show_notes))
logging.info(f"Slug: {slug}")
with open(input_dir + "md/" + file) as episode:
try:
config, show_notes = parse_file(episode)
logging.info(f"Config: {config}")
logging.info(f"Show Notes: {show_notes}")
except ParseError as err:
logging.error(f"Error while parsing file: {file}")
logging.error(err)
return 2
logging.info("Appending to EpisodeList")
podcast.append(
Episode(
date,
slug,
config["title"],
show_notes,
input_dir + "videos/" + gen_name(date, slug) + ".mp4",
input_dir + "audio/" + gen_name(date, slug),
config
)
)
if not path.isdir(output_dir + "subscribe"):
os.mkdir(output_dir + "subscribe")
logging.info("Generating subscribe page")
with open(input_dir + "subscribe.json") as subscribe, \
open(output_dir + "subscribe/index.html", "w") as html:
html.write(env.get_template("subscribe.html").render(
relative="..",
subscriptions=json.load(subscribe)
))
if not path.isdir(output_dir + "donate"):
os.mkdir(output_dir + "donate")
logging.info("Generating donate page")
with open(input_dir + "donate.json") as donate, \
open(output_dir + "donate/index.html", "w") as html:
html.write(env.get_template("donate.html").render(
relative="..",
donations=json.load(donate)
))
logging.info("Sorting podcasts")
podcast.sort()
podcast.generate_thumnails()
podcast.generate_atom()
podcast.generate_site()
logging.info("Generating thumbnails")
podcast.generate_thumbnails()
logging.info("Generating archives pages")
podcast.generate_archives()
logging.info("Generating RSS feeds")
with open(input_dir + "header.rss") as header:
podcast.generate_rss(header.read())
logging.info("Generating Site")
podcast.generate_site(root)
logging.info("Copying Overrides")
shutil.copytree(input_dir + "overrides", output_dir, dirs_exist_ok=True)
logging.info("Done")
return 0
if __name__ == "__main__":
main()
sys.exit(main(parse_args()))

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
jinja2
markdown
mutagen

View File

@@ -1,28 +1,23 @@
/* latin */
@font-face {
font-family: 'Comfortaa';
font-family: 'Lato';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(../fonts/Comfortaa-Latin-Light.woff) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url(../fonts/Lato-Regular.ttf);
}
/* latin */
@font-face {
font-family: 'Comfortaa';
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/Comfortaa-Latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url(../fonts/Montserrat-Bold.ttf);
}
@mixin body-font {
font-family: "Comfortaa", sans-serif;
font-weight: 300;
font-family: "Lato", sans-serif;
}
@mixin title-font {
font-family: "Comfortaa", sans-serif;
font-weight: 700;
font-family: "Montserrat", sans-serif;
}

View File

@@ -44,7 +44,7 @@ body {
}
.bottom_bg {
background-image: linear-gradient(#26a4c4, #afdde9);
height: 100 - $top-height;
height: 100vh - $top-height;
width: 100vw;
position: fixed;
top: $top-height;
@@ -56,7 +56,7 @@ body {
.wrapper {
min-height: $top-height;
align-items: flex-end;
padding-bottom: 2em;
padding-bottom: 1em;
.player {
margin: auto;
width: 80%;
@@ -69,21 +69,86 @@ body {
}
}
.shownotes {
padding: 2em 1em 1em 1em;
padding: 2em 1em 0;
margin: auto;
width: 80%;
a {
color: #292929;
padding: 0.3em;
border-radius: 1.6em;
transition-duration: 0.2s;
}
a:hover {
color: #FFF;
background-color: #000;
text-decoration: none;
transition-duration: 0.2s;
}
}
ul.archives, ul.donate {
list-style-type: none;
width: 100%;
padding: 0;
text-align: center;
li {
a {
text-decoration: none;
color: #000;
transition-duration: 0.2s;
}
a:hover, a:active {
transition-duration: 0.2s;
background-color: #000;
color: #FFF;
padding: 0.3em;
border-radius: 1.6em;
}
margin: 0.5em 1em;
}
}
.subscribe {
display: flex;
width: 600px;
justify-content: space-between;
flex-wrap: wrap;
margin: auto;
.subscription {
width: 200px;
color: #000;
text-align: center;
transition-duration: 0.2em;
padding: 1rem 0;
span {
color: inherit;
font-size: 100px;
text-decoration: none;
}
p {
@include title-font;
width: 100%;
font-size: 1rem;
}
}
.subscription:hover, .subscription:active {
text-decoration: none;
transition-duration: 0.2em;
background-color: #000;
color: #FFF;
border-radius: 30px;
}
}
}
}
.bottom {
min-height: 100vh - $top-height;
min-height: 95vh - $top-height;
margin-bottom: 5vh;
.wrapper {
min-height: 100vh - $top-height;
min-height: 95vh - $top-height;
align-items: center;
justify-content: space-around;
justify-content: space-between;
flex-direction: column;
padding-top: 2em;
padding-top: 1em;
height: 100%;
.content {
display: flex;
@@ -98,12 +163,27 @@ body {
border-style: solid;
border-radius: 1.5em;
text-decoration: none;
transition-duration: 0.4s;
}
.button:hover, .button:active {
transition-duration: 0.4s;
background-color: #000;
color: #FFF;
}
}
a.donate-link {
margin: 1em 0;
color: #292929;
padding: 0.3em;
border-radius: 1.6em;
transition-duration: 0.2s;
}
a:hover {
color: #FFF;
background-color: #000;
text-decoration: none;
transition-duration: 0.2s;
}
footer {
margin-top: 1em;
padding: 0 1em;
@@ -113,11 +193,13 @@ body {
color: #292929;
padding: 0.3em;
border-radius: 1.6em;
transition-duration: 0.2s;
}
a:hover {
color: #FFF;
background-color: #000;
text-decoration: none;
transition-duration: 0.2s;
}
}
}
@@ -141,10 +223,32 @@ h1 {
@include title-font;
font-size: 4em;
text-align: center;
a {
color: #000;
text-decoration: none;
}
}
@media (max-width: 800px) {
body {
.top {
.wrapper {
.subscribe {
width: 400px;
}
}
}
}
}
@media (max-width: 600px) {
body {
.top_bg {
height: $top-height-phone;
}
.bottom_bg {
height: 100vh - $top-height-phone;
top: $top-height-phone;
}
.top {
min-height: $top-height-phone;
.wrapper {
@@ -159,9 +263,9 @@ h1 {
}
}
.bottom {
min-height: 100vh - $top-height-phone;
min-height: 95vh - $top-height-phone;
.wrapper {
min-height: 100vh - $top-height-phone;
min-height: 95vh - $top-height-phone;
.content {
.button {
font-size: 1em;
@@ -176,6 +280,17 @@ h1 {
}
}
@media (max-width: 480px) {
body {
.top {
.wrapper {
.subscribe {
width: 200px;
}
}
}
}
}
@keyframes move_right {
from {left: 0}

15
subscribe.html Normal file
View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}Subscribe | Redacted Life{% endblock %}
{% block stylesheets %}
<link href="{{relative}}/assets/fork-awesome/css/fork-awesome.min.css" rel="stylesheet" type="text/css">
{% endblock %}
{% block left_content %}
<div class="subscribe">
{% for subscription in subscriptions %}
<a href={{subscription.link}} class="subscription">
<span class="fa fa-{{subscription.icon}}">
<p>{{subscription.text}}</p>
</a>
{% endfor %}
</div>
{% endblock %}