added Stufff
This commit is contained in:
parent
f1fe4aa05a
commit
d9b50ac103
10
.env
10
.env
@ -1,14 +1,14 @@
|
||||
# JupyterHub Notebook Viewer Configuration
|
||||
|
||||
# Core Settings
|
||||
JUPYTERHUB_SHARED_DIR=/shared
|
||||
APP_TITLE=JupyterHub Notebook Viewer
|
||||
JUPYTERHUB_SHARED_DIR=./shared
|
||||
APP_TITLE="IFN Shared HUB"
|
||||
|
||||
# Flask Settings
|
||||
FLASK_HOST=0.0.0.0
|
||||
FLASK_PORT=5000
|
||||
FLASK_PORT=5001
|
||||
FLASK_DEBUG=True
|
||||
FLASK_SECRET_KEY=your-secret-key-change-in-production
|
||||
FLASK_SECRET_KEY="hoeuhfou0a9ufm08fwncznf0aauuf"
|
||||
|
||||
# File Handling
|
||||
MAX_FILE_SIZE=16777216
|
||||
@ -19,5 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
|
||||
ENABLE_DOWNLOAD=True
|
||||
ENABLE_API=True
|
||||
|
||||
# UI Settings
|
||||
THEME=dark
|
||||
|
@ -19,6 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
|
||||
# Feature Toggles
|
||||
ENABLE_DOWNLOAD=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.
298
app.py
298
app.py
@ -1,5 +1,6 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template, request, jsonify, send_file, abort
|
||||
import nbformat
|
||||
@ -25,26 +26,27 @@ app.config.update({
|
||||
'ENABLE_DOWNLOAD': os.environ.get('ENABLE_DOWNLOAD', 'True').lower() == 'true',
|
||||
'ENABLE_API': os.environ.get('ENABLE_API', 'True').lower() == 'true',
|
||||
'APP_TITLE': os.environ.get('APP_TITLE', 'JupyterHub Notebook Viewer'),
|
||||
'THEME': os.environ.get('THEME', 'dark'), # dark or light
|
||||
})
|
||||
|
||||
def get_notebook_files(directory):
|
||||
"""Recursively get all notebook files from directory"""
|
||||
notebooks = []
|
||||
allowed_extensions = app.config['ALLOWED_EXTENSIONS']
|
||||
|
||||
dir = request.args.get('dir', '')
|
||||
print(dir)
|
||||
try:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
if any(file.endswith(ext) for ext in allowed_extensions):
|
||||
full_path = os.path.join(root, file)
|
||||
relative_path = os.path.relpath(full_path, directory)
|
||||
mtime = datetime.fromtimestamp(os.path.getmtime(full_path)).strftime("%d.%m.%Y %H:%M")
|
||||
notebooks.append({
|
||||
'name': file,
|
||||
'path': relative_path,
|
||||
'path': os.path.join(dir, relative_path),
|
||||
'full_path': full_path,
|
||||
'size': os.path.getsize(full_path),
|
||||
'modified': os.path.getmtime(full_path)
|
||||
'size': round(os.path.getsize(full_path) / 1024, 2),
|
||||
'modified': mtime
|
||||
})
|
||||
except (OSError, PermissionError) as 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:
|
||||
return f"<div class='alert alert-danger'>Error converting notebook: {str(e)}</div>"
|
||||
|
||||
@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)
|
||||
|
||||
# Breadcrumb navigation
|
||||
def get_breadcrumbs(current_dir):
|
||||
breadcrumbs = []
|
||||
if current_dir:
|
||||
parts = current_dir.split(os.sep)
|
||||
@ -105,6 +95,21 @@ def index():
|
||||
'name': part,
|
||||
'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',
|
||||
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']):
|
||||
abort(403)
|
||||
|
||||
# Security check - ensure path is within shared directory
|
||||
if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
|
||||
abort(403)
|
||||
|
||||
breadcrumbs = get_breadcrumbs(notebook_path)
|
||||
html_content = convert_notebook_to_html(full_path)
|
||||
|
||||
return render_template('notebook.html',
|
||||
html_content=html_content,
|
||||
notebook_name=os.path.basename(notebook_path),
|
||||
notebook_path=notebook_path,
|
||||
config=app.config)
|
||||
html_content=html_content,
|
||||
notebook_name=os.path.basename(notebook_path),
|
||||
notebook_path=notebook_path,
|
||||
breadcrumbs=breadcrumbs,
|
||||
current_dir=notebook_path,
|
||||
config=app.config)
|
||||
|
||||
@app.route('/download/<path:notebook_path>')
|
||||
def download_notebook(notebook_path):
|
||||
@ -153,7 +156,7 @@ def download_notebook(notebook_path):
|
||||
abort(403)
|
||||
|
||||
# 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)
|
||||
|
||||
return send_file(full_path, as_attachment=True)
|
||||
@ -184,247 +187,30 @@ def api_notebooks():
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return render_template('error.html',
|
||||
error_code=404,
|
||||
error_message="Notebook or directory not found"), 404
|
||||
error_code=404,
|
||||
error_message="Notebook or directory not found",
|
||||
current_dir="404"), 404
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(error):
|
||||
return render_template('error.html',
|
||||
error_code=403,
|
||||
error_message="Access forbidden"), 403
|
||||
error_code=403,
|
||||
error_message="Access forbidden",
|
||||
current_dir="403"), 403
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(error):
|
||||
return render_template('error.html',
|
||||
error_code=500,
|
||||
error_message="Internal server error"), 500
|
||||
return render_template('error.html',
|
||||
error_code=500,
|
||||
error_message="Internal server error",
|
||||
current_dir="500"), 500
|
||||
|
||||
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
|
||||
@app.template_filter('datetime')
|
||||
def datetime_filter(timestamp):
|
||||
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"Shared directory: {app.config['SHARED_DIRECTORY']}")
|
||||
|
@ -85,7 +85,7 @@
|
||||
netcat
|
||||
|
||||
# Process management
|
||||
supervisor
|
||||
#supervisor
|
||||
];
|
||||
|
||||
# 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 %}
|
Loading…
Reference in New Issue
Block a user