Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
@ -3,9 +3,6 @@ name: Release
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -45,28 +42,3 @@ jobs:
|
|||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
publish_head:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Login to Gitea Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: git.dws.rip
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GLOBAL_KEY }}
|
|
||||||
|
|
||||||
- name: Build and push "head" image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: git.dws.rip/${{ github.repository }}:head
|
|
13
config.toml
13
config.toml
@ -1,13 +0,0 @@
|
|||||||
[paths]
|
|
||||||
content_dir = "/home/dubey/projects/foldsite/docs/content"
|
|
||||||
templates_dir = "/home/dubey/projects/foldsite/docs/templates"
|
|
||||||
styles_dir = "/home/dubey/projects/foldsite/docs/styles"
|
|
||||||
|
|
||||||
[server]
|
|
||||||
listen_address = "0.0.0.0"
|
|
||||||
listen_port = 8080
|
|
||||||
enable_admin_browser = false
|
|
||||||
admin_password = "password"
|
|
||||||
max_threads = 4
|
|
||||||
debug = false
|
|
||||||
access_log = true
|
|
@ -1,35 +0,0 @@
|
|||||||
# Configuration
|
|
||||||
|
|
||||||
## Configuration file
|
|
||||||
|
|
||||||
Foldsite is configured using a TOML file (default: `config.toml`). This file specifies paths to content, templates, and styles, as well as server settings.
|
|
||||||
|
|
||||||
Example `config.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[paths]
|
|
||||||
content_dir = "/home/myuser/site/content"
|
|
||||||
templates_dir = "/home/myuser/site/themes/cobalt/templates"
|
|
||||||
styles_dir = "/home/myuser/site/themes/cobalt/styles"
|
|
||||||
|
|
||||||
[server]
|
|
||||||
listen_address = "127.0.0.1"
|
|
||||||
listen_port = 8080
|
|
||||||
debug = false
|
|
||||||
access_log = true
|
|
||||||
max_threads = 4
|
|
||||||
admin_browser = false
|
|
||||||
admin_password = "your_admin_password"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Settings
|
|
||||||
|
|
||||||
- **`listen_address`**: The IP address the server listens on (default: `127.0.0.1`).
|
|
||||||
- **`listen_port`**: The port the server listens on (default: `8080`).
|
|
||||||
- **`debug`**: Enables or disables debug mode (default: `false`). In debug mode, the server will automatically reload when code changes are detected, and more detailed error messages will be shown.
|
|
||||||
- **`access_log`**: Enables or disables access logging (default: `true`). If enabled, access logs will be written to standard output.
|
|
||||||
- **`max_threads`**: The maximum number of threads to use for handling requests (default: `4`). This setting directly impacts the concurrency of the server.
|
|
||||||
- **`admin_browser`**: Enables or disables the built-in file manager (default: `false`).
|
|
||||||
- **`admin_password`**: Sets the password for the file manager. Required if `admin_browser` is `true`.
|
|
||||||
|
|
||||||
The `Configuration` class (`/foldsite/src/config/config.py`) is responsible for loading and parsing this configuration file. It also sets global variables (`CONTENT_DIR`, `TEMPLATES_DIR`, `STYLES_DIR`) for easy access to these directories throughout the application. Errors are raised if the config file is missing, invalid, or lacks required sections (like `paths` or `server`).
|
|
@ -1,56 +0,0 @@
|
|||||||
## Rendering Process
|
|
||||||
|
|
||||||
The `RouteManager` class (`/foldsite/src/routes/routes.py`) and `render_page` function (`/foldsite/src/rendering/renderer.py`) are central to the rendering process.
|
|
||||||
|
|
||||||
### How Foldsite Determines File Types
|
|
||||||
|
|
||||||
The `determine_type` function (in `renderer.py`) is crucial for figuring out how to handle a given file or directory. It examines file extensions and directory contents to classify files into broad categories (defined in `GENERIC_FILE_MAPPING` in `/foldsite/src/rendering/__init__.py`):
|
|
||||||
|
|
||||||
* **`document`**: Files with extensions like `.md`, `.txt`, and `.html`.
|
|
||||||
* **`image`**: Files with extensions like `.png`, `.jpg`, `.jpeg`, `.gif`, and `.svg`.
|
|
||||||
* **`directory`**: Directories. If a directory contains files, the most common file extension within that directory is used to infer the directory's "type".
|
|
||||||
* **`other`**: Files that don't match any of the above categories.
|
|
||||||
* **`multimedia`**: This is a combination that contains `image`.
|
|
||||||
|
|
||||||
### Template Search
|
|
||||||
|
|
||||||
When a request comes in, Foldsite searches for an appropriate template in the `templates` directory. The search logic is implemented in `render_page` and follows a specific order, prioritizing more specific templates:
|
|
||||||
|
|
||||||
1. **Exact Path Match:** If a template exists with the exact same path relative to the `templates` directory as the requested content file (but with a `.html` extension), it's used. For example, if the request is for `/about/team.md`, and a template exists at `templates/about/team.md.html`, that template will be used.
|
|
||||||
|
|
||||||
2. **Folder-Specific Template:** If the requested path is a directory, Foldsite looks for a `__folder.html` template within that directory. For example, if the request is for `/blog/`, and `templates/blog/__folder.html` exists, it will be used.
|
|
||||||
|
|
||||||
3. **Type and Extension-Specific Templates:** Foldsite searches for templates named `__{type}.{extension}.html` within the requested directory and its parent directories, moving upwards. For instance, if requesting `/blog/post1.md`, it would look for:
|
|
||||||
* `templates/blog/__file.md.html`
|
|
||||||
* `templates/__file.md.html`
|
|
||||||
|
|
||||||
4. **Type and Category-Specific Templates:** Similar to the above, but searches for `__{type}.{category}.html`. If requesting an image at `/images/logo.png`, it looks for:
|
|
||||||
|
|
||||||
* `templates/images/__file.image.html`
|
|
||||||
* `templates/__file.image.html`
|
|
||||||
|
|
||||||
5. **Base Template:** Finally, if no other template is found, `templates/base.html` is used as a fallback. This template *must* exist; otherwise, an exception is raised.
|
|
||||||
|
|
||||||
### Style Search
|
|
||||||
|
|
||||||
CSS styles are searched similarly to templates, prioritizing specificity:
|
|
||||||
|
|
||||||
1. **Exact Path Match:** A CSS file with the exact same path as the requested content file (relative to the `styles` directory) is used. For example, `/about/team.md` would look for `styles/about/team.md.css`.
|
|
||||||
|
|
||||||
2. **Type and Extension-Specific Styles:** Searches for `__{type}.{extension}.css` in the requested directory and its parent directories. For example, `/blog/post1.md` would look for:
|
|
||||||
|
|
||||||
* `styles/blog/__file.md.css`
|
|
||||||
* `styles/__file.md.css`
|
|
||||||
|
|
||||||
3. **Type and Category-Specific Styles:** Similar to the above, but searches for `__{type}.{category}.css`.
|
|
||||||
|
|
||||||
* `styles/images/__file.image.css`
|
|
||||||
* `styles/__file.image.css`
|
|
||||||
|
|
||||||
4. **Base Style:** `styles/base.css` is always included.
|
|
||||||
|
|
||||||
The discovered styles are added to the `styles` variable, which is passed to the Jinja2 template. The order ensures that more specific styles override general ones.
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
The `render_error_page` function (in `renderer.py`) handles errors. If a requested resource is not found (404 error) or if an exception occurs during processing, this function is called. It looks for a template named `__error.html` in the `templates` directory. If found, it's used to render the error page; otherwise, a default error page is generated. The error code, message, and description are passed to the template.
|
|
3
docs/content/added-tools.md
Normal file
3
docs/content/added-tools.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Added Tools for the Template
|
||||||
|
|
||||||
|
Foldsite provides additional tools for templates, such as functions to get sibling content files, text document previews, and folder contents.
|
19
docs/content/configuration.md
Normal file
19
docs/content/configuration.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
The configuration file is written in TOML format and contains various settings for the application. Below is an example configuration file (`config.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[paths]
|
||||||
|
content_dir = "example/content"
|
||||||
|
templates_dir = "templates"
|
||||||
|
styles_dir = "styles"
|
||||||
|
|
||||||
|
[server]
|
||||||
|
listen_address = "127.0.0.1"
|
||||||
|
listen_port = 8080
|
||||||
|
debug = false
|
||||||
|
access_log = true
|
||||||
|
max_threads = 4
|
||||||
|
admin_browser = false
|
||||||
|
admin_password = "your_admin_password"
|
||||||
|
```
|
12
docs/content/deployment.md
Normal file
12
docs/content/deployment.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
To deploy Foldsite, you can use Docker. Below is an example Dockerfile:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.13.2-bookworm
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["python", "main.py"]
|
||||||
|
```
|
16
docs/content/docker-compose-example.md
Normal file
16
docs/content/docker-compose-example.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Docker Compose Example
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` file to deploy Foldsite using Docker Compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
foldsite:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
environment:
|
||||||
|
- CONFIG_PATH=config.toml
|
||||||
|
```
|
39
docs/content/example-usages.md
Normal file
39
docs/content/example-usages.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Example Usages for Tools and Types
|
||||||
|
|
||||||
|
### Example Usage of `get_sibling_content_files`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ul>
|
||||||
|
{% for file in get_sibling_content_files('path/to/directory') %}
|
||||||
|
<li>{{ file[0] }} - {{ file[1] }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Usage of `get_text_document_preview`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div>
|
||||||
|
{{ get_text_document_preview('path/to/document.md') }}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Usage of `get_sibling_content_folders`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ul>
|
||||||
|
{% for folder in get_sibling_content_folders('path/to/directory') %}
|
||||||
|
<li>{{ folder[0] }} - {{ folder[1] }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Usage of `get_folder_contents`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ul>
|
||||||
|
{% for item in get_folder_contents('path/to/directory') %}
|
||||||
|
<li>{{ item.name }} - {{ item.path }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
```
|
0
docs/content/folder-layout.md
Normal file
0
docs/content/folder-layout.md
Normal file
23
docs/content/how-template-written.md
Normal file
23
docs/content/how-template-written.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# How a Template is Written
|
||||||
|
|
||||||
|
Templates are written in HTML and use Jinja2 syntax for dynamic content. Below is an example template (`base.html`):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
|
||||||
|
{% for style in styles %}
|
||||||
|
<link rel="stylesheet" href="{{ style }}">
|
||||||
|
{% endfor %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
@ -1,3 +1,20 @@
|
|||||||
# Foldsite Documentation
|
# Foldsite Documentation
|
||||||
|
|
||||||
Foldsite acts as a dynamic site generator. It takes content primarily from Markdown files, combines it with HTML templates, applies CSS styles, and serves the resulting pages. It supports features like image thumbnails, Markdown rendering with frontmatter, and a built-in file manager for administrative tasks. Foldsite is dynamic in the sense that content is rendered on demand, rather than generating static HTML up-front.
|
Welcome to the Foldsite documentation. This site will guide you through the setup, configuration, and usage of Foldsite.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Introduction](introduction.md)
|
||||||
|
2. [Configuration](configuration.md)
|
||||||
|
3. [Template Setup](template-setup.md)
|
||||||
|
4. [Site Setup](site-setup.md)
|
||||||
|
5. [Style Setup](style-setup.md)
|
||||||
|
6. [Template and Style Search](template-style-search.md)
|
||||||
|
7. [How a Template is Written](how-template-written.md)
|
||||||
|
8. [Jinja Primer](jinja-primer.md)
|
||||||
|
9. [Added Tools for the Template](added-tools.md)
|
||||||
|
10. [Tool Input and Return Types](tool-input-return-types.md)
|
||||||
|
11. [Example Usages for Tools and Types](example-usages.md)
|
||||||
|
12. [Deployment](deployment.md)
|
||||||
|
13. [Docker Compose Example](docker-compose-example.md)
|
||||||
|
14. [Folder Layout](folder-layout.md)
|
||||||
|
5
docs/content/introduction.md
Normal file
5
docs/content/introduction.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
Foldsite is a dynamic site generator built with Python and Flask. It allows you to create and manage a website using Markdown content, HTML templates, and CSS styles.
|
||||||
|
|
||||||
|
This documentation will guide you through the setup, configuration, and usage of Foldsite.
|
8
docs/content/jinja-primer.md
Normal file
8
docs/content/jinja-primer.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Jinja Primer
|
||||||
|
|
||||||
|
Jinja2 is a templating engine for Python. It allows you to include dynamic content in your HTML templates. Below are some basic Jinja2 syntax examples:
|
||||||
|
|
||||||
|
- Variables: `{{ variable }}`
|
||||||
|
- Loops: `{% for item in list %} ... {% endfor %}`
|
||||||
|
- Conditionals: `{% if condition %} ... {% endif %}`
|
||||||
|
- Includes: `{% include 'template.html' %}`
|
3
docs/content/site-setup.md
Normal file
3
docs/content/site-setup.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Site Setup
|
||||||
|
|
||||||
|
The site content is stored in the `content` directory. Each Markdown file represents a page on your site. The directory structure of the `content` directory determines the URL structure of your site.
|
3
docs/content/style-setup.md
Normal file
3
docs/content/style-setup.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Style Setup
|
||||||
|
|
||||||
|
Styles are CSS files that define the appearance of your web pages. They are stored in the `styles` directory. You can create specific styles for different types of content and categories.
|
3
docs/content/template-setup.md
Normal file
3
docs/content/template-setup.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Template Setup
|
||||||
|
|
||||||
|
Templates are HTML files that define the structure of your web pages. They are stored in the `templates` directory. Each template can include other templates and use Jinja2 syntax for dynamic content.
|
3
docs/content/template-style-search.md
Normal file
3
docs/content/template-style-search.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Template and Style Search
|
||||||
|
|
||||||
|
Templates and styles are searched in a specific order to apply the most specific styles first, followed by more general styles, and finally the base style.
|
13
docs/content/tool-input-return-types.md
Normal file
13
docs/content/tool-input-return-types.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Tool Input and Return Types
|
||||||
|
|
||||||
|
### `get_sibling_content_files(path: str) -> list`
|
||||||
|
Returns a list of sibling content files in the specified directory.
|
||||||
|
|
||||||
|
### `get_text_document_preview(path: str) -> str`
|
||||||
|
Generates a preview of the text document located at the given path.
|
||||||
|
|
||||||
|
### `get_sibling_content_folders(path: str) -> list`
|
||||||
|
Returns a list of sibling content folders within a specified directory.
|
||||||
|
|
||||||
|
### `get_folder_contents(path: str) -> list`
|
||||||
|
Retrieves the contents of a folder and returns a list of `TemplateFile` objects.
|
@ -3,12 +3,7 @@ article {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
article h1,
|
article h1, article h2, article h3, article h4, article h5, article h6 {
|
||||||
article h2,
|
|
||||||
article h3,
|
|
||||||
article h4,
|
|
||||||
article h5,
|
|
||||||
article h6 {
|
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,8 +11,14 @@ article p {
|
|||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
article pre {
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
article code {
|
article code {
|
||||||
background: var(--code-background-color);
|
background: #f4f4f4;
|
||||||
padding: 0.2rem 0.4rem;
|
padding: 0.2rem 0.4rem;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
@ -1,280 +1,50 @@
|
|||||||
/* http://meyerweb.com/eric/tools/css/reset/
|
body {
|
||||||
v2.0 | 20110126
|
font-family: Arial, sans-serif;
|
||||||
License: none (public domain)
|
line-height: 1.6;
|
||||||
*/
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
div,
|
|
||||||
span,
|
|
||||||
applet,
|
|
||||||
object,
|
|
||||||
iframe,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
p,
|
|
||||||
blockquote,
|
|
||||||
pre,
|
|
||||||
a,
|
|
||||||
abbr,
|
|
||||||
acronym,
|
|
||||||
address,
|
|
||||||
big,
|
|
||||||
cite,
|
|
||||||
code,
|
|
||||||
del,
|
|
||||||
dfn,
|
|
||||||
em,
|
|
||||||
img,
|
|
||||||
ins,
|
|
||||||
kbd,
|
|
||||||
q,
|
|
||||||
s,
|
|
||||||
samp,
|
|
||||||
small,
|
|
||||||
strike,
|
|
||||||
strong,
|
|
||||||
sub,
|
|
||||||
sup,
|
|
||||||
tt,
|
|
||||||
var,
|
|
||||||
b,
|
|
||||||
u,
|
|
||||||
i,
|
|
||||||
center,
|
|
||||||
dl,
|
|
||||||
dt,
|
|
||||||
dd,
|
|
||||||
ol,
|
|
||||||
ul,
|
|
||||||
li,
|
|
||||||
fieldset,
|
|
||||||
form,
|
|
||||||
label,
|
|
||||||
legend,
|
|
||||||
table,
|
|
||||||
caption,
|
|
||||||
tbody,
|
|
||||||
tfoot,
|
|
||||||
thead,
|
|
||||||
tr,
|
|
||||||
th,
|
|
||||||
td,
|
|
||||||
article,
|
|
||||||
aside,
|
|
||||||
canvas,
|
|
||||||
details,
|
|
||||||
embed,
|
|
||||||
figure,
|
|
||||||
figcaption,
|
|
||||||
footer,
|
|
||||||
header,
|
|
||||||
hgroup,
|
|
||||||
menu,
|
|
||||||
nav,
|
|
||||||
output,
|
|
||||||
ruby,
|
|
||||||
section,
|
|
||||||
summary,
|
|
||||||
time,
|
|
||||||
mark,
|
|
||||||
audio,
|
|
||||||
video {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
|
||||||
font-size: 100%;
|
|
||||||
font: inherit;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HTML5 display-role reset for older browsers */
|
header {
|
||||||
article,
|
background: #333;
|
||||||
aside,
|
color: #fff;
|
||||||
details,
|
padding: 1rem 0;
|
||||||
figcaption,
|
text-align: center;
|
||||||
figure,
|
|
||||||
footer,
|
|
||||||
header,
|
|
||||||
hgroup,
|
|
||||||
menu,
|
|
||||||
nav,
|
|
||||||
section {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
header h1 {
|
||||||
line-height: 1;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol,
|
nav ul {
|
||||||
ul {
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote,
|
nav ul li {
|
||||||
q {
|
display: inline;
|
||||||
quotes: none;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote:before,
|
nav ul li a {
|
||||||
blockquote:after,
|
color: #fff;
|
||||||
q:before,
|
|
||||||
q:after {
|
|
||||||
content: '';
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property --font-color {
|
|
||||||
syntax: "<color>";
|
|
||||||
inherits: true;
|
|
||||||
initial-value: oklch(25.11% 0.006 258.36);
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --background-color {
|
|
||||||
syntax: "<color>";
|
|
||||||
inherits: true;
|
|
||||||
initial-value: #F6F0F0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --code-background-color {
|
|
||||||
syntax: "<color>";
|
|
||||||
inherits: true;
|
|
||||||
initial-value: #c7c1c1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --hover-color {
|
|
||||||
syntax: "<color>";
|
|
||||||
inherits: true;
|
|
||||||
initial-value: #A4B465;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --url-color {
|
|
||||||
syntax: "<color>";
|
|
||||||
inherits: true;
|
|
||||||
initial-value: #626F47;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--font-color: oklch(91.87% 0.003 264.54);
|
|
||||||
--background-color: #29261f;
|
|
||||||
--hover-color: #626F47;
|
|
||||||
--url-color: #A4B465;
|
|
||||||
--code-background-color: #3d392e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
font-optical-sizing: auto;
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-variation-settings:
|
|
||||||
"wdth" 100;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
color: var(--font-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--url-color);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.25s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
nav ul li a:hover {
|
||||||
color: var(--hover-color);
|
text-decoration: underline;
|
||||||
transition: all 0.25s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
main {
|
||||||
color: #C14600;
|
padding: 2rem;
|
||||||
transition: all 0.25s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited:hover {
|
footer {
|
||||||
color: var(--hover-color);
|
background: #333;
|
||||||
transition: all 0.25s ease-in-out;
|
color: #fff;
|
||||||
}
|
text-align: center;
|
||||||
|
padding: 1rem 0;
|
||||||
@supports (font-size-adjust: 1) {
|
position: fixed;
|
||||||
.content {
|
bottom: 0;
|
||||||
font-size-adjust: 0.5;
|
width: 100%;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: square;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
line-height: 160%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
line-height: calc(1ex / 0.32);
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
max-width: 80ch;
|
|
||||||
padding-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h1 {
|
|
||||||
font-size: 2.5em;
|
|
||||||
line-height: calc(1ex / 0.42);
|
|
||||||
margin: calc(1ex / 0.42) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h2 {
|
|
||||||
font-size: 2em;
|
|
||||||
line-height: calc(1ex / 0.42);
|
|
||||||
margin: calc(1ex / 0.42) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h3 {
|
|
||||||
font-size: 1.75em;
|
|
||||||
line-height: calc(1ex / 0.38);
|
|
||||||
margin: calc(1ex / 0.38) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h4 {
|
|
||||||
font-size: 1.5em;
|
|
||||||
line-height: calc(1ex / 0.37);
|
|
||||||
margin: calc(1ex / 0.37) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content p {
|
|
||||||
font-size: 1em;
|
|
||||||
line-height: calc(1ex / 0.32);
|
|
||||||
margin: calc(1ex / 0.32) 0;
|
|
||||||
text-align: justify;
|
|
||||||
hyphens: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
padding-top: 4rem;
|
|
||||||
line-height: calc(1ex / 0.32);
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
.holder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
5
docs/templates/__file.md.html
vendored
5
docs/templates/__file.md.html
vendored
@ -1,7 +1,4 @@
|
|||||||
<article>
|
<article>
|
||||||
{{ content|safe }}
|
{{ content|safe }}
|
||||||
</article>
|
</article>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css">
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/dockerfile.min.js"></script>
|
|
||||||
<script>hljs.highlightAll();</script>
|
|
||||||
|
53
docs/templates/base.html
vendored
53
docs/templates/base.html
vendored
@ -1,43 +1,42 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
|
||||||
{% for style in styles %}
|
{% for style in styles %}
|
||||||
<link rel="stylesheet" href="/styles{{ style }}" type="text/css">
|
<link rel="stylesheet" href="{{ style }}">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="holder">
|
<header>
|
||||||
<div class="sidebar"> <!-- Changed <sidebar> to <div> -->
|
<h1>Foldsite Documentation</h1>
|
||||||
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">⌂ Home</a></li>
|
<li><a href="{{ url_for('default_route', path='index.md') }}">Home</a></li>
|
||||||
<hr>
|
<li><a href="{{ url_for('default_route', path='introduction.md') }}">Introduction</a></li>
|
||||||
{% for f in get_folder_contents() %}
|
<li><a href="{{ url_for('default_route', path='configuration.md') }}">Configuration</a></li>
|
||||||
{% if not f.is_dir %}
|
<li><a href="{{ url_for('default_route', path='template-setup.md') }}">Template Setup</a></li>
|
||||||
{% if f.proper_name == "index" %}
|
<li><a href="{{ url_for('default_route', path='site-setup.md') }}">Site Setup</a></li>
|
||||||
{% else %}
|
<li><a href="{{ url_for('default_route', path='style-setup.md') }}">Style Setup</a></li>
|
||||||
<li><a href="/{{ f.path }}">{{ f.proper_name }}</a></li>
|
<li><a href="{{ url_for('default_route', path='template-style-search.md') }}">Template and Style Search</a></li>
|
||||||
{% endif %}
|
<li><a href="{{ url_for('default_route', path='how-template-written.md') }}">How a Template is Written</a></li>
|
||||||
{% endif %}
|
<li><a href="{{ url_for('default_route', path='jinja-primer.md') }}">Jinja Primer</a></li>
|
||||||
{% endfor %}
|
<li><a href="{{ url_for('default_route', path='added-tools.md') }}">Added Tools</a></li>
|
||||||
|
<li><a href="{{ url_for('default_route', path='tool-input-return-types.md') }}">Tool Input and Return Types</a></li>
|
||||||
|
<li><a href="{{ url_for('default_route', path='example-usages.md') }}">Example Usages</a></li>
|
||||||
|
<li><a href="{{ url_for('default_route', path='deployment.md') }}">Deployment</a></li>
|
||||||
|
<li><a href="{{ url_for('default_route', path='docker-compose-example.md') }}">Docker Compose Example</a></li>
|
||||||
|
<li><a href="{{ url_for('default_route', path='folder-layout.md') }}">Folder Layout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</nav>
|
||||||
<div class="content"> <!-- <main> tag remains the same -->
|
</header>
|
||||||
|
<main>
|
||||||
{{ content|safe }}
|
{{ content|safe }}
|
||||||
<div class="footer">
|
</main>
|
||||||
<p>© DWS</p>
|
<footer>
|
||||||
</div>
|
<p>© 2023 Foldsite</p>
|
||||||
</div>
|
</footer>
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -8,7 +8,6 @@ dependencies = [
|
|||||||
"bs4>=0.0.2",
|
"bs4>=0.0.2",
|
||||||
"flask>=3.1.0",
|
"flask>=3.1.0",
|
||||||
"gunicorn>=23.0.0",
|
"gunicorn>=23.0.0",
|
||||||
"jinja2>=3.1.6",
|
|
||||||
"mistune>=3.1.1",
|
"mistune>=3.1.1",
|
||||||
"pillow>=10.4.0",
|
"pillow>=10.4.0",
|
||||||
"python-frontmatter>=1.1.0",
|
"python-frontmatter>=1.1.0",
|
||||||
|
@ -16,10 +16,8 @@ gunicorn==23.0.0
|
|||||||
# via foldsite (pyproject.toml)
|
# via foldsite (pyproject.toml)
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
# via flask
|
# via flask
|
||||||
jinja2==3.1.6
|
jinja2==3.1.5
|
||||||
# via
|
# via flask
|
||||||
# foldsite (pyproject.toml)
|
|
||||||
# flask
|
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
# via rich
|
# via rich
|
||||||
markupsafe==3.0.2
|
markupsafe==3.0.2
|
||||||
|
@ -30,16 +30,15 @@ def generate_thumbnail(image_path, resize_percent, min_width, max_width):
|
|||||||
try:
|
try:
|
||||||
exif = img.info['exif']
|
exif = img.info['exif']
|
||||||
orientation = img._getexif().get(0x0112, 1) # 0x0112 is the EXIF orientation tag
|
orientation = img._getexif().get(0x0112, 1) # 0x0112 is the EXIF orientation tag
|
||||||
print(f"EXIF orientation: {orientation}, {image_path}")
|
|
||||||
if orientation == 3:
|
if orientation == 3:
|
||||||
img = img.rotate(180, expand=True)
|
img = img.rotate(180, expand=True)
|
||||||
elif orientation == 6:
|
elif orientation == 6:
|
||||||
img = img.rotate(0, expand=True)
|
img = img.rotate(270, expand=True)
|
||||||
elif orientation == 8:
|
elif orientation == 8:
|
||||||
img = img.rotate(180, expand=True)
|
img = img.rotate(90, expand=True)
|
||||||
except (AttributeError, KeyError, IndexError):
|
except (AttributeError, KeyError, IndexError):
|
||||||
# cases: image don't have getexif
|
# cases: image don't have getexif
|
||||||
exif = b""
|
exif = None
|
||||||
|
|
||||||
# Save the thumbnail to a BytesIO object
|
# Save the thumbnail to a BytesIO object
|
||||||
thumbnail_io = BytesIO()
|
thumbnail_io = BytesIO()
|
||||||
|
@ -149,10 +149,10 @@ def create_filemanager_blueprint(base_dir, url_prefix='/files', auth_password=No
|
|||||||
</ul>
|
</ul>
|
||||||
<button onclick="bulkCut()">Bulk Cut Selected</button>
|
<button onclick="bulkCut()">Bulk Cut Selected</button>
|
||||||
<hr>
|
<hr>
|
||||||
<h2>Upload File(s)</h2>
|
<h2>Upload File</h2>
|
||||||
<form action="{{ url_for('filemanager.upload') }}" method="post" enctype="multipart/form-data">
|
<form action="{{ url_for('filemanager.upload') }}" method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="path" value="{{ rel_path }}">
|
<input type="hidden" name="path" value="{{ rel_path }}">
|
||||||
<input type="file" name="file" multiple>
|
<input type="file" name="file">
|
||||||
<input type="submit" value="Upload">
|
<input type="submit" value="Upload">
|
||||||
</form>
|
</form>
|
||||||
<hr>
|
<hr>
|
||||||
@ -276,13 +276,11 @@ def create_filemanager_blueprint(base_dir, url_prefix='/files', auth_password=No
|
|||||||
return "Invalid path", 400
|
return "Invalid path", 400
|
||||||
if not os.path.isdir(abs_path):
|
if not os.path.isdir(abs_path):
|
||||||
return "Not a directory", 400
|
return "Not a directory", 400
|
||||||
files = request.files.getlist('file')
|
file = request.files.get('file')
|
||||||
if files:
|
if file:
|
||||||
for file in files:
|
|
||||||
if file and file.filename:
|
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
file.save(os.path.join(abs_path, filename))
|
file.save(os.path.join(abs_path, filename))
|
||||||
flash("Uploaded files successfully")
|
flash("Uploaded successfully")
|
||||||
return redirect(url_for('filemanager.index', path=rel_path))
|
return redirect(url_for('filemanager.index', path=rel_path))
|
||||||
|
|
||||||
@filemanager.route('/rename', methods=['POST'])
|
@filemanager.route('/rename', methods=['POST'])
|
||||||
|
20
uv.lock
generated
20
uv.lock
generated
@ -81,7 +81,6 @@ dependencies = [
|
|||||||
{ name = "bs4" },
|
{ name = "bs4" },
|
||||||
{ name = "flask" },
|
{ name = "flask" },
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "jinja2" },
|
|
||||||
{ name = "mistune" },
|
{ name = "mistune" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "python-frontmatter" },
|
{ name = "python-frontmatter" },
|
||||||
@ -95,7 +94,6 @@ requires-dist = [
|
|||||||
{ name = "bs4", specifier = ">=0.0.2" },
|
{ name = "bs4", specifier = ">=0.0.2" },
|
||||||
{ name = "flask", specifier = ">=3.1.0" },
|
{ name = "flask", specifier = ">=3.1.0" },
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
|
||||||
{ name = "mistune", specifier = ">=3.1.1" },
|
{ name = "mistune", specifier = ">=3.1.1" },
|
||||||
{ name = "pillow", specifier = ">=10.4.0" },
|
{ name = "pillow", specifier = ">=10.4.0" },
|
||||||
{ name = "python-frontmatter", specifier = ">=1.1.0" },
|
{ name = "python-frontmatter", specifier = ">=1.1.0" },
|
||||||
@ -127,14 +125,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
version = "3.1.6"
|
version = "3.1.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markupsafe" },
|
{ name = "markupsafe" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
|
sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
|
{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -188,11 +186,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mistune"
|
name = "mistune"
|
||||||
version = "3.1.2"
|
version = "3.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/80/f7/f6d06304c61c2a73213c0a4815280f70d985429cda26272f490e42119c1a/mistune-3.1.2.tar.gz", hash = "sha256:733bf018ba007e8b5f2d3a9eb624034f6ee26c4ea769a98ec533ee111d504dff", size = 94613 }
|
sdist = { url = "https://files.pythonhosted.org/packages/c6/1d/6b2b634e43bacc3239006e61800676aa6c41ac1836b2c57497ed27a7310b/mistune-3.1.1.tar.gz", hash = "sha256:e0740d635f515119f7d1feb6f9b192ee60f0cc649f80a8f944f905706a21654c", size = 94645 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/92/30b4e54c4d7c48c06db61595cffbbf4f19588ea177896f9b78f0fbe021fd/mistune-3.1.2-py3-none-any.whl", hash = "sha256:4b47731332315cdca99e0ded46fc0004001c1299ff773dfb48fbe1fd226de319", size = 53696 },
|
{ url = "https://files.pythonhosted.org/packages/c6/02/c66bdfdadbb021adb642ca4e8a5ed32ada0b4a3e4b39c5d076d19543452f/mistune-3.1.1-py3-none-any.whl", hash = "sha256:02106ac2aa4f66e769debbfa028509a275069dcffce0dfa578edd7b991ee700a", size = 53696 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -307,7 +305,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.15.2"
|
version = "0.15.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
@ -315,9 +313,9 @@ dependencies = [
|
|||||||
{ name = "shellingham" },
|
{ name = "shellingham" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
|
{ url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user