Spaces:
Sleeping
Sleeping
| 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") |