Compare commits
86 Commits
f3b564b806
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 36cf092ab0 | |||
| 934b471c36 | |||
| c3858fb750 | |||
| b9507378ba | |||
| 56a417ece8 | |||
| 67e3941bcc | |||
| 4aaa1fe32d | |||
| 0f4eca7dff | |||
| 3718999740 | |||
| 2cd062bce0 | |||
| ec2c5dda98 | |||
| 7f79015199 | |||
| 8a2b8c3532 | |||
| 742c61aba2 | |||
| 83a8f9e438 | |||
| 6cc247dd5f | |||
| 1ae4c16782 | |||
| 1673133bba | |||
| f20ec2f258 | |||
| 509943577e | |||
| bf46ee0219 | |||
| c335577045 | |||
| f77e1f64ad | |||
| 9c8b914884 | |||
| 2debeb7f85 | |||
| bbe9726968 | |||
| dc51fa3ba7 | |||
| 64c79d9eb9 | |||
| 3fcf0f71b0 | |||
| ddf2c54880 | |||
| 762c309760 | |||
| 1f964e9b88 | |||
| 438c91d226 | |||
| 92c1b75480 | |||
| 3b3d93e089 | |||
| 14103bc551 | |||
| 07e612699e | |||
| ddfcde30c3 | |||
| 67cf24cf42 | |||
| 8a6ccebbaa | |||
| 828b8ccf8a | |||
| f18f8d7d24 | |||
| 02c3adb06e | |||
| a9a6589f7f | |||
| 7ada67433e | |||
| 7c56b73716 | |||
| 420a3484ca | |||
| 74abf0aac6 | |||
| cbb52d74a4 | |||
| 1b1ea501a8 | |||
| 3f249936ea | |||
| 4e4ad45acd | |||
| a9feece9b7 | |||
| db68be7338 | |||
| 512520dad4 | |||
| ac2bedac72 | |||
| e3b1d7dc89 | |||
| ebe30e375b | |||
| 94cd879449 | |||
| 189ca4c94b | |||
| 510e122a9f | |||
| b8bf5845c6 | |||
| 2d3f967a10 | |||
| 0d3c24da02 | |||
| 8052675c1e | |||
| 1f9708b5f2 | |||
| 1592e7cb28 | |||
| f010b18883 | |||
| 93b56fab81 | |||
| fa15504505 | |||
| 4ff09f124c | |||
| fcf23f3ebf | |||
| dbab12d1b9 | |||
| 5e69e952ec | |||
| ddc6aefa4d | |||
| 6b412749ea | |||
| ff9c51200e | |||
| 9836a3ddd5 | |||
| 9a8cc9157a | |||
| 8e74399c75 | |||
| b5b96600c9 | |||
| 1bf4917f7a | |||
| 788ec81cb9 | |||
| ca65105cf9 | |||
| c315d7d5ac | |||
| d0263e8a05 |
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
zola
|
zola
|
||||||
|
zbuild.sh
|
||||||
static/processed_images/
|
static/processed_images/
|
||||||
themes/yunohost/
|
themes/yunohost/
|
||||||
public/
|
public/
|
||||||
|
|||||||
19
README.md
@@ -1,4 +1,23 @@
|
|||||||
# blog.zlinux.ru
|
# blog.zlinux.ru
|
||||||
Мой блог на [zola](https://github.com/getzola/zola) с измененной темой оформления (gruvbox) от [tabi](https://github.com/welpo/tabi).
|
Мой блог на [zola](https://github.com/getzola/zola) с измененной темой оформления (gruvbox) от [tabi](https://github.com/welpo/tabi).
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```$ sudo pacman -S zola```
|
||||||
|
|
||||||
|
Клонируем репозиторий
|
||||||
|
|
||||||
|
```$ git clone https://gitea.zlinux.ru/zloy_linux/blog.zlinux.ru.git ~/blog```
|
||||||
|
|
||||||
|
переходим в директорию нашего блога
|
||||||
|
|
||||||
|
```$ cd ~/blog```
|
||||||
|
|
||||||
|
Собираем и запускаем
|
||||||
|
|
||||||
|
```
|
||||||
|
$ zola build
|
||||||
|
$ zola serve
|
||||||
|
```
|
||||||
|
|
||||||
<img src="screenshot.png">
|
<img src="screenshot.png">
|
||||||
|
|||||||
130
config.toml
@@ -200,6 +200,19 @@ fediverse_creator = { handle = "zloy_linux", domain = "zlinux.ru" }
|
|||||||
show_previous_next_article_links = true
|
show_previous_next_article_links = true
|
||||||
invert_previous_next_article_links = true
|
invert_previous_next_article_links = true
|
||||||
|
|
||||||
|
# For multilingual sites: show current language code on the language switcher.
|
||||||
|
show_selected_language_code_in_language_switcher = false
|
||||||
|
|
||||||
|
# Enable iine like buttons on all posts: https://iine.to/
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
|
||||||
|
iine = true
|
||||||
|
iine_icon = "thumbs_up" # See https://iine.to/#customise
|
||||||
|
# Unify like counts across all language versions of the same page.
|
||||||
|
# When enabled, likes on /es/blog/hello/ will count towards /blog/hello/ (default language).
|
||||||
|
iine_unified_languages = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
enable_csp = false
|
enable_csp = false
|
||||||
|
|
||||||
|
|
||||||
@@ -213,7 +226,7 @@ enable_csp = false
|
|||||||
#]
|
#]
|
||||||
|
|
||||||
|
|
||||||
remote_repository_url = "https://gitea.zlinux.ru/gitea/zloy_linux/blog.zlinux.ru.git"
|
remote_repository_url = "https://gitea.zlinux.ru/zloy_linux/blog.zlinux.ru.git"
|
||||||
remote_repository_git_platform = "auto"
|
remote_repository_git_platform = "auto"
|
||||||
remote_repository_branch = "main"
|
remote_repository_branch = "main"
|
||||||
show_remote_changes = true
|
show_remote_changes = true
|
||||||
@@ -227,6 +240,20 @@ code_block_name_links = true
|
|||||||
|
|
||||||
|
|
||||||
compact_tags = false
|
compact_tags = false
|
||||||
|
|
||||||
|
short_date_format = ""
|
||||||
|
|
||||||
|
# Date format used for the archive page.
|
||||||
|
# Default is "06 July" in English and "%d %b" in other languages.
|
||||||
|
archive_date_format = ""
|
||||||
|
|
||||||
|
# Per-language date format overrides.
|
||||||
|
# Examples: Spanish uses "3 de febrero de 2024", German uses "3. Februar 2024"
|
||||||
|
date_formats = [
|
||||||
|
{ lang = "ru", long = "%d %B %Y", short = "%-d %b %Y", archive = "%d %b" },
|
||||||
|
{ lang = "en", long = "%d. %B %Y", short = "%d.%m.%Y" },
|
||||||
|
]
|
||||||
|
|
||||||
tag_sorting = "frequency"
|
tag_sorting = "frequency"
|
||||||
|
|
||||||
menu = [
|
menu = [
|
||||||
@@ -235,6 +262,7 @@ menu = [
|
|||||||
{ name = "articles", url = "articles", trailing_slash = true },
|
{ name = "articles", url = "articles", trailing_slash = true },
|
||||||
{ name = "tags", url = "tags", trailing_slash = true },
|
{ name = "tags", url = "tags", trailing_slash = true },
|
||||||
{ name = "archive", url = "archive", trailing_slash = true },
|
{ name = "archive", url = "archive", trailing_slash = true },
|
||||||
|
{ name = "git", url = "https://gitea.zlinux.ru/zloy_linux", trailing_slash = true },
|
||||||
]
|
]
|
||||||
|
|
||||||
feed_icon = true
|
feed_icon = true
|
||||||
@@ -242,20 +270,108 @@ email = "emxveV9saW51eEB6bGludXgucnU="
|
|||||||
encode_plaintext_email = true
|
encode_plaintext_email = true
|
||||||
|
|
||||||
socials = [
|
socials = [
|
||||||
{ name = "sharkey", url = "https://zlinux.ru/@zloy_linux", icon = "sharkey" },
|
{ name = "microblog", url = "https://log.zlinux.ru/", icon = "microblog" },
|
||||||
{ name = "telegram", url = "https://t.me/#", icon = "telegram" },
|
{ name = "telegram", url = "https://t.me/#", icon = "telegram" },
|
||||||
{ name = "peertube", url = "https://video.zlinux.ru/a/zloy_linux/video-channels", icon = "peertube" },
|
# { name = "peertube", url = "https://video.zlinux.ru/a/zloy_linux/video-channels", icon = "peertube" },
|
||||||
# { name = "youtube", url = "https://youtube.com/#", icon = "youtube" },
|
# { name = "youtube", url = "https://youtube.com/#", icon = "youtube" },
|
||||||
{ name = "pixelfed", url = "https://pixelfed.social/zloy_linuxoid", icon = "pixelfeed" },
|
{ name = "pixelfed", url = "https://pixelfed.social/zloy_linuxoid", icon = "pixelfed" },
|
||||||
]
|
]
|
||||||
|
|
||||||
footer_menu = [
|
footer_menu = [
|
||||||
# {url = "about", name = "about", trailing_slash = true},
|
# {url = "about", name = "about", trailing_slash = true},
|
||||||
# {url = "privacy", name = "privacy", trailing_slash = true},
|
# {url = "privacy", name = "privacy", trailing_slash = true},
|
||||||
|
{ name = "projects", url = "projects", trailing_slash = true },
|
||||||
|
{ url = "https://zloy.goatcounter.com", name = "статистика", trailing_slash = true },
|
||||||
{ url = "sitemap.xml", name = "sitemap", trailing_slash = false },
|
{ url = "sitemap.xml", name = "sitemap", trailing_slash = false },
|
||||||
]
|
]
|
||||||
|
|
||||||
[extra.analytics]
|
[extra.analytics]
|
||||||
service = "umami"
|
#service = "umami"
|
||||||
id = "4ee44323-925a-401b-996d-2b6a63dfc527"
|
#id = "4ee44323-925a-401b-996d-2b6a63dfc527"
|
||||||
self_hosted_url = "https://stats.zlinux.ru"
|
#self_hosted_url = "https://stats.zlinux.ru"
|
||||||
|
service = "goatcounter"
|
||||||
|
id = "zloy"
|
||||||
|
#self_hosted_url = "https://zloy.goatcounter.com"
|
||||||
|
|
||||||
|
# Optional: For Umami, enable this option to respect users' Do Not Track (DNT) settings. The default is true.
|
||||||
|
do_not_track = true
|
||||||
|
|
||||||
|
[extra.isso]
|
||||||
|
enabled_for_all_posts = true # Enables Isso on all posts. It can be enabled on individual posts by setting `isso = true` in the [extra] section of a post's front matter.
|
||||||
|
automatic_loading = true # If set to false, a "Load comments" button will be shown.
|
||||||
|
endpoint_url = "https://comment.zlinux.ru/" # Accepts relative paths like "/comments/" or "/isso/", as well as full urls like "https://example.com/comments/". Include the trailing slash.
|
||||||
|
page_id_is_slug = true # If true, it will use the relative path for the default language as id; this is the only way to share comments between languages. If false, it will use the entire url as id.
|
||||||
|
lang = "" # Leave blank to match the page's language.
|
||||||
|
max_comments_top = "inf" # Number of top level comments to show by default. If some comments are not shown, an “X Hidden” link is shown.
|
||||||
|
max_comments_nested = "5" # Number of nested comments to show by default. If some comments are not shown, an “X Hidden” link is shown.
|
||||||
|
avatar = true
|
||||||
|
voting = true
|
||||||
|
page_author_hashes = "" # hash (or list of hashes) of the author.
|
||||||
|
lazy_loading = true # Loads when the comments are in the viewport (using the Intersection Observer API).
|
||||||
|
|
||||||
|
|
||||||
|
[extra.webmentions]
|
||||||
|
# To disable for a specific section or page, set webmentions = false in that page/section's front matter's [extra] section.
|
||||||
|
enable = false
|
||||||
|
# Specify the domain registered with webmention.io.
|
||||||
|
domain = ""
|
||||||
|
|
||||||
|
# The HTML ID for the object to fill in with the webmention data.
|
||||||
|
# Defaults to "webmentions"
|
||||||
|
# id = "webmentions"
|
||||||
|
|
||||||
|
# data configuration for the webmention.min.js script
|
||||||
|
# The base URL to use for this page. Defaults to window.location
|
||||||
|
# page_url =
|
||||||
|
|
||||||
|
# Additional URLs to check, separated by |s
|
||||||
|
# add_urls
|
||||||
|
|
||||||
|
# The maximum number of words to render in reply mentions.
|
||||||
|
# wordcount = 20
|
||||||
|
|
||||||
|
# The maximum number of mentions to retrieve. Defaults to 30.
|
||||||
|
# max_webmentions = 30
|
||||||
|
|
||||||
|
# By default, Webmentions render using the mf2 'url' element, which plays
|
||||||
|
# nicely with webmention bridges (such as brid.gy and telegraph)
|
||||||
|
# but allows certain spoofing attacks. If you would like to prevent
|
||||||
|
# spoofing, set this to a non-empty string (e.g. "true").
|
||||||
|
# prevent_spoofing
|
||||||
|
|
||||||
|
# What to order the responses by; defaults to 'published'. See
|
||||||
|
# https://github.com/aaronpk/webmention.io#api
|
||||||
|
# sort_by
|
||||||
|
|
||||||
|
# The order to sort the responses by; defaults to 'up' (i.e. oldest
|
||||||
|
# first). See https://github.com/aaronpk/webmention.io#api
|
||||||
|
# sort_dir
|
||||||
|
|
||||||
|
# If set to a non-empty string (e.g. "true"), will display comment-type responses
|
||||||
|
# (replies/mentions/etc.) as being part of the reactions
|
||||||
|
# (favorites/bookmarks/etc.) instead of in a separate comment list.
|
||||||
|
# comments_are_reactions = "true"
|
||||||
|
|
||||||
|
# h-card configuration
|
||||||
|
# Will identify you on the indieweb (see https://microformats.org/wiki/h-card)
|
||||||
|
[extra.hcard]
|
||||||
|
# Enable home page h-card.
|
||||||
|
enable = true
|
||||||
|
# Add your email to the card if extra.email is set and not encoded.
|
||||||
|
with_mail = true
|
||||||
|
# Add your social links ('socials' config) to the card.
|
||||||
|
with_social_links = true
|
||||||
|
# Homepage url. Defaults to the value of 'base_url'.
|
||||||
|
homepage = "https://zlinux.ru"
|
||||||
|
avatar = "img/avator.webp"
|
||||||
|
# Display name, default to the value of 'author'.
|
||||||
|
full_name = "Zloy Linux"
|
||||||
|
# Small bio, as shown on social media profiles.
|
||||||
|
biography = "Linux Man"
|
||||||
|
#
|
||||||
|
# You can add any property from https://microformats.org/wiki/h-card#Properties
|
||||||
|
# Make sure to replace all '-' characters by '_'
|
||||||
|
# Examples:
|
||||||
|
p_nickname = "zloy_linux"
|
||||||
|
# p_locality = "Bordeaux"
|
||||||
|
p_country_name = "Russia"
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ sort_by = "date"
|
|||||||
header = {title = "Привет, я Злой!", img = "img/avator.webp", img_alt = "I'm ZLOY" }
|
header = {title = "Привет, я Злой!", img = "img/avator.webp", img_alt = "I'm ZLOY" }
|
||||||
section_path = "blog/_index.md"
|
section_path = "blog/_index.md"
|
||||||
max_posts = 5
|
max_posts = 5
|
||||||
|
projects_path = "projects/_index.md"
|
||||||
|
max_projects = 3
|
||||||
|
show_projects_first = false
|
||||||
social_media_card = "index.jpg"
|
social_media_card = "index.jpg"
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 44 KiB |
210
content/articles/authorized-ssh-key/index.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
+++
|
||||||
|
title = "Авторизация на сервере с использованием SSH-ключей"
|
||||||
|
date = 2025-03-09
|
||||||
|
description = "При установлении соединения с удалённым сервером, будь то через SSH или другие протоколы, критически важно подтвердить свою идентичность для получения доступа к системе. Существует несколько методов аутентификации, но наиболее безопасным и удобным считается использование SSH-ключей. В данном материале мы разберём принципы работы данной технологии, её преимущества перед парольной аутентификацией, а также порядок настройки на практике."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["authorization", "ssh", "server"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = false
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/index_authorized-ssh-key.webp"
|
||||||
|
+++
|
||||||
|
|
||||||
|
## Что такое авторизация с ключами?
|
||||||
|
|
||||||
|
Авторизация с ключами (или метод, основанный на паре открытого и закрытого ключа) — это способ идентификации, при котором вместо обычного пароля используются два криптографических ключа:
|
||||||
|
|
||||||
|
- `Закрытый ключ (private key)` — ваш личный "секрет", который остаётся только у вас и никому не разглашается.
|
||||||
|
- `Открытый ключ (public key)` — своего рода "замок", который вы устанавливаете на сервере, чтобы он распознавал вас как авторизованного пользователя.
|
||||||
|
|
||||||
|
Когда вы подключаетесь, сервер сравнивает ваш закрытый ключ с открытым ключом, который у него есть. Если они совпадают, вы получаете доступ. Это похоже на то, как если бы у вас был ключ от двери, а серверу достаточно проверить, подходит ли он к замку.
|
||||||
|
|
||||||
|
## Чем это лучше паролей?
|
||||||
|
|
||||||
|
1. **Надёжность:** Пароли уязвимы к угадыванию или взлому через перебор (brute force), тогда как ключи — это сложные зашифрованные данные, которые почти невозможно разгадать.
|
||||||
|
2. **Простота использования:** После настройки процесс подключения становится автоматическим, и вам не приходится каждый раз вводить пароль.
|
||||||
|
3. **Защита от перехвата:** Даже если кто-то взломает соединение, закрытый ключ остаётся в безопасности, так как он не передаётся по сети.
|
||||||
|
|
||||||
|
## Принцип работы SSH-аутентификации по ключам:
|
||||||
|
|
||||||
|
1. **Создание ключевой пары**
|
||||||
|
На вашем компьютере генерируется пара ключей — открытый и закрытый. Это можно сделать с помощью команды `ssh-keygen`. Важно: закрытый ключ хранится только на вашем компьютере, а открытый нужно передать на сервер.
|
||||||
|
|
||||||
|
2. **Установка открытого ключа на сервер**
|
||||||
|
Открытый ключ добавляется в специальный файл `authorized_keys` на сервере. Это можно сделать через команду `ssh-copy-id` или вручную, скопировав содержимое файла с открытым ключом.
|
||||||
|
|
||||||
|
3. **Процесс аутентификации**
|
||||||
|
При попытке подключения происходит следующее:
|
||||||
|
|
||||||
|
- Сервер генерирует случайное сообщение
|
||||||
|
- Ваш компьютер “подписывает” это сообщение закрытым ключом
|
||||||
|
- Подписанное сообщение отправляется обратно на сервер
|
||||||
|
- Сервер проверяет подпись с помощью открытого ключа
|
||||||
|
- Если подпись верна — доступ предоставляется
|
||||||
|
|
||||||
|
Такая система обеспечивает высокий уровень безопасности, поскольку закрытый ключ никогда не передаётся по сети, а открытый ключ не может быть использован для дешифровки данных.
|
||||||
|
|
||||||
|
## Настройка SSH-доступа с использованием ключей
|
||||||
|
|
||||||
|
Этот гайд поможет вам настроить безопасный способ подключения к серверу через SSH с использованием ключей вместо паролей. Мы рассмотрим процесс на примере Linux/macOS, но те же принципы применимы и для Windows.
|
||||||
|
|
||||||
|
### 1. Создание ключевой пары
|
||||||
|
|
||||||
|
1. **Запустите терминал на вашем компьютере**
|
||||||
|
2. **Выполните команду генерации ключей:**
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh-keygen -t rsa -b 4096
|
||||||
|
```
|
||||||
|
|
||||||
|
где:
|
||||||
|
|
||||||
|
- `-t rsa` определяет тип ключа
|
||||||
|
- `-b 4096` устанавливает длину ключа для повышенной безопасности
|
||||||
|
|
||||||
|
3. **Следуйте инструкциям:**
|
||||||
|
|
||||||
|
- Выберите место сохранения (по умолчанию `~/.ssh/id_rsa`)
|
||||||
|
- При желании установите пароль для дополнительной защиты ключа
|
||||||
|
|
||||||
|
**В результате вы получите:**
|
||||||
|
|
||||||
|
- Закрытый ключ (`id_rsa`) - храните в надёжном месте
|
||||||
|
- Открытый ключ (`id_rsa.pub`) - будет отправлен на сервер
|
||||||
|
|
||||||
|
### 2. Установка ключа на сервере
|
||||||
|
|
||||||
|
1. Подключитесь к серверу обычным способом:
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh username@server_ip
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Перенесите открытый ключ:
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh-copy-id username@server_ip
|
||||||
|
```
|
||||||
|
|
||||||
|
**Альтернативный способ:**
|
||||||
|
|
||||||
|
- Откройте файл `~/.ssh/authorized_keys` на сервере
|
||||||
|
- Добавьте содержимое вашего `id_rsa.pub`
|
||||||
|
|
||||||
|
### 3. Тестирование подключения
|
||||||
|
|
||||||
|
1. Завершите текущую SSH-сессию:
|
||||||
|
|
||||||
|
```
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Попробуйте подключиться снова:
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh username@server_ip
|
||||||
|
```
|
||||||
|
|
||||||
|
Если всё настроено верно, вы войдёте автоматически. При наличии пароля для ключа система запросит его ввод.
|
||||||
|
|
||||||
|
### 4. Усиление безопасности (опционально)
|
||||||
|
|
||||||
|
1. Отредактируйте конфигурационный файл SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo nano /etc/ssh/sshd_config
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Измените параметры:
|
||||||
|
|
||||||
|
```
|
||||||
|
PasswordAuthentication no
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Перезапустите SSH-сервер:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo systemctl restart sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
После этих действий доступ к серверу будет возможен только с использованием SSH-ключей, что значительно повысит безопасность системы.
|
||||||
|
|
||||||
|
### Важные рекомендации:
|
||||||
|
|
||||||
|
- Регулярно проверяйте список авторизованных ключей
|
||||||
|
- Храните резервные копии приватных ключей
|
||||||
|
- Используйте разные ключи для разных серверов
|
||||||
|
- Не передавайте приватные ключи по незащищённым каналам
|
||||||
|
|
||||||
|
|
||||||
|
## Рекомендации по работе с SSH-ключами
|
||||||
|
|
||||||
|
### Основные правила безопасности
|
||||||
|
|
||||||
|
1. **Защита приватного ключа**
|
||||||
|
|
||||||
|
- Храните его с правами доступа 600
|
||||||
|
- Не передавайте никому
|
||||||
|
- Делайте резервные копии в надёжных местах
|
||||||
|
|
||||||
|
2. **Организация ключей**
|
||||||
|
|
||||||
|
- Создавайте отдельные пары ключей для разных серверов
|
||||||
|
- Используйте понятную систему наименования
|
||||||
|
- Регулярно проверяйте список авторизованных ключей
|
||||||
|
|
||||||
|
|
||||||
|
### Работа с SSH-агентом
|
||||||
|
|
||||||
|
Для удобства работы можно использовать `ssh-agent`:
|
||||||
|
|
||||||
|
```
|
||||||
|
eval $(ssh-agent)
|
||||||
|
ssh-add ~/.ssh/id_rsa
|
||||||
|
```
|
||||||
|
|
||||||
|
Это позволит не вводить пароль от приватного ключа при каждом подключении.
|
||||||
|
|
||||||
|
### Устранение типичных проблем
|
||||||
|
|
||||||
|
1. **Проверьте права доступа:**
|
||||||
|
|
||||||
|
- На клиенте: `chmod 700 ~/.ssh` и `chmod 600 ~/.ssh/id_rsa`
|
||||||
|
- На сервере: `chmod 700 ~/.ssh` и `chmod 600 ~/.ssh/authorized_keys`
|
||||||
|
|
||||||
|
2. **Проверьте настройки сервера:**
|
||||||
|
|
||||||
|
- В файле `/etc/ssh/sshd_config` должны быть строки:
|
||||||
|
|
||||||
|
```
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
AuthorizedKeysFile .ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Используйте отладочную информацию:**
|
||||||
|
|
||||||
|
- Подключайтесь с параметром -v: `ssh -v username@server`
|
||||||
|
- Проверяйте логи сервера: `sudo tail -f /var/log/auth.log`
|
||||||
|
|
||||||
|
### Дополнительные меры безопасности
|
||||||
|
<br>
|
||||||
|
- Отключите вход по паролю:
|
||||||
|
|
||||||
|
```
|
||||||
|
PasswordAuthentication no
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ограничьте доступ по IP:
|
||||||
|
|
||||||
|
```
|
||||||
|
AllowUsers username@192.168.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
- Используйте ключи длиной не менее 4096 бит
|
||||||
|
- Регулярно проверяйте файлы `authorized_keys` на наличие посторонних ключей
|
||||||
|
- При утере приватного ключа немедленно удалите соответствующий публичный ключ с серверов
|
||||||
|
|
||||||
|
Следуя этим рекомендациям, вы сможете обеспечить надёжную и безопасную работу с SSH-ключами.
|
||||||
|
After Width: | Height: | Size: 84 KiB |
123
content/articles/autoconnect-bluetooth-phones/index.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
+++
|
||||||
|
title = "Автоматическое переключение звука на Bluetooth-наушники в Linux с помощью udev и wpctl"
|
||||||
|
date = 2025-09-07
|
||||||
|
description = "Если вы используете Linux и часто подключаете Bluetooth-наушники, наверняка замечали, что звук не всегда автоматически переключается на новое устройство. В этом посте мы разберём простой и надёжный способ автоматизации этого процесса с помощью udev и wpctl (PipeWire)."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["bluetooth", "udev", "wpctl", "pipewire"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = true
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/index_autoconnect-bluetooth-phones.webp"
|
||||||
|
+++
|
||||||
|
|
||||||
|
## Вступление
|
||||||
|
|
||||||
|
В Linux есть два популярных звуковых сервиса: PulseAudio и PipeWire. PipeWire постепенно становится стандартом, и в сочетании с WirePlumber можно настроить автоматическое переключение. Но иногда WirePlumber не срабатывает, или событие обрабатывается с задержкой. Решение — использовать udev, который отслеживает события подключения устройств и запускает скрипт.
|
||||||
|
|
||||||
|
## Шаг 1: Проверяем PipeWire и wpctl
|
||||||
|
|
||||||
|
Убедитесь, что PipeWire и WirePlumber запущены:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status pipewire
|
||||||
|
systemctl --user status wireplumber
|
||||||
|
```
|
||||||
|
|
||||||
|
Также полезно посмотреть список аудиоустройств:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wpctl status
|
||||||
|
```
|
||||||
|
|
||||||
|
Вы увидите свои колонки и Bluetooth-устройства:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
Audio
|
||||||
|
├─ Sinks:
|
||||||
|
│ * 136. alsa_output.usb-ARTURIA_ArturiaMsd-00.analog-stereo
|
||||||
|
│ 55. bluez_output.XX_XX_XX_XX_XX_XX.a2dp-sink
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 2: Создаём скрипт для переключения звука
|
||||||
|
|
||||||
|
Создаём скрипт `/usr/local/bin/bt-audio-autoswitch.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
ACTION=$1
|
||||||
|
STATEFILE="/run/user/$(id -u)/bt-audio-last-sink"
|
||||||
|
LOGTAG="bt-audio-autoswitch"
|
||||||
|
|
||||||
|
# Текущее устройство вывода
|
||||||
|
get_default_sink() {
|
||||||
|
wpctl status | awk '/\*.*sink/ {print $2; exit}' | tr -d '[]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Идентификатор первого Bluetooth sink
|
||||||
|
get_bt_sink() {
|
||||||
|
wpctl status | awk '/bluez_output/ {print $2; exit}' | tr -d '[]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ждём обновления PipeWire
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if [ "$ACTION" = "add" ]; then
|
||||||
|
CURRENT=$(get_default_sink)
|
||||||
|
if [ -n "$CURRENT" ]; then
|
||||||
|
echo "$CURRENT" > "$STATEFILE"
|
||||||
|
logger "$LOGTAG: сохранил текущее устройство $CURRENT"
|
||||||
|
fi
|
||||||
|
BT_ID=$(get_bt_sink)
|
||||||
|
if [ -n "$BT_ID" ]; then
|
||||||
|
wpctl set-default "$BT_ID"
|
||||||
|
logger "$LOGTAG: переключил звук на $BT_ID"
|
||||||
|
fi
|
||||||
|
elif [ "$ACTION" = "remove" ]; then
|
||||||
|
if [ -f "$STATEFILE" ]; then
|
||||||
|
PREV=$(cat "$STATEFILE")
|
||||||
|
if [ -n "$PREV" ]; then
|
||||||
|
wpctl set-default "$PREV"
|
||||||
|
logger "$LOGTAG: вернул звук на $PREV"
|
||||||
|
fi
|
||||||
|
rm -f "$STATEFILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Делаем скрипт исполняемым:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod +x /usr/local/bin/bt-audio-autoswitch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 3: Создаём udev-правило
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Подключение Bluetooth аудио
|
||||||
|
ACTION=="add", SUBSYSTEM=="sound", KERNEL=="card*", RUN+="/usr/local/bin/bt-audio-autoswitch.sh add"
|
||||||
|
|
||||||
|
# Отключение Bluetooth аудио
|
||||||
|
ACTION=="remove", SUBSYSTEM=="sound", KERNEL=="card*", RUN+="/usr/local/bin/bt-audio-autoswitch.sh remove"
|
||||||
|
```
|
||||||
|
|
||||||
|
Применяем новые правила:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 4: Проверяем работу
|
||||||
|
|
||||||
|
Подключаем Bluetooth-наушники. Скрипт автоматически сохранит текущее устройство и переключит звук на наушники.
|
||||||
|
|
||||||
|
Отключаем наушники. Скрипт вернёт звук на сохранённое устройство (например, ваши колонки).
|
||||||
|
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
Использование udev + wpctl позволяет надёжно автоматизировать переключение аудио на Bluetooth-наушники даже в случае, если WirePlumber не успевает сработать. Этот подход легко настраивается и не требует ручного выбора устройства при каждом подключении.
|
||||||
|
After Width: | Height: | Size: 109 KiB |
108
content/articles/disable-screensaver-fullscreen/index.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
+++
|
||||||
|
title = "Блокируем скринсейвер в полноэкранном режиме браузера Vivaldi"
|
||||||
|
date = 2025-10-31
|
||||||
|
description = "Если при просмотре видео в полноэкранном режиме в Vivaldi (или любом другом браузере) скринсейвер всё равно включается, то в этой статье вы узнаете как это исправить"
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["xscreensaver", "wmctrl", "xprop", "X11", "vivaldi", "browser"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = false
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/disable-screensaver-fullscreen.png"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Иногда во время просмотра видео или презентации в браузере активируется `xscreensaver` — экран блокируется, несмотря на то, что пользователь фактически «активен».
|
||||||
|
|
||||||
|
Чтобы этого не происходило, можно написать небольшой bash-скрипт, который будет следить за окном браузера и отключать скринсейвер, когда он в полноэкранном режиме.
|
||||||
|
|
||||||
|
## 💾 Установка необходимых пакетов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S wmctrl xorg-xprop
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Скрипт
|
||||||
|
|
||||||
|
Создаём файл `/usr/local/bin/disable_screensaver_fullscreen.sh` и вставляем следующее содержимое:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
XSCREENSAVER_CMD="xscreensaver-command"
|
||||||
|
CHECK_INTERVAL=30
|
||||||
|
|
||||||
|
# Функция для деактивации скринсейвера (вызывается постоянно, пока Vivaldi в полноэкранном режиме)
|
||||||
|
deactivate_screensaver() {
|
||||||
|
# echo "Vivaldi в полноэкранном режиме. Отключаем скринсейвер."
|
||||||
|
"$XSCREENSAVER_CMD" -deactivate >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Мониторинг полноэкранного режима Vivaldi запущен (только деактивация скринсейвера)."
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
VIVALDI_WINDOW_ID=$(wmctrl -l -x | grep "vivaldi-stable.Vivaldi" | awk '{print $1}' | head -n 1)
|
||||||
|
|
||||||
|
if [ -n "$VIVALDI_WINDOW_ID" ]; then # Проверяем, что Vivaldi запущен
|
||||||
|
# Vivaldi запущен, проверяем полноэкранный режим
|
||||||
|
if xprop -id "$VIVALDI_WINDOW_ID" _NET_WM_STATE | grep -q "_NET_WM_STATE_FULLSCREEN"; then
|
||||||
|
deactivate_screensaver # Vivaldi в полноэкранном режиме: деактивируем
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$CHECK_INTERVAL"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Сделаем скрипт исполняемым:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x /usr/local/bin/disable_screensaver_fullscreen.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Автозапуск при старте системы
|
||||||
|
|
||||||
|
Создайте файл:
|
||||||
|
`~/.config/autostart/block-screensaver-vivaldi.desktop`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Block Screensaver in Vivaldi Fullscreen
|
||||||
|
Exec=/usr/local/bin/block-screensaver-vivaldi.sh
|
||||||
|
Hidden=false
|
||||||
|
NoDisplay=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
```
|
||||||
|
|
||||||
|
В bspwm достаточно добавить в `~/.config/bspwm/bspwm.conf` строчку
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/usr/local/bin/vivaldi-stop-screensaver.sh &
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧠 Как это работает
|
||||||
|
|
||||||
|
Скрипт каждые 30 секунд:
|
||||||
|
|
||||||
|
- Проверяет, запущен ли браузер Vivaldi.
|
||||||
|
- Если окно в состоянии fullscreen, выполняет `xscreensaver-command -deactivate`, тем самым сбрасывая таймер бездействия.
|
||||||
|
|
||||||
|
Такой подход не мешает обычной работе скринсейвера, а только предотвращает его запуск во время полноэкранного просмотра видео.
|
||||||
|
|
||||||
|
- Нагрузка: sleep 30 → менее 0.1% CPU.
|
||||||
|
- Безопасность: только чтение X11, не требует root.
|
||||||
|
- Логи: раскомментируйте echo в функции для отладки.
|
||||||
|
|
||||||
|
## 🧩 Совместимость
|
||||||
|
|
||||||
|
Скрипт можно адаптировать под:
|
||||||
|
|
||||||
|
- Chromium / Brave / Chrome — заменив vivaldi-stable.Vivaldi на chromium.Chromium или соответствующее имя.
|
||||||
|
- Firefox — используйте Navigator.Firefox.
|
||||||
|
|
||||||
|
## ✅ Вывод
|
||||||
|
|
||||||
|
Простой, но эффективный способ избавить себя от внезапного включения заставки во время фильмов, YouTube и стримов.
|
||||||
|
Пусть xscreensaver отдыхает, когда ты смотришь видео — а не мешает! 😄
|
||||||
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 67 KiB |
80
content/articles/dunst/index.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
+++
|
||||||
|
title = "Dunst - добавляем к уведомлениям звуковое сопровождение"
|
||||||
|
date = 2025-03-14
|
||||||
|
description = "Dunst — это легковесный и настраиваемый демон уведомлений для Linux. В этой статье для примера мы рассмотрим, как настроить Dunst так, чтобы он проигрывал звук после завершения загрузки файла в браузере Vivaldi."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["dunst", "vivaldi", "notification"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = true
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/index_dunst.webp"
|
||||||
|
+++
|
||||||
|
|
||||||
|
## Определение уведомления
|
||||||
|
|
||||||
|
Прежде чем создать правило для Dunst, необходимо выяснить, какие параметры содержит уведомление о завершении загрузки.
|
||||||
|
|
||||||
|
Откройте терминал и запустите одну из следующих команд:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dunst -print
|
||||||
|
```
|
||||||
|
|
||||||
|
или
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dbus-monitor "interface='org.freedesktop.Notifications'"
|
||||||
|
```
|
||||||
|
|
||||||
|
Затем загрузите файл через Vivaldi и обратите внимание на появившееся уведомление. Важно определить 'appname' и 'summary', которые используются для фильтрации уведомлений.
|
||||||
|
|
||||||
|
Получим вот такой вывод
|
||||||
|
|
||||||
|
```conf
|
||||||
|
...
|
||||||
|
string "Vivaldi"
|
||||||
|
uint32 0
|
||||||
|
string "file:///tmp/..com.vivaldi.Vivaldi.aPjVzZ"
|
||||||
|
string "Загрузка завершена"
|
||||||
|
string "Vivaldi
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройка правила в Dunst
|
||||||
|
|
||||||
|
Открываем конфигурационный файл Dunst `~/.config/dunst/dunstrc` добавляем в конец файла следующее правило:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[vivaldi_sound]
|
||||||
|
appname = "Vivaldi"
|
||||||
|
summary = "Загрузка завершена"
|
||||||
|
script = "~/.config/dunst/scripts/vivaldi-sound.sh"
|
||||||
|
# new_icon = "~/.config/dunst/scripts/download.svg"
|
||||||
|
```
|
||||||
|
**Разбор параметров:**
|
||||||
|
|
||||||
|
- `appname` = "vivaldi" — фильтр по приложению (может быть Vivaldi-stable, уточните в отладчике dunst -print).
|
||||||
|
- `summary` = "Загрузка завершена" — заголовок уведомления (может отличаться, проверьте с помощью dbus-monitor).
|
||||||
|
- `script` = "vivaldi-sound.sh" — ссылка на скрипт
|
||||||
|
- `new_icon` = "путь до изображения" - при желании можно сменить иконку в уведомлении
|
||||||
|
|
||||||
|
|
||||||
|
Содержимое `vivaldi-sound.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/sh
|
||||||
|
paplay ~/.config/dunst/scripts/sounds/zvonkiy-korotkiy-zvuk-uvedomleniya.ogg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Перезапуск Dunst
|
||||||
|
|
||||||
|
После сохранения изменений нужно перезапустить Dunst
|
||||||
|
|
||||||
|
`pkill dunst && dunst &`
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
Теперь после завершения загрузки файла в Vivaldi будет воспроизводить звуковой сигнал через Dunst. Вы можете настроить другие уведомления аналогичным способом, например, добавить звуки для других событий, таких как ошибки или завершение работы программ.
|
||||||
BIN
content/articles/dunst/social_cards/index_dunst.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
116
content/articles/sshfs/index.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
+++
|
||||||
|
title = "SSHFS: Монтирование удаленных файловых систем через SSH"
|
||||||
|
date = 2025-03-09
|
||||||
|
description = "SSHFS (SSH File System) позволяет монтировать удаленные файловые системы через SSH, обеспечивая безопасный и удобный доступ к файлам на удаленных серверах."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["sshfs", "ssh", "server"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = false
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/index-sshfs.webp"
|
||||||
|
+++
|
||||||
|
|
||||||
|
## Введение
|
||||||
|
|
||||||
|
[SSHFS](https://github.com/libfuse/sshfs) – это мощный инструмент для работы с удаленными файлами, обеспечивающий удобство и безопасность благодаря использованию SSH. Он полезен для резервного копирования, совместной работы и доступа к файлам на удаленных серверах без сложной настройки.
|
||||||
|
|
||||||
|
## Установка SSHFS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S sshfs
|
||||||
|
```
|
||||||
|
## Монтирование удаленной файловой системы
|
||||||
|
|
||||||
|
1. Создайте локальную папку для монтирования:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir /mnt/sshfs
|
||||||
|
```
|
||||||
|
2. Подключите удаленную файловую систему:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshfs user@remote_host:/remote_folder /mnt/sshfs
|
||||||
|
```
|
||||||
|
|
||||||
|
- `user` – имя пользователя на удаленном сервере
|
||||||
|
- `remote_host` – IP-адрес или доменное имя сервера
|
||||||
|
- `/remote_folder` – каталог, который нужно смонтировать
|
||||||
|
- `/mnt/sshfs` – локальная папка, куда будет смонтирован удаленный ресурс
|
||||||
|
|
||||||
|
Если используется нестандартный порт указать можно так:
|
||||||
|
```bash
|
||||||
|
sshfs -p 223 user@remote_host:/remote_folder /mnt/sshfs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Автоматическое монтирование при загрузке
|
||||||
|
|
||||||
|
Добавьте в `/etc/fstab` строку:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
user@remote_host:/remote_folder /mnt/sshfs fuse.sshfs defaults,_netdev,user,idmap=user,allow_other,reconnect 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Создайте файл с учетными данными `~/.sshfs_credentials`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "password" > ~/.sshfs_credentials
|
||||||
|
chmod 600 ~/.sshfs_credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
с содежимым:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
username=ТВОЙ_ЛОГИН
|
||||||
|
password=ТВОЙ_ПАРОЛЬ
|
||||||
|
```
|
||||||
|
|
||||||
|
Монтирование с учётом `~/.sshfs_credentials`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshfs -o password_stdin,credentials=~/.sshfs_credentials user@remote_host:/remote_folder /mnt/sshfs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Размонтирование файловой системы
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fusermount -u /mnt/sshfs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Дополнительные параметры**
|
||||||
|
|
||||||
|
- `-o reconnect` – автоматически переподключаться при разрыве связи
|
||||||
|
- `-o allow_other` – разрешить доступ другим пользователям системы
|
||||||
|
- `-o idmap=user` – сопоставлять идентификаторы пользователей
|
||||||
|
|
||||||
|
|
||||||
|
## Монтирование с помощью systemd
|
||||||
|
|
||||||
|
{{ admonition(type="danger", icon="tip", title="Внимание!", text="Авторизация на сервере должна проходить по ключу") }}
|
||||||
|
|
||||||
|
Создаём юнит-файл systemd `~/.config/systemd/user/server.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Mount /mnt/sshfs/
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=sshfs -p 666 user@remote_host:/remote_folder /mnt/sshfs
|
||||||
|
ExecStop=umount /mnt/sshfs/
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
# Запуск и автозагрузка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable now server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь `/mnt/sshfs/` автоматически монтируется при загрузке.
|
||||||
BIN
content/articles/sshfs/social_cards/index-sshfs.webp
Normal file
|
After Width: | Height: | Size: 67 KiB |
@@ -19,22 +19,31 @@ social_media_card = "social_cards/index-post-ia.webp"
|
|||||||
|
|
||||||
Этот ИИ оказался самым тупым и выдал
|
Этот ИИ оказался самым тупым и выдал
|
||||||
|
|
||||||
<img src="img/perplexity.png">
|
<div class="gallery">
|
||||||
|
<a href="img/perplexity.png">
|
||||||
|
<img src="img/perplexity.png" alt="Превью">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Grok
|
## Grok
|
||||||
|
|
||||||
Оказался поумнее
|
Оказался поумнее
|
||||||
|
|
||||||
<img src="img/grok.png">
|
<div class="gallery">
|
||||||
|
<a href="img/grok.png">
|
||||||
|
<img src="img/grok.png" alt="Grok">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
## chatGPT
|
## chatGPT
|
||||||
|
|
||||||
Выдал примерно тоже самое, что и Grok
|
Выдал примерно тоже самое, что и Grok
|
||||||
|
|
||||||
<img src="img/chat_gpt.png">
|
<div class="gallery">
|
||||||
|
<a href="img/chat_gpt.png">
|
||||||
|
<img src="img/chat_gpt.png" alt="ChatGPT">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Свой вариант
|
## Свой вариант
|
||||||
|
|
||||||
|
|||||||
128
content/blog/polybar-stop-screensaver/index.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
+++
|
||||||
|
title = "Переписал скрипт stop-screensaver и добавил на панель Polybar"
|
||||||
|
date = 2025-11-22
|
||||||
|
description = "В прошлом [посте](https://blog.zlinux.ru/articles/disable-screensaver-fullscreen/) выкладывал скрипт, который отключал скринсейвер при просмотре видео в браузере. Позже понял, что держать его запущенным постоянно - не лучшая идея."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["xscreensaver", "polybar", "playerctl"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = false
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/polybar-stop-screensaver.png"
|
||||||
|
+++
|
||||||
|
|
||||||
|
В прошлом [посте](https://blog.zlinux.ru/articles/disable-screensaver-fullscreen/) выкладывал скрипт, который отключал скринсейвер при просмотре видео в браузере. Позже понял, что держать его запущенным постоянно - не лучшая идея.
|
||||||
|
|
||||||
|
## Новый скрипт
|
||||||
|
|
||||||
|
Переписал скрипт `/usr/local/bin/stop-screensaver.sh` и максимально упростил:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
while true; do
|
||||||
|
status=$(playerctl --player=vivaldi status 2>/dev/null)
|
||||||
|
if [[ "$status" == "Playing" ]]; then
|
||||||
|
xscreensaver-command -deactivate >/dev/null
|
||||||
|
fi
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Для удобства написал полноценный модуль для `Polybar`, который позволяет включать и выключать эту функцию по клику. Для работы понадобится всего два небольших скрипта.
|
||||||
|
|
||||||
|
|
||||||
|
## Статусный скрипт
|
||||||
|
|
||||||
|
Показывает иконку "ICON_ON" когда демон запущен, и "ICON_OFF" - когда нет) `/home/$USER/.config/polybar/scripts/stop_screensaver_status.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
STATEFILE="/tmp/stop-screensaver.state"
|
||||||
|
ICON_ON=""
|
||||||
|
ICON_OFF=""
|
||||||
|
|
||||||
|
if [ -f "$STATEFILE" ]; then
|
||||||
|
state=$(cat "$STATEFILE")
|
||||||
|
else
|
||||||
|
state=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# fallback проверка процесса, если statefile отсутствует/неактуален
|
||||||
|
if pgrep -f "/usr/local/bin/stop-screensaver.sh" >/dev/null; then
|
||||||
|
state="running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$state" = "running" ]; then
|
||||||
|
echo " $ICON_ON "
|
||||||
|
else
|
||||||
|
echo " $ICON_OFF "
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Кликабельный скрипт
|
||||||
|
|
||||||
|
Скрипт для запуска/остановки демона. `/home/$USER/.config/polybar/scripts/stop_screensaver_toggle.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
SCRIPT="/usr/local/bin/stop-screensaver.sh"
|
||||||
|
STATEFILE="/tmp/stop-screensaver.state"
|
||||||
|
|
||||||
|
if pgrep -f "$SCRIPT" >/dev/null; then
|
||||||
|
pkill -f "$SCRIPT"
|
||||||
|
else
|
||||||
|
nohup "$SCRIPT" >/dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
# записать текущее состояние
|
||||||
|
if pgrep -f "$SCRIPT" >/dev/null; then
|
||||||
|
echo "running" >"$STATEFILE"
|
||||||
|
else
|
||||||
|
echo "stopped" >"$STATEFILE"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Делаем файлы исполняемыми
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.config/polybar/scripts/stop_screensaver_*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Добавляем в polybar
|
||||||
|
|
||||||
|
В конфиге `/home/$USER/.config/polybar/config.ini` прописываем:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[module/stop_screensaver]
|
||||||
|
type = custom/script
|
||||||
|
exec = ~/.config/polybar/scripts/stop_screensaver_status.sh
|
||||||
|
interval = 2
|
||||||
|
click-left = ~/.config/polybar/scripts/stop_screensaver_toggle.sh
|
||||||
|
label-background = ${gruvbox.green-alt}
|
||||||
|
label-foreground = ${gruvbox.black}
|
||||||
|
```
|
||||||
|
|
||||||
|
и добавляем расположение модуля `stop_screensaver` в modules-left/modules-center/modules-right
|
||||||
|
|
||||||
|
|
||||||
|
<div class="gallery">
|
||||||
|
<a href="stop-screensaver-off.png" data-caption="Значок на панеле Polybar">
|
||||||
|
<img src="stop-screensaver-off.png" alt="stop_screensaver выключен">
|
||||||
|
</a>
|
||||||
|
<a href="stop-screensaver-on.png">
|
||||||
|
<img src="stop-screensaver-on.png" alt="stop_screensaver включен">
|
||||||
|
</a>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 72 KiB |
BIN
content/blog/polybar-stop-screensaver/stop-screensaver-off.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
content/blog/polybar-stop-screensaver/stop-screensaver-on.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
100
content/blog/refused-syncthing/index.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
+++
|
||||||
|
title = "Отказался от Syncthing"
|
||||||
|
date = 2025-11-28
|
||||||
|
description = "За последние годы я был преданным пользователем Syncthing. Он честно отрабатывал синхронизацию дотфайлов, музыки, баз паролей и хранилища Obsidian между ПК, домашним сервером и телефоном."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["syncthing", "rsync", "music", "keepass", "seafile", "webdav", "git", "obsidian", "sync", "android", "archlinux"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = false
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/refused-syncthing.png"
|
||||||
|
+++
|
||||||
|
|
||||||
|
За последние годы я был преданным пользователем `Syncthing`. Он честно отрабатывал синхронизацию дотфайлов, музыки, баз паролей и хранилища `Obsidian` между ПК, домашним сервером и телефоном.
|
||||||
|
|
||||||
|
Но со временем накопился «технический долг»: постоянные конфликты в `KeePassXC`, лишний трафик, батарея на телефоне, а главное — ощущение, что я использую универсальный молоток там, где достаточно отвёртки. В итоге я полностью отказался от `Syncthing` и перешёл на три узкоспециализированных, но гораздо более надёжных и лёгких решения.
|
||||||
|
|
||||||
|
## 1. Музыка для Navidrome → rsync (односторонняя синхронизация)
|
||||||
|
|
||||||
|
Раньше я синхронизировал ~60 ГБ музыки в обе стороны через `Syncthing`.
|
||||||
|
|
||||||
|
Теперь всё просто: на ПК лежит «золотая» копия библиотеки, а на сервере я обновляю её только когда хочу.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -avzh --progress --stats --delete --itemize-changes --exclude='ненужное' /home/zloy_linux/Музыка/ user@server.ru:/home/user/music/
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему `rsync`, а не `Syncthing`:
|
||||||
|
|
||||||
|
- Никаких конфликтов и дублей
|
||||||
|
- --delete убирает удалённые файлы
|
||||||
|
- Полный контроль: я решаю, когда и что синхронизировать
|
||||||
|
- Можно запускать по расписанию через cron/anacron или вручную после добавления альбомов
|
||||||
|
|
||||||
|
**Значение опций:**
|
||||||
|
**-z** сжатие (полезно, если канал медленный).
|
||||||
|
**-h** человекочитаемые размеры.
|
||||||
|
**--stats** - статистика по передаче.
|
||||||
|
**--delete** - убирает удалённые файлы
|
||||||
|
**--exclude** исключения.
|
||||||
|
|
||||||
|
## 2. Пароли KeePassXC → Seafile + WebDAV (одна общая база)
|
||||||
|
|
||||||
|
`Syncthing` постоянно создавал .sync-conflict файлы при одновременном редактировании базы с телефона и ПК. Решение оказалось проще простого:
|
||||||
|
|
||||||
|
1. Поднял `Seafile`
|
||||||
|
2. Включил `WebDAV` в библиотеке
|
||||||
|
3. В `KeePassXC` на всех устройствах указал один и тот же URL:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
https://cloud.userdomain.ru:/seafdav/library/sync/paroli.kdbx
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь:
|
||||||
|
|
||||||
|
- Одна база, никаких конфликтов
|
||||||
|
- Автоматическая блокировка при открытии
|
||||||
|
- История изменений в Seafile на 180 дней (если вдруг что-то удалишь)
|
||||||
|
|
||||||
|
## 3. Заметки Obsidian → Git
|
||||||
|
|
||||||
|
Раньше я синхронизировал всё хранилице через `Syncthing` → конфликты, битые ссылки, тормоза на мобильном. Сейчас:
|
||||||
|
|
||||||
|
- Хранилище лежит в приватном репозитории на своём Gitea (можно GitHub, GitLab, Codeberg)
|
||||||
|
- В `Obsidian` установлен плагин `Git`
|
||||||
|
- Настройки плагина:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
Commit message: "auto commit: {{date}}"
|
||||||
|
Auto commit every: 3 минуты
|
||||||
|
Auto pull/push on startup: включено
|
||||||
|
Pull on mobile: включено
|
||||||
|
```
|
||||||
|
|
||||||
|
На телефоне - приложение `Obsidian` + `git`. Плюсы:
|
||||||
|
|
||||||
|
- Полная история изменений (можно откатить удалённую заметку за секунду)
|
||||||
|
- Никаких конфликтов - `git` сам делает merge
|
||||||
|
- Работает даже без интернета (коммиты локально, пуш при подключении)
|
||||||
|
|
||||||
|
## Итог: что я получил после отказа от Syncthing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Что синхронизирую | Было (Syncthing) | Стало (2025) | Выигрыш |
|
||||||
|
|-------------------------|------------------------------------------|---------------------------------------|----------------------------------------------------|
|
||||||
|
| Музыка (~60 ГБ) | Двусторонняя синхронизация, конфликты | rsync (односторонне) | -90 % трафика, нет дублей, полный контроль |
|
||||||
|
| Пароли (KeePassXC) | Постоянные .kdbx.sync-conflict файлы | Seafile + WebDAV (одна общая база) | Ноль конфликтов, блокировка, история изменений |
|
||||||
|
| Заметки (Obsidian) | Конфликты, битые ссылки, тормоза | Obsidian Git + личный Gitea | История коммитов, ветки, оффлайн-работа, автопуш |
|
||||||
|
| Батарея телефона | Syncthing в фоне всё время | Только rsync по необходимости | +2–3 часа автономности |
|
||||||
|
| Трафик и нагрузка | Постоянный скан и передача | Только когда я сам запускаю | Минимальный фоновый трафик |
|
||||||
|
|
||||||
|
|
||||||
|
**Syncthing** - отличный инструмент, но он решает задачу «синхронизировать всё со всем».
|
||||||
|
|
||||||
|
А мне оказалось достаточно трёх точечных решений, которые делают ровно то, что нужно - и ничего лишнего.
|
||||||
|
|
||||||
|
2025 год — год простых и надёжных решений. `rsync` + `Seafile` `WebDAV` + `Obsidian` `Git`.
|
||||||
|
After Width: | Height: | Size: 90 KiB |
@@ -17,7 +17,11 @@ social_media_card = "social_cards/index-post-predprosmotr.webp"
|
|||||||
|
|
||||||
При добавлении в пост ссылки должна отображаться предварительная информация о содержимом веб-страницы, а в данном случае получалось вот это
|
При добавлении в пост ссылки должна отображаться предварительная информация о содержимом веб-страницы, а в данном случае получалось вот это
|
||||||
|
|
||||||
<img src="not_preview.webp" />
|
<div class="gallery">
|
||||||
|
<a href="not_preview.webp">
|
||||||
|
<img src="not_preview.webp" alt="Предварительный просмотр недоступен">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
Поначалу копался в настройках инстанса, но оказалось, проблема крылась во внутреннем DNS, который возвращал для домена локальный IP-адрес
|
Поначалу копался в настройках инстанса, но оказалось, проблема крылась во внутреннем DNS, который возвращал для домена локальный IP-адрес
|
||||||
|
|
||||||
@@ -34,7 +38,11 @@ Address: ::1
|
|||||||
|
|
||||||
Сменил DNS сервер, стало выдавать глобальный IP и всё заработало.
|
Сменил DNS сервер, стало выдавать глобальный IP и всё заработало.
|
||||||
|
|
||||||
<img src="yes_preview.webp" />
|
<div class="gallery">
|
||||||
|
<a href="yes_preview.webp">
|
||||||
|
<img src="yes_preview.webp" alt="Предварительный просмотр доступен">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
Поигрался с ссылками и оказалось
|
Поигрался с ссылками и оказалось
|
||||||
|
|
||||||
|
|||||||
BIN
content/blog/zamena-ssd-na-servere/clonezilla.webp
Normal file
|
After Width: | Height: | Size: 159 KiB |
46
content/blog/zamena-ssd-na-servere/index.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
+++
|
||||||
|
title = "Замена SSD на сервере"
|
||||||
|
date = 2025-03-06
|
||||||
|
description = "Пришло время для расширения дискового пространства, поскольку свободных осталось ~30GB, осталось опеределиться с выбором, что покупать."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["server", "ssd", "clonezilla"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = false
|
||||||
|
mermaid = false
|
||||||
|
social_media_card = "social_cards/index_zamena-ssd-server.webp"
|
||||||
|
+++
|
||||||
|
|
||||||
|
## Выбор
|
||||||
|
Не хотелось долго ждать заказ, поэтому решил воспользоваться ближайшим магазином DNS. В моём мини-ПК сервере есть поддержка как `SATA`, так и `M.2 SATA`, и, конечно, приоритет был за `M.2`. Однако по сути скорость у них одинаковая — разница лишь в интерфейсе подключения.
|
||||||
|
|
||||||
|
В последние годы появилось много сомнительных китайских SSD, которые могут выйти из строя буквально после первого включения. Как оказалось, выбор не так велик: `ADATA`, `Apacer`, `TEAMGROUP` и `Tammuz` — не самые внушающие доверие бренды.
|
||||||
|
|
||||||
|
Среди SATA SSD ассортимент оказался шире, и в наличии был [Samsung 870 EVO](https://www.dns-shop.ru/product/cd3ad695f76bed20/500-gb-25-sata-nakopitel-samsung-870-evo-mz-77e500bw/). Такой же, но в формате `M.2 NVMe`, уже два года работает в моём основном ПК без проблем. Выбор очевиден.
|
||||||
|
|
||||||
|
<div class="gallery">
|
||||||
|
<a href="samsung-ssd.webp">
|
||||||
|
<img src="samsung-ssd.webp" alt="Samsung 870 EVO и какой - то ноунейм">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Замена SSD
|
||||||
|
|
||||||
|
На фото выше — комплектный SSD неизвестного бренда и сомнительного происхождения. Удивительно, но он проработал почти год.
|
||||||
|
Интересный момент: диск, подключённый через `SATA`, определяется как `sda1`, а `M.2 SATA` — как `sdb1`. Выходит, что первым по приоритету считается именно SATA-интерфейс.
|
||||||
|
|
||||||
|
## Перенос данных
|
||||||
|
|
||||||
|
Привет и уважение разработчикам замечательной [Clonezilla](https://clonezilla.org) — просто, быстро и удобно (ну, если не считать великолепный `dd`).
|
||||||
|
|
||||||
|
<div class="gallery">
|
||||||
|
<a href="clonezilla.webp">
|
||||||
|
<img src="clonezilla.webp" alt="Клонировние старого ssd">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Итог
|
||||||
|
|
||||||
|
На всё ушло около 10 минут, зато теперь у меня есть свободные 400 ГБ — можно продолжать заполнять данными.
|
||||||
BIN
content/blog/zamena-ssd-na-servere/samsung-ssd.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 91 KiB |
@@ -32,7 +32,9 @@ Looking for detailed instructions or tips on using **tabi**? The [blog](https://
|
|||||||
|
|
||||||
Contributions are much appreciated! We appreciate bug reports, improvements to translations or documentation (however minor), feature requests… Check out the [Contributing Guidelines](https://github.com/welpo/tabi/blob/main/CONTRIBUTING.md) to learn how you can help. Thank you!
|
Contributions are much appreciated! We appreciate bug reports, improvements to translations or documentation (however minor), feature requests… Check out the [Contributing Guidelines](https://github.com/welpo/tabi/blob/main/CONTRIBUTING.md) to learn how you can help. Thank you!
|
||||||
|
|
||||||
## License
|
## license
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
The code is available under the [MIT license](https://choosealicense.com/licenses/mit/).
|
The code is available under the [MIT license](https://choosealicense.com/licenses/mit/).
|
||||||
|
|
||||||
|
|||||||
11
content/projects/_index.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
+++
|
||||||
|
title = "Проекты"
|
||||||
|
sort_by = "weight"
|
||||||
|
template = "cards.html"
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
social_media_card = "projects/projects.jpg"
|
||||||
|
show_reading_time = false
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
+++
|
||||||
9
content/projects/microblog.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
+++
|
||||||
|
title = "Микроблог"
|
||||||
|
description = "Zloy про Arch, терминал, велосипеды и всё, что движется (и компилируется)."
|
||||||
|
weight = 1
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
local_image = "projects/microblog.svg"
|
||||||
|
link_to = "https://log.zlinux.ru"
|
||||||
|
+++
|
||||||
BIN
content/projects/projects.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
9
content/projects/searxng.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
+++
|
||||||
|
title = "Searxng"
|
||||||
|
description = "Поисковая система"
|
||||||
|
weight = 3
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
local_image = "projects/searxng.svg"
|
||||||
|
link_to = "https://search.zlinux.ru/searxng/"
|
||||||
|
+++
|
||||||
BIN
content/soft/lossless-cut/img/lossless-cut-export.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
content/soft/lossless-cut/img/lossless-cut-main.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
70
content/soft/lossless-cut/index.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
+++
|
||||||
|
title = "LosslessCut – Лёгкий и быстрый редактор видео"
|
||||||
|
date = 2025-03-22
|
||||||
|
description = "LosslessCut — это бесплатный и кроссплатформенный инструмент для быстрой обрезки и монтажа видео и аудио файлов без перекодирования. Он разработан для тех, кому нужно быстро вырезать фрагменты из медиафайлов, сохранив оригинальное качество."
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["video", "editor", "ffmpeg"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
quick_navigation_buttons = true
|
||||||
|
toc = true
|
||||||
|
mermaid = true
|
||||||
|
social_media_card = "social_cards/index-lossless-cut.webp"
|
||||||
|
+++
|
||||||
|
|
||||||
|
<div class="gallery">
|
||||||
|
<a href="img/lossless-cut-main.webp">
|
||||||
|
<img src="img/lossless-cut-main.webp" alt="Внешний вид Lossless-Cut">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
- Быстрая обрезка и нарезка – позволяет вырезать нужные фрагменты видео и аудио без потерь.
|
||||||
|
- Поддержка множества форматов – работает с MP4, MKV, WEBM, MOV, AAC, MP3 и другими контейнерами.
|
||||||
|
- Без перекодирования (lossless) – не ухудшает качество, поскольку не выполняет повторную компрессию.
|
||||||
|
- Извлечение аудиодорожек – позволяет выделить аудиофайлы из видео.
|
||||||
|
- Поддержка субтитров и метаданных – можно сохранять и удалять субтитры и другие потоки.
|
||||||
|
- Создание скриншотов – позволяет делать кадры из видео в высоком разрешении.
|
||||||
|
- Слияние файлов – объединяет несколько медиафайлов одинакового формата.
|
||||||
|
- Разделение по главам – поддерживает работу с файлами, содержащими главы (например, Blu-ray рипы).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="gallery">
|
||||||
|
<a href="img/lossless-cut-export.webp">
|
||||||
|
<img src="img/lossless-cut-export.webp" alt="Параметры экспорта">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Преимущества
|
||||||
|
|
||||||
|
✔️ Скорость – операции выполняются моментально, так как нет необходимости в перекодировании.
|
||||||
|
✔️ Лёгкость использования – интуитивно понятный интерфейс, не требует сложной настройки.
|
||||||
|
✔️ Кроссплатформенность – доступен для Windows, macOS и Linux.
|
||||||
|
✔️ Open Source – проект с открытым исходным кодом, доступный на GitHub.
|
||||||
|
|
||||||
|
## Недостатки
|
||||||
|
|
||||||
|
❌ Не поддерживает сложный монтаж (например, переходы или наложение эффектов).
|
||||||
|
❌ Возможны проблемы с редактированием некоторых кодеков (например, HEVC).
|
||||||
|
❌ Нет автоматического исправления ошибок в файлах.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay losslesscut-bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
LosslessCut идеально подходит для быстрой и эффективной нарезки видео и аудио без потерь качества. Это простой инструмент для тех, кто не хочет возиться с громоздкими видеоредакторами.
|
||||||
|
|
||||||
|
**Подходит для:**
|
||||||
|
✅ Быстрой нарезки записей с экрана или камеры.
|
||||||
|
✅ Вырезания рекламы из записей трансляций.
|
||||||
|
✅ Извлечения звука из видео.
|
||||||
|
|
||||||
|
|
||||||
|
* GitHub: [https://github.com/mifi/lossless-cut](https://github.com/mifi/lossless-cut)
|
||||||
BIN
content/soft/lossless-cut/social_cards/index-lossless-cut.webp
Normal file
|
After Width: | Height: | Size: 179 KiB |
@@ -13,7 +13,11 @@ mermaid = true
|
|||||||
social_media_card = "social_cards/index-soft-nmail.webp"
|
social_media_card = "social_cards/index-soft-nmail.webp"
|
||||||
+++
|
+++
|
||||||
|
|
||||||
<img src="img/nmail.png">
|
<div class="gallery">
|
||||||
|
<a href="img/nmail.png">
|
||||||
|
<img src="img/nmail.png" alt="Внешний вид Nmail">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Описание
|
## Описание
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
+++
|
+++
|
||||||
title = "Файловый менеджер Yazi"
|
title = "Файловый менеджер Yazi"
|
||||||
date = 2025-02-25
|
date = 2025-02-25
|
||||||
|
updated = 2025-03-09
|
||||||
description = "Yazi — это современный, быстрый и настраиваемый файловый менеджер для терминала, написанный на Rust. Он поддерживает удобную навигацию, предпросмотр файлов, работу с вкладками и мощные клавиатурные сочетания, вдохновлённые ranger и nnn."
|
description = "Yazi — это современный, быстрый и настраиваемый файловый менеджер для терминала, написанный на Rust. Он поддерживает удобную навигацию, предпросмотр файлов, работу с вкладками и мощные клавиатурные сочетания, вдохновлённые ranger и nnn."
|
||||||
|
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
@@ -31,7 +32,11 @@ Yazi (что означает "утка") - это файловый менедж
|
|||||||
- SCP и другие протоколы 🌐 – можно настроить для удалённой работы с файлами.
|
- SCP и другие протоколы 🌐 – можно настроить для удалённой работы с файлами.
|
||||||
|
|
||||||
|
|
||||||
<img src="img/yazi_screen.png">
|
<div class="gallery">
|
||||||
|
<a href="img/yazi_screen.png">
|
||||||
|
<img src="img/yazi_screen.png" alt="Внешний вид приложения">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
@@ -39,6 +44,10 @@ Yazi (что означает "утка") - это файловый менедж
|
|||||||
sudo pacman -S yazi
|
sudo pacman -S yazi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Демо
|
||||||
|
|
||||||
|
<iframe title="Демо менеджера файлов Yazi" width="560" height="315" src="https://video.zlinux.ru/videos/embed/fc3ee4db-8899-4c76-9310-39e59551164b?warningTitle=0" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||||
|
|
||||||
## Ссылки
|
## Ссылки
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
237
sass/baguetteBox.scss
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/*!
|
||||||
|
* baguetteBox.js
|
||||||
|
* @author feimosi
|
||||||
|
* @version 1.13.0
|
||||||
|
* @url https://github.com/feimosi/baguetteBox.js
|
||||||
|
*/
|
||||||
|
#baguetteBox-overlay {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000000;
|
||||||
|
background-color: #222;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
-webkit-transition: opacity 0.5s ease;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image figure {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image img {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
-webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||||
|
-moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image figcaption {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: normal;
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
height: 50%;
|
||||||
|
width: 1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#baguetteBox-slider {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-transition: left 0.4s ease, -webkit-transform 0.4s ease;
|
||||||
|
transition: left 0.4s ease, -webkit-transform 0.4s ease;
|
||||||
|
transition: left 0.4s ease, transform 0.4s ease;
|
||||||
|
transition: left 0.4s ease, transform 0.4s ease, -webkit-transform 0.4s ease, -moz-transform 0.4s ease;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider.bounce-from-right {
|
||||||
|
-webkit-animation: bounceFromRight 0.4s ease-out;
|
||||||
|
animation: bounceFromRight 0.4s ease-out;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider.bounce-from-left {
|
||||||
|
-webkit-animation: bounceFromLeft 0.4s ease-out;
|
||||||
|
animation: bounceFromLeft 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes bounceFromRight {
|
||||||
|
0% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
margin-left: -30px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounceFromRight {
|
||||||
|
0% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
margin-left: -30px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes bounceFromLeft {
|
||||||
|
0% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes bounceFromLeft {
|
||||||
|
0% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.baguetteBox-button#previous-button, .baguetteBox-button#next-button {
|
||||||
|
top: 50%;
|
||||||
|
top: calc(50% - 30px);
|
||||||
|
width: 44px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.baguetteBox-button {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
-moz-border-radius: 15%;
|
||||||
|
border-radius: 15%;
|
||||||
|
background-color: #323232;
|
||||||
|
background-color: rgba(50, 50, 50, 0.5);
|
||||||
|
color: #ddd;
|
||||||
|
font: 1.6em sans-serif;
|
||||||
|
-webkit-transition: background-color 0.4s ease;
|
||||||
|
transition: background-color 0.4s ease;
|
||||||
|
}
|
||||||
|
.baguetteBox-button:focus, .baguetteBox-button:hover {
|
||||||
|
background-color: rgba(50, 50, 50, 0.9);
|
||||||
|
}
|
||||||
|
.baguetteBox-button#next-button {
|
||||||
|
right: 2%;
|
||||||
|
}
|
||||||
|
.baguetteBox-button#previous-button {
|
||||||
|
left: 2%;
|
||||||
|
}
|
||||||
|
.baguetteBox-button#close-button {
|
||||||
|
top: 20px;
|
||||||
|
right: 2%;
|
||||||
|
right: calc(2% + 6px);
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
.baguetteBox-button svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Preloader
|
||||||
|
Borrowed from http://tobiasahlin.com/spinkit/
|
||||||
|
*/
|
||||||
|
.baguetteBox-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-left: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.baguetteBox-double-bounce1,
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
opacity: 0.6;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
-webkit-animation: bounce 2s infinite ease-in-out;
|
||||||
|
animation: bounce 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
-webkit-animation-delay: -1s;
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-moz-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "/home/zloy_linux/blog//themes/tabi/sass/main.scss";
|
@import "../themes/tabi/sass/main.scss";
|
||||||
|
@import "./baguetteBox.scss";
|
||||||
|
|
||||||
// main.scss
|
// main.scss
|
||||||
|
|
||||||
@@ -34,20 +35,12 @@ article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gallery {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 4fr));
|
|
||||||
grid-auto-rows: auto;
|
|
||||||
grid-auto-flow: dense;
|
|
||||||
gap: 18px;
|
|
||||||
margin-top: 4vmin;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery img {
|
.gallery img {
|
||||||
border-radius: 1rem;
|
max-width: 100%;
|
||||||
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
height: auto;
|
||||||
transition: transform 200ms ease;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
cursor: zoom-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery img:hover {
|
.gallery img:hover {
|
||||||
|
|||||||
BIN
screenshot.png
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 216 KiB |
4788
static/projects/microblog.svg
Normal file
|
After Width: | Height: | Size: 360 KiB |
30
static/projects/peertube.svg
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 505 505" xml:space="preserve">
|
||||||
|
<circle style="fill:#54C0EB;" cx="252.5" cy="252.5" r="252.5"/>
|
||||||
|
<path style="fill:#324A5E;" d="M395.3,113.6H109.7c-10.8,0-19.5,8.7-19.5,19.5V372c0,10.8,8.7,19.5,19.5,19.5h285.6
|
||||||
|
c10.8,0,19.5-8.7,19.5-19.5V133C414.8,122.3,406.1,113.6,395.3,113.6z"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#FFFFFF;" d="M389.3,312H115.7c-0.5,0-0.9-0.4-0.9-0.9V193.9c0-0.5,0.4-0.9,0.9-0.9h273.6c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v117.3C390.2,311.6,389.8,312,389.3,312z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M152.7,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C153.5,170.6,153.1,171,152.7,171z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M231.5,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C232.4,170.6,232,171,231.5,171z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M310.4,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C311.3,170.6,310.9,171,310.4,171z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M389.3,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C390.2,170.6,389.8,171,389.3,171z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M152.7,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C153.5,367.3,153.1,367.6,152.7,367.6z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M231.5,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C232.4,367.3,232,367.6,231.5,367.6z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M310.4,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C311.3,367.3,310.9,367.6,310.4,367.6z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M389.3,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
|
||||||
|
v31.9C390.2,367.3,389.8,367.6,389.3,367.6z"/>
|
||||||
|
</g>
|
||||||
|
<path style="fill:#FF7058;" d="M228.1,225.3v54.4c0,6.6,7.4,10.5,12.8,6.8l39.8-27.2c4.8-3.3,4.8-10.3,0-13.6l-39.8-27.2
|
||||||
|
C235.5,214.8,228.1,218.7,228.1,225.3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
56
static/projects/searxng.svg
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 92 92"
|
||||||
|
height="92mm"
|
||||||
|
width="92mm">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
transform="translate(-40.921303,-17.416526)"
|
||||||
|
id="layer1">
|
||||||
|
<circle
|
||||||
|
r="0"
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:12;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
cy="92"
|
||||||
|
cx="75"
|
||||||
|
id="path3713" />
|
||||||
|
<circle
|
||||||
|
r="30"
|
||||||
|
cy="53.902557"
|
||||||
|
cx="75.921303"
|
||||||
|
id="path834"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 67.514849,37.91524 a 18,18 0 0 1 21.051475,3.312407 18,18 0 0 1 3.137312,21.078282"
|
||||||
|
id="path852"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<rect
|
||||||
|
transform="rotate(-46.234709)"
|
||||||
|
ry="1.8669105e-13"
|
||||||
|
y="122.08995"
|
||||||
|
x="3.7063529"
|
||||||
|
height="39.963303"
|
||||||
|
width="18.846331"
|
||||||
|
id="rect912"
|
||||||
|
style="opacity:1;fill:#3050ff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
44
static/projects/sharkey.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<circle style="fill:#C2D5D8;" cx="256" cy="256" r="256"/>
|
||||||
|
<path style="fill:#406A80;" d="M220.428,126.024c29.388-9.24,131.336-20.624,166.144,21.992
|
||||||
|
c-37.188-2.208-73.928,7.048-88.812,32.544C276.272,170.788,220.428,126.024,220.428,126.024z"/>
|
||||||
|
<path style="fill:#395F73;" d="M284.676,173.172c5.012,3.152,9.544,5.78,13.084,7.388c12.252-20.972,39.28-30.936,69.208-32.588
|
||||||
|
C334.028,146.224,301.68,153.532,284.676,173.172z"/>
|
||||||
|
<path style="fill:#406A80;" d="M0,107.6c4.58,19.296,12.048,36.22,21.616,51.332c25.992-0.528,57.968,4.224,77.828,16.136
|
||||||
|
c-13.956,7.488-33.664,13.316-51.428,16.284c84.928,83.304,246.884,96.6,300.288,168.308l29.832-64.176
|
||||||
|
C243.936,46.028,75.684,76.38,0,107.6z"/>
|
||||||
|
<path style="fill:#395F73;" d="M370.892,311.084l7.252-15.592C270.392,95.168,140.664,75.288,54.552,91.396
|
||||||
|
C143.28,85.752,269.964,124.304,370.892,311.084z"/>
|
||||||
|
<path style="fill:#406A80;" d="M136.668,226.996c-22.052,7.348-38.216,37.724-40.664,57.312
|
||||||
|
c16.168-6.372,51.924-34.284,70.06-28.652C150.868,235.816,136.668,226.996,136.668,226.996z"/>
|
||||||
|
<path style="fill:#395F73;" d="M136.668,226.996c-3.812,1.272-7.432,3.268-10.852,5.748c5.552,4.616,13.464,12.064,21.776,22.916
|
||||||
|
c-12.14-3.772-32.204,7.52-48.984,17.292c-1.268,3.992-2.164,7.856-2.604,11.36c16.168-6.376,51.924-34.284,70.06-28.652
|
||||||
|
C150.868,235.816,136.668,226.996,136.668,226.996z"/>
|
||||||
|
<path style="fill:#F5F5F5;" d="M348.304,359.668l6.132-13.188c-13.084-48.716-217.768-119.484-238.768-161.364
|
||||||
|
c23.576-33.428-67.044-39.724-97.856-32.752c1.256,2.2,2.46,4.436,3.804,6.572c25.992-0.528,57.968,4.224,77.828,16.136
|
||||||
|
c-13.956,7.488-33.664,13.316-51.428,16.284C132.944,274.664,294.9,287.96,348.304,359.668z"/>
|
||||||
|
<path style="fill:#406A80;" d="M378.136,295.492c15.704-17.064,72.028-41.556,114.156-40.084
|
||||||
|
c-10.284,19.104-27.432,35.52-42.988,29.64c-13.72,12.252-46.964,32.064-55.592,58.824c-8.428,26.116-11.78,89.596-7.132,110.904
|
||||||
|
c-13.96-12.48-40.688-70.924-38.264-95.096S378.136,295.492,378.136,295.492z"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#395F73;" d="M381.408,343.872c-6.5,20.136-9.96,62.404-9.084,90.5c5.136,9.312,10.232,16.796,14.252,20.404
|
||||||
|
c-4.64-21.296-1.288-84.776,7.132-110.904c8.5-26.368,40.852-45.964,54.928-58.252C444.752,286.58,390.032,317.1,381.408,343.872z"
|
||||||
|
/>
|
||||||
|
<path style="fill:#395F73;" d="M479.8,255.744c-7.472,13.752-18.488,25.992-29.872,29.484c15.388,5.228,32.216-10.96,42.364-29.82
|
||||||
|
C488.252,255.264,484.056,255.428,479.8,255.744z"/>
|
||||||
|
</g>
|
||||||
|
<path style="fill:#F5F5F5;" d="M121.548,137.78c-3.056,0-6.1-1.312-8.36-3.424c-2.212-2.252-3.396-5.324-3.396-8.404
|
||||||
|
c0-3.192,1.184-6.268,3.396-8.416c4.476-4.36,12.244-4.36,16.584,0c2.244,2.144,3.528,5.228,3.528,8.416
|
||||||
|
c0,3.08-1.284,6.152-3.528,8.272C127.66,136.468,124.592,137.78,121.548,137.78z"/>
|
||||||
|
<path style="fill:#242424;" d="M121.548,131.9c-1.528,0-3.056-0.648-4.176-1.708c-1.112-1.124-1.704-2.664-1.704-4.2
|
||||||
|
c0-1.6,0.588-3.14,1.704-4.216c2.24-2.184,6.116-2.184,8.292,0c1.116,1.076,1.756,2.608,1.756,4.216c0,1.536-0.64,3.076-1.756,4.132
|
||||||
|
C124.592,131.252,123.08,131.9,121.548,131.9z"/>
|
||||||
|
<path style="fill:#406A80;" d="M160.668,218.916c-9.888,36.744,35.252,98.212,28.404,118.784
|
||||||
|
c12.752-15.188,43.588-58.916,35.268-84.876C216.016,226.868,160.668,218.916,160.668,218.916z"/>
|
||||||
|
<path style="fill:#395F73;" d="M224.34,252.828c-8.324-25.96-63.672-33.916-63.672-33.916c-0.204,0.756-0.252,1.584-0.412,2.368
|
||||||
|
c16.748,3.924,45.912,13.252,51.768,31.548c6.34,19.756-10.024,49.796-23.824,69.728c1.6,6.156,2.12,11.36,0.86,15.148
|
||||||
|
C201.816,322.516,232.66,278.78,224.34,252.828z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -83,6 +83,9 @@ load_comments = "إظهار التعليقات"
|
|||||||
copied = "تم النسخ!"
|
copied = "تم النسخ!"
|
||||||
copy_code_to_clipboard = "نسخ الشِفرة إلى الحافظة"
|
copy_code_to_clipboard = "نسخ الشِفرة إلى الحافظة"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "M'agrada aquesta publicació"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "مُشَغل بواسطة"
|
powered_by = "مُشَغل بواسطة"
|
||||||
and = "و"
|
and = "و"
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ powered_by = "Propulsat per"
|
|||||||
and = "i"
|
and = "i"
|
||||||
site_source = "Codi del lloc"
|
site_source = "Codi del lloc"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "M'agrada aquesta publicació"
|
||||||
|
|
||||||
# 404 error.
|
# 404 error.
|
||||||
# https://welpo.github.io/tabi/404.html
|
# https://welpo.github.io/tabi/404.html
|
||||||
page_missing = "La pàgina que has sol·licitat sembla que no existeix"
|
page_missing = "La pàgina que has sol·licitat sembla que no existeix"
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ load_comments = "Kommentare laden"
|
|||||||
copied = "Kopiert!"
|
copied = "Kopiert!"
|
||||||
copy_code_to_clipboard = "Code in die Zwischenablage kopieren"
|
copy_code_to_clipboard = "Code in die Zwischenablage kopieren"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Dieser Beitrag gefällt mir"
|
||||||
|
|
||||||
# Footer.
|
# Footer.
|
||||||
powered_by = "Angetrieben von"
|
powered_by = "Angetrieben von"
|
||||||
and = "und"
|
and = "und"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "Load comments"
|
|||||||
copied = "Copied!"
|
copied = "Copied!"
|
||||||
copy_code_to_clipboard = "Copy code to clipboard"
|
copy_code_to_clipboard = "Copy code to clipboard"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Like this post"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Powered by"
|
powered_by = "Powered by"
|
||||||
and = "&"
|
and = "&"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "Cargar comentarios"
|
|||||||
copied = "Copiado!"
|
copied = "Copiado!"
|
||||||
copy_code_to_clipboard = "Copiar código al portapapeles"
|
copy_code_to_clipboard = "Copiar código al portapapeles"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Me gusta esta publicación"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Impulsado por"
|
powered_by = "Impulsado por"
|
||||||
and = "y"
|
and = "y"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "Lae kommentaarid"
|
|||||||
copied = "Kopeeritud!"
|
copied = "Kopeeritud!"
|
||||||
copy_code_to_clipboard = "Kopeeri kood lõikelauale"
|
copy_code_to_clipboard = "Kopeeri kood lõikelauale"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Mulle meeldib see postitus"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Toetab"
|
powered_by = "Toetab"
|
||||||
and = "ja"
|
and = "ja"
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ load_comments = "بارگذاری نظرات"
|
|||||||
copied = "کپی شد!"
|
copied = "کپی شد!"
|
||||||
copy_code_to_clipboard = "کپی کد به کلیپبورد"
|
copy_code_to_clipboard = "کپی کد به کلیپبورد"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "این مقاله را دوست دارم"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "قدرت گرفته از"
|
powered_by = "قدرت گرفته از"
|
||||||
and = "و"
|
and = "و"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "Afficher les commentaires"
|
|||||||
copied = "Copié !"
|
copied = "Copié !"
|
||||||
copy_code_to_clipboard = "Copier le code dans le presse-papier"
|
copy_code_to_clipboard = "Copier le code dans le presse-papier"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "J'aime cet article"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Propulsé par"
|
powered_by = "Propulsé par"
|
||||||
and = "et"
|
and = "et"
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ load_comments = "कमेंट्स लोड करें"
|
|||||||
copied = "कॉपी किया गया!"
|
copied = "कॉपी किया गया!"
|
||||||
copy_code_to_clipboard = "कोड क्लिपबोर्ड में कॉपी करें"
|
copy_code_to_clipboard = "कोड क्लिपबोर्ड में कॉपी करें"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "मुझे यह पोस्ट पसंद है"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "चालित द्वारा"
|
powered_by = "चालित द्वारा"
|
||||||
and = "और"
|
and = "और"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "Carica commenti"
|
|||||||
copied = "Copiato!"
|
copied = "Copiato!"
|
||||||
copy_code_to_clipboard = "Copia codice negli appunti"
|
copy_code_to_clipboard = "Copia codice negli appunti"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Mi piace questo post"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Alimentato da"
|
powered_by = "Alimentato da"
|
||||||
and = "e"
|
and = "e"
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ load_comments = "コメントを読む"
|
|||||||
copied = "コピーしました!"
|
copied = "コピーしました!"
|
||||||
copy_code_to_clipboard = "コードをクリップボードにコピー"
|
copy_code_to_clipboard = "コードをクリップボードにコピー"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "いいね!"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Powered by"
|
powered_by = "Powered by"
|
||||||
and = "と"
|
and = "と"
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ load_comments = "댓글 불러오기"
|
|||||||
copied = "복사됨!"
|
copied = "복사됨!"
|
||||||
copy_code_to_clipboard = "코드를 클립보드에 복사"
|
copy_code_to_clipboard = "코드를 클립보드에 복사"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "이 글이 좋아요"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "제공됨"
|
powered_by = "제공됨"
|
||||||
and = "&"
|
and = "&"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "Laad opmerkingen"
|
|||||||
copied = "Gekopieerd!"
|
copied = "Gekopieerd!"
|
||||||
copy_code_to_clipboard = "Kopieer code naar klembord"
|
copy_code_to_clipboard = "Kopieer code naar klembord"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Vind ik leuk"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Aangedreven door"
|
powered_by = "Aangedreven door"
|
||||||
and = "&"
|
and = "&"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "ମତାମତ ଲୋଡ କରନ୍ତୁ"
|
|||||||
copied = "କପି ହେଲା!"
|
copied = "କପି ହେଲା!"
|
||||||
copy_code_to_clipboard = "କ୍ଲିପବୋର୍ଡକୁ କପି କରନ୍ତୁ"
|
copy_code_to_clipboard = "କ୍ଲିପବୋର୍ଡକୁ କପି କରନ୍ତୁ"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "ମୋର ଏହି ପୋସ୍ଟ ଭଲ ଲାଗେ"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "ଚାଳିତ ଦ୍ୱାରା"
|
powered_by = "ଚାଳିତ ଦ୍ୱାରା"
|
||||||
and = "ଏବଂ"
|
and = "ଏବଂ"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "Carregar comentários"
|
|||||||
copied = "Copiado!"
|
copied = "Copiado!"
|
||||||
copy_code_to_clipboard = "Copiar código para a área de transferência"
|
copy_code_to_clipboard = "Copiar código para a área de transferência"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Gosto desta publicação"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Impulsionado por"
|
powered_by = "Impulsionado por"
|
||||||
and = "e"
|
and = "e"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ few_results = "$NUMBER результата" # 2, 3, 4 but not 12-14
|
|||||||
many_results = "$NUMBER результатов" # 5-9, 0, 11-14, and others
|
many_results = "$NUMBER результатов" # 5-9, 0, 11-14, and others
|
||||||
|
|
||||||
# Navigation.
|
# Navigation.
|
||||||
|
skip_to_content = "Перейти к содержанию"
|
||||||
pinned = "Закреплено"
|
pinned = "Закреплено"
|
||||||
jump_to_posts = "Перейти к записям"
|
jump_to_posts = "Перейти к записям"
|
||||||
read_more = "Читать далее"
|
read_more = "Читать далее"
|
||||||
@@ -79,6 +80,9 @@ load_comments = "Загрузить комментарии"
|
|||||||
copied = "Скопировано!"
|
copied = "Скопировано!"
|
||||||
copy_code_to_clipboard = "Скопировать код в буфер обмена"
|
copy_code_to_clipboard = "Скопировать код в буфер обмена"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Мне нравится эта статья"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Под управлением"
|
powered_by = "Под управлением"
|
||||||
and = "&"
|
and = "&"
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ load_comments = "Завантажити коментарі"
|
|||||||
copied = "Скопійовано!"
|
copied = "Скопійовано!"
|
||||||
copy_code_to_clipboard = "Копіювати код у буфер обміну"
|
copy_code_to_clipboard = "Копіювати код у буфер обміну"
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "Мені подобається ця стаття"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "Під управлінням"
|
powered_by = "Під управлінням"
|
||||||
and = "та"
|
and = "та"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "载入留言"
|
|||||||
copied = "已复制!" # Machine translated.
|
copied = "已复制!" # Machine translated.
|
||||||
copy_code_to_clipboard = "复制代码到剪贴板" # Machine translated.
|
copy_code_to_clipboard = "复制代码到剪贴板" # Machine translated.
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "喜欢这篇文章"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "网站基于"
|
powered_by = "网站基于"
|
||||||
and = "和"
|
and = "和"
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ load_comments = "載入留言"
|
|||||||
copied = "已复制!" # Machine translated.
|
copied = "已复制!" # Machine translated.
|
||||||
copy_code_to_clipboard = "复制代码到剪贴板" # Machine translated.
|
copy_code_to_clipboard = "复制代码到剪贴板" # Machine translated.
|
||||||
|
|
||||||
|
# iine appreciation button.
|
||||||
|
like_this_post = "喜歡這篇文章"
|
||||||
|
|
||||||
# Footer: Powered by Zola and tabi.
|
# Footer: Powered by Zola and tabi.
|
||||||
powered_by = "網站基於"
|
powered_by = "網站基於"
|
||||||
and = "和"
|
and = "和"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
@use 'parts/_header-anchor.scss';
|
@use 'parts/_header-anchor.scss';
|
||||||
@use 'parts/_header.scss';
|
@use 'parts/_header.scss';
|
||||||
@use 'parts/_home-banner.scss';
|
@use 'parts/_home-banner.scss';
|
||||||
|
@use 'parts/_iine.scss';
|
||||||
@use 'parts/_image-hover.scss';
|
@use 'parts/_image-hover.scss';
|
||||||
@use 'parts/_image-toggler.scss';
|
@use 'parts/_image-toggler.scss';
|
||||||
@use 'parts/_image.scss';
|
@use 'parts/_image.scss';
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
@use 'parts/_table.scss';
|
@use 'parts/_table.scss';
|
||||||
@use 'parts/_tags.scss';
|
@use 'parts/_tags.scss';
|
||||||
@use 'parts/_theme-switch.scss';
|
@use 'parts/_theme-switch.scss';
|
||||||
|
@use 'parts/_webmention.scss';
|
||||||
@use 'parts/_zola-error.scss';
|
@use 'parts/_zola-error.scss';
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -70,7 +72,7 @@
|
|||||||
@else {
|
@else {
|
||||||
--background-color: #282828;
|
--background-color: #282828;
|
||||||
--bg-0: #2f2f2f;
|
--bg-0: #2f2f2f;
|
||||||
--bg-1: #3c3c3c;
|
--bg-1: #1d2021;
|
||||||
--bg-2: #171717;
|
--bg-2: #171717;
|
||||||
--bg-3: #535555;
|
--bg-3: #535555;
|
||||||
--hover-color: black;
|
--hover-color: black;
|
||||||
@@ -196,7 +198,7 @@ article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
display: block;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: -0.15em;
|
margin-top: -0.15em;
|
||||||
color: var(--text-color-high-contrast);
|
color: var(--text-color-high-contrast);
|
||||||
@@ -230,6 +232,7 @@ h2 {
|
|||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
font-weight: 550;
|
font-weight: 550;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
|
color: #fabd2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ footer nav {
|
|||||||
|
|
||||||
.social > img {
|
.social > img {
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
width: 1.5rem;
|
width: 2.2rem;
|
||||||
height: auto;
|
height: auto;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,11 +98,10 @@ header {
|
|||||||
.tag {
|
.tag {
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.separator {
|
.separator {
|
||||||
margin-inline-end: 0.2rem;
|
margin-inline-end: 0.2rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher {
|
.language-switcher {
|
||||||
@@ -125,8 +124,30 @@ header {
|
|||||||
background: var(--meta-color);
|
background: var(--meta-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-switcher-icon-with-code {
|
||||||
|
margin-inline-end: 0.3rem;
|
||||||
|
width: 0.7rem;
|
||||||
|
height: 0.7rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-switcher-icon-code {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.15rem;
|
||||||
|
z-index: 10;
|
||||||
|
inset-inline-start: 0.7rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--meta-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
37
themes/tabi/sass/parts/_iine.scss
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
.iine-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-family: var(--sans-serif-font);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
margin-left: .2rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iine-auto-buttons {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
@@ -48,6 +48,9 @@ ul {
|
|||||||
|
|
||||||
.title-container {
|
.title-container {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
|
.social {
|
||||||
|
margin-inline-start: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-divider {
|
.bottom-divider {
|
||||||
@@ -85,30 +88,21 @@ a {
|
|||||||
// External link styles with `external_links_class = "external"`.
|
// External link styles with `external_links_class = "external"`.
|
||||||
main {
|
main {
|
||||||
--external-link-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M11 5h-6v14h14v-6'/%3E%3Cpath d='M13 11l7 -7'/%3E%3Cpath d='M21 3h-6M21 3v6'/%3E%3C/g%3E%3C/svg%3E");
|
--external-link-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M11 5h-6v14h14v-6'/%3E%3Cpath d='M13 11l7 -7'/%3E%3Cpath d='M21 3h-6M21 3v6'/%3E%3C/g%3E%3C/svg%3E");
|
||||||
a.external:not(:has(img, svg, video, picture, figure)) {
|
|
||||||
display: inline-block;
|
|
||||||
padding-inline-end: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.external:not(:has(img, svg, video, picture, figure))::after {
|
a.external:not(:has(img, svg, video, picture, figure))::after {
|
||||||
-webkit-mask-image: var(--external-link-icon);
|
|
||||||
-webkit-mask-size: 100% 100%;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
vertical-align: -0.05em;
|
||||||
top: 50%;
|
margin-inline-start: 0.1em;
|
||||||
transform: translateY(-50%);
|
|
||||||
mask-image: var(--external-link-icon);
|
|
||||||
mask-size: 100% 100%;
|
|
||||||
margin-inline-start: 0.2em;
|
|
||||||
inset-inline-end: 0;
|
|
||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
width: 0.8em;
|
width: 0.8em;
|
||||||
height: 0.8em;
|
height: 0.8em;
|
||||||
content: '';
|
content: '';
|
||||||
|
-webkit-mask-image: var(--external-link-icon);
|
||||||
|
-webkit-mask-size: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:dir(rtl) a.external:not(:has(img, svg, video, picture, figure))::after {
|
&:dir(rtl) a.external:not(:has(img, svg, video, picture, figure))::after {
|
||||||
transform: translateY(-50%) rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta a.external:not(:has(img, svg, video, picture, figure))::after {
|
.meta a.external:not(:has(img, svg, video, picture, figure))::after {
|
||||||
@@ -334,3 +328,28 @@ details summary {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
border-radius: 0 0 5px 0;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: var(--hover-color);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skip-link:focus {
|
||||||
|
top: 0;
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
outline: 2px solid var(--text-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ $padding: 2.5rem;
|
|||||||
|
|
||||||
@media only screen and (max-width: 1100px) {
|
@media only screen and (max-width: 1100px) {
|
||||||
.bloglist-container {
|
.bloglist-container {
|
||||||
grid-template-columns: 1fr;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinned-label svg {
|
.pinned-label svg {
|
||||||
|
|||||||
149
themes/tabi/sass/parts/_webmention.scss
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#webmentions {
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.2em;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-inline-end: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-icon,
|
||||||
|
span {
|
||||||
|
margin-inline-end: .3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li,
|
||||||
|
p {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.likes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.8s ease-out, z-index 0s linear 0.4s;
|
||||||
|
margin-bottom: .375rem;
|
||||||
|
margin-inline-start: -.75rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.3) translateY(-4px);
|
||||||
|
z-index: 10;
|
||||||
|
transition: transform 0.05s ease-out, z-index 0s linear 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
border: 2px solid var(--background-color, white);
|
||||||
|
border-radius: 50%;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg-0);
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 80%;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-author {
|
||||||
|
font-style: bold;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-url {
|
||||||
|
font-style: italic;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-author {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
margin-inline-end: .625rem;
|
||||||
|
width: 2rem;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 20px 0px 0px 20px;
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
padding-inline: 1rem 1rem;
|
||||||
|
padding-block: .75rem;
|
||||||
|
width: calc(60% - 2rem);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 0px 20px 20px 0px;
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
padding-inline: 0.7rem 0.7rem;
|
||||||
|
padding-block: .75rem;
|
||||||
|
width: 7rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 12 KiB |
7
themes/tabi/static/js/baguetteBox.min.js
vendored
Normal file
2
themes/tabi/static/js/katex.min.js
vendored
2073
themes/tabi/static/js/mermaid.min.js
vendored
412
themes/tabi/static/js/webmention.js
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
/* webmention.js
|
||||||
|
|
||||||
|
Simple thing for embedding webmentions from webmention.io into a page, client-side.
|
||||||
|
|
||||||
|
(c)2018-2022 fluffy (http://beesbuzz.biz)
|
||||||
|
2025 mmai (https://misc.rhumbs.fr)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
Basic usage:
|
||||||
|
|
||||||
|
<script src="/path/to/webmention.js" data-param="val" ... async />
|
||||||
|
<div id="webmentions"></div>
|
||||||
|
|
||||||
|
Allowed parameters:
|
||||||
|
|
||||||
|
page-url:
|
||||||
|
|
||||||
|
The base URL to use for this page. Defaults to window.location
|
||||||
|
|
||||||
|
add-urls:
|
||||||
|
|
||||||
|
Additional URLs to check, separated by |s
|
||||||
|
|
||||||
|
id:
|
||||||
|
|
||||||
|
The HTML ID for the object to fill in with the webmention data.
|
||||||
|
Defaults to "webmentions"
|
||||||
|
|
||||||
|
wordcount:
|
||||||
|
|
||||||
|
The maximum number of words to render in reply mentions.
|
||||||
|
|
||||||
|
max-webmentions:
|
||||||
|
|
||||||
|
The maximum number of mentions to retrieve. Defaults to 30.
|
||||||
|
|
||||||
|
prevent-spoofing:
|
||||||
|
|
||||||
|
By default, Webmentions render using the mf2 'url' element, which plays
|
||||||
|
nicely with webmention bridges (such as brid.gy and telegraph)
|
||||||
|
but allows certain spoofing attacks. If you would like to prevent
|
||||||
|
spoofing, set this to a non-empty string (e.g. "true").
|
||||||
|
|
||||||
|
sort-by:
|
||||||
|
|
||||||
|
What to order the responses by; defaults to 'published'. See
|
||||||
|
https://github.com/aaronpk/webmention.io#api
|
||||||
|
|
||||||
|
sort-dir:
|
||||||
|
|
||||||
|
The order to sort the responses by; defaults to 'up' (i.e. oldest
|
||||||
|
first). See https://github.com/aaronpk/webmention.io#api
|
||||||
|
|
||||||
|
comments-are-reactions:
|
||||||
|
|
||||||
|
If set to a non-empty string (e.g. "true"), will display comment-type responses
|
||||||
|
(replies/mentions/etc.) as being part of the reactions
|
||||||
|
(favorites/bookmarks/etc.) instead of in a separate comment list.
|
||||||
|
|
||||||
|
A more detailed example:
|
||||||
|
|
||||||
|
<!-- If you want to translate the UI -->
|
||||||
|
<script src="/path/to/umd/i18next.js"></script>
|
||||||
|
<script>
|
||||||
|
// Setup i18next as described in https://www.i18next.com/overview/getting-started#basic-sample
|
||||||
|
</script>
|
||||||
|
<!-- Otherwise, only using the following is fine -->
|
||||||
|
<script src="/path/to/webmention.min.js"
|
||||||
|
data-id="webmentionContainer"
|
||||||
|
data-wordcount="30"
|
||||||
|
data-prevent-spoofing="true"
|
||||||
|
data-comments-are-reactions="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Begin LibreJS code licensing
|
||||||
|
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Shim i18next
|
||||||
|
window.i18next = window.i18next || {
|
||||||
|
t: function t(/** @type {string} */key) { return key; }
|
||||||
|
}
|
||||||
|
const t = window.i18next.t.bind(window.i18next);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the configuration value.
|
||||||
|
*
|
||||||
|
* @param {string} key The configuration key.
|
||||||
|
* @param {string} dfl The default value.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getCfg(key, dfl) {
|
||||||
|
return document.currentScript.getAttribute("data-" + key) || dfl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refurl = getCfg("page-url", window.location.href.replace(/#.*$/, ""));
|
||||||
|
const addurls = getCfg("add-urls", undefined);
|
||||||
|
const containerID = getCfg("id", "webmentions");
|
||||||
|
/** @type {Number} */
|
||||||
|
const textMaxWords = getCfg("wordcount");
|
||||||
|
const maxWebmentions = getCfg("max-webmentions", 30);
|
||||||
|
const mentionSource = getCfg("prevent-spoofing") ? "wm-source" : "url";
|
||||||
|
const sortBy = getCfg("sort-by", "published");
|
||||||
|
const sortDir = getCfg("sort-dir", "up");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip the protocol off a URL.
|
||||||
|
*
|
||||||
|
* @param {string} url The URL to strip protocol off.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function stripurl(url) {
|
||||||
|
return url.substr(url.indexOf('//'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduplicate multiple mentions from the same source URL.
|
||||||
|
*
|
||||||
|
* @param {Array<Reaction>} mentions Mentions of the source URL.
|
||||||
|
* @return {Array<Reaction>}
|
||||||
|
*/
|
||||||
|
function dedupe(mentions) {
|
||||||
|
/** @type {Array<Reaction>} */
|
||||||
|
const filtered = [];
|
||||||
|
/** @type {Record<string, boolean>} */
|
||||||
|
const seen = {};
|
||||||
|
|
||||||
|
mentions.forEach(function (r) {
|
||||||
|
// Strip off the protocol (i.e. treat http and https the same)
|
||||||
|
const source = stripurl(r.url);
|
||||||
|
if (!seen[source]) {
|
||||||
|
filtered.push(r);
|
||||||
|
seen[source] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format comments as HTML.
|
||||||
|
*
|
||||||
|
* @param {Array<Reaction>} comments The comments to format.
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
function formatComments(type, comments) {
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="webcomments">
|
||||||
|
<h3 id="replies-header">` + getIcon('comment') + ` <span>` + comments.length + `</span> ` + type + `s </h3>
|
||||||
|
<ol aria-labelledby="replies-header" role="list">`;
|
||||||
|
comments.forEach(function (comment) {
|
||||||
|
let content = '';
|
||||||
|
if (comment.hasOwnProperty('content')) {
|
||||||
|
if (comment.content.hasOwnProperty('html')) {
|
||||||
|
content = comment.content.html;
|
||||||
|
} else if (comment.content.hasOwnProperty('text')) {
|
||||||
|
content = comment.content.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<li class="comment h-entry">
|
||||||
|
<div>
|
||||||
|
<a class="comment_author u-author"
|
||||||
|
href="`+ comment.author.url + `"
|
||||||
|
target="_blank"
|
||||||
|
title="`+ comment.author.name + `"
|
||||||
|
rel="noreferrer">
|
||||||
|
<img
|
||||||
|
src="`+ comment.author.photo + `"
|
||||||
|
alt=""
|
||||||
|
class="u-photo"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
>
|
||||||
|
<span class="p-author">`+ comment.author.name + `</span>
|
||||||
|
</a>`;
|
||||||
|
if (comment.published) {
|
||||||
|
const published = new Date(comment.published);
|
||||||
|
html += `
|
||||||
|
<time class="dt-published" datetime="`+ comment.published + `">` + published.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }) + `</time>`;
|
||||||
|
}
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="e-entry">`+ content + `
|
||||||
|
<a class="u-url"
|
||||||
|
href="`+ comment.url + `"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
source
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += `
|
||||||
|
</ol >
|
||||||
|
</div >
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Reaction
|
||||||
|
* @property {string} url
|
||||||
|
* @property {Object?} author
|
||||||
|
* @property {string?} author.name
|
||||||
|
* @property {string?} author.photo
|
||||||
|
* @property {Object?} content
|
||||||
|
* @property {string?} content.text
|
||||||
|
* @property {RSVPEmoji?} rsvp
|
||||||
|
* @property {MentionType?} wm-property
|
||||||
|
* @property {string?} wm-source
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getIcon(name) {
|
||||||
|
|
||||||
|
if (name == 'like') {
|
||||||
|
return `<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M95.997 41.986l-.026-.035C85.746 28.36 68.428 21.423 51.165 24.881 30.138 29.094 15.004 47.558 15 69.003c0 24.413 14.906 47.964 39.486 70.086 8.43 7.586 17.437 14.468 26.444 20.533.728.49 1.444.967 2.148 1.43l1.39.909 1.355.872 1.317.835.645.403 1.259.78 1.194.726 1.032.619 1.38.807.418.236a6 6 0 005.864 0l1.138-.654 1.154-.684 1.118-.675.614-.376 1.26-.779a212 212 0 00.644-.403l1.317-.835 1.355-.872 1.39-.909c.704-.463 1.42-.94 2.148-1.43 9.007-6.065 18.015-12.947 26.444-20.533C162.094 116.967 177 93.416 177 69.004c-.004-21.446-15.138-39.91-36.165-44.123-17.07-3.42-34.174 3.323-44.43 16.568l-.408.537zm42.48-5.338c15.421 3.09 26.52 16.63 26.523 32.357 0 19.607-12.438 39.847-33.532 59.357l-1.316 1.205c-.22.201-.443.402-.666.603-7.977 7.18-16.548 13.727-25.118 19.498l-.745.5c-.74.494-1.466.973-2.177 1.437l-1.402.906-1.359.864-.662.416-1.292.8-.732.446-.73-.446-1.292-.8-.662-.416-1.36-.864-1.4-.906a235.406 235.406 0 01-2.923-1.937c-8.57-5.77-17.14-12.319-25.118-19.498l-.666-.603-1.316-1.205C39.438 108.852 27 88.612 27 69.004c.003-15.726 11.102-29.267 26.523-32.356 15.253-3.056 30.565 4.954 36.756 19.208l.204.478c2.084 4.878 9.009 4.85 11.053-.045 6.062-14.511 21.52-22.73 36.941-19.641z" fill="currentColor" /></svg> `;
|
||||||
|
} else if (name == 'repost') {
|
||||||
|
return `<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M18.472 146.335l-.075-.184a5.968 5.968 0 01-.216-.684l-.014-.056a5.643 5.643 0 01-.082-.397l-.013-.083a5.886 5.886 0 01-.072-.96V144c0-.157.006-.313.018-.467l.006-.075c.012-.132.028-.261.048-.39l.016-.095c.008-.05.017-.1.027-.149.005-.019.008-.038.012-.058.028-.133.06-.264.096-.393l.026-.088a5.86 5.86 0 01.482-1.159l.043-.077a5.642 5.642 0 01.31-.49l.015-.022.076-.104.044-.059a3.856 3.856 0 01.165-.208l.052-.061c.102-.12.21-.236.321-.348l18-18a6 6 0 018.661 8.303l-.175.183L38.484 138H120c23.196 0 42-18.804 42-42a6 6 0 0112 0c0 29.525-23.696 53.516-53.107 53.993L120 150H38.486l7.757 7.757a6 6 0 01.175 8.303l-.175.183a6 6 0 01-8.303.175l-.183-.175-18-18-.145-.151a6.036 6.036 0 01-.829-1.125l-.058-.105a4.08 4.08 0 01-.06-.114l-.04-.077a4.409 4.409 0 01-.139-.3l-.014-.036zM154.06 25.582l.183.175 18 18a6.036 6.036 0 01.974 1.276l.058.105c.02.035.038.07.056.105l.043.086a4.411 4.411 0 01.14.3l.014.036a5.965 5.965 0 01.291.868l.014.056c.032.13.059.263.082.397l.013.083a5.886 5.886 0 01.067.692v.014a6.11 6.11 0 01-.013.692l-.006.075a5.856 5.856 0 01-.048.39l-.016.095c-.008.05-.017.1-.027.149-.005.019-.008.038-.012.058-.028.133-.06.264-.096.393l-.026.088a5.86 5.86 0 01-.482 1.159l-.043.077-.052.09-.029.048a6.006 6.006 0 01-.32.478l-.044.059a3.857 3.857 0 01-.165.208l-.052.061a6.34 6.34 0 01-.176.197l-.145.15-18 18a6 6 0 01-8.661-8.302l.175-.183L153.514 54H72c-23.196 0-42 18.804-42 42a6 6 0 11-12 0c0-29.525 23.696-53.516 53.107-53.993L72 42h81.516l-7.759-7.757a6 6 0 01-.175-8.303l.175-.183a6 6 0 018.303-.175z" fill="currentColor" /></svg> `;
|
||||||
|
} else if (name == 'comment') {
|
||||||
|
return `<svg width = "24" height = "24" viewBox = "0 0 150 150" xmlns = "http://www.w3.org/2000/svg" > <path d="M75-.006a75 75 0 0174.997 74.31l.003.69c0 41.422-33.579 75-75 75H11.75c-6.49 0-11.75-5.26-11.75-11.75v-63.25a75 75 0 0175-75zm0 12a63 63 0 00-63 63v63h63c34.446 0 62.435-27.645 62.992-61.93l.008-1.041-.003-.633A63 63 0 0075 11.994zm21 72a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45zm0-24a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45z" fill="currentColor" /></svg> `;
|
||||||
|
} else if (name == 'bookmark') {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M 6.0097656 2 C 4.9143111 2 4.0097656 2.9025988 4.0097656 3.9980469 L 4 22 L 12 19 L 20 22 L 20 20.556641 L 20 4 C 20 2.9069372 19.093063 2 18 2 L 6.0097656 2 z M 6.0097656 4 L 18 4 L 18 19.113281 L 12 16.863281 L 6.0019531 19.113281 L 6.0097656 4 z" fill="currentColor"></path>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a list of reactions as HTML.
|
||||||
|
*
|
||||||
|
* @param {Array<Reaction>} reacts List of reactions to format
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
function formatReactions(type, reacts) {
|
||||||
|
let html = `
|
||||||
|
<div class="color--primary" >
|
||||||
|
<h3 id=`+ type + ` - header"> ` + getIcon(type) + ` <span>` + reacts.length + `</span> ` + type + `s </h3>
|
||||||
|
|
||||||
|
<ol class="likes" role = "list" aria - labelledby="`+ type + `-header"> `;
|
||||||
|
|
||||||
|
reacts.forEach(function (react) {
|
||||||
|
html += `
|
||||||
|
<li class="h-card">
|
||||||
|
<a class="u-url"
|
||||||
|
href="`+ react.author.url + `
|
||||||
|
target="_blank"
|
||||||
|
rel = "noreferrer"
|
||||||
|
title = "`+ react.author.name + `" >
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="lazy mentions__image u-photo"
|
||||||
|
src="`+ react.author.photo + `"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
>
|
||||||
|
<span class="p-author visually-hidden" aria-hidden="true">{{ author }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</ol >
|
||||||
|
</div >
|
||||||
|
`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef WebmentionResponse
|
||||||
|
* @type {Object}
|
||||||
|
* @property {Array<Reaction>} children
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register event listener.
|
||||||
|
*/
|
||||||
|
window.addEventListener("load", async function () {
|
||||||
|
const container = document.getElementById(containerID);
|
||||||
|
if (!container) {
|
||||||
|
// no container, so do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = [stripurl(refurl)];
|
||||||
|
if (!!addurls) {
|
||||||
|
addurls.split('|').forEach(function (url) {
|
||||||
|
pages.push(stripurl(url));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiURL = `https://webmention.io/api/mentions.jf2?per-page=${maxWebmentions}&sort-by=${sortBy}&sort-dir=${sortDir}`;
|
||||||
|
|
||||||
|
pages.forEach(function (path) {
|
||||||
|
apiURL += `&target[]=${encodeURIComponent('http:' + path)}&target[]=${encodeURIComponent('https:' + path)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// apiURL = 'http://127.0.0.1:1111/test_webmentions.jf2';
|
||||||
|
/** @type {WebmentionResponse} */
|
||||||
|
let json = {};
|
||||||
|
try {
|
||||||
|
// const response = await window.fetch(apiURL);
|
||||||
|
const response = await window.fetch(apiURL);
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
json = await response.json();
|
||||||
|
} else {
|
||||||
|
console.error("Could not parse response");
|
||||||
|
new Error(response.statusText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Purposefully not escalate further, i.e. no UI update
|
||||||
|
console.error("Request failed", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Array<Reaction>} */
|
||||||
|
let comments = [];
|
||||||
|
/** @type {Array<Reaction>} */
|
||||||
|
let mentions = [];
|
||||||
|
/** @type {Array<Reaction>} */
|
||||||
|
const bookmarks = [];
|
||||||
|
/** @type {Array<Reaction>} */
|
||||||
|
const likes = [];
|
||||||
|
/** @type {Array<Reaction>} */
|
||||||
|
const reposts = [];
|
||||||
|
/** @type {Array<Reaction>} */
|
||||||
|
const follows = [];
|
||||||
|
|
||||||
|
/** @type {Record<MentionType, Array<Reaction>>} */
|
||||||
|
const mapping = {
|
||||||
|
"in-reply-to": comments,
|
||||||
|
"like-of": likes,
|
||||||
|
"repost-of": reposts,
|
||||||
|
"bookmark-of": bookmarks,
|
||||||
|
"follow-of": follows,
|
||||||
|
"mention-of": mentions,
|
||||||
|
"rsvp": comments
|
||||||
|
};
|
||||||
|
|
||||||
|
json.children.forEach(function (child) {
|
||||||
|
// Map each mention into its respective container
|
||||||
|
const store = mapping[child['wm-property']];
|
||||||
|
if (store) {
|
||||||
|
store.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// format the comment-type things
|
||||||
|
let formattedMentions = '';
|
||||||
|
if (mentions.length > 0) {
|
||||||
|
formattedMentions = formatComments('mention', dedupe(mentions));
|
||||||
|
}
|
||||||
|
|
||||||
|
let formattedComments = '';
|
||||||
|
if (comments.length > 0) {
|
||||||
|
formattedComments = formatComments('comment', dedupe(comments));
|
||||||
|
}
|
||||||
|
|
||||||
|
// format likes
|
||||||
|
let likesStr = '';
|
||||||
|
if (likes.length > 0) {
|
||||||
|
likesStr = formatReactions('like', dedupe(likes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// format reposts
|
||||||
|
let repostsStr = '';
|
||||||
|
if (reposts.length > 0) {
|
||||||
|
repostsStr = formatReactions('repost', dedupe(reposts));
|
||||||
|
}
|
||||||
|
|
||||||
|
// format bookmarks
|
||||||
|
let bookmarksStr = '';
|
||||||
|
if (bookmarks.length > 0) {
|
||||||
|
bookmarksStr = formatReactions('bookmark', dedupe(bookmarks));
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = `<div id="webmentions">${repostsStr}${likesStr}${bookmarksStr}${formattedComments}${formattedMentions}</div>`;
|
||||||
|
});
|
||||||
|
}());
|
||||||
|
|
||||||
|
// End-of-file marker for LibreJS
|
||||||
|
// @license-end
|
||||||
66
themes/tabi/static/js/webmention.min.js
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
(()=>{function t(t,e){return document.currentScript.getAttribute("data-"+t)||e}window.i18next=window.i18next||{t:function(t){return t}},window.i18next.t.bind(window.i18next);let m=t("page-url",window.location.href.replace(/#.*$/,"")),f=t("add-urls",void 0),e=t("id","webmentions"),g=(t("wordcount"),t("max-webmentions",30)),v=(t("prevent-spoofing"),t("sort-by","published")),b=t("sort-dir","up");function y(t){return t.substr(t.indexOf("//"))}function x(t){let l=[],a={};return t.forEach(function(t){var e=y(t.url);a[e]||(l.push(t),a[e]=!0)}),l}function L(t,e){let a=`
|
||||||
|
<div class="webcomments">
|
||||||
|
<h3 id="replies-header">`+o("comment")+" <span>"+e.length+"</span> "+t+`s </h3>
|
||||||
|
<ol aria-labelledby="replies-header" role="list">`;return e.forEach(function(t){let e="";var l;t.hasOwnProperty("content")&&(t.content.hasOwnProperty("html")?e=t.content.html:t.content.hasOwnProperty("text")&&(e=t.content.text)),a+=`
|
||||||
|
<li class="comment h-entry">
|
||||||
|
<div>
|
||||||
|
<a class="comment_author u-author"
|
||||||
|
href="`+t.author.url+`"
|
||||||
|
target="_blank"
|
||||||
|
title="`+t.author.name+`"
|
||||||
|
rel="noreferrer">
|
||||||
|
<img
|
||||||
|
src="`+t.author.photo+`"
|
||||||
|
alt=""
|
||||||
|
class="u-photo"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
>
|
||||||
|
<span class="p-author">`+t.author.name+`</span>
|
||||||
|
</a>`,t.published&&(l=new Date(t.published),a+=`
|
||||||
|
<time class="dt-published" datetime="`+t.published+'">'+l.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})+"</time>"),a+=`
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="e-entry">`+e+`
|
||||||
|
<a class="u-url"
|
||||||
|
href="`+t.url+`"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
source
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
`}),a+=`
|
||||||
|
</ol >
|
||||||
|
</div >
|
||||||
|
`}function o(t){return"like"==t?'<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M95.997 41.986l-.026-.035C85.746 28.36 68.428 21.423 51.165 24.881 30.138 29.094 15.004 47.558 15 69.003c0 24.413 14.906 47.964 39.486 70.086 8.43 7.586 17.437 14.468 26.444 20.533.728.49 1.444.967 2.148 1.43l1.39.909 1.355.872 1.317.835.645.403 1.259.78 1.194.726 1.032.619 1.38.807.418.236a6 6 0 005.864 0l1.138-.654 1.154-.684 1.118-.675.614-.376 1.26-.779a212 212 0 00.644-.403l1.317-.835 1.355-.872 1.39-.909c.704-.463 1.42-.94 2.148-1.43 9.007-6.065 18.015-12.947 26.444-20.533C162.094 116.967 177 93.416 177 69.004c-.004-21.446-15.138-39.91-36.165-44.123-17.07-3.42-34.174 3.323-44.43 16.568l-.408.537zm42.48-5.338c15.421 3.09 26.52 16.63 26.523 32.357 0 19.607-12.438 39.847-33.532 59.357l-1.316 1.205c-.22.201-.443.402-.666.603-7.977 7.18-16.548 13.727-25.118 19.498l-.745.5c-.74.494-1.466.973-2.177 1.437l-1.402.906-1.359.864-.662.416-1.292.8-.732.446-.73-.446-1.292-.8-.662-.416-1.36-.864-1.4-.906a235.406 235.406 0 01-2.923-1.937c-8.57-5.77-17.14-12.319-25.118-19.498l-.666-.603-1.316-1.205C39.438 108.852 27 88.612 27 69.004c.003-15.726 11.102-29.267 26.523-32.356 15.253-3.056 30.565 4.954 36.756 19.208l.204.478c2.084 4.878 9.009 4.85 11.053-.045 6.062-14.511 21.52-22.73 36.941-19.641z" fill="currentColor" /></svg> ':"repost"==t?'<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M18.472 146.335l-.075-.184a5.968 5.968 0 01-.216-.684l-.014-.056a5.643 5.643 0 01-.082-.397l-.013-.083a5.886 5.886 0 01-.072-.96V144c0-.157.006-.313.018-.467l.006-.075c.012-.132.028-.261.048-.39l.016-.095c.008-.05.017-.1.027-.149.005-.019.008-.038.012-.058.028-.133.06-.264.096-.393l.026-.088a5.86 5.86 0 01.482-1.159l.043-.077a5.642 5.642 0 01.31-.49l.015-.022.076-.104.044-.059a3.856 3.856 0 01.165-.208l.052-.061c.102-.12.21-.236.321-.348l18-18a6 6 0 018.661 8.303l-.175.183L38.484 138H120c23.196 0 42-18.804 42-42a6 6 0 0112 0c0 29.525-23.696 53.516-53.107 53.993L120 150H38.486l7.757 7.757a6 6 0 01.175 8.303l-.175.183a6 6 0 01-8.303.175l-.183-.175-18-18-.145-.151a6.036 6.036 0 01-.829-1.125l-.058-.105a4.08 4.08 0 01-.06-.114l-.04-.077a4.409 4.409 0 01-.139-.3l-.014-.036zM154.06 25.582l.183.175 18 18a6.036 6.036 0 01.974 1.276l.058.105c.02.035.038.07.056.105l.043.086a4.411 4.411 0 01.14.3l.014.036a5.965 5.965 0 01.291.868l.014.056c.032.13.059.263.082.397l.013.083a5.886 5.886 0 01.067.692v.014a6.11 6.11 0 01-.013.692l-.006.075a5.856 5.856 0 01-.048.39l-.016.095c-.008.05-.017.1-.027.149-.005.019-.008.038-.012.058-.028.133-.06.264-.096.393l-.026.088a5.86 5.86 0 01-.482 1.159l-.043.077-.052.09-.029.048a6.006 6.006 0 01-.32.478l-.044.059a3.857 3.857 0 01-.165.208l-.052.061a6.34 6.34 0 01-.176.197l-.145.15-18 18a6 6 0 01-8.661-8.302l.175-.183L153.514 54H72c-23.196 0-42 18.804-42 42a6 6 0 11-12 0c0-29.525 23.696-53.516 53.107-53.993L72 42h81.516l-7.759-7.757a6 6 0 01-.175-8.303l.175-.183a6 6 0 018.303-.175z" fill="currentColor" /></svg> ':"comment"==t?'<svg width = "24" height = "24" viewBox = "0 0 150 150" xmlns = "http://www.w3.org/2000/svg" > <path d="M75-.006a75 75 0 0174.997 74.31l.003.69c0 41.422-33.579 75-75 75H11.75c-6.49 0-11.75-5.26-11.75-11.75v-63.25a75 75 0 0175-75zm0 12a63 63 0 00-63 63v63h63c34.446 0 62.435-27.645 62.992-61.93l.008-1.041-.003-.633A63 63 0 0075 11.994zm21 72a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45zm0-24a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45z" fill="currentColor" /></svg> ':"bookmark"==t?`<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M 6.0097656 2 C 4.9143111 2 4.0097656 2.9025988 4.0097656 3.9980469 L 4 22 L 12 19 L 20 22 L 20 20.556641 L 20 4 C 20 2.9069372 19.093063 2 18 2 L 6.0097656 2 z M 6.0097656 4 L 18 4 L 18 19.113281 L 12 16.863281 L 6.0019531 19.113281 L 6.0097656 4 z" fill="currentColor"></path>
|
||||||
|
</svg>`:void 0}function k(t,e){let l=`
|
||||||
|
<div class="color--primary" >
|
||||||
|
<h3 id=`+t+' - header"> '+o(t)+" <span>"+e.length+"</span> "+t+`s </h3>
|
||||||
|
|
||||||
|
<ol class="likes" role = "list" aria - labelledby="`+t+'-header"> ';return e.forEach(function(t){l+=`
|
||||||
|
<li class="h-card">
|
||||||
|
<a class="u-url"
|
||||||
|
href="`+t.author.url+`
|
||||||
|
target="_blank"
|
||||||
|
rel = "noreferrer"
|
||||||
|
title = "`+t.author.name+`" >
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="lazy mentions__image u-photo"
|
||||||
|
src="`+t.author.photo+`"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
>
|
||||||
|
<span class="p-author visually-hidden" aria-hidden="true">{{ author }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`}),l+=`
|
||||||
|
</ol >
|
||||||
|
</div >
|
||||||
|
`}window.addEventListener("load",async function(){var c=document.getElementById(e);if(c){let e=[y(m)],l=(f&&f.split("|").forEach(function(t){e.push(y(t))}),`https://webmention.io/api/mentions.jf2?per-page=${g}&sort-by=${v}&sort-dir=`+b),t=(e.forEach(function(t){l+=`&target[]=${encodeURIComponent("http:"+t)}&target[]=`+encodeURIComponent("https:"+t)}),{});try{200<=(h=await window.fetch(l)).status&&h.status<300?t=await h.json():(console.error("Could not parse response"),new Error(h.statusText))}catch(t){console.error("Request failed",t)}var h,d=[],u=[],p=[],w=[];let a={"in-reply-to":h=[],"like-of":p,"repost-of":w,"bookmark-of":u,"follow-of":[],"mention-of":d,rsvp:h},o=(t.children.forEach(function(t){var e=a[t["wm-property"]];e&&e.push(t)}),""),n=(0<d.length&&(o=L("mention",x(d))),""),r=(0<h.length&&(n=L("comment",x(h))),""),i=(0<p.length&&(r=k("like",x(p))),""),s=(0<w.length&&(i=k("repost",x(w))),"");0<u.length&&(s=k("bookmark",x(u))),c.innerHTML=`<div id="webmentions">${i}${r}${s}${n}${o}</div>`}})})();
|
||||||
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.8 KiB |
BIN
themes/tabi/static/social_icons/email1.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
themes/tabi/static/social_icons/microblog.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
themes/tabi/static/social_icons/pixelfed.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 14 KiB |
BIN
themes/tabi/static/social_icons/rss1.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
BIN
themes/tabi/static/social_icons/telegram1.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
themes/tabi/static/social_icons/xmpp.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -7,6 +7,18 @@
|
|||||||
{# Set locale for date #}
|
{# Set locale for date #}
|
||||||
{% set date_locale = macros_translate::translate(key="date_locale", default="en_GB", language_strings=language_strings) %}
|
{% set date_locale = macros_translate::translate(key="date_locale", default="en_GB", language_strings=language_strings) %}
|
||||||
|
|
||||||
|
{#- Check for language-specific date formats -#}
|
||||||
|
{%- set language_format = "" -%}
|
||||||
|
{%- if config.extra.date_formats -%}
|
||||||
|
{%- for format_config in config.extra.date_formats -%}
|
||||||
|
{%- if format_config.lang == lang -%}
|
||||||
|
{%- if format_config.archive -%}
|
||||||
|
{%- set_global language_format = format_config.archive -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
<div class="archive">
|
<div class="archive">
|
||||||
<ul class="list-with-title">
|
<ul class="list-with-title">
|
||||||
{%- set source_paths = section.extra.section_path | default(value="blog/") -%}
|
{%- set source_paths = section.extra.section_path | default(value="blog/") -%}
|
||||||
@@ -55,7 +67,13 @@
|
|||||||
<li class="listing-item">
|
<li class="listing-item">
|
||||||
<div class="post-time">
|
<div class="post-time">
|
||||||
<span class="date">
|
<span class="date">
|
||||||
{{ post.date | date(format="%d %b", locale=date_locale) }}
|
{%- if language_format -%}
|
||||||
|
{{ post.date | date(format=language_format, locale=date_locale) }}
|
||||||
|
{%- elif config.extra.archive_date_format -%}
|
||||||
|
{{ post.date | date(format=config.extra.archive_date_format, locale=date_locale) }}
|
||||||
|
{%- else -%}
|
||||||
|
{{ post.date | date(format="%d %b", locale=date_locale) }}
|
||||||
|
{%- endif -%}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ post.permalink }}" title="{{ post.title }}">{{ post.title | markdown(inline=true) | safe }}</a>
|
<a href="{{ post.permalink }}" title="{{ post.title }}">{{ post.title | markdown(inline=true) | safe }}</a>
|
||||||
|
|||||||
@@ -66,7 +66,9 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
" rel="alternate" type="text/html"/>
|
" rel="alternate" type="text/html"/>
|
||||||
<generator uri="https://www.getzola.org/">Zola</generator>
|
<generator uri="https://www.getzola.org/">Zola</generator>
|
||||||
<updated>{{ last_updated | date(format="%+") }}</updated>
|
{%- if last_updated -%}
|
||||||
|
<updated>{{ last_updated | date(format="%+") }}</updated>
|
||||||
|
{%- endif -%}
|
||||||
<id>{{ feed_url | safe }}</id>
|
<id>{{ feed_url | safe }}</id>
|
||||||
{%- for page in pages %}
|
{%- for page in pages %}
|
||||||
{%- if macros_settings::evaluate_setting_priority(setting="hide_from_feed", page=page, default_global_value=false) == "true" -%}
|
{%- if macros_settings::evaluate_setting_priority(setting="hide_from_feed", page=page, default_global_value=false) == "true" -%}
|
||||||
@@ -97,11 +99,11 @@
|
|||||||
<id>{{ page.permalink | safe }}</id>
|
<id>{{ page.permalink | safe }}</id>
|
||||||
{% if config.extra.full_content_in_feed %}
|
{% if config.extra.full_content_in_feed %}
|
||||||
<content type="html">{{ page.content }}</content>
|
<content type="html">{{ page.content }}</content>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% if page.summary -%}
|
{% if page.description -%}
|
||||||
|
<summary type="html">{{ page.description }}</summary>
|
||||||
|
{% elif page.summary -%}
|
||||||
<summary type="html">{{ page.summary | striptags | trim_end_matches(pat=".") | safe }}…</summary>
|
<summary type="html">{{ page.summary | striptags | trim_end_matches(pat=".") | safe }}…</summary>
|
||||||
{% elif page.description -%}
|
|
||||||
<summary type="html">{{ page.description }}</summary>
|
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
</entry>
|
</entry>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
{% import "macros/feed_utils.html" as feed_utils %}
|
||||||
{% import "macros/format_date.html" as macros_format_date %}
|
{% import "macros/format_date.html" as macros_format_date %}
|
||||||
{% import "macros/list_posts.html" as macros_list_posts %}
|
{% import "macros/list_posts.html" as macros_list_posts %}
|
||||||
{% import "macros/page_header.html" as macros_page_header %}
|
{% import "macros/page_header.html" as macros_page_header %}
|
||||||
{% import "macros/rel_attributes.html" as macros_rel_attributes %}
|
{% import "macros/rel_attributes.html" as macros_rel_attributes %}
|
||||||
|
{% import "macros/series_page.html" as macros_series_page %}
|
||||||
{% import "macros/settings.html" as macros_settings %}
|
{% import "macros/settings.html" as macros_settings %}
|
||||||
{% import "macros/table_of_contents.html" as macros_toc %}
|
{% import "macros/table_of_contents.html" as macros_toc %}
|
||||||
|
{% import "macros/target_attribute.html" as macros_target_attribute %}
|
||||||
{% import "macros/translate.html" as macros_translate %}
|
{% import "macros/translate.html" as macros_translate %}
|
||||||
{% import "macros/series_page.html" as macros_series_page %}
|
|
||||||
|
|
||||||
{# Load the internationalisation data for the current language from
|
{# Load the internationalisation data for the current language from
|
||||||
the .toml files in the user's '/i18n' folder, falling back to the theme's.
|
the .toml files in the user's '/i18n' folder, falling back to the theme's.
|
||||||
@@ -32,8 +34,9 @@ This variable will hold all the text strings for the language #}
|
|||||||
{% include "partials/header.html" %}
|
{% include "partials/header.html" %}
|
||||||
|
|
||||||
<body{% if lang in rtl_languages %} dir="rtl"{% endif %}{% if config.extra.override_serif_with_sans %} class="use-sans-serif"{% endif %}>
|
<body{% if lang in rtl_languages %} dir="rtl"{% endif %}{% if config.extra.override_serif_with_sans %} class="use-sans-serif"{% endif %}>
|
||||||
|
<a href="#main-content" id="skip-link">{{ macros_translate::translate(key="skip_to_content", default="Skip to content", language_strings=language_strings) }}</a>
|
||||||
{% include "partials/nav.html" %}
|
{% include "partials/nav.html" %}
|
||||||
<div class="content">
|
<div class="content" id="main-content">
|
||||||
|
|
||||||
{# Post page is the default #}
|
{# Post page is the default #}
|
||||||
{% block main_content %}
|
{% block main_content %}
|
||||||
@@ -41,6 +44,18 @@ This variable will hold all the text strings for the language #}
|
|||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
</div>
|
</div>
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" %}
|
||||||
|
|
||||||
|
{# Users can optionally provide this template to add content to the body element. #}
|
||||||
|
{% include "tabi/extend_body.html" ignore missing %}
|
||||||
|
|
||||||
|
<!-- baguetteBox JS -->
|
||||||
|
<script defer src="{{ get_url(path='js/baguetteBox.min.js', trailing_slash=false) | safe }}"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
baguetteBox.run('.gallery');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
17
themes/tabi/templates/macros/feed_utils.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{#- Feed utility macros -#}
|
||||||
|
|
||||||
|
{#- Zola 0.19.0 uses `generate_feeds`. Prior versions use `generate_feed` -#}
|
||||||
|
{%- macro get_generate_feed() -%}
|
||||||
|
{{- config.generate_feeds | default(value=config.generate_feed) -}}
|
||||||
|
{%- endmacro get_generate_feed -%}
|
||||||
|
|
||||||
|
{%- macro get_feed_url() -%}
|
||||||
|
{{- config.feed_filenames[0] | default(value=(config.feed_filename)) -}}
|
||||||
|
{%- endmacro get_feed_url -%}
|
||||||
|
|
||||||
|
{#- Check footer feed icon conditions -#}
|
||||||
|
{%- macro should_show_footer_feed_icon() -%}
|
||||||
|
{%- set generate_feed = feed_utils::get_generate_feed() == "true" -%}
|
||||||
|
{%- set feed_url = feed_utils::get_feed_url() -%}
|
||||||
|
{{- generate_feed and config.extra.feed_icon and feed_url -}}
|
||||||
|
{%- endmacro should_show_footer_feed_icon -%}
|
||||||
@@ -3,7 +3,23 @@
|
|||||||
{#- Set locale -#}
|
{#- Set locale -#}
|
||||||
{%- set date_locale = macros_translate::translate(key="date_locale", default="en_GB", language_strings=language_strings) -%}
|
{%- set date_locale = macros_translate::translate(key="date_locale", default="en_GB", language_strings=language_strings) -%}
|
||||||
|
|
||||||
{%- if config.extra.short_date_format and short -%}
|
{#- Check for language-specific date formats -#}
|
||||||
|
{%- set language_format = "" -%}
|
||||||
|
{%- if config.extra.date_formats -%}
|
||||||
|
{%- for format_config in config.extra.date_formats -%}
|
||||||
|
{%- if format_config.lang == lang -%}
|
||||||
|
{%- if short and format_config.short -%}
|
||||||
|
{%- set_global language_format = format_config.short -%}
|
||||||
|
{%- elif not short and format_config.long -%}
|
||||||
|
{%- set_global language_format = format_config.long -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if language_format -%}
|
||||||
|
{{ date | date(format=language_format, locale=date_locale) }}
|
||||||
|
{%- elif config.extra.short_date_format and short -%}
|
||||||
{{ date | date(format=config.extra.short_date_format, locale=date_locale) }}
|
{{ date | date(format=config.extra.short_date_format, locale=date_locale) }}
|
||||||
{%- elif config.extra.long_date_format and not short -%}
|
{%- elif config.extra.long_date_format and not short -%}
|
||||||
{{ date | date(format=config.extra.long_date_format, locale=date_locale) }}
|
{{ date | date(format=config.extra.long_date_format, locale=date_locale) }}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
<li class="date">{{- macros_format_date::format_date(date=post.date, short=false, language_strings=language_strings) -}}</li>
|
<li class="date">{{- macros_format_date::format_date(date=post.date, short=false, language_strings=language_strings) -}}</li>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if show_date and show_updated -%}
|
{%- if show_date and show_updated -%}
|
||||||
<li class="mobile-only">{{- separator -}}</li>
|
<li class="mobile-only separator">{{- separator -}}</li>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if show_updated -%}
|
{%- if show_updated -%}
|
||||||
{%- set last_updated_str = macros_translate::translate(key="last_updated_on", default="Updated on $DATE", language_strings=language_strings) -%}
|
{%- set last_updated_str = macros_translate::translate(key="last_updated_on", default="Updated on $DATE", language_strings=language_strings) -%}
|
||||||
@@ -142,9 +142,9 @@
|
|||||||
|
|
||||||
<div class="description">
|
<div class="description">
|
||||||
{% if post.description %}
|
{% if post.description %}
|
||||||
<p>{{ post.description }}</p>
|
<p>{{ post.description | markdown(inline=true) | safe }}</p>
|
||||||
{% elif post.summary %}
|
{% elif post.summary %}
|
||||||
<p>{{ post.summary | striptags | trim_end_matches(pat=".") | safe }}…</p>
|
<p>{{ post.summary | markdown(inline=true) | trim_end_matches(pat=".") | safe }}…</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<a class="readmore" href="{{ post.permalink }}">{{ macros_translate::translate(key="read_more", default="Read more", language_strings=language_strings) }} <span class="arrow">→</span></a>
|
<a class="readmore" href="{{ post.permalink }}">{{ macros_translate::translate(key="read_more", default="Read more", language_strings=language_strings) }} <span class="arrow">→</span></a>
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
{% macro page_header(title) %}
|
{% macro page_header(title, show_feed_icon=false) %}
|
||||||
|
|
||||||
|
{% set rel_attributes = macros_rel_attributes::rel_attributes() | trim %}
|
||||||
|
|
||||||
|
|
||||||
|
{%- set blank_target = macros_target_attribute::target_attribute(new_tab=config.markdown.external_links_target_blank) -%}
|
||||||
|
|
||||||
<h1 class="title-container section-title bottom-divider">
|
<h1 class="title-container section-title bottom-divider">
|
||||||
{{ title }}
|
{{ title -}}
|
||||||
|
{% if show_feed_icon %}
|
||||||
|
{%- set feed_url = feed_utils::get_feed_url() -%}
|
||||||
|
<a class="no-hover-padding social" rel="{{ rel_attributes }}" {{ blank_target }} href="{{ get_url(path=term.path ~ feed_url, lang=lang, trailing_slash=false) | safe }}">
|
||||||
|
<img loading="lazy" alt="feed" title="feed" src="{{ get_url(path='/social_icons/rss.svg') }}">
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{% endmacro page_header %}
|
{% endmacro page_header %}
|
||||||
|
|||||||
11
themes/tabi/templates/macros/target_attribute.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% macro target_attribute(new_tab) %}
|
||||||
|
|
||||||
|
{%- set blank_target = "" -%}
|
||||||
|
|
||||||
|
{%- if new_tab -%}
|
||||||
|
{%- set blank_target = "target=_blank" -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{{ blank_target }}
|
||||||
|
|
||||||
|
{% endmacro target_attribute %}
|
||||||
@@ -5,11 +5,7 @@
|
|||||||
|
|
||||||
{%- set rel_attributes = macros_rel_attributes::rel_attributes() | trim -%}
|
{%- set rel_attributes = macros_rel_attributes::rel_attributes() | trim -%}
|
||||||
|
|
||||||
{%- if config.markdown.external_links_target_blank -%}
|
{%- set blank_target = macros_target_attribute::target_attribute(new_tab=config.markdown.external_links_target_blank) -%}
|
||||||
{%- set blank_target = "target=_blank" -%}
|
|
||||||
{%- else -%}
|
|
||||||
{%- set blank_target = "" -%}
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{# Debugging #}
|
{# Debugging #}
|
||||||
{# <div><pre>
|
{# <div><pre>
|
||||||
@@ -61,6 +57,8 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
</pre></div>
|
</pre></div>
|
||||||
|
|
||||||
{% set settings_to_test = [
|
{% set settings_to_test = [
|
||||||
|
"iine",
|
||||||
|
"iine_icon",
|
||||||
"enable_cards_tag_filtering",
|
"enable_cards_tag_filtering",
|
||||||
"footnote_backlinks",
|
"footnote_backlinks",
|
||||||
"add_src_to_code_block",
|
"add_src_to_code_block",
|
||||||
@@ -102,14 +100,16 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div> #}
|
</div> #}
|
||||||
|
|
||||||
{# {{ __tera_context }} #}
|
{# {{ __tera_context }} #}
|
||||||
{# End debugging #}
|
{# End debugging #}
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<article>
|
<article class="h-entry">
|
||||||
<h1 class="article-title">
|
<h1 class="p-name article-title">
|
||||||
{{ page.title | markdown(inline=true) | safe }}
|
{{ page.title | markdown(inline=true) | safe }}
|
||||||
</h1>
|
</h1>
|
||||||
|
<a class="u-url u-uid" href="{{ page.permalink | safe }}"></a>
|
||||||
|
|
||||||
<ul class="meta">
|
<ul class="meta">
|
||||||
{#- Draft indicator -#}
|
{#- Draft indicator -#}
|
||||||
@@ -118,7 +118,7 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{#- Author(s) -#}
|
{#- Author(s) -#}
|
||||||
{% if page.authors or config.author and macros_settings::evaluate_setting_priority(setting="show_author", page=page, default_global_value=false) == "true" %}
|
{%- if page.authors or config.author and macros_settings::evaluate_setting_priority(setting="show_author", page=page, default_global_value=false) == "true" -%}
|
||||||
{%- if page.authors -%}
|
{%- if page.authors -%}
|
||||||
{%- set author_list = page.authors -%}
|
{%- set author_list = page.authors -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
@@ -126,29 +126,36 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if author_list | length == 1 -%}
|
{%- if author_list | length == 1 -%}
|
||||||
{%- set author_string = author_list.0 -%}
|
{%- set author_string = '<span class="p-author">' ~ author_list.0 ~ '</span>' -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{%- set last_author = author_list | last -%}
|
{%- set last_author = author_list | last -%}
|
||||||
{%- set other_authors = author_list | slice(end=-1) -%}
|
{%- set other_authors = author_list | slice(end=-1) -%}
|
||||||
{%- set author_separator = macros_translate::translate(key="author_separator", default=", ", language_strings=language_strings) -%}
|
{%- set author_separator = macros_translate::translate(key="author_separator", default=", ", language_strings=language_strings) -%}
|
||||||
|
{%- set author_separator = '</span>' ~ author_separator ~ '<span class="p-author">' -%}
|
||||||
{%- set conjunction = macros_translate::translate(key="author_conjunction", default=" and ", language_strings=language_strings) -%}
|
{%- set conjunction = macros_translate::translate(key="author_conjunction", default=" and ", language_strings=language_strings) -%}
|
||||||
|
{%- set conjunction = '</span>' ~ conjunction ~ '<span class="p-author">' -%}
|
||||||
{%- set author_string = other_authors | join(sep=author_separator) -%}
|
{%- set author_string = other_authors | join(sep=author_separator) -%}
|
||||||
{%- set author_string = author_string ~ conjunction ~ last_author -%}
|
{%- set author_string = author_string ~ conjunction ~ last_author -%}
|
||||||
|
{%- set author_string = '<span class="p-author">' ~ author_string ~ '</span>' -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- set by_author = macros_translate::translate(key="by_author", default="By $AUTHOR", language_strings=language_strings) -%}
|
{%- set by_author = macros_translate::translate(key="by_author", default="By $AUTHOR", language_strings=language_strings) -%}
|
||||||
<li>{{ by_author | replace(from="$AUTHOR", to=author_string) }}</li>
|
<li>{{ by_author | replace(from="$AUTHOR", to=author_string) | safe }}</li>
|
||||||
{%- set previous_visible = true -%}
|
{%- set previous_visible = true -%}
|
||||||
{% endif %}
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if config.extra.hcard and config.extra.hcard.enable and ( not author_list or author_list is containing(config.author)) -%}
|
||||||
|
{% include "partials/hcard_small.html" %}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
{%- set separator_with_class = "<span class='separator' aria-hidden='true'>" ~ separator ~ "</span>"-%}
|
{%- set separator_with_class = "<span class='separator' aria-hidden='true'>" ~ separator ~ "</span>"-%}
|
||||||
|
|
||||||
{#- Date -#}
|
{#- Date -#}
|
||||||
{% if page.date and macros_settings::evaluate_setting_priority(setting="show_date", page=page, default_global_value=true) == "true" %}
|
{%- if page.date and macros_settings::evaluate_setting_priority(setting="show_date", page=page, default_global_value=true) == "true" -%}
|
||||||
<li>{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}{{ macros_format_date::format_date(date=page.date, short=true, language_strings=language_strings) }}</li>
|
<li><time class="dt-published" datetime="{{ page.date }}">{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}{{ macros_format_date::format_date(date=page.date, short=true, language_strings=language_strings) }}</time></li>
|
||||||
{#- Variable to keep track of whether we've shown a section, to avoid separators as the first element -#}
|
{#- Variable to keep track of whether we've shown a section, to avoid separators as the first element -#}
|
||||||
{%- set previous_visible = true -%}
|
{%- set previous_visible = true -%}
|
||||||
{% endif %}
|
{%- endif -%}
|
||||||
|
|
||||||
{#- Reading time -#}
|
{#- Reading time -#}
|
||||||
{%- if macros_settings::evaluate_setting_priority(setting="show_reading_time", page=page, default_global_value=true) == "true" -%}
|
{%- if macros_settings::evaluate_setting_priority(setting="show_reading_time", page=page, default_global_value=true) == "true" -%}
|
||||||
@@ -160,7 +167,7 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
{%- if page.taxonomies and page.taxonomies.tags -%}
|
{%- if page.taxonomies and page.taxonomies.tags -%}
|
||||||
<li class="tag">{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}{{- macros_translate::translate(key="tags", default="tags", language_strings=language_strings) | capitalize -}}: </li>
|
<li class="tag">{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}{{- macros_translate::translate(key="tags", default="tags", language_strings=language_strings) | capitalize -}}: </li>
|
||||||
{%- for tag in page.taxonomies.tags -%}
|
{%- for tag in page.taxonomies.tags -%}
|
||||||
<li class="tag"><a href="{{ get_taxonomy_url(kind='tags', name=tag, lang=lang) | safe }}">{{ tag }}</a>
|
<li class="tag"><a class="p-category" href="{{ get_taxonomy_url(kind='tags', name=tag, lang=lang) | safe }}">{{ tag }}</a>
|
||||||
{%- if not loop.last -%}
|
{%- if not loop.last -%}
|
||||||
,
|
,
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -175,7 +182,7 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
{%- set formatted_date = macros_format_date::format_date(date=page.updated, short=true, language_strings=language_strings) -%}
|
{%- set formatted_date = macros_format_date::format_date(date=page.updated, short=true, language_strings=language_strings) -%}
|
||||||
{%- set updated_str = last_updated_str | replace(from="$DATE", to=formatted_date) -%}
|
{%- set updated_str = last_updated_str | replace(from="$DATE", to=formatted_date) -%}
|
||||||
{%- set previous_visible = true -%}
|
{%- set previous_visible = true -%}
|
||||||
</ul><ul class="meta last-updated"><li>{{ updated_str }}</li>
|
</ul><ul class="meta last-updated"><li><time class="dt-updated" datetime="{{ page.updated }}">{{ updated_str }}</time></li>
|
||||||
{#- Show link to remote changes if enabled -#}
|
{#- Show link to remote changes if enabled -#}
|
||||||
{%- if config.extra.remote_repository_url and macros_settings::evaluate_setting_priority(setting="show_remote_changes", page=page, default_global_value=true) == "true" -%}
|
{%- if config.extra.remote_repository_url and macros_settings::evaluate_setting_priority(setting="show_remote_changes", page=page, default_global_value=true) == "true" -%}
|
||||||
<li>{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}<a class="external" href="{% include "partials/history_url.html" %}" {{ blank_target }} rel="{{ rel_attributes }}">{{ macros_translate::translate(key="see_changes", default="See changes", language_strings=language_strings) }}</a></li>
|
<li>{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}<a class="external" href="{% include "partials/history_url.html" %}" {{ blank_target }} rel="{{ rel_attributes }}">{{ macros_translate::translate(key="see_changes", default="See changes", language_strings=language_strings) }}</a></li>
|
||||||
@@ -227,7 +234,12 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
{{ macros_toc::toc(page=page, header=true, language_strings=language_strings) }}
|
{{ macros_toc::toc(page=page, header=true, language_strings=language_strings) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class="body">
|
{#- Optional Summary paragraph for readers -#}
|
||||||
|
{% if page.description %}
|
||||||
|
<p class="p-summary" hidden>{{ page.description }}</p>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
<section class="e-content body">
|
||||||
{#- Replace series_intro placeholder -#}
|
{#- Replace series_intro placeholder -#}
|
||||||
{%- set content_with_intro = page.content -%}
|
{%- set content_with_intro = page.content -%}
|
||||||
{%- if "<!-- series_intro -->" in page.content -%}
|
{%- if "<!-- series_intro -->" in page.content -%}
|
||||||
@@ -263,6 +275,11 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
{{ processed_content | replace(from="<!-- toc -->", to=macros_toc::toc(page=page, header=false, language_strings=language_strings)) | safe }}
|
{{ processed_content | replace(from="<!-- toc -->", to=macros_toc::toc(page=page, header=false, language_strings=language_strings)) | safe }}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#- iine button -#}
|
||||||
|
{%- if macros_settings::evaluate_setting_priority(setting="iine", page=page, default_global_value=false) == "true" -%}
|
||||||
|
{% include "partials/iine_button.html" %}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
{% if macros_settings::evaluate_setting_priority(setting="show_previous_next_article_links", page=page, default_global_value=true) == "true" %}
|
{% if macros_settings::evaluate_setting_priority(setting="show_previous_next_article_links", page=page, default_global_value=true) == "true" %}
|
||||||
{%- if page.lower or page.higher -%}
|
{%- if page.lower or page.higher -%}
|
||||||
{% set next_label = macros_translate::translate(key="next", default="Next", language_strings=language_strings) %}
|
{% set next_label = macros_translate::translate(key="next", default="Next", language_strings=language_strings) %}
|
||||||
@@ -333,6 +350,13 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
|
|||||||
{% if comment_system %}
|
{% if comment_system %}
|
||||||
{% include "partials/comments.html" %}
|
{% include "partials/comments.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{#- Webmentions -#}
|
||||||
|
{%- set global_webmentions_enabled = config.extra.webmentions.enable | default(value=false) -%}
|
||||||
|
{%- set page_webmentions_enabled = page.extra.webmentions | default(value=global_webmentions_enabled) -%}
|
||||||
|
{%- set webmentions_enabled = global_webmentions_enabled and page_webmentions_enabled != false or page_webmentions_enabled == true -%}
|
||||||
|
{%- if webmentions_enabled -%}
|
||||||
|
{%- include "partials/webmentions.html" -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
data-website-id="{{ analytics_id }}"
|
data-website-id="{{ analytics_id }}"
|
||||||
src="https://cloud.umami.is/script.js"
|
src="https://cloud.umami.is/script.js"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
data-do-not-track="true">
|
{% if config.extra.analytics.do_not_track %}data-do-not-track="true"{% endif %}>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% elif analytics_service == "plausible" %}
|
{% elif analytics_service == "plausible" %}
|
||||||
|
|||||||
@@ -7,11 +7,7 @@
|
|||||||
{% break %}
|
{% break %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Determine which URL to use, default is page.permalink #}
|
{# Determine which URL to use, default is page.permalink #}
|
||||||
{%- if page.extra.link_to and config.markdown.external_links_target_blank -%}
|
{%- set blank_target = macros_target_attribute::target_attribute(new_tab=config.markdown.external_links_target_blank and page.extra.link_to) -%}
|
||||||
{%- set blank_target = "target=_blank" -%}
|
|
||||||
{%- else -%}
|
|
||||||
{%- set blank_target = "" -%}
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{% set target_url = page.extra.link_to | default(value=page.permalink) %}
|
{% set target_url = page.extra.link_to | default(value=page.permalink) %}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ content="default-src 'self'
|
|||||||
{%- set giscus_enabled = config.extra.giscus.enabled_for_all_posts or page.extra.giscus -%}
|
{%- set giscus_enabled = config.extra.giscus.enabled_for_all_posts or page.extra.giscus -%}
|
||||||
{%- set hyvortalk_enabled = config.extra.hyvortalk.enabled_for_all_posts or page.extra.hyvortalk -%}
|
{%- set hyvortalk_enabled = config.extra.hyvortalk.enabled_for_all_posts or page.extra.hyvortalk -%}
|
||||||
{%- set isso_enabled = config.extra.isso.enabled_for_all_posts or page.extra.isso -%}
|
{%- set isso_enabled = config.extra.isso.enabled_for_all_posts or page.extra.isso -%}
|
||||||
|
{%- if page -%}
|
||||||
|
{%- set iine_enabled = macros_settings::evaluate_setting_priority(setting="iine", page=page, default_global_value=false) == "true" -%}
|
||||||
|
{%- endif -%}
|
||||||
{%- if page -%}
|
{%- if page -%}
|
||||||
{%- set mermaid_enabled = macros_settings::evaluate_setting_priority(setting="mermaid", page=page, default_global_value=false) == "true" -%}
|
{%- set mermaid_enabled = macros_settings::evaluate_setting_priority(setting="mermaid", page=page, default_global_value=false) == "true" -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -50,10 +53,21 @@ content="default-src 'self'
|
|||||||
{%- set script_src = script_src ~ " " ~ " utteranc.es" -%}
|
{%- set script_src = script_src ~ " " ~ " utteranc.es" -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if mermaid_enabled and not serve_local_mermaid -%}
|
{%- if (mermaid_enabled and not serve_local_mermaid) or iine_enabled -%}
|
||||||
{%- set script_src = script_src ~ " " ~ " cdn.jsdelivr.net" -%}
|
{%- set script_src = script_src ~ " " ~ " cdn.jsdelivr.net" -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
|
{#- Check if a webmention system is enabled to allow the necessary domains and directives -#}
|
||||||
|
{%- if config.extra.webmentions.enable -%}
|
||||||
|
{%- set connect_src = connect_src ~ " webmention.io" -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{#- Check if iine like buttons are enabled to allow the necessary domains -#}
|
||||||
|
{%- if iine_enabled -%}
|
||||||
|
{%- set connect_src = connect_src ~ " vhiweeypifbwacashxjz.supabase.co" -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
|
||||||
{#- Append WebSocket for Zola serve mode -#}
|
{#- Append WebSocket for Zola serve mode -#}
|
||||||
{%- if config.mode == "serve" -%}
|
{%- if config.mode == "serve" -%}
|
||||||
{%- set connect_src = connect_src ~ " ws:" -%}
|
{%- set connect_src = connect_src ~ " ws:" -%}
|
||||||
@@ -61,19 +75,19 @@ content="default-src 'self'
|
|||||||
|
|
||||||
{%- for domain in config.extra.allowed_domains -%}
|
{%- for domain in config.extra.allowed_domains -%}
|
||||||
{%- if domain.directive == "connect-src" -%}
|
{%- if domain.directive == "connect-src" -%}
|
||||||
{%- set configured_connect_src = domain.domains | join(sep=' ') -%}
|
{%- set configured_connect_src = domain.domains | join(sep=' ') | safe -%}
|
||||||
{%- set_global connect_src = connect_src ~ " " ~ configured_connect_src -%}
|
{%- set_global connect_src = connect_src ~ " " ~ configured_connect_src -%}
|
||||||
{%- continue -%}
|
{%- continue -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if domain.directive == "script-src" -%}
|
{%- if domain.directive == "script-src" -%}
|
||||||
{%- set configured_script_src = domain.domains | join(sep=' ') -%}
|
{%- set configured_script_src = domain.domains | join(sep=' ') | safe -%}
|
||||||
{%- set_global script_src = script_src ~ " " ~ configured_script_src -%}
|
{%- set_global script_src = script_src ~ " " ~ configured_script_src -%}
|
||||||
{%- continue -%}
|
{%- continue -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{#- Handle directives that are not connect-src -#}
|
{#- Handle directives that are not connect-src -#}
|
||||||
{{ domain.directive }} {{ domain.domains | join(sep=' ') -}}
|
{{ domain.directive }} {{ domain.domains | join(sep=' ') | safe -}}
|
||||||
|
|
||||||
{%- if domain.directive == "style-src" -%}
|
{%- if domain.directive == "style-src" -%}
|
||||||
{%- if utterances_enabled or hyvortalk_enabled or mermaid_enabled %} 'unsafe-inline'
|
{%- if utterances_enabled or hyvortalk_enabled or mermaid_enabled %} 'unsafe-inline'
|
||||||
|
|||||||