added Stufff

This commit is contained in:
DerGrumpf 2025-06-23 16:52:42 +02:00
parent f1fe4aa05a
commit d9b50ac103
18 changed files with 564 additions and 266 deletions

10
.env
View File

@ -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

View File

@ -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

Binary file not shown.

298
app.py
View File

@ -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']}")

View File

@ -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
View 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
View 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;
}

View 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
View 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
View 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;
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 %}