Compare commits
2
Commits
dev
..
262bcfa2dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
262bcfa2dd | ||
|
|
e5bfcdfe3f |
No files matched your search
@@ -11,12 +11,6 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -33,6 +27,5 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name == 'push' }}
|
push: ${{ github.event_name == 'push' }}
|
||||||
tags: g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe:latest
|
tags: g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe:latest
|
||||||
labels: ${{ steps.tags.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
+2
-2
@@ -5,5 +5,5 @@ __pycache__/
|
|||||||
*$py.class
|
*$py.class
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
*.css
|
static/style/*.css
|
||||||
*.css.map
|
static/style/*.css.map
|
||||||
+12
-35
@@ -1,46 +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
|
WORKDIR /build
|
||||||
COPY ./blueprints ./blueprints
|
COPY ./static/style ./style
|
||||||
|
RUN sass ./style:./style \
|
||||||
RUN sass ./blueprints:./blueprints \
|
|
||||||
--no-source-map \
|
--no-source-map \
|
||||||
--style=compressed \
|
--style=compressed
|
||||||
--quiet
|
|
||||||
|
|
||||||
FROM python:3.11-slim
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY . .
|
||||||
|
COPY --from=sass /build/style/ ./static/style/
|
||||||
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY . .
|
ENV FLASK_ENV=production
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
COPY --from=sass-builder /build/blueprints/ ./blueprints/
|
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--workers", "4"]
|
||||||
|
|
||||||
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"]
|
|
||||||
@@ -1,27 +1,79 @@
|
|||||||
from modules import locale
|
from os import system as console
|
||||||
|
from configparser import ConfigParser
|
||||||
from blueprints.root import bp as root_bp
|
from htmlmin import minify
|
||||||
from blueprints.risdeveau import bp as rdv_bp
|
from flask import (
|
||||||
|
Flask,
|
||||||
import blueprints.root.modules.style
|
g,
|
||||||
import blueprints.risdeveau.modules.style
|
request,
|
||||||
|
render_template,
|
||||||
from flask import Flask
|
)
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, static_folder=None, subdomain_matching=True)
|
translations_cache = {}
|
||||||
|
|
||||||
app.before_request(locale.before_request)
|
def load_translations(lang):
|
||||||
app.context_processor(locale.inject_translations)
|
if lang not in translations_cache:
|
||||||
|
translations_cache[lang] = {}
|
||||||
|
|
||||||
app.register_blueprint(root_bp)
|
try:
|
||||||
app.register_blueprint(rdv_bp)
|
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:
|
if app.debug:
|
||||||
blueprints.root.modules.style.compile_styles()
|
console("sass static/style/main.scss static/style/main.css")
|
||||||
blueprints.risdeveau.modules.style.compile_styles()
|
console("sass static/style/risdeveau.scss static/style/risdeveau.css")
|
||||||
|
|
||||||
app.config['SERVER_NAME'] = "localhost:5000"
|
@app.route("/")
|
||||||
else:
|
def index():
|
||||||
app.config['SERVER_NAME'] = "lair.moe"
|
return minify(render_template('index.html'), remove_empty_space=True)
|
||||||
|
|
||||||
|
@app.route("/host")
|
||||||
|
def host():
|
||||||
|
return minify(render_template('host.html'), remove_empty_space=True)
|
||||||
|
|
||||||
|
@app.route("/us")
|
||||||
|
def us():
|
||||||
|
return minify(render_template('us.html'), remove_empty_space=True)
|
||||||
|
|
||||||
|
@app.route("/risdeveau")
|
||||||
|
def risdeveau():
|
||||||
|
return minify(render_template('personal/risdeveau.html'), remove_empty_space=True)
|
||||||
@@ -1,103 +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
|
|
||||||
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)
|
|
||||||
@@ -1,123 +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
|
|
||||||
|
|
||||||
|
|
||||||
@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())
|
|
||||||
@@ -1,95 +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
|
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
@@ -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,136 +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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@@ -1,120 +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 {
|
|
||||||
&: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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{% 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>
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -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.html')
|
|
||||||
|
|
||||||
@bp.route("/host")
|
|
||||||
def host():
|
|
||||||
return render_tmpl('host.html')
|
|
||||||
|
|
||||||
@bp.route("/us")
|
|
||||||
def us():
|
|
||||||
return render_tmpl('us.html')
|
|
||||||
@@ -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')}")
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
@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; }
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ about host = Über Server
|
|||||||
|
|
||||||
[index]
|
[index]
|
||||||
altfronts = Altfronts
|
altfronts = Altfronts
|
||||||
|
bottom_text = Außerdem bieten wir {glitchtip}, {baikal} und {freshrss} für Mitglieder unserer Gruppe an!
|
||||||
|
|
||||||
[index.descr]
|
[index.descr]
|
||||||
sharkey = Föderierter Microblogging-Dienst auf Basis des ActivityPub-Protokolls
|
sharkey = Föderierter Microblogging-Dienst auf Basis des ActivityPub-Protokolls
|
||||||
|
|||||||
+1
-5
@@ -5,14 +5,10 @@ contact us = Contact us
|
|||||||
about us = About us
|
about us = About us
|
||||||
about host = About host
|
about host = About host
|
||||||
|
|
||||||
contacts = Contacts
|
|
||||||
donate = Donate
|
|
||||||
|
|
||||||
description = Small personal site
|
|
||||||
|
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
altfronts = Altfronts
|
altfronts = Altfronts
|
||||||
|
bottom_text = We also have {glitchtip}, {baikal} and {freshrss} for members of our squad!
|
||||||
|
|
||||||
[index.descr]
|
[index.descr]
|
||||||
sharkey = ActivityPub-based federated microblogging service
|
sharkey = ActivityPub-based federated microblogging service
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ about host = À propos de serveur
|
|||||||
|
|
||||||
[index]
|
[index]
|
||||||
altfronts = Altfronts
|
altfronts = Altfronts
|
||||||
|
bottom_text = On a aussi {glitchtip}, {baikal} et {freshrss} pour les membres du groupe !
|
||||||
|
|
||||||
[index.descr]
|
[index.descr]
|
||||||
sharkey = Service de microblogging fédéré avec ActivityPub
|
sharkey = Service de microblogging fédéré avec ActivityPub
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ about host = サーバーについて
|
|||||||
|
|
||||||
[index]
|
[index]
|
||||||
altfronts = 代替フロントエンド
|
altfronts = 代替フロントエンド
|
||||||
|
bottom_text = メンバーには {glitchtip}、{baikal}、{freshrss} も使えるよ!
|
||||||
|
|
||||||
[index.descr]
|
[index.descr]
|
||||||
sharkey = ActivityPubを使った連合型マイクロブログ
|
sharkey = ActivityPubを使った連合型マイクロブログ
|
||||||
|
|||||||
+1
-5
@@ -5,14 +5,10 @@ contact us = Для связи
|
|||||||
about us = О нас
|
about us = О нас
|
||||||
about host = О хосте
|
about host = О хосте
|
||||||
|
|
||||||
contacts = Контакты
|
|
||||||
donate = Донат
|
|
||||||
|
|
||||||
description = Небольшой личный сайт
|
|
||||||
|
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
altfronts = Альтфронты
|
altfronts = Альтфронты
|
||||||
|
bottom_text = Ещё у нас есть {glitchtip}, {baikal} и {freshrss} для участников нашей группы!
|
||||||
|
|
||||||
[index.descr]
|
[index.descr]
|
||||||
sharkey = Федеративная микроблогинговая система поверх протокола ActivityPub
|
sharkey = Федеративная микроблогинговая система поверх протокола ActivityPub
|
||||||
|
|||||||
@@ -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}
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
Flask==3.1.1
|
Flask==3.1.1
|
||||||
gunicorn
|
gunicorn
|
||||||
htmlmin2
|
htmlmin2
|
||||||
requests
|
|
||||||
APScheduler
|
|
||||||
musicbrainzngs
|
|
||||||
python-magic
|
|
||||||
@@ -7,7 +7,6 @@ pkgs.mkShell {
|
|||||||
|
|
||||||
buildInputs = with pypkgs; [
|
buildInputs = with pypkgs; [
|
||||||
python
|
python
|
||||||
python-magic
|
|
||||||
virtualenv
|
virtualenv
|
||||||
pkgs.nodePackages.sass
|
pkgs.nodePackages.sass
|
||||||
];
|
];
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,29 @@
|
|||||||
@use "sass:color";
|
@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 {
|
html {
|
||||||
@@ -9,9 +33,9 @@ html {
|
|||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: theme.$base;
|
background-color: $base;
|
||||||
font-family: Pixeloid, PixelMPlus;
|
font-family: Pixeloid, PixelMPlus;
|
||||||
color: theme.$text;
|
color: $text;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -31,7 +55,7 @@ h1 {
|
|||||||
a {
|
a {
|
||||||
color: unset;
|
color: unset;
|
||||||
text: {
|
text: {
|
||||||
decoration: underline {color: theme.$blue};
|
decoration: underline {color: $blue};
|
||||||
underline-offset: 1px;
|
underline-offset: 1px;
|
||||||
}
|
}
|
||||||
transition: 0.3s ease;
|
transition: 0.3s ease;
|
||||||
@@ -44,7 +68,7 @@ a {
|
|||||||
transition: none !important;
|
transition: none !important;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transform: scale(.98) !important;
|
transform: scale(.98) !important;
|
||||||
background-color: theme.$mantle !important;
|
background-color: $mantle !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.block {
|
&.block {
|
||||||
@@ -52,7 +76,7 @@ a {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.02) translateY(-.25rem);
|
transform: scale(1.02) translateY(-.25rem);
|
||||||
background-color: theme.$surface1;
|
background-color: $surface1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +92,7 @@ ul {
|
|||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: theme.$mantle;
|
background-color: $mantle;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
font-size: larger;
|
font-size: larger;
|
||||||
|
|
||||||
@@ -81,7 +105,7 @@ footer {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
background-color: theme.$mantle;
|
background-color: $mantle;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
column-gap: 4ch;
|
column-gap: 4ch;
|
||||||
@@ -89,48 +113,38 @@ footer {
|
|||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: Monocraft, monospace;
|
font-family: Monocraft, monospace;
|
||||||
background-color: theme.$mantle;
|
background-color: $mantle;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 .25rem;
|
padding: 0 .25rem;
|
||||||
color: theme.$subtext0;
|
color: $subtext0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transition: .3s ease;
|
transition: .3s ease;
|
||||||
background-color: theme.$crust;
|
background-color: $crust;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: theme.$surface0;
|
background-color: $surface0;
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: -.5rem -.5rem 1rem;
|
|
||||||
padding: .5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
|
||||||
background-color: theme.$surface1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + & {
|
& + & {
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
background-color: color.mix(theme.$surface0, theme.$red, 60%);
|
background-color: color.mix($surface0, $red, 60%);
|
||||||
}
|
}
|
||||||
&.orange {
|
&.orange {
|
||||||
background-color: color.mix(theme.$surface0, theme.$peach, 60%);
|
background-color: color.mix($surface0, $peach, 60%);
|
||||||
}
|
}
|
||||||
&.green {
|
&.green {
|
||||||
background-color: color.mix(theme.$surface0, theme.$green, 60%);
|
background-color: color.mix($surface0, $green, 60%);
|
||||||
&:hover { background-color: color.mix(theme.$surface1, theme.$green, 60%); }
|
&:hover { background-color: color.mix($surface1, $green, 60%); }
|
||||||
&:active { background-color: color.mix(theme.$mantle, theme.$green, 60%) !important; }
|
&:active { background-color: color.mix($mantle, $green, 60%) !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
& .header {
|
& .header {
|
||||||
@@ -192,39 +206,6 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.\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 {
|
::-webkit-scrollbar {
|
||||||
width: .5rem;
|
width: .5rem;
|
||||||
|
|
||||||
@@ -233,15 +214,15 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-track {
|
&-track {
|
||||||
background-color: theme.$base;
|
background-color: $base;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-thumb {
|
&-thumb {
|
||||||
background-color: theme.$overlay0;
|
background-color: $overlay0;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: theme.$overlay1;
|
background-color: $overlay1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,13 +13,6 @@
|
|||||||
></script>
|
></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="mock-email" content="admin@example.com">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include 'header.tmpl' %}
|
{% include 'header.tmpl' %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<div>lair.moe 🄯 2025 - 2026</div>
|
<div>lair.moe © 2025</div>
|
||||||
<div><a href="https://g.lair.moe/Sweetbread/lair.moe">{{ _('site source') }}</a></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>
|
<div>{{ _('contact us') }}: <a href="mailto:admin@lair.moe">admin@lair.moe</a></div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<header>
|
<header>
|
||||||
{%- if request.path != url_for('.index') %}
|
{%- if request.path != url_for('index') %}
|
||||||
<a href="{{ url_for('.index') }}">Lair</a>
|
<a href="{{ url_for('index') }}">Lair</a>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<div></div>
|
<div></div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
<div class="header-links">
|
<div class="header-links">
|
||||||
{%- for (l, t) in (
|
{%- for (l, t) in (
|
||||||
('.us', _('about us')),
|
('us', _('about us')),
|
||||||
('.host', _('about host'))
|
('host', _('about host'))
|
||||||
) %}
|
) %}
|
||||||
{%- if url_for(l) == request.path %}
|
{%- if url_for(l) == request.path %}
|
||||||
<strong>{{ t }}</strong>
|
<strong>{{ t }}</strong>
|
||||||
File renamed without changes.
@@ -49,4 +49,20 @@
|
|||||||
<span class="mono">200:ee1:bad2:1732:4b91:c3e3:2f08:29b3</span>
|
<span class="mono">200:ee1:bad2:1732:4b91:c3e3:2f08:29b3</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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.lair.moe" target="_blank"><strong>Baikal</strong></a>',
|
||||||
|
freshrss='<a href="https://rss.lair.moe" target="_blank"><strong>FreshRSS</strong></a>',
|
||||||
|
) | safe
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="webring">
|
||||||
|
<a class="block" href="https://webring.otomir23.me/lair/prev"><</a>
|
||||||
|
<a class="block" href="https://webring.otomir23.me/">Otoring</a>
|
||||||
|
<a class="block" href="https://webring.otomir23.me/lair/next">></a>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -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.lair.moe/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') }}">Lair</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block title %}О нас{% endblock %}
|
{% block title %}О нас{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<a href="{{ url_for('risdeveau.index') }}" class="block green">
|
<a href="{{ url_for('risdeveau') }}" class="block green">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img src="/static/icon/us/risdeveau.webp" class="icon"/>
|
<img src="/static/icon/us/risdeveau.webp" class="icon"/>
|
||||||
Sweetbread
|
Sweetbread
|
||||||
@@ -11,7 +11,15 @@
|
|||||||
Главный админ, занимается почти всеми сервисами. Создал этот сайт
|
Главный админ, занимается почти всеми сервисами. Создал этот сайт
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="block orange disabled">
|
<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">
|
<div class="header">
|
||||||
<img src="/static/icon/us/chest.webp" class="icon"/>
|
<img src="/static/icon/us/chest.webp" class="icon"/>
|
||||||
Chest
|
Chest
|
||||||
Reference in new issue
Block a user