13 Commits

Author SHA1 Message Date
Sweetbread 8c36a3e3a6 Add some services 2025-10-11 02:06:30 +03:00
Sweetbread 4b6449a7a0 Change host info 2025-10-11 02:06:30 +03:00
Sweetbread 3fefa976ea Improve style
- Add more depth to .block
- Make footer lighter
- Stylize scrollbar
2025-10-11 02:06:30 +03:00
Sweetbread d0e38f14ac Add mine page 2025-10-11 02:06:30 +03:00
Sweetbread f56c7831ed style: change sizes to rem
Docker Build and Push / build-and-push (push) Successful in 2m5s
2025-09-04 19:15:27 +03:00
Sweetbread 01600d8e50 style: decrease margins 2025-09-04 17:31:32 +03:00
Sweetbread befb88d498 feat: add tracker script
Docker Build and Push / build-and-push (push) Successful in 1m9s
2025-08-18 14:25:44 +03:00
Sweetbread 217ba01f12 update host specs
Docker Build and Push / build-and-push (push) Successful in 1m10s
2025-08-18 14:20:23 +03:00
Sweetbread 96a3ca543b l10n: add en, de, ja and fr
Docker Build and Push / build-and-push (push) Successful in 3m16s
2025-08-18 14:15:53 +03:00
Sweetbread 3a5e991870 sec: add production server
Docker Build and Push / build-and-push (push) Successful in 1m31s
2025-08-07 21:18:49 +03:00
Sweetbread 5da241b62e ci: add docker-build 2025-08-07 21:16:59 +03:00
Sweetbread 74c4f1c1f8 feat: add dockerfile 2025-08-06 19:56:05 +03:00
Sweetbread f42d2f887f Init commit 2025-08-06 19:56:05 +03:00
72 changed files with 422 additions and 1447 deletions
+4 -9
View File
@@ -11,16 +11,10 @@ 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.lair.moe
registry: g.codrs.ru
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -32,7 +26,8 @@ jobs:
with:
context: .
push: ${{ github.event_name == 'push' }}
tags: g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe:latest
labels: ${{ steps.tags.outputs.labels }}
tags: |
g.codrs.ru/${{ vars.DOCKER_USERNAME }}/codrs.ru:latest
g.codrs.ru/${{ vars.DOCKER_USERNAME }}/codrs.ru:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
+2 -2
View File
@@ -5,5 +5,5 @@ __pycache__/
*$py.class
.python-version
*.css
*.css.map
static/style/*.css
static/style/*.css.map
+12 -36
View File
@@ -1,47 +1,23 @@
FROM node:18-alpine AS sass-builder
FROM node:18-alpine as sass
RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass@latest --omit=dev --no-fund --no-audit
RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass
WORKDIR /build
COPY ./blueprints ./blueprints
RUN sass ./blueprints:./blueprints \
COPY ./static/style ./style
RUN sass ./style:./style \
--no-source-map \
--style=compressed \
--quiet
--style=compressed
FROM python:3.11-slim
RUN apt-get update && \
apt-get install --no-install-recommends -y \
libmagic1 \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
COPY . .
COPY --from=sass /build/style/ ./static/style/
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
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"]
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--workers", "4"]
+70 -26
View File
@@ -1,34 +1,78 @@
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
app = Flask(
__name__,
static_folder=None,
subdomain_matching=True,
template_folder="blueprints"
from os import system as console
from configparser import ConfigParser
from flask import (
Flask,
g,
request,
render_template,
)
app.jinja_env.add_extension('pypugjs.ext.jinja.PyPugJSExtension')
app.before_request(locale.before_request)
app.context_processor(locale.inject_translations)
translations_cache = {}
app.register_blueprint(root_bp)
app.register_blueprint(rdv_bp)
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'), '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}
if app.debug:
blueprints.root.modules.style.compile_styles()
blueprints.risdeveau.modules.style.compile_styles()
console("sass static/style/main.scss static/style/main.css")
console("sass static/style/risdeveau.scss static/style/risdeveau.css")
app.config['SERVER_NAME'] = "localhost:5000"
else:
app.config['SERVER_NAME'] = "lair.moe"
@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.route("/risdeveau")
def risdeveau():
return render_template('personal/risdeveau.html')
-106
View File
@@ -1,106 +0,0 @@
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, refresh_cache as lb_refresh
from .modules.api.steam import data as steam_data, refresh_cache as steam_refresh
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 utmsmp(unix: int) -> str:
return datetime \
.utcfromtimestamp(unix) \
.strftime('%Y-%m-%d %H:%M:%S')
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
)
@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,
"utmsmp": utmsmp,
"rtmsmp": rtmsmp
}
@bp.route("/")
def index():
lb_refresh()
steam_refresh()
return render_tmpl('index.pug', **args)
@bp.route("/m/<module>")
def module(module):
if module == "listenbrainz":
lb_refresh()
elif module == "steam":
steam_refresh()
if none_match := request.headers.get('if-none-match'):
match module:
case "listenbrainz":
if none_match == lb_data['etag']:
return '', 304
case "steam":
if none_match == steam_data['etag']:
return '', 304
return render_tmpl(f'{module}.pug', **args)
-193
View File
@@ -1,193 +0,0 @@
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
from .scheduler_guard import should_start_scheduler
from .shared_cache import atomic_write_json, cache_file, load_json_if_newer
@dataclass
class Cache:
data = {}
last_updated = time()
status = None
data = {
"caches": {
"now": Cache(),
"listens": Cache()
},
"last_updated": time(),
"etag": ""
}
_IS_WRITER = should_start_scheduler()
_CACHE_PATH = cache_file("listenbrainz")
_CACHE_MTIME = 0.0
def refresh_cache() -> None:
"""Refresh in-memory cache from the shared JSON file (for non-writer workers)."""
global _CACHE_MTIME
if _IS_WRITER:
return
payload, mtime = load_json_if_newer(_CACHE_PATH, _CACHE_MTIME)
if payload is None:
return
try:
data["etag"] = payload.get("etag", data.get("etag", ""))
data["last_updated"] = payload.get("last_updated", data.get("last_updated", time()))
caches = payload.get("caches", {})
for key, cache in data.get("caches", {}).items():
if isinstance(caches, dict) and key in caches and isinstance(caches[key], dict):
c = caches[key]
cache.data = c.get("data", cache.data)
cache.last_updated = c.get("last_updated", cache.last_updated)
cache.status = c.get("status", cache.status)
finally:
_CACHE_MTIME = mtime
def _persist_cache() -> None:
"""Persist current cache state to the shared JSON file (writer worker only)."""
if not _IS_WRITER:
return
payload = {
"etag": data.get("etag", ""),
"last_updated": data.get("last_updated", time()),
"caches": {
k: {
"data": v.data,
"last_updated": v.last_updated,
"status": v.status,
}
for k, v in data.get("caches", {}).items()
},
}
atomic_write_json(_CACHE_PATH, payload)
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):
changed = False
prev_status = cache.status
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()
changed = True
else:
cache.status = f'error: {response.status_code}'
except Exception as e:
cache.status = f'error: {str(e)}'
if prev_status != cache.status:
changed = True
if changed:
_persist_cache()
scheduler = BackgroundScheduler()
if _IS_WRITER:
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
)
_persist_cache()
scheduler.start()
atexit.register(lambda: scheduler.shutdown())
else:
refresh_cache()
@@ -1,35 +0,0 @@
from __future__ import annotations
import os
import fcntl
from typing import Optional
_LOCK_FD: Optional[int] = None # keep fd open for process lifetime
def should_start_scheduler() -> bool:
"""Return True in exactly one process (the scheduler owner).
Under gunicorn --workers N, code is imported in N processes. If APScheduler
starts at import time, you get N schedulers => N× API calls.
We prevent that by taking a non-blocking exclusive lock on a lockfile.
"""
global _LOCK_FD
if _LOCK_FD is not None:
return True
lock_path = os.environ.get("LAIR_SCHED_LOCK", "/tmp/lair-scheduler.lock")
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o644)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
_LOCK_FD = fd
return True
except BlockingIOError:
os.close(fd)
return False
except Exception:
try:
os.close(fd)
finally:
return False
@@ -1,53 +0,0 @@
from __future__ import annotations
import json
import os
import tempfile
from typing import Any, Optional, Tuple
def cache_dir() -> str:
"""Directory where the scheduler owner writes shared cache JSON files."""
d = os.environ.get("LAIR_CACHE_DIR", "/tmp/lair-cache")
os.makedirs(d, exist_ok=True)
return d
def cache_file(name: str) -> str:
return os.path.join(cache_dir(), f"{name}.json")
def atomic_write_json(path: str, payload: dict) -> None:
"""Write JSON atomically so readers never observe partial content."""
parent = os.path.dirname(path) or "."
fd, tmp = tempfile.mkstemp(prefix=".tmp-", dir=parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, separators=(",", ":"))
os.replace(tmp, path)
finally:
try:
if os.path.exists(tmp):
os.unlink(tmp)
except Exception:
pass
def load_json_if_newer(path: str, last_mtime: float) -> Tuple[Optional[dict], float]:
"""Load JSON only if the file exists and has mtime newer than last_mtime."""
try:
st = os.stat(path)
except FileNotFoundError:
return None, last_mtime
except Exception:
return None, last_mtime
mtime = float(st.st_mtime)
if mtime <= float(last_mtime):
return None, last_mtime
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f), mtime
except Exception:
return None, last_mtime
-173
View File
@@ -1,173 +0,0 @@
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
from .scheduler_guard import should_start_scheduler
from .shared_cache import atomic_write_json, cache_file, load_json_if_newer
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": ""
}
_IS_WRITER = should_start_scheduler()
_CACHE_PATH = cache_file("steam")
_CACHE_MTIME = 0.0
def refresh_cache() -> None:
"""Refresh in-memory cache from the shared JSON file (for non-writer workers)."""
global _CACHE_MTIME
if _IS_WRITER:
return
payload, mtime = load_json_if_newer(_CACHE_PATH, _CACHE_MTIME)
if payload is None:
return
try:
data["etag"] = payload.get("etag", data.get("etag", ""))
data["last_updated"] = payload.get("last_updated", data.get("last_updated", time()))
caches = payload.get("caches", {})
for key, cache in data.get("caches", {}).items():
if isinstance(caches, dict) and key in caches and isinstance(caches[key], dict):
c = caches[key]
cache.data = c.get("data", cache.data)
cache.last_updated = c.get("last_updated", cache.last_updated)
cache.status = c.get("status", cache.status)
finally:
_CACHE_MTIME = mtime
def _persist_cache() -> None:
"""Persist current cache state to the shared JSON file (writer worker only)."""
if not _IS_WRITER:
return
payload = {
"etag": data.get("etag", ""),
"last_updated": data.get("last_updated", time()),
"caches": {
k: {
"data": v.data,
"last_updated": v.last_updated,
"status": v.status,
}
for k, v in data.get("caches", {}).items()
},
}
atomic_write_json(_CACHE_PATH, payload)
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.append(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):
changed = False
prev_status = cache.status
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()
changed = True
else:
cache.status = f'error: {response.status_code}'
except Exception as e:
cache.status = f'error: {str(e)}'
if prev_status != cache.status:
changed = True
if changed:
_persist_cache()
if TOKEN:
scheduler = BackgroundScheduler()
if _IS_WRITER:
scheduler.add_job(
func=lambda: api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=MY_ID),
trigger=IntervalTrigger(minutes=15),
id='risdeveau.steam.recent',
replace_existing=True
)
scheduler.add_job(
func=lambda: api_request(data['caches']['owned'], "IPlayerService", "GetOwnedGames", steamid=MY_ID, include_appinfo=1, include_played_free_games=1),
trigger=IntervalTrigger(minutes=60),
id='risdeveau.steam.owned',
replace_existing=True
)
_persist_cache()
scheduler.start()
api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=MY_ID)
api_request(data['caches']['owned'], "IPlayerService", "GetOwnedGames", steamid=MY_ID, include_appinfo=1, include_played_free_games=1)
atexit.register(lambda: scheduler.shutdown())
else:
refresh_cache()
else:
msg = "STEAM_TOKEN is not defined"
print(msg)
for c in data["caches"].values():
c.status = msg
if _IS_WRITER:
_persist_cache()
else:
refresh_cache()
-9
View File
@@ -1,9 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

@@ -1,68 +0,0 @@
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);
}
},
}));
});
@@ -1,100 +0,0 @@
@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 {
display: flex;
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
@@ -1,16 +0,0 @@
div
.88-31
a.disabled(href="https://chest.lair.moe")
img(src="/static/img/88x31/gf.png")
a(href="//lair.moe")
img(src="/static/img/88x31/lair.gif")
a#pie(href="https://preview.about.akarpov.ru")
img(src="/static/img/88x31/withpie.gif")
.88-31
a(href="https://g.lair.moe/Sweetbread/nixos-config")
img(src="/static/img/88x31/nixos.webp")
img(src="/static/img/88x31/teto.webp")
@@ -1,40 +0,0 @@
.block
h3 Development
.blocks.badges
a.block(href="//g.lair.moe/Sweetbread")
img.img(src="/static/img/service/gitea.webp")
| Gitea
a.block(href="https://github.com/VerySweetBread")
img.img(src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png")
| GitHub
a.block(href="https://git.kolibrios.org/Sweetbread")
img.img(src="https://git.kolibrios.org/assets/img/logo.svg")
| KolibriOS Git
h3 Contacts
.blocks.badges
a.block(href="https://matrix.to/#/@risdeveau:lair.moe")
img.img(src="https://matrix.org/assets/favimg.ico")
| Matrix
a.block(href="//b.lair.moe/@risdeveau")
img.img(src="/static/img/service/sharkey.webp")
| Fediverse
a.block(href="https://discord.com/users/459823895256498186")
img.img(src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d80db9971f10a9757c99_Symbol.svg")
| Discord
a.block(href="mailto:risdeveau@lair.moe") Mail
h3 Game accounts
.blocks.badges
a.block(href="https://steamcommunity.com/id/risdeveau")
img.img(src="https://store.steampowered.com/favimg.ico")
| Steam
a.block(href="https://gamebanana.com/members/3899828")
img.img(src="https://images.gamebanana.com/static/img/favimg/favimg.ico")
| GameBanana
-14
View File
@@ -1,14 +0,0 @@
div
h3 Wallets
.blocks.qr
.block.qr
p POL, BNB
img(src="/static/img/wallets/evm.webp")
.block.qr
p TON
img(src="/static/img/wallets/ton.webp")
.block.qr
p XMR
img(src="/static/img/wallets/xmr.webp")
-50
View File
@@ -1,50 +0,0 @@
doctype html
html(lang="en")
head
title Sweet Bread
each f in ('tw', 'main', 'risdeveau')
link(rel="stylesheet", href="/static/style/#{f}.css")
link(rel="img" type="image/webp" href="/static/img/us/risdeveau.webp")
script(src="/static/script/rtime.js")
script(
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
)
script(
src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"
)
script(defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
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}
]
}'
)
body
header
a(href=url_for('root.index')) Lair
main
include risdeveau/templates/info.pug
include risdeveau/templates/contacts.pug
include risdeveau/templates/listenbrainz.pug
include risdeveau/templates/steam.pug
include risdeveau/templates/donate.pug
include risdeveau/templates/88x31.pug
-35
View File
@@ -1,35 +0,0 @@
.block
table
tr
th DoB
td 2005-01-13
tr
th Languages
td
table
tr
td Russian
td Native
tr
td English
td B2
tr
td French
td A1?
tr
td German
td A2?
tr
td Japanese
td Beginner
tr
th Student
td
table
tr
td Programmer
td 2.5/4yr.
tr
td Translator
td 2.5/3yr.
@@ -1,32 +0,0 @@
mixin track_block(track, is_active=false)
- set act_cls = "active" if is_active else ""
.block.track(class=act_cls)
if track.cover_url
img(src=track.cover_url)
div
p
b= track.artist_name
p= track.track_name
if not is_active
p(
x-data="rtime(#{track.listened_at})",
x-text="`Listened ${timeString}`",
:class="textColorClass"
)
.block(
hx-get="/m/listenbrainz",
hx-trigger="every 15s",
hx-swap="outerHTML",
hx-headers='{"If-None-Match": "#{lb.etag}"}'
)
h2
a(href="https://listenbrainz.org/user/risdeveau/") Listenbrainz
if lb.caches.now.data and lb.caches.now.data.listens[0]
+track_block(lb.caches.now.data.listens[0], true)
if lb.caches.listens.data and lb.caches.listens.data.listens
each track in lb.caches.listens.data.listens
+track_block(track)
-52
View File
@@ -1,52 +0,0 @@
mixin game_block(g, show_last_played=false)
a.block(href="https://store.steampowered.com/app/#{g.appid}", target="_blank")
picture
source(media="(max-width: 45rem)", srcset=g.v_cover)
img(src=g.h_cover)
div
strong= g.name
if g.playtime_2weeks
p Played last 2 weeks: #{tmsmp(g.playtime_2weeks * 60)}
p
- var lin = g.playtime_linux_forever > 0;
- var win = g.playtime_windows_forever > 0;
| Total played:
if lin
= tmsmp(g.playtime_linux_forever * 60)
| (
abbr(title="On Linux") L
| )
if lin and win
= " + "
if win
= tmsmp(g.playtime_windows_forever * 60)
| (
abbr(title="On Windows") W
| )
if lin and win
= " = "
= tmsmp(g.playtime_forever * 60)
if show_last_played and g.rtime_last_played != 0
p Last played: #{utmsmp(g.rtime_last_played)}
.block.steam(
hx-get="/m/steam",
hx-trigger="every 1m",
hx-swap="outerHTML",
hx-headers='{ "If-None-Match": "#{steam.etag}" }'
)
h2
a(href="https://steamcommunity.com/id/risdeveau") Steam
if steam.caches.recent.data.games
h3 Recently played:
each g in steam.caches.recent.data.games
+game_block(g)
p(x-data="rtime(#{steam.caches.recent.last_updated})", x-text="`Last updated: ${timeString}`")
if steam.caches.owned.data.games
h3 Top played games:
- var owned_games = steam.caches.owned.data.games | sort(attribute="playtime_forever", reverse=true)
each g in owned_games[:5]
+game_block(g, true)
p(x-data="rtime(#{steam.caches.owned.last_updated})", x-text="`Last updated: ${timeString}`")
-28
View File
@@ -1,28 +0,0 @@
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.pug')
@bp.route("/host")
def host():
return render_tmpl('host.pug')
@bp.route("/us")
def us():
return render_tmpl('us.pug')
-9
View File
@@ -1,9 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -1,25 +0,0 @@
// 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;
-8
View File
@@ -1,8 +0,0 @@
@use "catppuccin" as theme;
.t {
&-red { color: theme.$red; }
&-orange { color: theme.$peach; }
&-yellow { color: theme.$yellow; }
&-green { color: theme.$green; }
}
-33
View File
@@ -1,33 +0,0 @@
doctype html
html(lang=g.locale)
head
title Lair
link(rel="stylesheet" href="/static/style/main.css")
link(rel="img" type="image/webp" href="/static/img/lair.webp")
script(src="/static/script/copy-mono.js")
script(
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
)
meta(name="viewport" content="width=device-width, initial-scale=1.0")
meta(name="mock-email" content="admin@example.com")
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/img/lair.webp")
meta(property="og:description" value=_("description"))
body
include root/templates/header.pug
h1
block title
main
block content
include root/templates/footer.pug
-9
View File
@@ -1,9 +0,0 @@
footer
div
| lair.moe &#127279; 2025 - 2026</div>
div
a(href="https://g.lair.moe/Sweetbread/lair.moe")= _('site source')
div
= _('contact us')
= ": "
a(href="mailto:admin@lair.moe") admin@lair.moe
-17
View File
@@ -1,17 +0,0 @@
header
if request.path != url_for('.index')
a(href=url_for('.index')) Lair
else
div
.header-links
-
var links = (
('.us', _('about us')),
('.host', _('about host')),
)
each l, t in links
if url_for(l) == request.path
strong= t
else
a(href=url_for(l))= t
-20
View File
@@ -1,20 +0,0 @@
extends root/templates/base.pug
block title
= _('about host')
block content
a.block(href="https://play2go.cloud/?ref_id=4baFoOIp5QE" target="_blank")
strong= _("host:hoster")
| : play2go
p= _('host:hoster_descr')
.block
strong= _("host:specifications")
| :
ul
li CPU: Ryzen 9@3.4GHz (4 cores)
li RAM: 8 GB
li SSD: 150 GB
li ETH: 500Mb/s
li Loc: Deutschland, Frankfurt am Main
-70
View File
@@ -1,70 +0,0 @@
extends root/templates/base.pug
block title
img.img(src="/static/img/lair.webp")
| Lair
block content
a.block(href="https://b.lair.moe" target="_blank")
.header
img.img(src="/static/img/service/sharkey.webp")
strong Sharkey
p= _('index.descr:sharkey')
a.block(href="https://g.lair.moe" target="_blank")
.header
img.img(src="/static/img/service/gitea.webp")
strong Gitea
p= _('index.descr:gitea')
.block
p
a(href="https://m.lair.moe" target="_blank")
strong Matrix
| &mdash; {{ _('index.descr:matrix') }}
p
a(href="//c.lair.moe" target="_blank")
strong Copyparty
| &mdash; {{ _('index.descr:copyparty') }}
p
a(href="https://tools.lair.moe" target="_blank")
strong IT-tools
| &mdash; {{ _('index.descr:tools') }}
p
a(href="https://vert.lair.moe" target="_blank")
strong Vert
| &mdash; {{ _('index.descr:vert') }}
.block
strong= _('index:altfronts')
p
a(href="https://s.lair.moe" target="_blank")
strong 4get
| &mdash; {{ _('index.descr:4get') }}
p
a(href="https://tl.lair.moe" target="_blank")
strong TransLite
| &mdash; {{ _('index.descr:tl') }}
p
a(href="https://lyr.lair.moe" target="_blank")
strong Intellectual
| &mdash; {{ _('index.descr:lyr') }}
.block
div
strong DNS
| :
ul
li
span.mono 64.188.64.176
li
| DoT:
span.mono lair.moe:853
li
| DoH:
span.mono dns.lair.moe
p
strong Yggdrasil
| :
span.mono 200:ee1:bad2:1732:4b91:c3e3:2f08:29b3
-17
View File
@@ -1,17 +0,0 @@
extends root/templates/base.pug
block title
| О нас
block content
a.block.green(href=url_for('risdeveau.index'))
.header
img.img(src="/static/img/us/risdeveau.webp")
| Sweetbread
| Главный админ, занимается почти всеми сервисами. Создал этот сайт
.block.orange.disabled
.header
img.img(src="/static/img/us/chest.webp")
| Chest
| Должна была помогать делать этот сайт
+1 -3
View File
@@ -7,7 +7,7 @@ about host = Über Server
[index]
altfronts = Altfronts
bottom_text = Außerdem bieten wir {glitchtip}, {baikal} und {freshrss} für Mitglieder unserer Gruppe an!
[index.descr]
sharkey = Föderierter Microblogging-Dienst auf Basis des ActivityPub-Protokolls
@@ -17,8 +17,6 @@ copyparty = Cloud-Dateispeicher
4get = Proxy-Suchmaschine
tools = Satz verschiedener Werkzeuge
vert = Dateiumwandler
tl = Altfront für beliebte Suchmaschinen
lyr = Altfront für Genius
[host]
+1 -8
View File
@@ -5,14 +5,9 @@ contact us = Contact us
about us = About us
about host = About host
contacts = Contacts
donate = Donate
description = Small personal site
[index]
altfronts = Altfronts
bottom_text = We also have {glitchtip}, {baikal} and {freshrss} for members of our squad!
[index.descr]
sharkey = ActivityPub-based federated microblogging service
@@ -22,8 +17,6 @@ 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]
+1 -3
View File
@@ -7,7 +7,7 @@ about host = À propos de serveur
[index]
altfronts = Altfronts
bottom_text = On a aussi {glitchtip}, {baikal} et {freshrss} pour les membres du groupe !
[index.descr]
sharkey = Service de microblogging fédéré avec ActivityPub
@@ -17,8 +17,6 @@ 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]
+1 -3
View File
@@ -7,7 +7,7 @@ about host = サーバーについて
[index]
altfronts = 代替フロントエンド
bottom_text = メンバーには {glitchtip}、{baikal}、{freshrss} も使えるよ!
[index.descr]
sharkey = ActivityPubを使った連合型マイクロブログ
@@ -17,8 +17,6 @@ copyparty = クラウドファイルストレージ
4get = プロキシ検索エンジン
tools = 様々なツールのセット
vert = ファイル変換ツール
tl = 人気検索エンジン向けの代替フロントエンド
lyr = Genius向けの代替フロントエンド
[host]
+1 -8
View File
@@ -5,14 +5,9 @@ contact us = Для связи
about us = О нас
about host = О хосте
contacts = Контакты
donate = Донат
description = Небольшой личный сайт
[index]
altfronts = Альтфронты
bottom_text = Ещё у нас есть {glitchtip}, {baikal} и {freshrss} для участников нашей группы!
[index.descr]
sharkey = Федеративная микроблогинговая система поверх протокола ActivityPub
@@ -22,8 +17,6 @@ copyparty = облачное хранилище файлов
4get = прокси-поисковик
tools = набор разнообразных утилит
vert = конвертация файлов
tl = альтфронт для популярных поисковиков
lyr = альтфронт для Genius
[host]
-47
View File
@@ -1,47 +0,0 @@
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}
-6
View File
@@ -1,8 +1,2 @@
Flask==3.1.1
gunicorn
htmlmin2
requests
APScheduler
musicbrainzngs
python-magic
git+https://github.com/VerySweetBread/pypugjs
+4 -4
View File
@@ -3,17 +3,17 @@ let
pypkgs = pkgs.python3Packages;
in
pkgs.mkShell {
name = "lair.moe";
name = "codrs.ru";
buildInputs = with pypkgs; [
python
python-magic
virtualenv
pkgs.dart-sass
# pkgs.nodejs
pkgs.nodePackages.sass
];
shellHook = ''
if [ ! -d ".venv" ]; then
if [ ! -d "venv" ]; then
python -m venv .venv
fi
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -1,5 +1,29 @@
@use "sass:color";
@use "catppuccin" as theme;
// 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 {
@@ -9,9 +33,9 @@ html {
body {
display: flex;
flex-direction: column;
background-color: theme.$base;
background-color: $base;
font-family: Pixeloid, PixelMPlus;
color: theme.$text;
color: $text;
width: 100%;
height: 100%;
margin: 0;
@@ -31,7 +55,7 @@ h1 {
a {
color: unset;
text: {
decoration: underline {color: theme.$blue};
decoration: underline {color: $blue};
underline-offset: 1px;
}
transition: 0.3s ease;
@@ -44,7 +68,7 @@ a {
transition: none !important;
display: inline-block;
transform: scale(.98) !important;
background-color: theme.$mantle !important;
background-color: $mantle !important;
}
&.block {
@@ -52,7 +76,7 @@ a {
&:hover {
transform: scale(1.02) translateY(-.25rem);
background-color: theme.$surface1;
background-color: $surface1;
}
}
}
@@ -68,20 +92,16 @@ ul {
header {
display: flex;
justify-content: space-between;
background-color: theme.$mantle;
background-color: $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;
background-color: $mantle;
margin-top: 2rem;
padding: 1rem;
column-gap: 4ch;
@@ -89,48 +109,38 @@ footer {
.mono {
font-family: Monocraft, monospace;
background-color: theme.$mantle;
background-color: $mantle;
border-radius: 2px;
padding: 0 .25rem;
color: theme.$subtext0;
color: $subtext0;
overflow-wrap: anywhere;
&:hover {
transition: .3s ease;
background-color: theme.$crust;
background-color: $crust;
}
}
.block {
display: block;
background-color: theme.$surface0;
background-color: $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%);
background-color: color.mix($surface0, $red, 60%);
}
&.orange {
background-color: color.mix(theme.$surface0, theme.$peach, 60%);
background-color: color.mix($surface0, $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; }
background-color: color.mix($surface0, $green, 60%);
&:hover { background-color: color.mix($surface1, $green, 60%); }
&:active { background-color: color.mix($mantle, $green, 60%) !important; }
}
& .header {
@@ -138,7 +148,7 @@ footer {
align-items: center;
font-size: x-large;
.img {
.icon {
margin-right: .5ch;
}
}
@@ -174,56 +184,12 @@ footer {
}
}
.img {
.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;
@@ -233,15 +199,15 @@ footer {
}
&-track {
background-color: theme.$base;
background-color: $base;
}
&-thumb {
background-color: theme.$overlay0;
background-color: $overlay0;
border-radius: .25rem;
&:hover {
background-color: theme.$overlay1;
background-color: $overlay1;
}
}
}
+33
View File
@@ -0,0 +1,33 @@
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;
}
}
+27
View File
@@ -0,0 +1,27 @@
<!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>
<script
src="https://track.codrs.ru/api/script.js"
data-site-id="1"
defer
></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
@@ -0,0 +1,5 @@
<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>
+20
View File
@@ -0,0 +1,20 @@
<header>
{%- if request.path != url_for('index') %}
<a href="{{ url_for('index') }}">Coders Squad</a>
{%- else %}
<div></div>
{%- endif %}
<div class="header-links">
{%- for (l, t) in (
('us', _('about us')),
('host', _('about host'))
) %}
{%- if url_for(l) == request.path %}
<strong>{{ t }}</strong>
{%- else %}
<a href="{{ url_for(l) }}">{{ t }}</a>
{%- endif %}
{%- endfor %}
</div>
</header>
+21
View File
@@ -0,0 +1,21 @@
{% extends 'base.tmpl' %}
{% block title %}{{ _('about host') }}{% endblock %}
{% block content %}
<a href="https://play2go.cloud/?ref_id=4baFoOIp5QE" target="_blank" class="block">
<strong>{{ _("host:hoster") }}</strong>: play2go
<p>{{ _('host:hoster_descr') }}</p>
</a>
<div class="block">
<strong>{{ _("host:specifications") }}</strong>:
<ul>
<li>CPU: Ryzen i9@3.4GHz (4 cores)</li>
<li>RAM: 8 GiB</li>
<li>SSD: 150 GB</li>
<li>ETH: 500Mb/s</li>
<li>Loc: Deutchland, Frankfurt am Mein</li>
</ul>
</div>
{% endblock %}
+56
View File
@@ -0,0 +1,56 @@
{% 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>
<p><a href="https://tools.codrs.ru" target="_blank"><strong>IT-tools</strong></a> &mdash; {{ _('index.descr:tools') }}</p>
<p><a href="https://vert.codrs.ru" target="_blank"><strong>Vert</strong></a> &mdash; {{ _('index.descr:vert') }}</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">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 %}
+88
View File
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<title>Sweet Bread</title>
<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="https://track.codrs.ru/api/script.js"
data-site-id="1"
defer
></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header>
<a href="{{ url_for('index') }}">Coders Squad</a>
</header>
<main>
<h3>Development</h3>
<div class="blocks badges">
<a class="block" href="//g.codrs.ru/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.codrs.ru/@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@codrs.ru">
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>
<h3>Wallets</h3>
<div class="blocks qr">
<div class="block qr">
<p>POL, BNB</p>
<img src="/static/img/risdeveau/wallets/evm.webp">
</div>
<div class="block qr">
<p>TON</p>
<img src="/static/img/risdeveau/wallets/ton.webp">
</div>
<div class="block qr">
<p>XMR</p>
<img src="/static/img/risdeveau/wallets/xmr.webp">
</div>
</div>
</main>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
{% extends 'base.tmpl' %}
{% block title %}О нас{% endblock %}
{% block content %}
<a href="{{ url_for('risdeveau') }}" class="block green">
<div class="header">
<img src="/static/icon/us/risdeveau.webp" class="icon"/>
Sweetbread
</div>
Главный админ, занимается почти всеми сервисами. Создал этот сайт
</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="header">
<img src="/static/icon/us/chest.webp" class="icon"/>
Chest
</div>
Должна была помогать делать этот сайт
</div>
{% endblock %}