41 Commits

Author SHA1 Message Date
Sweetbread 85d1e2be85 tmp 2026-02-10 10:27:45 +03:00
Sweetbread e6bc69aa3e Add rtime for steam 2026-02-07 20:05:29 +03:00
Sweetbread a0cfa765bd Cover replacing for LB
Docker Build and Push / build-and-push (push) Successful in 20s
2026-02-07 19:25:12 +03:00
Sweetbread 62b8f056fa Fix covers
Docker Build and Push / build-and-push (push) Successful in 20s
2026-02-07 18:39:58 +03:00
Sweetbread 0edf6e35d7 Reactive updating of related time 2026-02-07 18:39:58 +03:00
Sweetbread db11fabe1a Refactoring 2026-02-07 18:39:58 +03:00
Sweetbread 0f5d1b5221 Don't send data if not modified 2026-02-06 18:56:38 +03:00
Sweetbread 9935bc7f1f Add polling
Docker Build and Push / build-and-push (push) Successful in 20s
2026-02-05 23:04:19 +03:00
Sweetbread 42ffd887d2 Update docker image 2026-02-05 23:04:19 +03:00
Sweetbread 96b73e4751 Add OG meta 2026-02-05 23:04:19 +03:00
Sweetbread 0aa2477f35 Add Steam info 2026-02-05 23:04:19 +03:00
Sweetbread c17bf8ca83 Delete otoring >: 2026-02-04 15:31:04 +03:00
Sweetbread ea5156aa6c Rework my page and add info from listenbrainz 2026-02-04 15:31:04 +03:00
Sweetbread e97985b624 Update /us
Docker Build and Push / build-and-push (push) Successful in 32s
2026-01-19 01:02:54 +03:00
Sweetbread 3e2eb74c76 Add 88x31 2026-01-19 00:39:01 +03:00
Sweetbread 8c02395d0d Add otoring
Docker Build and Push / build-and-push (push) Successful in 34s
2026-01-18 19:02:48 +03:00
Sweetbread 60bece505c Update footer 2026-01-18 19:00:32 +03:00
Sweetbread c56f88911c Remove private sites 2026-01-18 19:00:32 +03:00
Sweetbread b28f7886e1 wip: update my page 2026-01-18 19:00:32 +03:00
Sweetbread 335eaa0545 Move locale things into module 2026-01-17 23:51:32 +03:00
Sweetbread 4b2720f8b7 Put personal pages into subdomains 2026-01-17 23:51:32 +03:00
Sweetbread aedd47ba36 Change (temporary?) icon 2026-01-17 23:51:32 +03:00
Sweetbread e1f4021ed5 Add mock email 2026-01-13 16:10:47 +03:00
Sweetbread c806dfff6c Change to new domain 2026-01-13 16:10:47 +03:00
Sweetbread 000465079b Add altfronts block 2026-01-13 16:10:47 +03:00
Sweetbread 97280e2d7c ci: remove per commit images 2026-01-13 16:10:47 +03:00
Sweetbread 25edd56306 Remove typos in host 2026-01-13 16:10:47 +03:00
Sweetbread e776f09b0f Remove whitespaces before sending 2026-01-13 16:10:47 +03:00
Sweetbread c0dc079a71 Add some services 2026-01-13 14:58:10 +03:00
Sweetbread 0a5807e62e Change host info 2026-01-13 14:58:10 +03:00
Sweetbread f60a1940ac Improve style
- Add more depth to .block
- Make footer lighter
- Stylize scrollbar
2026-01-13 14:58:10 +03:00
Sweetbread 7351e28bcd Add mine page 2026-01-13 14:58:10 +03:00
Sweetbread 74afee8f5b style: change sizes to rem 2026-01-13 14:58:10 +03:00
Sweetbread 5f417b433e style: decrease margins 2026-01-13 14:58:10 +03:00
Sweetbread 39d6f469cb feat: add tracker script 2026-01-13 14:58:10 +03:00
Sweetbread 78f658241c update host specs 2026-01-13 14:58:10 +03:00
Sweetbread 4622e730c1 l10n: add en, de, ja and fr 2026-01-13 14:58:10 +03:00
Sweetbread e6ba705862 sec: add production server 2026-01-13 14:58:09 +03:00
Sweetbread cfd81884db ci: add docker-build 2026-01-13 14:58:09 +03:00
Sweetbread 8c75448836 feat: add dockerfile 2026-01-13 14:58:09 +03:00
Sweetbread f5afca83d2 Init commit 2026-01-13 14:58:09 +03:00
66 changed files with 1574 additions and 365 deletions
+9 -4
View File
@@ -11,10 +11,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: 'Docker Tags'
id: tags
uses: cssnr/docker-tags-action@v2
with:
images: 'g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe'
- name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: g.codrs.ru
registry: g.lair.moe
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -26,8 +32,7 @@ jobs:
with:
context: .
push: ${{ github.event_name == 'push' }}
tags: |
g.codrs.ru/${{ vars.DOCKER_USERNAME }}/codrs.ru:latest
g.codrs.ru/${{ vars.DOCKER_USERNAME }}/codrs.ru:${{ github.sha }}
tags: g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe:latest
labels: ${{ steps.tags.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+2 -2
View File
@@ -5,5 +5,5 @@ __pycache__/
*$py.class
.python-version
static/style/*.css
static/style/*.css.map
*.css
*.css.map
+35 -12
View File
@@ -1,23 +1,46 @@
FROM node:18-alpine as sass
FROM node:18-alpine AS sass-builder
RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass
RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass@latest --omit=dev --no-fund --no-audit
WORKDIR /build
COPY ./static/style ./style
RUN sass ./style:./style \
--no-source-map \
--style=compressed
COPY ./blueprints ./blueprints
RUN sass ./blueprints:./blueprints \
--no-source-map \
--style=compressed \
--quiet
FROM python:3.11-slim
RUN apt-get update && \
apt-get install --no-install-recommends -y \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
COPY --from=sass /build/style/ ./static/style/
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
COPY . .
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--workers", "4"]
COPY --from=sass-builder /build/blueprints/ ./blueprints/
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app
USER appuser
ENV FLASK_ENV=production \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
CMD ["gunicorn", "app:app", \
"-b", "0.0.0.0:80", \
"--workers", "4", \
"--worker-class", "sync", \
"--worker-tmp-dir", "/dev/shm", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info"]
+19 -65
View File
@@ -1,73 +1,27 @@
from os import system as console
from configparser import ConfigParser
from flask import (
Flask,
g,
request,
render_template,
)
from modules import locale
from blueprints.root import bp as root_bp
from blueprints.risdeveau import bp as rdv_bp
import blueprints.root.modules.style
import blueprints.risdeveau.modules.style
from flask import Flask
translations_cache = {}
app = Flask(__name__, static_folder=None, subdomain_matching=True)
def load_translations(lang):
if lang not in translations_cache:
translations_cache[lang] = {}
app.before_request(locale.before_request)
app.context_processor(locale.inject_translations)
try:
config = ConfigParser()
config.read(f'translations/{lang}.ini')
for section in config.sections():
translations_cache[lang][section] = dict(config.items(section))
except:
pass
return translations_cache[lang]
def get_locale():
return request.accept_languages.best_match(('en', 'ru', 'de', 'fr', 'ja'), 'en')
app = Flask(__name__)
@app.before_request
def before_request():
g.locale = get_locale()
g.translations = load_translations(g.locale)
@app.context_processor
def inject_translations():
def translate(text, **kwargs):
if ":" in text:
section, key = text.split(":", 1)
else:
section, key = "common", text
template = g.translations \
.get(section, {}) \
.get(key, f"${section}: {key}$")
try:
return template.format(**kwargs)
except:
return template
return {'_': translate}
app.register_blueprint(root_bp)
app.register_blueprint(rdv_bp)
if app.debug:
console("sass static/style/main.scss static/style/main.css")
blueprints.root.modules.style.compile_styles()
blueprints.risdeveau.modules.style.compile_styles()
@app.route("/")
def index():
return render_template('index.html')
@app.route("/host")
def host():
return render_template('host.html')
@app.route("/us")
def us():
return render_template('us.html')
app.config['SERVER_NAME'] = "localhost:5000"
else:
app.config['SERVER_NAME'] = "lair.moe"
+103
View File
@@ -0,0 +1,103 @@
import os
from datetime import datetime, timedelta
from pathlib import Path
from time import time
from flask import (
Blueprint,
abort,
make_response,
render_template,
request,
send_file,
)
import magic
from htmlmin import minify
from musicbrainzngs import get_image_front
from .modules.api.lb import data as lb_data
from .modules.api.steam import data as steam_data
def tmsmp(sec: int) -> str:
if sec == 0:
return 0
elif sec < 60:
return f"{sec} s"
elif sec < 60*60:
minutes = round(sec / 60, 1)
return f"{minutes:.0f} m" if minutes.is_integer() else f"{minutes:.1f} m"
elif sec < 60*60*24:
hours = round(sec / 3600, 1)
return f"{hours:.0f} h" if hours.is_integer() else f"{hours:.1f} h"
else:
days = round(sec / 86400, 1)
return f"{days:.0f} d" if days.is_integer() else f"{days:.1f} d"
def rtmsmp(unix: int) -> str:
return tmsmp(int(time() - unix))
bp = Blueprint(
"risdeveau",
__name__,
subdomain="risdeveau",
template_folder="..",
static_folder=None
)
def render_tmpl(filename: str, **kwargs) -> str:
template_path = os.path.join("risdeveau/templates", filename)
return minify(
render_template(template_path, **kwargs),
remove_empty_space=True,
remove_all_empty_space=True
)
@bp.route("/static/<path:filename>")
def static(filename: str):
static_folders = ("static", "../root/static")
for dir in static_folders:
if os.path.exists(path := os.path.join("blueprints/risdeveau", dir, filename)):
return send_file(path)
return abort(404)
@bp.route("/asset/mb/<mbid>")
def mb_cover(mbid):
r = make_response(image := get_image_front(mbid, "250"))
r.headers['Content-Type'] = magic.from_buffer(image[:2048], mime=True)
r.headers['Cache-Control'] = 'public, max-age=86400'
r.headers['Expires'] = (datetime.now() + timedelta(days=1)) \
.strftime('%a, %d %b %Y %H:%M:%S GMT')
return r
args = {
"lb": lb_data,
"steam": steam_data,
"tmsmp": tmsmp,
"rtmsmp": rtmsmp
}
@bp.route("/")
def index():
return render_tmpl('index.html', **args)
@bp.route("/m/<module>")
def module(module):
if modified_since := request.headers.get('if-modified-since'):
modified_since = int(modified_since)
none_match = request.headers.get('if-none-match')
if any((modified_since, none_match)):
match module:
case "listenbrainz":
if modified_since >= int(lb_data['last_updated']):
return '', 304
if none_match == lb_data['etag']:
return '', 304
case "steam":
if modified_since >= int(steam_data['last_updated']):
return '', 304
if none_match == steam_data['etag']:
return '', 304
return render_tmpl(f'{module}.htm', **args)
+123
View File
@@ -0,0 +1,123 @@
import atexit
import re
from dataclasses import dataclass
from hashlib import md5
from json import dumps
from time import time
from urllib.parse import parse_qs, urlparse
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
import requests
from flask import Flask, jsonify
@dataclass
class Cache:
data = {}
last_updated = time()
status = None
data = {
"caches": {
"now": Cache(),
"listens": Cache()
},
"last_updated": time(),
"etag": ""
}
def yt_cover(youtube_url):
parsed_url = urlparse(youtube_url)
if parsed_url.netloc in ("youtube.com", "music.youtube.com"):
query_params = parse_qs(parsed_url.query)
video_id = query_params.get('v', [None])[0]
elif parsed_url.netloc == 'youtu.be':
video_id = parsed_url.path[1:]
if not video_id:
return
return f"https://img.youtube.com/vi/{video_id}/2.jpg"
def parse_listens(json: dict) -> dict:
cover_replacing = {
"1e699948-c7c8-4bb2-9f8b-62e14b882a5d": "ca464c1d-5848-45bb-b92d-b1e4b00f9d65",
"0d516a93-061e-4a27-9cf7-f36e3a96f888": "5cc0c0c7-22f9-4a4b-a24c-f1a6732f813b",
"92ea5cc8-80e0-4da0-a10b-1bc2f8e8781e": "e8f3e14a-4794-4bab-b403-d562cdad4c2f",
}
new_json = {
"count": json["count"],
"listens": []
}
for track in json["listens"]:
listened_at = track.get("listened_at", 0)
track = track["track_metadata"]
new_track = {
"artist_name": track["artist_name"],
"track_name": track["track_name"],
"listened_at": listened_at
}
if mb := track.get("mbid_mapping"):
new_track["id"] = \
cover_replacing.get(mb["release_mbid"],
mb.get("caa_release_mbid",
mb["release_mbid"]
))
new_track["artist_name"] = mb["artists"][0]["artist_credit_name"]
new_track["track_name"] = mb["recording_name"]
elif info := track.get("additional_info"):
if info \
.get("music_service_name", "") \
.lower() in ("youtube", "youtube music"):
if cover := yt_cover(track["additional_info"]["origin_url"]):
new_track["cover_url"] = cover
if "cover_url" not in new_track.keys() and "id" in new_track.keys():
new_track["cover_url"] = "/asset/mb/" + new_track["id"]
new_json["listens"].append(new_track)
return new_json
def api_request(url: str, cache: Cache):
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
json = parse_listens(response.json().get("payload"))
cache.status = 'success'
if cache.data != json:
cache.data = json
cache.last_updated = time()
data['last_updated'] = time()
data['etag'] = md5(''.join(
( dumps(data['caches'][x].data) for x in data['caches'] )
).encode()).hexdigest()
else:
cache.status = f'error: {response.status_code}'
except Exception as e:
cache.status = f'error: {str(e)}'
scheduler = BackgroundScheduler()
scheduler.add_job(
func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/listens?count=5", data['caches']['listens']),
trigger=IntervalTrigger(minutes=1),
id='risdeveau.listenbrainz.listens',
replace_existing=True
)
scheduler.add_job(
func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/playing-now", data['caches']['now']),
trigger=IntervalTrigger(seconds=15),
id='risdeveau.listenbrainz.playing-now',
replace_existing=True
)
scheduler.start()
atexit.register(lambda: scheduler.shutdown())
+95
View File
@@ -0,0 +1,95 @@
import atexit
import re
from dataclasses import dataclass
from hashlib import md5
from json import dumps
from os import environ
from time import time
from urllib.parse import parse_qs, urlparse
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
import requests
from flask import Flask
TOKEN = environ.get("STEAM_TOKEN")
MY_ID = 76561198826355942
@dataclass
class Cache:
data = {}
last_updated = time()
status = None
data = {
"caches": {
"recent": Cache(),
"owned": Cache()
},
"last_updated": time(),
"etag": ""
}
def modify_game_list(json: dict) -> dict:
if 'games' in json.keys():
apps = (3301060, 404790, 1281930, 1920960, 1325960, 431960)
new_games = {}
for i, g in enumerate(json['games']):
if g['appid'] not in apps:
json['games'][i]['h_cover'] = f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{g['appid']}/header.jpg"
json['games'][i]['v_cover'] = f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{g['appid']}/library_600x900.jpg"
new_games[g['appid']] = json['games'][i]
json['games'] = new_games
return json
def steam_request(interface: str, method: str, v: int = 1, **kwargs) -> requests.Response:
return requests.get(
f"https://api.steampowered.com/{interface}/{method}/v{v:04}/",
params=dict({"key": TOKEN}, **kwargs),
timeout=10
)
def api_request(cache, *args, **kwargs):
try:
response = steam_request(*args, **kwargs)
if response.status_code == 200:
json = modify_game_list(response.json().get("response"))
cache.status = 'success'
if cache.data != json:
cache.data = json
cache.last_updated = time()
data['last_updated'] = time()
data['etag'] = md5(''.join(
( dumps(data['caches'][x].data) for x in data['caches'] )
).encode()).hexdigest()
else:
cache.status = f'error: {response.status_code}'
print("x")
except Exception as e:
cache.status = f'error: {str(e)}'
if TOKEN:
scheduler = BackgroundScheduler()
scheduler.add_job(
func=lambda: api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942),
trigger=IntervalTrigger(minutes=15),
id='risdeveau.steam.recent',
replace_existing=True
)
scheduler.add_job(
func=lambda: api_request(data['caches']['owned'], "IPlayerService", "GetOwnedGames", steamid=76561198826355942, include_appinfo=1, include_played_free_games=1),
trigger=IntervalTrigger(minutes=60),
id='risdeveau.steam.owned',
replace_existing=True
)
scheduler.start()
api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942)
api_request(data['caches']['owned'], "IPlayerService", "GetOwnedGames", steamid=76561198826355942, include_appinfo=1, include_played_free_games=1)
atexit.register(lambda: scheduler.shutdown())
else:
print("STEAM_TOKEN is not defined")
+9
View File
@@ -0,0 +1,9 @@
from os import system as console
from os.path import join
def compile_styles():
dir = "blueprints/risdeveau/static/style"
files = ("risdeveau",)
for file in files:
console(f"sass {join(dir, file+'.scss')} {join(dir, file+'.css')}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+136
View File
@@ -0,0 +1,136 @@
document.addEventListener('alpine:init', () => {
Alpine.data('rtime', (unixTimestamp) => ({
targetDate: new Date(unixTimestamp * 1000),
timeString: '',
timer: null,
interval: 1000,
colorClasses: {
green: 't-green',
yellow: 't-yellow',
orange: 't-orange',
red: 't-red'
},
currentColor: 'green',
get textColorClass() {
return this.colorClasses[this.currentColor];
},
init() {
this.updateTime();
this.timer = setInterval(() => this.updateTime(), this.interval);
this.$el.addEventListener('alpine:removing', () => {
if (this.interval) clearInterval(this.interval);
});
},
updateTime() {
const now = new Date();
const diffInSeconds = Math.floor((now - this.targetDate) / 1000);
const diffInMinutes = Math.floor(diffInSeconds / 60);
const diffInHours = Math.floor(diffInSeconds / 3600);
const diffInDays = Math.floor(diffInSeconds / 86400);
let newInterval = this.interval;
if (diffInSeconds < 60) {
this.timeString = 'a moment ago';
} else if (diffInMinutes < 60) {
this.timeString = `${diffInMinutes} m ago`;
newInterval = 10000;
} else if (diffInHours < 24) {
this.timeString = `${diffInHours} h ago`;
newInterval = 60000;
} else {
this.timeString = `${diffInDays} d ago`;
clearInterval(this.timer);
}
if (diffInMinutes <= 15) {
this.currentColor = 'green';
} else if (diffInHours < 1) {
this.currentColor = 'yellow';
} else if (diffInDays < 1) {
this.currentColor = 'orange';
} else {
this.currentColor = 'red';
}
if (this.interval != newInterval) {
clearInterval(this.timer)
this.timer = setInterval(() => this.updateTime(), newInterval);
}
},
}));
Alpine.data('steam_rtime', (unixTimestamp) => ({
targetDate: new Date(unixTimestamp * 1000),
timeString: '',
timer: null,
interval: 1000,
colorClasses: {
green: 't-green',
yellow: 't-yellow',
orange: 't-orange',
red: 't-red'
},
currentColor: 'green',
get textColorClass() {
return this.colorClasses[this.currentColor];
},
init() {
this.updateTime();
this.timer = setInterval(() => this.updateTime(), this.interval);
this.$el.addEventListener('alpine:removing', () => {
if (this.interval) clearInterval(this.interval);
});
},
updateTime() {
const now = new Date();
const diffInMinutes = Math.floor((now - this.targetDate) / 60000);
const diffInHours = Math.floor(diffInMinutes / 60);
const diffInDays = Math.floor(diffInHours / 24);
const diffInMonths = Math.floor(diffInDays / 30);
let newInterval = this.interval;
if (diffInMinutes < 60) {
this.timeString = `${diffInMinutes} m ago`;
newInterval = 10000;
} else if (diffInHours < 24) {
this.timeString = `${diffInHours} h ago`;
newInterval = 60000;
} else if (diffInDays < 30) {
this.timeString = `${diffInDays} d ago`;
clearInterval(this.timer);
} else {
this.timeString = `${diffInMonths} mth ago`;
clearInterval(this.timer);
}
if (diffInHours < 12) {
this.currentColor = 'green';
} else if (diffInDays < 1) {
this.currentColor = 'yellow';
} else if (diffInMonths < 6) {
this.currentColor = 'orange'
} else {
this.currentColor = 'red';
}
if (this.interval != newInterval) {
clearInterval(this.timer)
this.timer = setInterval(() => this.updateTime(), newInterval);
}
},
}));
});
@@ -0,0 +1,120 @@
@use "sass:color";
@use "../../../root/static/style/catppuccin" as theme;
h3 {
margin-block-end: 0;
}
.qr {
img { width: 100% }
p { text-align: center; }
&.blocks {
flex-wrap: nowrap;
overflow-x: auto;
width: calc(100vw - 1rem);
max-width: 45rem;
scroll: {
behavior: smooth;
snap-type: x mandatory;
}
&::-webkit-scrollbar { display: none; }
&:hover .block.qr:not(:hover) {
filter: blur(5px);
transition: all 0.3s ease;
}
}
&.block {
flex: 0 0 calc(100vw - 2rem);
scroll-snap-align: start;
max-width: 13.666rem;
}
}
.track {
display: flex;
&.active {
box-shadow: theme.$green 0 0 5px 0;
}
img {
width: 5rem;
height: 5rem;
object-fit: cover;
border-radius: .5rem;
}
}
.steam {
.block {
&:not(.popup) {
display: flex;
position: relative;
z-index: 1;
}
&.popup {
margin-top: -.5rem;
padding-top: 1rem;
background-color: theme.$mantle;
transition: all ease-out 300ms;
&.enter {}
&.off {
margin-bottom: -.5rem;
padding: 0 .5rem;
opacity: 0;
transform: translateY(-100%);
}
}
img {
height: 7rem;
margin-right: .5rem;
}
p {
margin: .5rem 0;
}
}
}
table, tbody {
vertical-align: baseline;
border-collapse: collapse;
tr {
border-radius: 10px;
&:hover {
background-color: color.change(theme.$surface1, $alpha:75%);
}
th {
text-align: unset;
}
th + td, td + td {
padding-left: 2rem;
}
}
}
#pie:hover {
animation: rotateY-animation 2s linear infinite;
transform-style: preserve-3d;
@keyframes rotateY-animation {
to {
transform: rotateY(0deg);
}
from {
transform: rotateY(360deg);
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<div>
<div class="88-31">
<a href="https://chest.lair.moe" class="disabled">
<img src="/static/img/88x31/gf.png"/>
</a>
<a href="https://preview.about.akarpov.ru" id="pie">
<img src="/static/img/88x31/withpie.gif"/>
</a>
</div>
<div class="88-31">
<a href="https://g.lair.moe/Sweetbread/nixos-config">
<img src="/static/img/88x31/nixos.webp"/>
</a>
<img src="/static/img/88x31/teto.webp"/>
</div>
</div>
@@ -0,0 +1,48 @@
<div class="block">
<h3>Development</h3>
<div class="blocks badges">
<a class="block" href="//g.lair.moe/Sweetbread">
<img class="icon" src="/static/icon/service/gitea.webp" />
Gitea
</a>
<a class="block" href="https://github.com/VerySweetBread">
<img class="icon" src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png" />
GitHub
</a>
<a class="block" href="https://git.kolibrios.org/Sweetbread">
<img class="icon" src="https://git.kolibrios.org/assets/img/logo.svg" />
KolibriOS Git
</a>
</div>
<h3>Contacts</h3>
<div class="blocks badges">
<a class="block" href="https://matrix.to/#/@risdeveau:codrs.ru">
<img class="icon" src="https://matrix.org/assets/favicon.ico" />
Matrix
</a>
<a class="block" href="//b.lair.moe/@risdeveau">
<img class="icon" src="/static/icon/service/sharkey.webp" />
Fediverse
</a>
<a class="block" href="https://discord.com/users/459823895256498186">
<img class="icon" src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d80db9971f10a9757c99_Symbol.svg" />
Discord
</a>
<a class="block" href="mailto:risdeveau@lair.moe">
Mail
</a>
</div>
<h3>Game accounts</h3>
<div class="blocks badges">
<a class="block" href="https://steamcommunity.com/id/risdeveau">
<img class="icon" src="https://store.steampowered.com/favicon.ico" />
Steam
</a>
<a class="block" href="https://gamebanana.com/members/3899828">
<img class="icon" src="https://images.gamebanana.com/static/img/favicon/favicon.ico" />
GameBanana
</a>
</div>
</div>
+19
View File
@@ -0,0 +1,19 @@
<div>
<h3>Wallets</h3>
<div class="blocks qr">
<div class="block qr">
<p>POL, BNB</p>
<img src="/static/img/wallets/evm.webp">
</div>
<div class="block qr">
<p>TON</p>
<img src="/static/img/wallets/ton.webp">
</div>
<div class="block qr">
<p>XMR</p>
<img src="/static/img/wallets/xmr.webp">
</div>
</div>
</div>
+57
View File
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>Sweet Bread</title>
<link rel="stylesheet" href="/static/style/tw.css">
<link rel="stylesheet" href="/static/style/main.css">
<link rel="stylesheet" href="/static/style/risdeveau.css">
<link rel="icon" type="image/webp" href="/static/icon/us/risdeveau.webp" />
<script src="/static/script/rtime.js"></script>
<script
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
></script>
<script
src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"
></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta
name="htmx-config"
content='{
"responseHandling":[
{"code":"204", "swap": false},
{"code":"304", "swap": false},
{"code":"[23]..", "swap": true},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": false, "error":true},
{"code":"...", "swap": true}
]
}'
/>
</head>
<body>
<header>
<a href="{{ url_for('root.index') }}">Lair</a>
</header>
<main>
{% for m in (
'info',
'contacts',
'listenbrainz',
'steam',
'donate',
'88x31'
) %}
{% include 'risdeveau/templates/%s.htm' % m %}
{% endfor %}
</main>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<div class="block">
<table>
<tr>
<th>DoB</th>
<td>2005-01-13</td>
</tr>
<tr>
<th>Languages</th>
<td>
<table>
<tr>
<td>Russian</td>
<td>Native</td>
</tr>
<tr>
<td>English</td>
<td>B2</td>
</tr>
<tr>
<td>French</td>
<td>A1?</td>
</tr>
<tr>
<td>German</td>
<td>A2?</td>
</tr>
<tr>
<td>Japanese</td>
<td>Beginner</td>
</tr>
</table>
</td>
</tr>
<tr>
<th>Student</th>
<td>
<table>
<tr>
<td>Programmer</td>
<td>2/4yr.</td>
</tr>
<tr>
<td>Translator</td>
<td>2/3yr.</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
@@ -0,0 +1,39 @@
{% macro track_block(track, is_active=false) %}
<div class="block track{% if is_active %} active{% endif %}">
{% if track.cover_url %}
<img src="{{ track.cover_url }}"/>
{% endif %}
<div>
<p><b>{{ track.artist_name }}</b></p>
<p>{{ track.track_name }}</p>
{% if not is_active %}
<p
x-data="rtime({{ track.listened_at }})"
x-text="`Listened ${timeString}`"
:class="textColorClass"
></p>
{% endif %}
</div>
</div>
{% endmacro %}
<div
class="block"
hx-get="/m/listenbrainz"
hx-trigger="every 15s"
hx-swap="outerHTML"
hx-headers='{
"If-Modified-Since": {{ lb.last_updated | int }},
"If-None-Match": "{{ lb.etag }}"
}'
>
<h2><a href="https://listenbrainz.org/user/risdeveau/">Listenbrainz</a></h2>
{% if lb.caches.now.data and lb.caches.now.data.listens.0 %}
{{ track_block(lb.caches.now.data.listens.0, is_active=true) }}
{% endif %}
{% if lb.caches.listens.data and lb.caches.listens.data.listens %}
{% for track in lb.caches.listens.data.listens %}
{{ track_block(track) }}
{% endfor %}
{% endif %}
</div>
+125
View File
@@ -0,0 +1,125 @@
{% if request.headers.get('hx-request') != "true" %}
<div x-data='{ "current": null, "total_mode": "T" }' class="mt-1">
{% endif %}
<div
class="block steam"
hx-get="/m/steam"
hx-trigger="every 1m"
hx-swap="outerHTML"
hx-headers='{
"If-Modified-Since": {{ steam.last_updated | int }},
"If-None-Match": "{{ steam.etag }}"
}'
>
<h2><a href="https://steamcommunity.com/id/risdeveau">Steam</a></h2>
{{ steam.caches.recent.status }}
{{ steam.caches.owned.status }}
{% if steam.caches.recent.data.games %}
<h3>Recently played:</h3>
{% for g in steam.caches.recent.data.games.values() %}
<div href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
<picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}">
</picture>
<div>
<strong>{{ g.name }}</strong>
<p>Played last 2 weeks: {{ tmsmp(g.playtime_2weeks*60) }}
<div>
<p
x-data='{ playtime: { L: "{{ tmsmp(g.playtime_linux_forever*60) }}", W: "{{ tmsmp(g.playtime_windows_forever*60) }}", T: "{{ tmsmp(g.playtime_forever*60) }}" }}'
x-text="`Total played: ${playtime[total_mode]}`"
>
</p>
<div>
<button @click="total_mode = 'L'" :class="total_mode == 'L' && 't-green'">L</button>
<button @click="total_mode = 'W'" :class="total_mode == 'W' && 't-green'">W</button>
<button @click="total_mode = 'T'" :class="total_mode == 'T' && 't-green'">T</button>
</div>
</div>
{% if steam.caches.owned.data.games %}
{% if steam.caches.owned.data.games[g.appid] %}
<p
x-data="steam_rtime({{ steam.caches.owned.data.games[g.appid].rtime_last_played }})"
x-text="`Last played: ${timeString}`"
:class="textColorClass"
></p>
{% else %}
<p class="t-red">Last played: Unknown</p>
{% endif %}
{% endif %}
</div>
</div>
{% endfor %}
<p
x-data="rtime({{steam.caches.recent.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %}
{% if steam.caches.owned.data.games %}
<h3>Top played games:</h3>
{% set owned_games = steam.caches.owned.data.games.values() | sort(attribute="playtime_forever", reverse=true) %}
{% for g in owned_games[:5] %}
<div
@click='current == {{ g.appid }} ? current = null : current = {{ g.appid }}'
href="https://store.steampowered.com/app/{{ g.appid }}"
class="block"
>
<picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}">
</picture>
<div>
<strong>{{ g.name }}</strong>
<p>
Total played:
{{ tmsmp(g.playtime_linux_forever*60) }} (<abbr title="On Linux">L</abbr>) +
{{ tmsmp(g.playtime_windows_forever*60) }} (<abbr title="On Windows">W</abbr>) =
{{ tmsmp(g.playtime_forever*60) }} (<abbr title="Total">T</abbr>)
</p>
{% if g.rtime_last_played != 0 %}
<p
x-data="steam_rtime({{ g.rtime_last_played }})"
x-text="`Last played: ${timeString}`"
:class="textColorClass"
></p>
{% endif %}
</div>
</div>
<div
class="block popup"
x-show="current == {{ g.appid }}"
x-transition:enter-start="off"
x-transition:leave-end="off"
>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
</div>
{% endfor %}
<p
x-data="rtime({{steam.caches.owned.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %}
</div>
{% if request.headers.get('hx-request') != "true" %}
</div>
{% endif %}
+28
View File
@@ -0,0 +1,28 @@
from htmlmin import minify
from flask import Blueprint, render_template, request, jsonify
bp = Blueprint(
"root",
__name__,
template_folder="templates",
static_folder="static"
)
def render_tmpl(filename: str) -> str:
return minify(
render_template(filename),
remove_empty_space=True
)
@bp.route("/")
def index():
return render_tmpl('index.html')
@bp.route("/host")
def host():
return render_tmpl('host.html')
@bp.route("/us")
def us():
return render_tmpl('us.html')
+9
View File
@@ -0,0 +1,9 @@
from os import system as console
from os.path import join
def compile_styles():
dir = "blueprints/root/static/style"
files = ("main", "tw")
for file in files:
console(f"sass {join(dir, file+'.scss')} {join(dir, file+'.css')}")
Binary file not shown.
@@ -0,0 +1,16 @@
M+ FONTS Copyright (C) 2002-2013 M+ FONTS PROJECT
-
LICENSE_E
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with
or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
http://mplus-fonts.sourceforge.jp/mplus-outline-fonts/
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
Monocraft: https://idreesinc.itch.io/monocraft
Pixeloid: https://ggbot.itch.io/pixeloid-font
PixelM+: https://itouhiro.hatenablog.com/entry/20130602/font
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

@@ -0,0 +1,25 @@
// Palette: Catppuccin Mocha
// https://catppuccin.com/palette/
$base: #1e1e2e;
$text: #cdd6f4;
$mantle: #181825;
$crust: #11111b;
$overlay0: #6c7086;
$overlay1: #7f849c;
$overlay2: #9399b2;
$surface0: #313244;
$surface1: #45475a;
$surface2: #585b70;
$subtext0: #a6adc8;
$subtext1: #bac2de;
$red: #f38ba8;
$yellow: #f9e2af;
$green: #a6e3a1;
$peach: #fab387;
$blue: #89b4fa;
$mauve: #8839ef;
+276
View File
@@ -0,0 +1,276 @@
@use "sass:color";
@use "catppuccin" as theme;
html {
height: 100%;
}
body {
display: flex;
flex-direction: column;
background-color: theme.$base;
font-family: Pixeloid, PixelMPlus;
color: theme.$text;
width: 100%;
height: 100%;
margin: 0;
}
main {
max-width: 45rem;
margin-inline: auto;
padding: 0 .5rem;
flex: 1 0 auto;
}
h1 {
text-align: center;
}
a {
color: unset;
text: {
decoration: underline {color: theme.$blue};
underline-offset: 1px;
}
transition: 0.3s ease;
&:hover {
text-underline-offset: 3px;
}
&:active {
transition: none !important;
display: inline-block;
transform: scale(.98) !important;
background-color: theme.$mantle !important;
}
&.block {
text-decoration: none;
&:hover {
transform: scale(1.02) translateY(-.25rem);
background-color: theme.$surface1;
}
}
}
p {
margin: .5rem;
}
ul {
margin-top: .25rem;
}
header {
display: flex;
justify-content: space-between;
background-color: theme.$mantle;
padding: .5rem;
font-size: larger;
.header-links * + * {
padding-left: 1ch;
}
}
footer {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
background-color: theme.$mantle;
margin-top: 2rem;
padding: 1rem;
column-gap: 4ch;
}
.mono {
font-family: Monocraft, monospace;
background-color: theme.$mantle;
border-radius: 2px;
padding: 0 .25rem;
color: theme.$subtext0;
overflow-wrap: anywhere;
&:hover {
transition: .3s ease;
background-color: theme.$crust;
}
}
.block {
display: block;
background-color: theme.$surface0;
border-radius: .5rem;
padding: .5rem;
h2 {
margin: -.5rem -.5rem 1rem;
padding: .5rem;
text-align: center;
}
.block {
background-color: theme.$surface1;
}
& + & {
margin-top: .5rem;
}
&.red {
background-color: color.mix(theme.$surface0, theme.$red, 60%);
}
&.orange {
background-color: color.mix(theme.$surface0, theme.$peach, 60%);
}
&.green {
background-color: color.mix(theme.$surface0, theme.$green, 60%);
&:hover { background-color: color.mix(theme.$surface1, theme.$green, 60%); }
&:active { background-color: color.mix(theme.$mantle, theme.$green, 60%) !important; }
}
& .header {
display: flex;
align-items: center;
font-size: x-large;
.icon {
margin-right: .5ch;
}
}
}
.blocks {
display: flex;
flex-wrap: wrap;
margin-top: .5rem;
gap: .5rem;
& + &,
& + .block,
.block + & {
margin-top: .5rem;
}
.block {
margin-top: 0;
}
}
.badges {
.block {
flex: 1;
text-wrap-mode: nowrap;
text-align: -webkit-center;
&:hover {
flex: 1.5;
transform: none;
}
}
}
.icon {
width: 1.5em;
vertical-align: middle;
border-radius: .2em;
}
.webring {
margin-top: 1rem;
display: flex;
justify-content: center;
a {
padding: .5rem 1rem;
margin: .1rem !important;
border-radius: 0;
}
}
.\38 8-31 {
margin-top: 2rem;
display: flex;
justify-content: center;
& + & {
margin-top: 0;
}
a, img {
width: 88px;
height: 31px;
}
img {
transition-timing-function: ease-out;
transition-duration: .2s;
&:hover {
transform: scale(1.5);
}
}
}
.disabled {
opacity: 50%;
cursor: not-allowed;
a {
pointer-events: none;
}
}
::-webkit-scrollbar {
width: .5rem;
&-button {
display: none;
}
&-track {
background-color: theme.$base;
}
&-thumb {
background-color: theme.$overlay0;
border-radius: .25rem;
&:hover {
background-color: theme.$overlay1;
}
}
}
@font-face {
font-family: Monocraft;
src: url("/static/font/Monocraft.ttc");
}
@font-face {
font-family: Pixeloid;
src:
url("/static/font/Pixeloid/woff/Sans.woff2") format("woff2"),
url("/static/font/Pixeloid/otf/Sans.otf") format("opentype");
}
@font-face {
font-family: Pixeloid;
src:
url("/static/font/Pixeloid/woff/Sans-Bold.woff2") format("woff2"),
url("/static/font/Pixeloid/otf/Sans-Bold.otf") format("opentype");
font-weight: bold;
}
@font-face {
font-family: PixelMPlus;
src: url("/static/font/PixelMPlus/Regular.ttf");
}
@font-face {
font-family: PixelMPlus;
src: url("/static/font/PixelMPlus/Bold.ttf");
font-weight: bold;
}
+14
View File
@@ -0,0 +1,14 @@
@use "catppuccin" as theme;
.m {
&t {
&-1 { margin-top: .5rem; }
}
}
.t {
&-red { color: theme.$red; }
&-orange { color: theme.$peach; }
&-yellow { color: theme.$yellow; }
&-green { color: theme.$green; }
}
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>Lair</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/webp" href="/static/icon/lair.webp" />
<script src="/static/script/copy-mono.js"> </script>
<script
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mock-email" content="admin@example.com">
<!-- og meta -->
<meta property="og:type" value="website" />
<meta property="og:url" value="https://lair.moe" />
<meta property="og:title" value="Lair.moe" />
<meta property="og:image" value="https://lair.moe/static/icon/lair.webp" />
<meta property="og:description" value="{{ _("description") }}" />
</head>
<body>
{% include 'header.tmpl' %}
<h1>{% block title %}{% endblock %}</h1>
<main>
{% block content %}{% endblock %}
</main>
{% include 'footer.tmpl' %}
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<footer>
<div>lair.moe &#127279; 2025 - 2026</div>
<div><a href="https://g.lair.moe/Sweetbread/lair.moe">{{ _('site source') }}</a></div>
<div>{{ _('contact us') }}: <a href="mailto:admin@lair.moe">admin@lair.moe</a></div>
</footer>
@@ -1,8 +1,14 @@
<header>
{%- if request.path != url_for('.index') %}
<a href="{{ url_for('.index') }}">Lair</a>
{%- else %}
<div></div>
{%- endif %}
<div class="header-links">
{%- for (l, t) in (
('us', _('about us')),
('host', _('about host'))
('.us', _('about us')),
('.host', _('about host'))
) %}
{%- if url_for(l) == request.path %}
<strong>{{ t }}</strong>
@@ -11,8 +17,4 @@
{%- endif %}
{%- endfor %}
</div>
{%- if request.path != url_for('index') %}
<a href="{{ url_for('index') }}">Coders Squad</a>
{%- endif %}
</header>
@@ -3,7 +3,7 @@
{% block title %}{{ _('about host') }}{% endblock %}
{% block content %}
<a href="https://play2go.cloud/" target="_blank" class="block">
<a href="https://play2go.cloud/?ref_id=4baFoOIp5QE" target="_blank" class="block">
<strong>{{ _("host:hoster") }}</strong>: play2go
<p>{{ _('host:hoster_descr') }}</p>
</a>
@@ -11,11 +11,11 @@
<div class="block">
<strong>{{ _("host:specifications") }}</strong>:
<ul>
<li>CPU: Ryzen i9</li>
<li>RAM: 16 GiB</li>
<li>SSD: 120 GiB</li>
<li>ETH: 500GiB/s</li>
<li>Loc: Deutchland, Frankfurt am Mein</li>
<li>CPU: Ryzen 9@3.4GHz (4 cores)</li>
<li>RAM: 8 GB</li>
<li>SSD: 150 GB</li>
<li>ETH: 500Mb/s</li>
<li>Loc: Deutschland, Frankfurt am Main</li>
</ul>
</div>
{% endblock %}
+52
View File
@@ -0,0 +1,52 @@
{% extends 'base.tmpl' %}
{% block title %}
<img src="/static/icon/lair.webp" class="icon" />
Lair
{% endblock %}
{% block content %}
<a href="https://b.lair.moe" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/sharkey.webp" class="icon"/>
<strong>Sharkey</strong>
</div>
<p>{{ _('index.descr:sharkey') }}</p>
</a>
<a href="https://g.lair.moe" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/gitea.webp" class="icon"/>
<strong>Gitea</strong>
</div>
<p>{{ _('index.descr:gitea') }}</p>
</a>
<div class="block">
<p><a href="https://m.codrs.ru" target="_blank"><strong>Matrix</strong></a> &mdash; {{ _('index.descr:matrix') }}</p>
<p><a href="https://c.lair.moe" target="_blank"><strong>Copyparty</strong></a> &mdash; {{ _('index.descr:copyparty') }}</p>
<p><a href="https://tools.lair.moe" target="_blank"><strong>IT-tools</strong></a> &mdash; {{ _('index.descr:tools') }}</p>
<p><a href="https://vert.lair.moe" target="_blank"><strong>Vert</strong></a> &mdash; {{ _('index.descr:vert') }}</p>
</div>
<div class="block">
<strong>{{ _('index:altfronts') }}</strong>
<p><a href="https://s.lair.moe" target="_blank"><strong>4get</strong></a> &mdash; {{ _('index.descr:4get') }}</p>
<p><a href="https://tl.lair.moe" target="_blank"><strong>TransLite</strong></a> &mdash; {{ _('index.descr:tl') }}</p>
<p><a href="https://lyr.lair.moe" target="_blank"><strong>Intellectual</strong></a> &mdash; {{ _('index.descr:lyr') }}</p>
</div>
<div class="block">
<div>
<strong>DNS</strong>:
<ul>
<li><span class="mono">64.188.64.176</span></li>
<li>DoT: <span class="mono">lair.moe:853</span></li>
<li>DoH: <span class="mono">dns.lair.moe</span></li>
</ul>
</div>
<p>
<strong>Yggdrasil</strong>:
<span class="mono">200:ee1:bad2:1732:4b91:c3e3:2f08:29b3</span>
</p>
</div>
{% endblock %}
@@ -3,7 +3,7 @@
{% block title %}О нас{% endblock %}
{% block content %}
<a href="#" class="block green">
<a href="{{ url_for('risdeveau.index') }}" class="block green">
<div class="header">
<img src="/static/icon/us/risdeveau.webp" class="icon"/>
Sweetbread
@@ -11,15 +11,7 @@
Главный админ, занимается почти всеми сервисами. Создал этот сайт
</a>
<div class="block red">
<div class="header">
<img src="/static/icon/us/zxcqirara.webp" class="icon"/>
zxcqirara
</div>
Второй админ, занимается майном и VPN
</div>
<div class="block orange">
<div class="block orange disabled">
<div class="header">
<img src="/static/icon/us/chest.webp" class="icon"/>
Chest
+5 -1
View File
@@ -7,7 +7,7 @@ about host = Über Server
[index]
bottom_text = Außerdem bieten wir {glitchtip}, {baikal} und {freshrss} für Mitglieder unserer Gruppe an!
altfronts = Altfronts
[index.descr]
sharkey = Föderierter Microblogging-Dienst auf Basis des ActivityPub-Protokolls
@@ -15,6 +15,10 @@ gitea = Open-Source, selbst gehosteter Git-Repository-Hoster
matrix = Föderierter Messenger
copyparty = Cloud-Dateispeicher
4get = Proxy-Suchmaschine
tools = Satz verschiedener Werkzeuge
vert = Dateiumwandler
tl = Altfront für beliebte Suchmaschinen
lyr = Altfront für Genius
[host]
+10 -1
View File
@@ -5,9 +5,14 @@ contact us = Contact us
about us = About us
about host = About host
contacts = Contacts
donate = Donate
description = Small personal site
[index]
bottom_text = We also have {glitchtip}, {baikal} and {freshrss} for members of our squad!
altfronts = Altfronts
[index.descr]
sharkey = ActivityPub-based federated microblogging service
@@ -15,6 +20,10 @@ gitea = Opensource selfhosted Git repository hosting
matrix = federated instant messenger
copyparty = cloud file storage
4get = proxy search engine
tools = set of various tools
vert = file converter
tl = altfront for popular search engines
lyr = altfront for Genius
[host]
+5 -1
View File
@@ -7,7 +7,7 @@ about host = À propos de serveur
[index]
bottom_text = On a aussi {glitchtip}, {baikal} et {freshrss} pour les membres du groupe !
altfronts = Altfronts
[index.descr]
sharkey = Service de microblogging fédéré avec ActivityPub
@@ -15,6 +15,10 @@ gitea = Hébergement de dépôts Git open source en auto-hébergé
matrix = Messagerie fédérée
copyparty = Stockage de fichiers en cloud
4get = Moteur de recherche proxy
tools = ensemble d'outils variés
vert = convertisseur de fichiers
tl = altfront pour les moteurs de recherche populaires
lyr = altfront pour Genius
[host]
+5 -1
View File
@@ -7,7 +7,7 @@ about host = サーバーについて
[index]
bottom_text = メンバーには {glitchtip}、{baikal}、{freshrss} も使えるよ!
altfronts = 代替フロントエンド
[index.descr]
sharkey = ActivityPubを使った連合型マイクロブログ
@@ -15,6 +15,10 @@ gitea = オープンソースのセルフホスティングGitリポジトリ
matrix = 連合型メッセンジャー
copyparty = クラウドファイルストレージ
4get = プロキシ検索エンジン
tools = 様々なツールのセット
vert = ファイル変換ツール
tl = 人気検索エンジン向けの代替フロントエンド
lyr = Genius向けの代替フロントエンド
[host]
+10 -1
View File
@@ -5,9 +5,14 @@ contact us = Для связи
about us = О нас
about host = О хосте
contacts = Контакты
donate = Донат
description = Небольшой личный сайт
[index]
bottom_text = Ещё у нас есть {glitchtip}, {baikal} и {freshrss} для участников нашей группы!
altfronts = Альтфронты
[index.descr]
sharkey = Федеративная микроблогинговая система поверх протокола ActivityPub
@@ -15,6 +20,10 @@ gitea = Selfhosted хранилище Git-репозиториев со своб
matrix = федеративный мессенджер
copyparty = облачное хранилище файлов
4get = прокси-поисковик
tools = набор разнообразных утилит
vert = конвертация файлов
tl = альтфронт для популярных поисковиков
lyr = альтфронт для Genius
[host]
+47
View File
@@ -0,0 +1,47 @@
from configparser import ConfigParser
from flask import g, request
translations_cache = {}
def load_translations(lang):
if lang not in translations_cache:
translations_cache[lang] = {}
try:
config = ConfigParser()
config.read(f'locale/{lang}.ini')
for section in config.sections():
translations_cache[lang][section] = dict(config.items(section))
except:
pass
return translations_cache[lang]
def get_locale():
return request.accept_languages.best_match(
('en', 'ru', 'de', 'fr', 'ja'),
) or 'en'
def before_request():
g.locale = get_locale()
g.translations = load_translations(g.locale)
def inject_translations():
def translate(text, **kwargs):
if ":" in text:
section, key = text.split(":", 1)
else:
section, key = "common", text
template = g.translations \
.get(section, {}) \
.get(key, f"${section}: {key}$")
try:
return template.format(**kwargs)
except:
return template
return {'_': translate}
+5 -1
View File
@@ -1,3 +1,7 @@
Flask==3.1.1
Flask-Babel
gunicorn
htmlmin2
requests
APScheduler
musicbrainzngs
python-magic
+3 -3
View File
@@ -3,17 +3,17 @@ let
pypkgs = pkgs.python3Packages;
in
pkgs.mkShell {
name = "codrs.ru";
name = "lair.moe";
buildInputs = with pypkgs; [
python
python-magic
virtualenv
# pkgs.nodejs
pkgs.nodePackages.sass
];
shellHook = ''
if [ ! -d "venv" ]; then
if [ ! -d ".venv" ]; then
python -m venv .venv
fi
-2
View File
@@ -1,2 +0,0 @@
Monocraft: https://idreesinc.itch.io/monocraft
Pixeloid: https://ggbot.itch.io/pixeloid-font
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

-168
View File
@@ -1,168 +0,0 @@
@use "sass:color";
// Palette: Catppuccin Mocha
// https://catppuccin.com/palette/
$base: #1e1e2e;
$text: #cdd6f4;
$mantle: #181825;
$crust: #11111b;
$overlay0: #6c7086;
$overlay1: #7f849c;
$overlay2: #9399b2;
$surface0: #313244;
$surface1: #45475a;
$surface2: #585b70;
$subtext0: #a6adc8;
$subtext1: #bac2de;
$red: #f38ba8;
$green: #a6e3a1;
$peach: #fab387;
$blue: #89b4fa;
$mauve: #8839ef;
html {
height: 100%;
}
body {
display: flex;
flex-direction: column;
background-color: $base;
font-family: Pixeloid;
color: $text;
width: 100%;
height: 100%;
margin: 0;
}
main {
max-width: 45rem;
margin-inline: auto;
padding: 0 8px;
flex: 1 0 auto;
}
h1 {
text-align: center;
}
a {
color: unset;
text-decoration: underline;
text-decoration-color: $blue;
text-underline-offset: 1px;
transition: 0.3s ease;
&:hover {
text-underline-offset: 3px;
}
&:active {
transition: none !important;
display: inline-block;
transform: scale(.98) !important;
}
&.block {
text-decoration: none;
&:hover {
transform: scale(1.02) translateY(-.25rem);
}
}
}
header {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
background-color: $mantle;
padding: 8px;
font-size: larger;
}
footer {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
background-color: $crust;
margin-top: 32px;
padding: 16px;
column-gap: 4ch;
}
.mono {
font-family: Monocraft, monospace;
background-color: $mantle;
border-radius: 2px;
padding: 0 4px;
color: $subtext0;
overflow-wrap: anywhere;
&:hover {
transition: .3s ease;
background-color: $crust;
}
}
.block {
display: block;
background-color: $surface0;
border-radius: 8px;
padding: 8px;
& + & {
margin-top: 8px;
}
&.red {
background-color: color.mix($surface0, $red, 60%);
}
&.orange {
background-color: color.mix($surface0, $peach, 60%);
}
&.green {
background-color: color.mix($surface0, $green, 60%);
}
& .header {
display: flex;
align-items: center;
font-size: x-large;
.icon {
margin-right: .5ch;
}
}
}
.icon {
width: 1.5em;
vertical-align: middle;
border-radius: .2em;
}
@font-face {
font-family: Monocraft;
src: url("/static/font/Monocraft.ttc");
}
@font-face {
font-family: Pixeloid;
src:
url("/static/font/Pixeloid/woff/Sans.woff2") format("woff2"),
url("/static/font/Pixeloid/otf/Sans.otf") format("opentype");
}
@font-face {
font-family: Pixeloid;
src:
url("/static/font/Pixeloid/woff/Sans-Bold.woff2") format("woff2"),
url("/static/font/Pixeloid/otf/Sans-Bold.otf") format("opentype");
font-weight: bold;
}
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Coders Squad</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/webp" href="/static/icon/codrs.webp" />
<script src="/static/script/copy-mono.js"> </script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
{% include 'header.tmpl' %}
<h1>{% block title %}{% endblock %}</h1>
<main>
{% block content %}{% endblock %}
</main>
{% include 'footer.tmpl' %}
</body>
</html>
-5
View File
@@ -1,5 +0,0 @@
<footer>
<div>codrs.ru &#169; 2025</div>
<div><a href="https://g.codrs.ru/Sweetbread/codrs.ru">{{ _('site source') }}</a></div>
<div>{{ _('contact us') }}: <a href="mailto:admin@codrs.ru">admin@codrs.ru</a></div>
</footer>
-54
View File
@@ -1,54 +0,0 @@
{% extends 'base.tmpl' %}
{% block title %}
<img src="/static/icon/codrs.webp" class="icon" />
Coders Squad
{% endblock %}
{% block content %}
<a href="https://b.codrs.ru" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/sharkey.webp" class="icon"/>
<strong>Sharkey</strong>
</div>
<p>{{ _('index.descr:sharkey') }}</p>
</a>
<a href="https://g.codrs.ru" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/gitea.webp" class="icon"/>
<strong>Gitea</strong>
</div>
<p>{{ _('index.descr:gitea') }}</p>
</a>
<div class="block">
<p><a href="https://m.codrs.ru" target="_blank"><strong>Matrix</strong></a> &mdash; {{ _('index.descr:matrix') }}</p>
<p><a href="https://c.codrs.ru" target="_blank"><strong>Copyparty</strong></a> &mdash; {{ _('index.descr:copyparty') }}</p>
<p><a href="https://s.codrs.ru" target="_blank"><strong>4get</strong></a> &mdash; {{ _('index.descr:4get') }}</p>
</div>
<div class="block">
<div>
<strong>DNS</strong>:
<ul>
<li><span class="mono">193.222.99.172</span></li>
<li>DoT: <span class="mono">codrs.ru:853</span></li>
<li>DoH: <span class="mono">dns.codrs.ru</span></li>
</ul>
</div>
<p>
<strong>Yggdrasil</strong>:
<span class="mono">200:ee1:bad2:1732:4b91:c3e3:2f08:29b3</span>
</p>
</div>
<div class="block">
{{
_('index:bottom_text',
glitchtip='<a href="https://bug.codrs.ru" target="_blank"><strong>GlitchTip</strong></a>',
baikal='<a href="https://dav.codrs.ru" target="_blank"><strong>Baikal</strong></a>',
freshrss='<a href="https://rss.codrs.ru" target="_blank"><strong>FreshRSS</strong></a>',
) | safe
}}
</div>
{% endblock %}