added Stufff
This commit is contained in:
		
							
								
								
									
										10
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.env
									
									
									
									
									
								
							@@ -1,14 +1,14 @@
 | 
				
			|||||||
# JupyterHub Notebook Viewer Configuration
 | 
					# JupyterHub Notebook Viewer Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Core Settings
 | 
					# Core Settings
 | 
				
			||||||
JUPYTERHUB_SHARED_DIR=/shared
 | 
					JUPYTERHUB_SHARED_DIR=./shared
 | 
				
			||||||
APP_TITLE=JupyterHub Notebook Viewer
 | 
					APP_TITLE="IFN Shared HUB"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Flask Settings
 | 
					# Flask Settings
 | 
				
			||||||
FLASK_HOST=0.0.0.0
 | 
					FLASK_HOST=0.0.0.0
 | 
				
			||||||
FLASK_PORT=5000
 | 
					FLASK_PORT=5001
 | 
				
			||||||
FLASK_DEBUG=True
 | 
					FLASK_DEBUG=True
 | 
				
			||||||
FLASK_SECRET_KEY=your-secret-key-change-in-production
 | 
					FLASK_SECRET_KEY="hoeuhfou0a9ufm08fwncznf0aauuf"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# File Handling
 | 
					# File Handling
 | 
				
			||||||
MAX_FILE_SIZE=16777216
 | 
					MAX_FILE_SIZE=16777216
 | 
				
			||||||
@@ -19,5 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
 | 
				
			|||||||
ENABLE_DOWNLOAD=True
 | 
					ENABLE_DOWNLOAD=True
 | 
				
			||||||
ENABLE_API=True
 | 
					ENABLE_API=True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# UI Settings
 | 
					 | 
				
			||||||
THEME=dark
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
 | 
				
			|||||||
# Feature Toggles
 | 
					# Feature Toggles
 | 
				
			||||||
ENABLE_DOWNLOAD=True
 | 
					ENABLE_DOWNLOAD=True
 | 
				
			||||||
ENABLE_API=True
 | 
					ENABLE_API=True
 | 
				
			||||||
 | 
					 | 
				
			||||||
# UI Settings
 | 
					 | 
				
			||||||
THEME=dark  # dark or light
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								__pycache__/app.cpython-311.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/app.cpython-311.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										296
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										296
									
								
								app.py
									
									
									
									
									
								
							@@ -1,5 +1,6 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from flask import Flask, render_template, request, jsonify, send_file, abort
 | 
					from flask import Flask, render_template, request, jsonify, send_file, abort
 | 
				
			||||||
import nbformat
 | 
					import nbformat
 | 
				
			||||||
@@ -25,26 +26,27 @@ app.config.update({
 | 
				
			|||||||
    'ENABLE_DOWNLOAD': os.environ.get('ENABLE_DOWNLOAD', 'True').lower() == 'true',
 | 
					    'ENABLE_DOWNLOAD': os.environ.get('ENABLE_DOWNLOAD', 'True').lower() == 'true',
 | 
				
			||||||
    'ENABLE_API': os.environ.get('ENABLE_API', 'True').lower() == 'true',
 | 
					    'ENABLE_API': os.environ.get('ENABLE_API', 'True').lower() == 'true',
 | 
				
			||||||
    'APP_TITLE': os.environ.get('APP_TITLE', 'JupyterHub Notebook Viewer'),
 | 
					    'APP_TITLE': os.environ.get('APP_TITLE', 'JupyterHub Notebook Viewer'),
 | 
				
			||||||
    'THEME': os.environ.get('THEME', 'dark'),  # dark or light
 | 
					 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_notebook_files(directory):
 | 
					def get_notebook_files(directory):
 | 
				
			||||||
    """Recursively get all notebook files from directory"""
 | 
					    """Recursively get all notebook files from directory"""
 | 
				
			||||||
    notebooks = []
 | 
					    notebooks = []
 | 
				
			||||||
    allowed_extensions = app.config['ALLOWED_EXTENSIONS']
 | 
					    allowed_extensions = app.config['ALLOWED_EXTENSIONS']
 | 
				
			||||||
    
 | 
					    dir = request.args.get('dir', '')
 | 
				
			||||||
 | 
					    print(dir)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        for root, dirs, files in os.walk(directory):
 | 
					        for root, dirs, files in os.walk(directory):
 | 
				
			||||||
            for file in files:
 | 
					            for file in files:
 | 
				
			||||||
                if any(file.endswith(ext) for ext in allowed_extensions):
 | 
					                if any(file.endswith(ext) for ext in allowed_extensions):
 | 
				
			||||||
                    full_path = os.path.join(root, file)
 | 
					                    full_path = os.path.join(root, file)
 | 
				
			||||||
                    relative_path = os.path.relpath(full_path, directory)
 | 
					                    relative_path = os.path.relpath(full_path, directory)
 | 
				
			||||||
 | 
					                    mtime = datetime.fromtimestamp(os.path.getmtime(full_path)).strftime("%d.%m.%Y %H:%M") 
 | 
				
			||||||
                    notebooks.append({
 | 
					                    notebooks.append({
 | 
				
			||||||
                        'name': file,
 | 
					                        'name': file,
 | 
				
			||||||
                        'path': relative_path,
 | 
					                        'path': os.path.join(dir, relative_path),
 | 
				
			||||||
                        'full_path': full_path,
 | 
					                        'full_path': full_path,
 | 
				
			||||||
                        'size': os.path.getsize(full_path),
 | 
					                        'size': round(os.path.getsize(full_path) / 1024, 2),
 | 
				
			||||||
                        'modified': os.path.getmtime(full_path)
 | 
					                        'modified': mtime 
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
    except (OSError, PermissionError) as e:
 | 
					    except (OSError, PermissionError) as e:
 | 
				
			||||||
        app.logger.error(f"Error accessing directory {directory}: {e}")
 | 
					        app.logger.error(f"Error accessing directory {directory}: {e}")
 | 
				
			||||||
@@ -84,19 +86,7 @@ def convert_notebook_to_html(notebook_path):
 | 
				
			|||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        return f"<div class='alert alert-danger'>Error converting notebook: {str(e)}</div>"
 | 
					        return f"<div class='alert alert-danger'>Error converting notebook: {str(e)}</div>"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route('/')
 | 
					def get_breadcrumbs(current_dir):
 | 
				
			||||||
def index():
 | 
					 | 
				
			||||||
    """Main page showing all notebooks"""
 | 
					 | 
				
			||||||
    current_dir = request.args.get('dir', '')
 | 
					 | 
				
			||||||
    full_current_dir = os.path.join(app.config['SHARED_DIRECTORY'], current_dir)
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if not os.path.exists(full_current_dir):
 | 
					 | 
				
			||||||
        abort(404)
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    notebooks = get_notebook_files(full_current_dir)
 | 
					 | 
				
			||||||
    directories = get_directory_structure(full_current_dir)
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    # Breadcrumb navigation
 | 
					 | 
				
			||||||
    breadcrumbs = []
 | 
					    breadcrumbs = []
 | 
				
			||||||
    if current_dir:
 | 
					    if current_dir:
 | 
				
			||||||
        parts = current_dir.split(os.sep)
 | 
					        parts = current_dir.split(os.sep)
 | 
				
			||||||
@@ -105,6 +95,21 @@ def index():
 | 
				
			|||||||
                'name': part,
 | 
					                'name': part,
 | 
				
			||||||
                'path': os.sep.join(parts[:i+1])
 | 
					                'path': os.sep.join(parts[:i+1])
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					    return breadcrumbs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/')
 | 
				
			||||||
 | 
					def index():
 | 
				
			||||||
 | 
					    """Main page showing all notebooks"""
 | 
				
			||||||
 | 
					    current_dir = request.args.get('dir', '')
 | 
				
			||||||
 | 
					    full_current_dir = os.path.join(app.config['SHARED_DIRECTORY'], current_dir)
 | 
				
			||||||
 | 
					    if not os.path.exists(full_current_dir):
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    notebooks = get_notebook_files(full_current_dir)
 | 
				
			||||||
 | 
					    directories = get_directory_structure(full_current_dir)
 | 
				
			||||||
 | 
					    print(directories)
 | 
				
			||||||
 | 
					    # Breadcrumb navigation
 | 
				
			||||||
 | 
					    breadcrumbs = get_breadcrumbs(current_dir) 
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return render_template('index.html', 
 | 
					    return render_template('index.html', 
 | 
				
			||||||
                         notebooks=notebooks, 
 | 
					                         notebooks=notebooks, 
 | 
				
			||||||
@@ -125,17 +130,15 @@ def view_notebook(notebook_path):
 | 
				
			|||||||
    if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
 | 
					    if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
 | 
				
			||||||
        abort(403)
 | 
					        abort(403)
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Security check - ensure path is within shared directory
 | 
					    breadcrumbs = get_breadcrumbs(notebook_path)
 | 
				
			||||||
    if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    html_content = convert_notebook_to_html(full_path)
 | 
					    html_content = convert_notebook_to_html(full_path)
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    return render_template('notebook.html', 
 | 
					    return render_template('notebook.html', 
 | 
				
			||||||
                         html_content=html_content,
 | 
					                        html_content=html_content,
 | 
				
			||||||
                         notebook_name=os.path.basename(notebook_path),
 | 
					                        notebook_name=os.path.basename(notebook_path),
 | 
				
			||||||
                         notebook_path=notebook_path,
 | 
					                        notebook_path=notebook_path,
 | 
				
			||||||
                         config=app.config)
 | 
					                        breadcrumbs=breadcrumbs,
 | 
				
			||||||
 | 
					                        current_dir=notebook_path,   
 | 
				
			||||||
 | 
					                        config=app.config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route('/download/<path:notebook_path>')
 | 
					@app.route('/download/<path:notebook_path>')
 | 
				
			||||||
def download_notebook(notebook_path):
 | 
					def download_notebook(notebook_path):
 | 
				
			||||||
@@ -153,7 +156,7 @@ def download_notebook(notebook_path):
 | 
				
			|||||||
        abort(403)
 | 
					        abort(403)
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Security check
 | 
					    # Security check
 | 
				
			||||||
    if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
 | 
					    if os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
 | 
				
			||||||
        abort(403)
 | 
					        abort(403)
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return send_file(full_path, as_attachment=True)
 | 
					    return send_file(full_path, as_attachment=True)
 | 
				
			||||||
@@ -184,247 +187,30 @@ def api_notebooks():
 | 
				
			|||||||
@app.errorhandler(404)
 | 
					@app.errorhandler(404)
 | 
				
			||||||
def not_found(error):
 | 
					def not_found(error):
 | 
				
			||||||
    return render_template('error.html', 
 | 
					    return render_template('error.html', 
 | 
				
			||||||
                         error_code=404, 
 | 
					                        error_code=404, 
 | 
				
			||||||
                         error_message="Notebook or directory not found"), 404
 | 
					                        error_message="Notebook or directory not found",
 | 
				
			||||||
 | 
					                        current_dir="404"), 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.errorhandler(403)
 | 
					@app.errorhandler(403)
 | 
				
			||||||
def forbidden(error):
 | 
					def forbidden(error):
 | 
				
			||||||
    return render_template('error.html', 
 | 
					    return render_template('error.html', 
 | 
				
			||||||
                         error_code=403, 
 | 
					                        error_code=403, 
 | 
				
			||||||
                         error_message="Access forbidden"), 403
 | 
					                        error_message="Access forbidden",
 | 
				
			||||||
 | 
					                        current_dir="403"), 403
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.errorhandler(500)
 | 
					@app.errorhandler(500)
 | 
				
			||||||
def server_error(error):
 | 
					def server_error(error):
 | 
				
			||||||
    return render_template('error.html',
 | 
					    return render_template('error.html',
 | 
				
			||||||
                         error_code=500, 
 | 
					                        error_code=500, 
 | 
				
			||||||
                         error_message="Internal server error"), 500
 | 
					                        error_message="Internal server error",
 | 
				
			||||||
 | 
					                        current_dir="500"), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
    # Create templates directory if it doesn't exist
 | 
					 | 
				
			||||||
    os.makedirs('templates', exist_ok=True)
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    # Determine theme classes
 | 
					 | 
				
			||||||
    navbar_class = "navbar-dark bg-dark" if app.config['THEME'] == 'dark' else "navbar-light bg-light"
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    # Basic templates
 | 
					 | 
				
			||||||
    index_template = f'''<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html lang="en">
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					 | 
				
			||||||
    <title>{{{{ config.APP_TITLE }}}}</title>
 | 
					 | 
				
			||||||
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
 | 
					 | 
				
			||||||
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <nav class="navbar {navbar_class}">
 | 
					 | 
				
			||||||
        <div class="container">
 | 
					 | 
				
			||||||
            <a class="navbar-brand" href="{{{{ url_for('index') }}}}">
 | 
					 | 
				
			||||||
                <i class="fas fa-book"></i> {{{{ config.APP_TITLE }}}}
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </nav>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="container mt-4">
 | 
					 | 
				
			||||||
        <!-- Breadcrumb Navigation -->
 | 
					 | 
				
			||||||
        {{% if breadcrumbs %}}
 | 
					 | 
				
			||||||
        <nav aria-label="breadcrumb">
 | 
					 | 
				
			||||||
            <ol class="breadcrumb">
 | 
					 | 
				
			||||||
                <li class="breadcrumb-item"><a href="{{{{ url_for('index') }}}}">Home</a></li>
 | 
					 | 
				
			||||||
                {{% for crumb in breadcrumbs %}}
 | 
					 | 
				
			||||||
                <li class="breadcrumb-item">
 | 
					 | 
				
			||||||
                    <a href="{{{{ url_for('index', dir=crumb.path) }}}}">{{{{ crumb.name }}}}</a>
 | 
					 | 
				
			||||||
                </li>
 | 
					 | 
				
			||||||
                {{% endfor %}}
 | 
					 | 
				
			||||||
            </ol>
 | 
					 | 
				
			||||||
        </nav>
 | 
					 | 
				
			||||||
        {{% endif %}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <h1 class="mb-4">
 | 
					 | 
				
			||||||
            {{% if current_dir %}}
 | 
					 | 
				
			||||||
                Notebooks in {{{{ current_dir }}}}
 | 
					 | 
				
			||||||
            {{% else %}}
 | 
					 | 
				
			||||||
                All Notebooks
 | 
					 | 
				
			||||||
            {{% endif %}}
 | 
					 | 
				
			||||||
        </h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <!-- Directories -->
 | 
					 | 
				
			||||||
        {{% if directories %}}
 | 
					 | 
				
			||||||
        <div class="row mb-4">
 | 
					 | 
				
			||||||
            <div class="col-12">
 | 
					 | 
				
			||||||
                <h3><i class="fas fa-folder"></i> Directories</h3>
 | 
					 | 
				
			||||||
                {{% for dir in directories %}}
 | 
					 | 
				
			||||||
                <div class="card mb-2">
 | 
					 | 
				
			||||||
                    <div class="card-body">
 | 
					 | 
				
			||||||
                        <h5 class="card-title">
 | 
					 | 
				
			||||||
                            <a href="{{{{ url_for('index', dir=dir.path) }}}}" class="text-decoration-none">
 | 
					 | 
				
			||||||
                                <i class="fas fa-folder text-warning"></i> {{{{ dir.name }}}}
 | 
					 | 
				
			||||||
                            </a>
 | 
					 | 
				
			||||||
                        </h5>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                {{% endfor %}}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        {{% endif %}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <!-- Notebooks -->
 | 
					 | 
				
			||||||
        <div class="row">
 | 
					 | 
				
			||||||
            <div class="col-12">
 | 
					 | 
				
			||||||
                <h3><i class="fas fa-file-code"></i> Notebooks</h3>
 | 
					 | 
				
			||||||
                {{% if notebooks %}}
 | 
					 | 
				
			||||||
                    {{% for notebook in notebooks %}}
 | 
					 | 
				
			||||||
                    <div class="card mb-3">
 | 
					 | 
				
			||||||
                        <div class="card-body">
 | 
					 | 
				
			||||||
                            <div class="row align-items-center">
 | 
					 | 
				
			||||||
                                <div class="col-md-8">
 | 
					 | 
				
			||||||
                                    <h5 class="card-title">
 | 
					 | 
				
			||||||
                                        <i class="fas fa-file-code text-primary"></i> {{{{ notebook.name }}}}
 | 
					 | 
				
			||||||
                                    </h5>
 | 
					 | 
				
			||||||
                                    <p class="card-text text-muted">
 | 
					 | 
				
			||||||
                                        <small>
 | 
					 | 
				
			||||||
                                            Size: {{{{ "%.1f"|format(notebook.size/1024) }}}} KB | 
 | 
					 | 
				
			||||||
                                            Modified: {{{{ notebook.modified|int|datetime }}}}
 | 
					 | 
				
			||||||
                                        </small>
 | 
					 | 
				
			||||||
                                    </p>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                                <div class="col-md-4 text-end">
 | 
					 | 
				
			||||||
                                    <a href="{{{{ url_for('view_notebook', notebook_path=notebook.path) }}}}" 
 | 
					 | 
				
			||||||
                                       class="btn btn-primary btn-sm me-2">
 | 
					 | 
				
			||||||
                                        <i class="fas fa-eye"></i> View
 | 
					 | 
				
			||||||
                                    </a>
 | 
					 | 
				
			||||||
                                    {{% if config.ENABLE_DOWNLOAD %}}
 | 
					 | 
				
			||||||
                                    <a href="{{{{ url_for('download_notebook', notebook_path=notebook.path) }}}}" 
 | 
					 | 
				
			||||||
                                       class="btn btn-secondary btn-sm">
 | 
					 | 
				
			||||||
                                        <i class="fas fa-download"></i> Download
 | 
					 | 
				
			||||||
                                    </a>
 | 
					 | 
				
			||||||
                                    {{% endif %}}
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    {{% endfor %}}
 | 
					 | 
				
			||||||
                    {{% if notebooks|length == config.NOTEBOOKS_PER_PAGE %}}
 | 
					 | 
				
			||||||
                    <div class="alert alert-info">
 | 
					 | 
				
			||||||
                        <i class="fas fa-info-circle"></i> Showing first {{{{ config.NOTEBOOKS_PER_PAGE }}}} notebooks. Configure NOTEBOOKS_PER_PAGE to show more.
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    {{% endif %}}
 | 
					 | 
				
			||||||
                {{% else %}}
 | 
					 | 
				
			||||||
                    <div class="alert alert-info">
 | 
					 | 
				
			||||||
                        <i class="fas fa-info-circle"></i> No notebooks found in this directory.
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                {{% endif %}}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>'''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    notebook_template = f'''<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html lang="en">
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					 | 
				
			||||||
    <title>{{{{ notebook_name }}}} - {{{{ config.APP_TITLE }}}}</title>
 | 
					 | 
				
			||||||
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
 | 
					 | 
				
			||||||
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        .notebook-content {{
 | 
					 | 
				
			||||||
            max-width: 100%;
 | 
					 | 
				
			||||||
            overflow-x: auto;
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
        .notebook-content img {{
 | 
					 | 
				
			||||||
            max-width: 100%;
 | 
					 | 
				
			||||||
            height: auto;
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <nav class="navbar {navbar_class}">
 | 
					 | 
				
			||||||
        <div class="container">
 | 
					 | 
				
			||||||
            <a class="navbar-brand" href="{{{{ url_for('index') }}}}">
 | 
					 | 
				
			||||||
                <i class="fas fa-book"></i> {{{{ config.APP_TITLE }}}}
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                {{% if config.ENABLE_DOWNLOAD %}}
 | 
					 | 
				
			||||||
                <a href="{{{{ url_for('download_notebook', notebook_path=notebook_path) }}}}" 
 | 
					 | 
				
			||||||
                   class="btn btn-outline-light btn-sm">
 | 
					 | 
				
			||||||
                    <i class="fas fa-download"></i> Download
 | 
					 | 
				
			||||||
                </a>
 | 
					 | 
				
			||||||
                {{% endif %}}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </nav>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="container-fluid mt-4">
 | 
					 | 
				
			||||||
        <div class="row">
 | 
					 | 
				
			||||||
            <div class="col-12">
 | 
					 | 
				
			||||||
                <h2 class="mb-3">
 | 
					 | 
				
			||||||
                    <i class="fas fa-file-code text-primary"></i> {{{{ notebook_name }}}}
 | 
					 | 
				
			||||||
                </h2>
 | 
					 | 
				
			||||||
                <div class="notebook-content">
 | 
					 | 
				
			||||||
                    {{{{ html_content|safe }}}}
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>'''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    error_template = f'''<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html lang="en">
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					 | 
				
			||||||
    <title>Error {{{{ error_code }}}} - {{{{ config.APP_TITLE if config else 'JupyterHub Notebook Viewer' }}}}</title>
 | 
					 | 
				
			||||||
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
 | 
					 | 
				
			||||||
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <nav class="navbar {navbar_class}">
 | 
					 | 
				
			||||||
        <div class="container">
 | 
					 | 
				
			||||||
            <a class="navbar-brand" href="{{{{ url_for('index') }}}}">
 | 
					 | 
				
			||||||
                <i class="fas fa-book"></i> {{{{ config.APP_TITLE if config else 'JupyterHub Notebook Viewer' }}}}
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </nav>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="container mt-5">
 | 
					 | 
				
			||||||
        <div class="row justify-content-center">
 | 
					 | 
				
			||||||
            <div class="col-md-6 text-center">
 | 
					 | 
				
			||||||
                <div class="alert alert-danger">
 | 
					 | 
				
			||||||
                    <h1><i class="fas fa-exclamation-triangle"></i></h1>
 | 
					 | 
				
			||||||
                    <h2>Error {{{{ error_code }}}}</h2>
 | 
					 | 
				
			||||||
                    <p>{{{{ error_message }}}}</p>
 | 
					 | 
				
			||||||
                    <a href="{{{{ url_for('index') }}}}" class="btn btn-primary">
 | 
					 | 
				
			||||||
                        <i class="fas fa-home"></i> Go Home
 | 
					 | 
				
			||||||
                    </a>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>'''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Write templates
 | 
					 | 
				
			||||||
    with open('templates/index.html', 'w') as f:
 | 
					 | 
				
			||||||
        f.write(index_template)
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    with open('templates/notebook.html', 'w') as f:
 | 
					 | 
				
			||||||
        f.write(notebook_template)
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    with open('templates/error.html', 'w') as f:
 | 
					 | 
				
			||||||
        f.write(error_template)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Add datetime filter
 | 
					    # Add datetime filter
 | 
				
			||||||
    @app.template_filter('datetime')
 | 
					    @app.template_filter('datetime')
 | 
				
			||||||
    def datetime_filter(timestamp):
 | 
					    def datetime_filter(timestamp):
 | 
				
			||||||
        from datetime import datetime
 | 
					        from datetime import datetime
 | 
				
			||||||
        return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
 | 
					        return datetime.fromtimestamp(timestamp).strftime('%d.%m.%Y %H:%M:%S')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    print(f"Starting {app.config['APP_TITLE']}")
 | 
					    print(f"Starting {app.config['APP_TITLE']}")
 | 
				
			||||||
    print(f"Shared directory: {app.config['SHARED_DIRECTORY']}")
 | 
					    print(f"Shared directory: {app.config['SHARED_DIRECTORY']}")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -85,7 +85,7 @@
 | 
				
			|||||||
          netcat
 | 
					          netcat
 | 
				
			||||||
          
 | 
					          
 | 
				
			||||||
          # Process management
 | 
					          # Process management
 | 
				
			||||||
          supervisor
 | 
					          #supervisor
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Shell script for easy setup
 | 
					        # Shell script for easy setup
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								src/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/app.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					from flask import Flask, render_template, request
 | 
				
			||||||
 | 
					import os 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app = Flask(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app_title = os.getenv('APP_TITLE', 'JupyShare')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_breadcrumbs():
 | 
				
			||||||
 | 
					    path_parts = request.path.split('/')[1:]  # Remove empty first element
 | 
				
			||||||
 | 
					    breadcrumbs = []
 | 
				
			||||||
 | 
					    accumulated_path = ''
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for i, part in enumerate(path_parts):
 | 
				
			||||||
 | 
					        if not part:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        accumulated_path += f'/{part}'
 | 
				
			||||||
 | 
					        breadcrumbs.append({
 | 
				
			||||||
 | 
					            'name': part.capitalize(),
 | 
				
			||||||
 | 
					            'url': accumulated_path,
 | 
				
			||||||
 | 
					            'is_active': (i == len(path_parts) - 1)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Always include home at the beginning
 | 
				
			||||||
 | 
					    if not breadcrumbs or breadcrumbs[0]['name'].lower() != 'home':
 | 
				
			||||||
 | 
					        breadcrumbs.insert(0, {
 | 
				
			||||||
 | 
					            'name': 'Home',
 | 
				
			||||||
 | 
					            'url': '/',
 | 
				
			||||||
 | 
					            'is_active': len(path_parts) == 0
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return breadcrumbs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/')
 | 
				
			||||||
 | 
					def home():
 | 
				
			||||||
 | 
					    print(get_breadcrumbs())
 | 
				
			||||||
 | 
					    return render_template('index.html', 
 | 
				
			||||||
 | 
					                            app_title=app_title,
 | 
				
			||||||
 | 
					                            breadcrumbs=get_breadcrumbs()
 | 
				
			||||||
 | 
					                           )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    app.run(debug=True)
 | 
				
			||||||
							
								
								
									
										79
									
								
								src/static/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/static/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					* {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					    font-family: Arial, sans-serif;
 | 
				
			||||||
 | 
					    line-height: 1.6;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main {
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    max-width: 800px;
 | 
				
			||||||
 | 
					    margin: 80px auto 0;
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    border: 2px solid red;
 | 
				
			||||||
 | 
					    width: 80%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h1 {
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar {
 | 
				
			||||||
 | 
					    background-color: #333;
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					    padding: 15px 0;
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    width: 100vw;
 | 
				
			||||||
 | 
					    left: 0;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    z-index: 1000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    max-width: 800px;
 | 
				
			||||||
 | 
					    margin: 0 auto;
 | 
				
			||||||
 | 
					    padding: 0 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.logo {
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					    font-size: 1.5em;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Breadcrumbs */
 | 
				
			||||||
 | 
					.breadcrumbs {
 | 
				
			||||||
 | 
					    padding: 15px 0;
 | 
				
			||||||
 | 
					    font-size: 0.9em;
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    margin-bottom: 20px;
 | 
				
			||||||
 | 
					    border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-link {
 | 
				
			||||||
 | 
					    color: #3498db;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-link:hover {
 | 
				
			||||||
 | 
					    text-decoration: underline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-separator {
 | 
				
			||||||
 | 
					    margin: 0 5px;
 | 
				
			||||||
 | 
					    color: #999;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-current {
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								src/templates/_breadcrumbs.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/templates/_breadcrumbs.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<div class="breadcrumbs">
 | 
				
			||||||
 | 
					    {% for crumb in breadcrumbs %}
 | 
				
			||||||
 | 
					        {% if not crumb.is_active %}
 | 
				
			||||||
 | 
					            <a href="{{ crumb.url }}" class="breadcrumb-link">{{ crumb.name }}</a>
 | 
				
			||||||
 | 
					            <span class="breadcrumb-separator">/</span>
 | 
				
			||||||
 | 
					        {% else %}
 | 
				
			||||||
 | 
					            <span class="breadcrumb-current">{{ crumb.name }}</span>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										24
									
								
								src/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/templates/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <title>{{ app_title }}</title>
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <nav class="navbar">
 | 
				
			||||||
 | 
					        <div class="nav-container">
 | 
				
			||||||
 | 
					            <a href="{{ url_for('home') }}" class="logo">{{ app_title }} </a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <main>
 | 
				
			||||||
 | 
					        <h1>All Notebooks</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {% include '_breadcrumbs.html' %}
 | 
				
			||||||
 | 
					        <p>This is a basic Flask application.</p>
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										233
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,233 @@
 | 
				
			|||||||
 | 
					/* Reset defaults */ 
 | 
				
			||||||
 | 
					.body {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    font-family: Arial, sans-serif;
 | 
				
			||||||
 | 
					    background-color: #f7f7f7;
 | 
				
			||||||
 | 
					    color: #333333;
 | 
				
			||||||
 | 
					    line-height: 1.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.main {
 | 
				
			||||||
 | 
					    max-width: 80vw;
 | 
				
			||||||
 | 
					    margin: 20px auto;
 | 
				
			||||||
 | 
					    padding: 15px;
 | 
				
			||||||
 | 
					    padding-top: 0px;
 | 
				
			||||||
 | 
					    background-color: white;
 | 
				
			||||||
 | 
					    border: 1px solid #e0e0e0;
 | 
				
			||||||
 | 
					    border-radius: 2px;
 | 
				
			||||||
 | 
					    box-shadow: 0 1px 2px rgba(0,0,0,0.7);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					i {
 | 
				
			||||||
 | 
					    color: #ffD43b;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.a {
 | 
				
			||||||
 | 
					    color: #007bff;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					    transition: color 0.15s ease-in-out;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.a:hover {
 | 
				
			||||||
 | 
					    color: #0056b3;
 | 
				
			||||||
 | 
					    text-decoration: underline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					hr {
 | 
				
			||||||
 | 
					    margin: 10px 0;
 | 
				
			||||||
 | 
					    margin-bottom: 15px;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Navbar */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar {
 | 
				
			||||||
 | 
					    position: sticky;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    background-color: white;
 | 
				
			||||||
 | 
					    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
 | 
				
			||||||
 | 
					    z-index: 1000;
 | 
				
			||||||
 | 
					    padding: 15px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar img {
 | 
				
			||||||
 | 
					    height: 68px;
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    max-width: 1200px;
 | 
				
			||||||
 | 
					    margin: 0 auto;
 | 
				
			||||||
 | 
					    padding: 0 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-middle {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-right {
 | 
				
			||||||
 | 
					    background-color: #f0f0f0;
 | 
				
			||||||
 | 
					    padding: 8px 15px;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					    border: 1px solid #ddd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Breadcrum */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					    padding: 0.75rem 1rem;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    list-style: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-item {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-item:not(:last-child) + .breadcrumb-item::before {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    padding: 0 0.25rem;
 | 
				
			||||||
 | 
					    color: #6c757d;
 | 
				
			||||||
 | 
					    content: "→";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-item:last-child {
 | 
				
			||||||
 | 
					    color: #495057;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-item.active {
 | 
				
			||||||
 | 
					    color: #6c757d;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					    cursor: default;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					nav[aria-label="breadcrumb"] {
 | 
				
			||||||
 | 
					    margin: 1rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Shared */
 | 
				
			||||||
 | 
					.row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.row h3 {
 | 
				
			||||||
 | 
					    padding: 0 15px;
 | 
				
			||||||
 | 
					    font-size: 1.4rem;
 | 
				
			||||||
 | 
					    margin-top: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.row a {
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.notebook-header {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    font-size: 1.5rem;
 | 
				
			||||||
 | 
					    margin: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Card */
 | 
				
			||||||
 | 
					.card-container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    border: 1px solid #6c757d;
 | 
				
			||||||
 | 
					    border-radius: 2px;
 | 
				
			||||||
 | 
					    margin: 10px;
 | 
				
			||||||
 | 
					    padding: 15px 25px;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-stats {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-stats p {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					/* Buttons */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.buttons {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    justify-content: right;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    gap: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button {
 | 
				
			||||||
 | 
					    padding: 5px 10px;
 | 
				
			||||||
 | 
					    border-radius: 5px;
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					    transition: all 0.3 ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button:hover {
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button i {
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.view-button {
 | 
				
			||||||
 | 
					    background-color: blue;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.download-button {
 | 
				
			||||||
 | 
					    background-color: grey;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Error */ 
 | 
				
			||||||
 | 
					.error {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error h1 {
 | 
				
			||||||
 | 
					    font-size: 3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error strong {
 | 
				
			||||||
 | 
					    font-size: 1.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error i {
 | 
				
			||||||
 | 
					    color: red;
 | 
				
			||||||
 | 
					    font-size: 8rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								templates/_breadcrumb.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								templates/_breadcrumb.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					<nav aria-label="breadcrumb">
 | 
				
			||||||
 | 
					    <ol class="breadcrumb">
 | 
				
			||||||
 | 
					        <li class="breadcrumb-item"><a href="{{ url_for('index') }}" class="a">Home</a></li>
 | 
				
			||||||
 | 
					        {% for crumb in breadcrumbs %}
 | 
				
			||||||
 | 
					        <li class="breadcrumb-item">
 | 
				
			||||||
 | 
					            <a href="{{ url_for('index', dir=crumb.path) }}" class="a">{{ crumb.name }}</a>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					        {% endfor %}
 | 
				
			||||||
 | 
					    </ol>
 | 
				
			||||||
 | 
					    <hr>
 | 
				
			||||||
 | 
					</nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										7
									
								
								templates/_folder.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								templates/_folder.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					<div class="card-container">
 | 
				
			||||||
 | 
					    <div class="card">
 | 
				
			||||||
 | 
					        <a href="{{ url_for('index', dir=dir.path) }}" class="a">
 | 
				
			||||||
 | 
					            <i class="fas fa-folder"></i> {{ dir.name }}
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										19
									
								
								templates/_navbar.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								templates/_navbar.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<nav class="navbar">
 | 
				
			||||||
 | 
					    <div class="navbar-container">
 | 
				
			||||||
 | 
					        <a href="{{ url_for('index') }}">
 | 
				
			||||||
 | 
					            <img src="https://www.tu-braunschweig.de/fileadmin/Logos_Einrichtungen/Institute_FK5/logo_IFN.svg"/>
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="navbar-middle">
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <h1>
 | 
				
			||||||
 | 
					                {% if current_dir %}
 | 
				
			||||||
 | 
					                    {{ current_dir }}
 | 
				
			||||||
 | 
					                {% else %}
 | 
				
			||||||
 | 
					                    All Notebooks
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					            </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>        
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</nav>
 | 
				
			||||||
							
								
								
									
										21
									
								
								templates/_notebook.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								templates/_notebook.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					<div class="card-container">
 | 
				
			||||||
 | 
					    <div class="card">
 | 
				
			||||||
 | 
					        <a href="{{ url_for('view_notebook', notebook_path=notebook.path) }}" class="a">
 | 
				
			||||||
 | 
					            <i class="fas fa-file-code"></i> {{ notebook.name }}
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="buttons">
 | 
				
			||||||
 | 
					            <div class="card-stats">
 | 
				
			||||||
 | 
					                <p>Last Modified: <strong>{{ notebook.modified }}</strong></p>
 | 
				
			||||||
 | 
					                <p>Size: <strong>{{ notebook.size }} KB</strong></p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {% if config.ENABLE_DOWNLOAD %}
 | 
				
			||||||
 | 
					            <a href="{{ url_for('download_notebook', notebook_path=notebook.path) }}" class="button download-button">
 | 
				
			||||||
 | 
					                <i class="fas fa-download"></i> Download
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										19
									
								
								templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								templates/base.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"/>
 | 
				
			||||||
 | 
					    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					    <title>{{ config.APP_TITLE }}</title>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body class="body">
 | 
				
			||||||
 | 
					    {% include '_navbar.html' %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <main class="main">
 | 
				
			||||||
 | 
					        {% include '_breadcrumb.html' %}
 | 
				
			||||||
 | 
					        {% block content %} 
 | 
				
			||||||
 | 
					        {% endblock %}
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										9
									
								
								templates/error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								templates/error.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					    <div class="error">
 | 
				
			||||||
 | 
					        <i class="fas fa-triangle-exclamation"></i>
 | 
				
			||||||
 | 
					        <h1>Error {{error_code}}</h1>
 | 
				
			||||||
 | 
					        <strong>{{error_message}}</strong>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										27
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					    <!-- Directories -->
 | 
				
			||||||
 | 
					    {% if directories %}
 | 
				
			||||||
 | 
					        <div class="row">
 | 
				
			||||||
 | 
					                <h3><i class="fas fa-folder"></i> Directories</h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {% for dir in directories %}
 | 
				
			||||||
 | 
					                {% include '_folder.html' %}
 | 
				
			||||||
 | 
					                {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <hr />
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Notebooks -->
 | 
				
			||||||
 | 
					    {% if notebooks %}
 | 
				
			||||||
 | 
					        <div class="row">
 | 
				
			||||||
 | 
					            <h3><i class="fas fa-file-code"></i> Notebooks</h3>
 | 
				
			||||||
 | 
					            {% for notebook in notebooks %}
 | 
				
			||||||
 | 
					            {% include '_notebook.html' %}
 | 
				
			||||||
 | 
					            {% endfor %}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										14
									
								
								templates/notebook.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								templates/notebook.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					{% extends 'base.html' %}
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					    <div class="notebook-header">
 | 
				
			||||||
 | 
					        <h2>
 | 
				
			||||||
 | 
					            <i class="fas fa-file-code text-primary"></i> {{ notebook_name }}
 | 
				
			||||||
 | 
					        </h2>
 | 
				
			||||||
 | 
					        {% if config.ENABLE_DOWNLOAD %}
 | 
				
			||||||
 | 
					            <a href="{{ url_for('download_notebook', notebook_path=notebook_path) }}" class="a button download-button">
 | 
				
			||||||
 | 
					                <i class="fas fa-download"></i> Download
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    {{ html_content|safe }}
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user