[add] pwa, icons, banner, themes
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Evan Reichard 2023-11-11 16:41:25 -05:00
parent 146c2e25d0
commit f85deba946
16 changed files with 211 additions and 80 deletions

View File

@ -1,2 +1,3 @@
recursive-include vreader/api *.py recursive-include vreader/api *.py
recursive-include vreader/templates * recursive-include vreader/templates *
recursive-include vreader/static *

View File

@ -1,15 +1,18 @@
docker_build_local: docker_build_local: build_tailwind
docker build -t vreader:latest . docker build -t vreader:latest .
docker_build_release_dev: docker_build_release_dev: build_tailwind
docker buildx build \ docker buildx build \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
-t gitea.va.reichard.io/evan/vreader:dev \ -t gitea.va.reichard.io/evan/vreader:dev \
--push . --push .
docker_build_release_latest: docker_build_release_latest: build_tailwind
docker buildx build \ docker buildx build \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
-t gitea.va.reichard.io/evan/vreader:latest \ -t gitea.va.reichard.io/evan/vreader:latest \
-t gitea.va.reichard.io/evan/vreader:`git describe --tags` \ -t gitea.va.reichard.io/evan/vreader:`git describe --tags` \
--push . --push .
build_tailwind:
tailwind build -o ./vreader/static/tailwind.css --minify

View File

@ -1,6 +1,14 @@
# VReader <p><img align="center" src="https://gitea.va.reichard.io/evan/VReader/raw/branch/master/banner.png"></p>
Turn YouTube videos into articles! I banged this one out in a couple of hours, so it's a bit scrappy. Will slowly improve it. <p align="center">
<a href="https://drone.va.reichard.io/evan/VReader" target="_blank">
<img src="https://drone.va.reichard.io/api/badges/evan/VReader/status.svg">
</a>
</p>
---
VReader allows you to take videos from YouTube and convert them into articles!
## Running Server ## Running Server

BIN
banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
banner.xcf Normal file

Binary file not shown.

View File

@ -2,6 +2,7 @@
pkgs.mkShell { pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
nodePackages.tailwindcss
python311 python311
]; ];
} }

16
tailwind.config.js Normal file
View File

@ -0,0 +1,16 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./vreader/templates/**/*.html", "./vreader/assets/**/*.{html,js}"],
theme: {
extend: {
colors: {
primary: "var(--color-primary)",
secondary: "var(--color-secondary)",
tertiary: "var(--color-tertiary)",
ptext: "var(--color-primary-text)",
stext: "var(--color-secondary-text)",
},
},
},
plugins: [],
};

View File

@ -1,13 +1,25 @@
from flask import Blueprint from flask import Blueprint
from flask import make_response, render_template from flask import make_response, render_template, send_from_directory
from html_sanitizer import Sanitizer from html_sanitizer import Sanitizer
from markdown import markdown from markdown import markdown
from vreader.config import Config from vreader.config import Config
import os import os
bp = Blueprint("common", __name__) bp = Blueprint("common", __name__)
sanitizer = Sanitizer() sanitizer = Sanitizer()
@bp.route("/static/<path:path>")
def static(path):
return send_from_directory("static", path)
@bp.route("/manifest.json")
def manifest():
return send_from_directory("static", "manifest.json")
@bp.route("/", methods=["GET"]) @bp.route("/", methods=["GET"])
def main_entry(): def main_entry():
# Get Files # Get Files
@ -30,6 +42,7 @@ def main_entry():
return make_response(render_template("index.html", articles=articles)) return make_response(render_template("index.html", articles=articles))
@bp.route("/articles/<id>", methods=["GET"]) @bp.route("/articles/<id>", methods=["GET"])
def article_item(id): def article_item(id):

View File

@ -4,17 +4,9 @@ from flask import Blueprint, request
from vreader.config import Config from vreader.config import Config
import vreader import vreader
bp = Blueprint("v1", __name__, url_prefix="/api/v1") bp = Blueprint("v1", __name__, url_prefix="/api/v1")
@bp.route("/articles", methods=["GET"])
def articles():
directory = str(Config.DATA_PATH)
all_files = os.listdir(directory)
markdown_files = [file for file in all_files if file.endswith(".md")]
articles = [parse_filename(file) for file in markdown_files]
return articles
@bp.route("/generate", methods=["POST"]) @bp.route("/generate", methods=["POST"])
def generate(): def generate():

View File

@ -4,18 +4,12 @@ from typing import Any, List
import json import json
import openai import openai
INITIAL_PROMPT_TEMPLATE = """ PROMPT_TEMPLATE = """
The following is a video transcription. Write a fully comprehensive article in markdown appropriately utilizing subsections. Be sure to only use the following transcription to write the article: The following is a video transcription. Write a fully comprehensive article in markdown appropriately utilizing subsections. Be sure to only use the following transcription to write the article:
{context} {context}
""" """
INITIAL_PROMPT_TEMPLATE_OLD = """
The following is a video transcription. Write a comprehensive article in markdown utilizing the following content:
{context}
"""
@dataclass @dataclass
class ChatCompletion: class ChatCompletion:
id: str id: str
@ -33,13 +27,12 @@ class OpenAIConnector:
# self.model = "gpt-3.5-turbo-16k" # self.model = "gpt-3.5-turbo-16k"
self.model = "gpt-3.5-turbo-1106" self.model = "gpt-3.5-turbo-1106"
self.word_cap = 1000
openai.api_key = api_key openai.api_key = api_key
def query(self, context: str) -> Any: def query(self, context: str) -> Any:
# Create Initial Prompt # Create Prompt
prompt = INITIAL_PROMPT_TEMPLATE.format(context = context) prompt = PROMPT_TEMPLATE.format(context = context)
messages = [{"role": "user", "content": prompt}] messages = [{"role": "user", "content": prompt}]
print("[OpenAIConnector] Running OAI Query") print("[OpenAIConnector] Running OAI Query")
@ -59,6 +52,7 @@ class OpenAIConnector:
# Return Response # Return Response
return { "title": title, "content": content } return { "title": title, "content": content }
def get_title(self, markdown: str): def get_title(self, markdown: str):
lines = markdown.split('\n') lines = markdown.split('\n')
for line in lines: for line in lines:

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -0,0 +1,17 @@
{
"name": "VReader",
"short_name": "VReader",
"lang": "en-US",
"theme_color": "#123636",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"purpose": "any",
"sizes": "512x512",
"src": "/static/icons/icon512.png",
"type": "image/png"
}
]
}

68
vreader/static/style.css Normal file
View File

@ -0,0 +1,68 @@
/* ----------------------------- */
/* -------- PWA Styling -------- */
/* ----------------------------- */
html,
body {
overscroll-behavior-y: none;
margin: 0px;
}
html {
height: calc(100% + env(safe-area-inset-bottom));
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
env(safe-area-inset-left);
}
main {
height: calc(100dvh - 4rem - env(safe-area-inset-top));
}
#container {
padding-bottom: calc(5em + env(safe-area-inset-bottom) * 2);
}
/* No Scrollbar - IE, Edge, Firefox */
* {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* No Scrollbar - WebKit */
*::-webkit-scrollbar {
display: none;
}
/* ----------------------------- */
/* ----------- Theme ----------- */
/* ----------------------------- */
@media (prefers-color-scheme: light) {
html {
--color-primary: #f3f4f6;
--color-secondary: #e5e7eb;
--color-tertiary: #d1d5db;
--color-primary-text: #1f2937;
--color-secondary-text: #4b5563;
}
}
@media (prefers-color-scheme: dark) {
html {
--color-primary: #091b1b;
--color-secondary: #123636;
--color-tertiary: #1f5c5c;
--color-primary-text: #ffffff;
--color-secondary-text: #d6f1ed;
}
}
/*
@media (prefers-color-scheme: dark) {
html {
--color-primary: #091B1B;
--color-secondary: #123636;
--color-tertiary: #1F5C5C;
--color-primary-text: #FFFFFF;
--color-secondary-text: #D6F1ED;
}
}
*/

File diff suppressed because one or more lines are too long

View File

@ -2,45 +2,59 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
name="viewport" <meta name="apple-mobile-web-app-capable" content="yes"/>
content="width=device-width, initial-scale=0.9, user-scalable=no, viewport-fit=cover" <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
/> <meta name="theme-color" content="#E5E7EB" media="(prefers-color-scheme: light)">
<title>VReader - {{ metadata.title }}</title> <meta name="theme-color" content="#123636" media="(prefers-color-scheme: dark)">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Markdown Overrides -->
<style> <style>
h1 { h1 {
color: var(--color-primary-text);
font-size: 1.50em !important; font-size: 1.50em !important;
font-weight: 500 !important; font-weight: 500 !important;
} }
h2 { h2 {
color: var(--color-primary-text);
font-size: 1.25em !important; font-size: 1.25em !important;
} }
p { p {
color: var(--color-secondary-text);
margin-top: 0.25em !important; margin-top: 0.25em !important;
margin-bottom: 1.5em !important; margin-bottom: 1.5em !important;
} }
</style> </style>
<title>VReader - {{ metadata.title }}</title>
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/tailwind.css">
</head> </head>
<body class="bg-slate-200"> <body class="text-ptext bg-secondary">
<header class="w-screen h-16 bg-slate-300 mb-5"> <header class="w-screen h-16 bg-secondary">
<div <div
class="flex px-2 h-16 w-11/12 md:w-5/6 mx-auto rounded bg-slate-300" class="flex px-2 h-16 w-11/12 md:w-5/6 mx-auto"
> >
<a class="font-bold flex justify-center items-center" href="/">All Articles</a> <a class="flex gap-2 justify-center items-center" href="/">
<img class="h-10 rounded items-center" src="/static/icons/icon512.png"></img>
<span class="font-bold flex justify-center items-center">VReader</span>
</a>
</div> </div>
</header> </header>
<div <main class="relative overflow-hidden bg-primary">
id="content" <div id="container" class="h-[100dvh] overflow-auto md:px-6 w-11/12 md:w-5/6 mx-auto mt-3">
class="w-11/12 md:w-5/6 mx-auto rounded px-10 py-5 bg-slate-300" <div id="content" class="rounded bg-secondary p-6 mb-3">
> <div class="flex justify-center w-full mb-6">
<div class="flex justify-center pb-5 w-full">
<a target="_blank" href="https://www.youtube.com/watch?v={{ metadata.video_id }}"> <a target="_blank" href="https://www.youtube.com/watch?v={{ metadata.video_id }}">
<img class="h-32 rounded" src="https://i.ytimg.com/vi_webp/{{ metadata.video_id }}/maxresdefault.webp"></img> <img class="h-32 rounded" src="https://i.ytimg.com/vi_webp/{{ metadata.video_id }}/maxresdefault.webp"></img>
</a> </a>
</div> </div>
<hr class="border-gray-500 pb-5" /> <hr class="border-primary pb-5" />
{{ markdown_html|safe }} {{ markdown_html|safe }}
</div> </div>
</div>
</main>
</body> </body>
</html> </html>

View File

@ -2,39 +2,50 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
name="viewport" <meta name="apple-mobile-web-app-capable" content="yes"/>
content="width=device-width, initial-scale=0.9, user-scalable=no, viewport-fit=cover" <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
/> <meta name="theme-color" content="#E5E7EB" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#123636" media="(prefers-color-scheme: dark)">
<title>VReader - Home</title> <title>VReader - Home</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/tailwind.css">
</head> </head>
<body class="bg-slate-200"> <body class="text-ptext bg-secondary">
<header class="w-screen h-16 bg-slate-300 mb-5"> <header class="w-screen h-16 bg-secondary">
<div <div
class="flex px-2 h-16 w-11/12 md:w-5/6 mx-auto rounded bg-slate-300" class="flex px-2 h-16 w-11/12 md:w-5/6 mx-auto"
> >
<a class="flex gap-2 justify-center items-center" href="/">
<img class="h-10 rounded items-center" src="/static/icons/icon512.png"></img>
<span class="font-bold flex justify-center items-center">VReader</span> <span class="font-bold flex justify-center items-center">VReader</span>
</a>
</div> </div>
</header> </header>
<main class="flex flex-col gap-4"> <main class="relative overflow-hidden bg-primary">
<div id="container" class="h-[100dvh] flex flex-col gap-3 px-4 overflow-auto md:px-6 mt-3">
<div id="submit" <div id="submit"
class="flex gap-4 items-center text-lg w-11/12 md:w-4/6 mx-auto rounded px-6 py-3 bg-slate-300" class="flex gap-4 items-center text-lg w-11/12 md:w-4/6 mx-auto rounded px-6 py-3 bg-secondary transition-all duration-200"
> >
<input type="text" placeholder="YouTube URL" class="w-full p-2 bg-gray-700 text-white"> <input type="text" placeholder="YouTube URL" class="w-full px-2 py-1 bg-tertiary text-stext rounded">
<button class="p-2 bg-gray-500 text-gray-800 hover:bg-gray-100" type="submit">Generate</button> <button class="px-2 py-1 bg-tertiary text-stext hover:bg-primary rounded">Generate</button>
</div> </div>
{% for article in articles %} {% for article in articles %}
<a <a
href="/articles/{{ article.video_id }}" href="/articles/{{ article.video_id }}"
class="flex items-center text-lg w-11/12 md:w-4/6 mx-auto rounded px-6 py-3 bg-slate-300 hover:bg-slate-400 transition-all duration-200" class="flex items-center text-lg w-11/12 md:w-4/6 mx-auto rounded px-6 py-3 bg-secondary hover:bg-tertiary transition-all duration-200"
> >
<img class="h-14 md:h-24 mr-6 rounded" src="https://i.ytimg.com/vi_webp/{{ article.video_id }}/maxresdefault.webp"></img> <img class="h-14 md:h-24 mr-6 rounded" src="https://i.ytimg.com/vi_webp/{{ article.video_id }}/maxresdefault.webp"></img>
<span>{{ article.title }}</span> <span>{{ article.title }}</span>
</a> </a>
{% endfor %} {% endfor %}
<div class="mb-0.5"></div>
</div>
</main> </main>
<script> <script>
const LOADING_SVG = `<svg const LOADING_SVG = `<svg
@ -92,14 +103,6 @@
return fetch(data.url, fetchObj).then((resp) => resp.json()); return fetch(data.url, fetchObj).then((resp) => resp.json());
} }
function getVideoArticle(videoID) {
return apiCall({
url: "/api/v1/generate",
method: "POST",
data: { video: videoID },
});
}
function generateAction(){ function generateAction(){
let inputEl = document.querySelector("input"); let inputEl = document.querySelector("input");
let inputVal = inputEl.value; let inputVal = inputEl.value;