plexi / pages /Study_Material_Hub.py
LazyHuman10
Sync lightweight pure-Python PDF conversion to space
8657f7c
import re
from html import escape
from pathlib import Path
from urllib.parse import quote
import streamlit as st
from streamlit_pdf_viewer import pdf_viewer
from utils import (
APP_ICON_PATH,
convert_office_to_pdf,
download_github_file,
get_manifest,
get_mime_type,
inject_theme,
render_page_header,
render_sidebar,
render_stat_cards,
summarize_manifest,
summarize_subject_catalog,
)
st.set_page_config(
page_title="Study Materials Hub", page_icon=APP_ICON_PATH, layout="wide"
)
inject_theme()
# No longer need strict MIME-type mappings since we check extensions robustly.
def format_file_label(filename):
"""Return a cleaner display label for a stored filename."""
stem = Path(filename).stem
return re.sub(r"[._-]+", " ", stem).strip() or filename
def get_file_type_label(filename, file_mime):
"""Return a short human-readable file type label."""
suffix = Path(filename).suffix.lower().lstrip(".")
if suffix:
return suffix
if file_mime.startswith("text/"):
return "text"
if file_mime == "application/pdf":
return "pdf"
return file_mime.rsplit("/", 1)[-1]
def display_pdf(file_content):
"""Display PDF using streamlit-pdf-viewer."""
pdf_viewer(file_content, width="100%", height=700)
def display_office_document(file_content, download_url, filename):
"""Display Word / PowerPoint files by converting to PDF server-side.
Uses pure Python libraries to extract content and render a PDF preview.
If the conversion fails or is unsupported, it falls back to a link
that opens the file in Microsoft Office Web Viewer in a new tab.
"""
suffix = Path(filename).suffix.lower().lstrip(".")
type_label = "presentation" if suffix in ("ppt", "pptx") else "document"
with st.spinner(f"Converting {type_label} to PDF for preview…"):
pdf_bytes = convert_office_to_pdf(file_content, filename)
if pdf_bytes:
pdf_viewer(pdf_bytes, width="100%", height=700)
st.caption(
f"Inline preview of `{format_file_label(filename)}` "
"(converted to PDF on the server)."
)
else:
# Fallback – open in Office Web Viewer in a new tab
encoded_url = quote(download_url, safe="")
preview_url = (
f"https://view.officeapps.live.com/op/view.aspx?src={encoded_url}"
)
st.markdown(
f"""
<section class="plexi-callout" style="text-align:center;padding:2.5rem 1.5rem;">
<div style="font-size:3rem;margin-bottom:0.6rem;">📄</div>
<div class="plexi-sidecard-title">{format_file_label(filename)}</div>
<div class="plexi-muted" style="margin-bottom:1rem;">
Server-side conversion is not available right now.
Open the {type_label} in Microsoft Office Web Viewer instead.
</div>
</section>
""",
unsafe_allow_html=True,
)
st.link_button(
f"🔗 Open {type_label.capitalize()} in Office Viewer",
preview_url,
use_container_width=True,
type="primary",
)
st.caption(
"Powered by Microsoft Office Web Viewer. "
"You can also download the file directly using the button on the right."
)
try:
manifest = get_manifest()
except Exception as err:
st.error(f"Failed to load materials catalog: {err}")
st.stop()
semester_names = sorted(manifest.keys()) if manifest else []
catalog_summary = summarize_manifest(manifest) if manifest else None
if not semester_names:
st.info("No study materials are available yet. Check back later.")
st.stop()
selected_semester = None
selected_subject = None
selected_type = None
selected_file_name = None
with st.container():
render_page_header(
"Material hub",
"Browse the catalog without losing context",
(
"Move from semester to file in a single flow, preview supported files in "
"place, and download the exact asset you want from the shared materials "
"repository."
),
badges=[
f"{catalog_summary['semester_count']} semesters"
if catalog_summary
else None,
f"{catalog_summary['file_count']} files" if catalog_summary else None,
"Inline document preview",
],
)
st.markdown(
'<div class="plexi-section-label">Refine Your Selection</div>',
unsafe_allow_html=True,
)
st.markdown(
"""
<section class="plexi-panel">
<div class="plexi-sidecard-title">Catalog filters</div>
<div class="plexi-muted">
Narrow the collection by semester, then drill into one subject and file.
</div>
</section>
""",
unsafe_allow_html=True,
)
filter_cols = st.columns(4, gap="medium")
with filter_cols[0]:
selected_semester = st.selectbox("Semester", semester_names, key="hub_semester")
subjects = sorted(manifest[selected_semester].keys())
with filter_cols[1]:
selected_subject = st.selectbox("Subject", subjects, key="hub_subject")
subject_data = manifest[selected_semester][selected_subject]
subject_summary = summarize_subject_catalog(subject_data)
types = subject_summary["types"]
with filter_cols[2]:
selected_type = st.selectbox("Material Type", types, key="hub_type")
files_list = subject_data[selected_type]
file_names = [file_entry["name"] for file_entry in files_list]
with filter_cols[3]:
selected_file_name = (
st.selectbox(
"File",
file_names,
key="hub_file",
format_func=format_file_label,
)
if file_names
else None
)
selected_file_obj = (
next((item for item in files_list if item["name"] == selected_file_name), None)
if selected_file_name
else None
)
render_stat_cards(
[
{
"label": "Current Subject",
"value": selected_subject,
"note": f"{selected_semester} collection currently in focus.",
},
{
"label": "Available Files",
"value": subject_summary["file_count"],
"note": "All assets available for this subject across material types.",
},
{
"label": "Material Types",
"value": subject_summary["type_count"],
"note": ", ".join(subject_summary["types"]),
},
{
"label": "Current Bucket",
"value": len(files_list),
"note": f"Files available inside {selected_type}.",
},
]
)
render_sidebar()
if not selected_file_obj:
st.info("No files were found for this combination yet.")
st.stop()
try:
file_content = download_github_file(selected_file_obj["download_url"])
file_mime = get_mime_type(selected_file_obj["name"])
except Exception as err:
st.error(f"Error loading file: {err}")
st.stop()
if not file_content:
st.error("The selected file could not be downloaded.")
st.stop()
st.markdown(
'<div class="plexi-section-label">Preview And Download</div>',
unsafe_allow_html=True,
)
preview_col, info_col = st.columns([1.7, 0.95], gap="large")
with preview_col:
st.markdown(
f"""
<section class="plexi-panel">
<div class="plexi-kicker">{selected_semester}</div>
<div class="plexi-sidecard-title">{format_file_label(selected_file_obj["name"])}</div>
<div class="plexi-muted">
{selected_subject} / {selected_type}
</div>
</section>
""",
unsafe_allow_html=True,
)
ext = Path(selected_file_obj["name"]).suffix.lower()
if ext == ".pdf":
display_pdf(file_content)
elif ext in (".ppt", ".pptx", ".doc", ".docx"):
display_office_document(
file_content, selected_file_obj["download_url"], selected_file_obj["name"]
)
elif file_mime.startswith("text/"):
# Basic text preview support (optional, if needed)
st.code(file_content.decode("utf-8", errors="replace"))
else:
st.info(
"Preview is not available for this file type. Download it to inspect the content."
)
with info_col:
st.markdown(
"""
<section class="plexi-callout">
<div class="plexi-sidecard-title">Selected file</div>
<div class="plexi-muted">
Download the current file or switch to another asset in the same bucket.
</div>
</section>
""",
unsafe_allow_html=True,
)
st.download_button(
label="Download File",
data=file_content,
file_name=selected_file_obj["name"],
mime=file_mime,
use_container_width=True,
type="primary",
)
st.markdown(
"""
<section class="plexi-panel">
<div class="plexi-sidecard-title">File details</div>
</section>
""",
unsafe_allow_html=True,
)
st.markdown(
f"""
<section class="plexi-meta">
<div class="plexi-meta-row">
<div class="plexi-meta-key">Semester</div>
<div class="plexi-meta-value">{escape(selected_semester)}</div>
</div>
<div class="plexi-meta-row">
<div class="plexi-meta-key">Subject</div>
<div class="plexi-meta-value">{escape(selected_subject)}</div>
</div>
<div class="plexi-meta-row">
<div class="plexi-meta-key">Material Type</div>
<div class="plexi-meta-value">{escape(selected_type)}</div>
</div>
<div class="plexi-meta-row">
<div class="plexi-meta-key">Format</div>
<div class="plexi-meta-value">{escape(get_file_type_label(selected_file_obj["name"], file_mime).upper())}</div>
</div>
</section>
""",
unsafe_allow_html=True,
)
st.markdown(
"""
<section class="plexi-panel">
<div class="plexi-sidecard-title">More in this bucket</div>
</section>
""",
unsafe_allow_html=True,
)
bucket_items = []
for file_name in file_names:
item_class = "current" if file_name == selected_file_obj["name"] else ""
label = "Current" if file_name == selected_file_obj["name"] else "Available"
bucket_items.append(
f'<li class="{item_class}">{escape(label)}: {escape(format_file_label(file_name))}</li>'
)
st.markdown(
f'<section class="plexi-meta"><ul class="plexi-filelist">{"".join(bucket_items)}</ul></section>',
unsafe_allow_html=True,
)