import streamlit as st import os import subprocess from streamlit_pdf_viewer import pdf_viewer from github import Github import shutil # Make sure this is imported at the top # --- Configuration --- TOPICS_DIR = "topics" TEMP_DIR = "temp_build" os.makedirs(TEMP_DIR, exist_ok=True) st.set_page_config(page_title="Physics Topics", layout="wide") # ========================================== # 🔒 AUTHENTICATION LOGIC (DUAL LEVEL) # ========================================== def check_login(): """ Returns: 'admin', 'viewer', or None """ if "user_role" not in st.session_state: st.session_state.user_role = None if st.session_state.user_role: return st.session_state.user_role st.title("🔒 Access Restricted") with st.form("login_form"): password_input = st.text_input("Enter Access Password", type="password") submit_button = st.form_submit_button("Login") if submit_button: if password_input == st.secrets["admin_password"]: st.session_state.user_role = "admin" st.success("Logged in as Administrator") st.rerun() elif password_input == st.secrets["viewer_password"]: st.session_state.user_role = "viewer" st.success("Logged in as Viewer") st.rerun() else: st.error("❌ Invalid Password") return None # STOP IF NOT LOGGED IN current_role = check_login() if not current_role: st.stop() # ========================================== # 🐙 GITHUB SYNC FUNCTIONS # ========================================== def push_to_github(local_path, content, commit_message): """ Updates the file on GitHub. local_path: 'topics/Topic1/file.tex' """ try: g = Github(st.secrets["github_token"]) repo = g.get_repo(st.secrets["github_repo"]) # Get the file from the repo to retrieve its SHA (needed for update) contents = repo.get_contents(local_path, ref=st.secrets["github_branch"]) # Update the file repo.update_file( path=contents.path, message=commit_message, content=content, sha=contents.sha, branch=st.secrets["github_branch"] ) return True, "Successfully pushed to GitHub!" except Exception as e: return False, f"GitHub Error: {str(e)}" def pull_from_github(): """ 1. Wipes the local 'topics' folder. 2. Downloads a fresh copy from GitHub. """ try: # 1. WIPE LOCAL FOLDER CLEAN if os.path.exists(TOPICS_DIR): shutil.rmtree(TOPICS_DIR) # Deletes the folder and everything inside # Re-create the empty folder os.makedirs(TOPICS_DIR, exist_ok=True) # 2. CONNECT TO GITHUB g = Github(st.secrets["github_token"]) repo = g.get_repo(st.secrets["github_repo"]) # 3. DOWNLOAD EVERYTHING contents = repo.get_contents(TOPICS_DIR, ref=st.secrets["github_branch"]) count = 0 while contents: file_content = contents.pop(0) if file_content.type == "dir": contents.extend(repo.get_contents(file_content.path, ref=st.secrets["github_branch"])) else: # Calculate where to save it locally # We strip the TOPICS_DIR prefix from the GitHub path to ensure correct nesting local_path = file_content.path # Ensure the subfolder exists os.makedirs(os.path.dirname(local_path), exist_ok=True) # Write the file with open(local_path, "wb") as f: f.write(file_content.decoded_content) count += 1 return True, f"Clean sync complete! Downloaded {count} files." except Exception as e: return False, str(e) # ========================================== # 🚀 MAIN APP LOGIC # ========================================== def get_topics(): if not os.path.exists(TOPICS_DIR): return [] return sorted([d for d in os.listdir(TOPICS_DIR) if os.path.isdir(os.path.join(TOPICS_DIR, d))]) def get_topic_files(topic): topic_path = os.path.join(TOPICS_DIR, topic) return sorted([f for f in os.listdir(topic_path) if f.lower().endswith(('.tex', '.pdf'))]) def compile_latex(file_path): file_name = os.path.basename(file_path) job_name = os.path.splitext(file_name)[0] # 1. Use ABSOLUTE PATH for the output directory # (This prevents "cannot find directory" errors) abs_temp_dir = os.path.abspath(TEMP_DIR) try: process = subprocess.run( [ "pdflatex", "-interaction=nonstopmode", f"-output-directory={abs_temp_dir}", # <--- CHANGE THIS f"-jobname={job_name}", file_path ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # Check for the PDF in the absolute path pdf_path = os.path.join(abs_temp_dir, f"{job_name}.pdf") # <--- AND THIS if process.returncode == 0 and os.path.exists(pdf_path): return pdf_path, None else: return None, process.stdout except Exception as e: return None, str(e) # --- SIDEBAR --- st.sidebar.title(f"Physics Archive ({current_role.title()})") # ADMIN ONLY: Sync Button if current_role == 'admin' or current_role == 'viewer': if st.sidebar.button("🔄 Pull from GitHub"): with st.spinner("Downloading latest files..."): success, msg = pull_from_github() if success: st.sidebar.success(msg) st.rerun() else: st.sidebar.error(msg) if st.sidebar.button("Logout", type="secondary"): st.session_state.user_role = None st.rerun() topics = get_topics() if not topics: st.sidebar.error("No topics found.") st.stop() selected_topic = st.sidebar.selectbox("Select Topic", topics) st.sidebar.markdown("---") files = get_topic_files(selected_topic) if not files: st.sidebar.warning("No files found.") st.stop() selected_file = st.sidebar.radio(f"Documents in {selected_topic}", files) # --- FILE PATHS --- # Relative path (for GitHub): "topics/Topic1/File.tex" rel_path = os.path.join(TOPICS_DIR, selected_topic, selected_file).replace("\\", "/") # Absolute path (for Python/OS): abs_path = os.path.abspath(rel_path) # --- COMPILATION LOGIC (VIEWER & ADMIN) --- # We track if a file was just saved to trigger a re-compile if "force_recompile" not in st.session_state: st.session_state.force_recompile = False if "last_processed" not in st.session_state: st.session_state.last_processed = None # If file changed OR we just saved an edit: if st.session_state.last_processed != rel_path or st.session_state.force_recompile: # 1. PDF Files if selected_file.lower().endswith(".pdf"): st.session_state.current_pdf = abs_path st.session_state.compilation_error = None # 2. LaTeX Files elif selected_file.lower().endswith(".tex"): with st.spinner(f"Compiling {selected_file}..."): pdf, log = compile_latex(abs_path) st.session_state.current_pdf = pdf st.session_state.compilation_error = log st.session_state.last_processed = rel_path st.session_state.force_recompile = False # --- TABS --- # Admin gets "Edit Source", Viewer gets "Source Code" tab_label = "✏️ Edit Source" if current_role == 'admin' else "📝 Source Code" tab_view, tab_edit = st.tabs(["📄 Document Viewer", tab_label]) # 1. VIEWER TAB with tab_view: if st.session_state.current_pdf and os.path.exists(st.session_state.current_pdf): col1, col2 = st.columns([6, 1]) with col1: st.success(f"**{selected_file}** loaded.") with col2: with open(st.session_state.current_pdf, "rb") as f: st.download_button("⬇️ PDF", f, file_name=selected_file.replace('.tex','.pdf'), mime="application/pdf", type="primary") st.markdown("---") pdf_viewer(st.session_state.current_pdf, width=800, height=1000) elif st.session_state.compilation_error: st.error("⚠️ Compilation Failed") with st.expander("Error Log"): st.code(st.session_state.compilation_error) # 2. EDIT / SOURCE TAB with tab_edit: if selected_file.lower().endswith(".pdf"): st.info("PDF files cannot be edited directly.") else: # Read current content with open(abs_path, "r", encoding="utf-8") as f: file_content = f.read() if current_role == 'admin': st.warning(f"⚠️ You are editing: {selected_file}") # The Text Editor new_content = st.text_area( "LaTeX Source", value=file_content, height=600, # CRITICAL FIX: Use the file path as the key so the box resets when you switch files key=abs_path ) # SAVE BUTTON if st.button("💾 Save to GitHub & Re-Render", type="primary"): if new_content != file_content: # 1. Save locally (so valid for compilation immediately) with open(abs_path, "w", encoding="utf-8") as f: f.write(new_content) # 2. Push to GitHub with st.spinner("Pushing to GitHub..."): success, msg = push_to_github( rel_path, new_content, f"Update {selected_file} via Streamlit Admin" ) if success: st.success(msg) # Trigger re-compile on next run st.session_state.force_recompile = True st.rerun() else: st.error(msg) else: st.info("No changes detected.") else: # Viewer Mode (Read Only) st.caption(f"Path: {rel_path}") st.code(file_content, language="latex")