forked from zovos/bot_tg
Update
This commit is contained in:
parent
b47e6bee81
commit
298a5d7d59
7 changed files with 450 additions and 32 deletions
116
games/uwu.py
116
games/uwu.py
|
|
@ -33,6 +33,7 @@ async def handle_uwu_cmd(message: types.Message):
|
||||||
|
|
||||||
tags += " order:random score:>300"
|
tags += " order:random score:>300"
|
||||||
|
|
||||||
|
async def fetch_uwu_post(tags: str) -> dict:
|
||||||
url = "https://e621.net/posts.json"
|
url = "https://e621.net/posts.json"
|
||||||
params = {
|
params = {
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
|
|
@ -47,28 +48,102 @@ async def handle_uwu_cmd(message: types.Message):
|
||||||
"User-Agent": f"Sex Bomba TG Bot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)"
|
"User-Agent": f"Sex Bomba TG Bot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with session.get(url, params=params, headers=headers, auth=auth) as resp:
|
||||||
async with session.get(url, params=params, headers=headers, auth=auth) as resp:
|
if resp.status != 200:
|
||||||
if resp.status != 200:
|
logger.error(f"e621 API Error: {resp.status}")
|
||||||
logger.error(f"e621 API Error: {resp.status}")
|
raise Exception("Не удалось получить картинку от сервера :(")
|
||||||
await message.reply("Не удалось получить картинку от сервера :(")
|
data = await resp.json()
|
||||||
return
|
|
||||||
data = await resp.json()
|
|
||||||
|
|
||||||
posts = data.get("posts", [])
|
posts = data.get("posts", [])
|
||||||
if not posts:
|
if not posts:
|
||||||
await message.reply("Ничего не найдено по этим тегам :(")
|
raise Exception("Ничего не найдено по этим тегам :(")
|
||||||
return
|
|
||||||
|
|
||||||
post = posts[0]
|
post = posts[0]
|
||||||
|
file_url = post.get("file", {}).get("url")
|
||||||
|
ext = post.get("file", {}).get("ext", "png")
|
||||||
|
if not file_url:
|
||||||
|
raise Exception("У найденного поста нет прямого URL изображения :(")
|
||||||
|
|
||||||
|
caption = random.choice(CAPTIONS)
|
||||||
|
return {
|
||||||
|
"url": file_url,
|
||||||
|
"ext": ext,
|
||||||
|
"caption": caption
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fetch_furtok_feed(safe: bool = True, page: int = 1) -> list[dict]:
|
||||||
|
rating = "rating:safe" if safe else "-rating:safe"
|
||||||
|
tags = f"{rating} animated order:random score:>200"
|
||||||
|
|
||||||
|
url = "https://e621.net/posts.json"
|
||||||
|
params = {
|
||||||
|
"tags": tags,
|
||||||
|
"limit": 10,
|
||||||
|
"page": page,
|
||||||
|
}
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if config.E621_LOGIN and config.E621_API_KEY:
|
||||||
|
auth = aiohttp.BasicAuth(config.E621_LOGIN, config.E621_API_KEY)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": f"Sex Bomba TG Bot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)"
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, params=params, headers=headers, auth=auth) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
logger.error(f"e621 API Error: {resp.status}")
|
||||||
|
raise Exception("Не удалось загрузить ленту :(")
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
posts = data.get("posts", [])
|
||||||
|
feed = []
|
||||||
|
for post in posts:
|
||||||
file_url = post.get("file", {}).get("url")
|
file_url = post.get("file", {}).get("url")
|
||||||
ext = post.get("file", {}).get("ext", "png")
|
ext = post.get("file", {}).get("ext", "")
|
||||||
if not file_url:
|
if not file_url or ext not in ("gif", "webm", "mp4"):
|
||||||
await message.reply("У найденного поста нет прямого URL изображения :(")
|
continue
|
||||||
return
|
sample_url = post.get("sample", {}).get("url") or file_url
|
||||||
|
feed.append({
|
||||||
|
"url": file_url,
|
||||||
|
"sample": sample_url,
|
||||||
|
"ext": ext,
|
||||||
|
"score": post.get("score", {}).get("total", 0),
|
||||||
|
"fav_count": post.get("fav_count", 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
return feed
|
||||||
|
|
||||||
|
async def handle_uwu_cmd(message: types.Message):
|
||||||
|
args = message.text.split(maxsplit=1)
|
||||||
|
|
||||||
|
if len(args) > 1 and args[1].strip() == "-h":
|
||||||
|
help_msg = (
|
||||||
|
"🐈 <b>Справка по тегам e621:</b>\n\n"
|
||||||
|
"Перечислите любые теги через пробел после команды.\n"
|
||||||
|
"• <b>rating</b>: <code>rating:safe</code> (безопасные), <code>rating:questionable</code>, <code>rating:explicit</code> (NSFW)\n"
|
||||||
|
"• <b>score</b>: <code>score:>500</code> (искать популярные посты с рейтингом больше 500)\n"
|
||||||
|
"• Исключить тег: поставьте минус, например <code>-fox</code>\n\n"
|
||||||
|
"<i>Пример:</i> <code>/uwu wolf -rating:explicit score:>100</code>\n"
|
||||||
|
"<i>По умолчанию:</i> <code>rating:safe score:>500 -animated</code>"
|
||||||
|
)
|
||||||
|
await message.reply(help_msg, parse_mode="HTML")
|
||||||
|
return
|
||||||
|
|
||||||
|
tags = "rating:safe score:>500 -animated"
|
||||||
|
if len(args) > 1:
|
||||||
|
tags = args[1].strip()
|
||||||
|
|
||||||
|
tags += " order:random score:>300"
|
||||||
|
|
||||||
|
try:
|
||||||
|
post = await fetch_uwu_post(tags)
|
||||||
|
file_url = post["url"]
|
||||||
|
ext = post["ext"]
|
||||||
|
caption = post["caption"]
|
||||||
|
|
||||||
caption = random.choice(CAPTIONS)
|
|
||||||
if ext in ("gif", "webm", "mp4"):
|
if ext in ("gif", "webm", "mp4"):
|
||||||
# Для анимаций: отправляем caption отдельным сообщением,
|
# Для анимаций: отправляем caption отдельным сообщением,
|
||||||
# а саму гифку/видео через URL напрямую — так Telegram
|
# а саму гифку/видео через URL напрямую — так Telegram
|
||||||
|
|
@ -84,4 +159,7 @@ async def handle_uwu_cmd(message: types.Message):
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error in uwu command")
|
logger.exception("Error in uwu command")
|
||||||
await message.reply("Произошла ошибка при обращении к API :(")
|
if str(e) in ["Не удалось получить картинку от сервера :(", "Ничего не найдено по этим тегам :(", "У найденного поста нет прямого URL изображения :("]:
|
||||||
|
await message.reply(str(e))
|
||||||
|
else:
|
||||||
|
await message.reply("Произошла ошибка при обращении к API :(")
|
||||||
|
|
|
||||||
25
main.py
25
main.py
|
|
@ -951,23 +951,30 @@ async def handle_help(message: Message):
|
||||||
async def handle_start(message: Message):
|
async def handle_start(message: Message):
|
||||||
webapp_url = os.getenv("WEBAPP_URL", "")
|
webapp_url = os.getenv("WEBAPP_URL", "")
|
||||||
if webapp_url:
|
if webapp_url:
|
||||||
kb = InlineKeyboardMarkup(inline_keyboard=[
|
if message.chat.type == "private":
|
||||||
[InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))]
|
kb = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
])
|
[InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))]
|
||||||
await message.answer("Йо, кент! Жми кнопку 👇", reply_markup=kb)
|
])
|
||||||
|
await message.answer("Йо, кент! Жми кнопку 👇", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await message.answer("Йо, кент! Mini App доступно только в личных сообщениях. Пиши мне в личку!")
|
||||||
else:
|
else:
|
||||||
await message.answer("Соси")
|
await message.answer("Соси")
|
||||||
|
|
||||||
async def handle_app_cmd(message: Message):
|
async def handle_app_cmd(message: Message):
|
||||||
"""Открыть Mini App — работает и в группах."""
|
"""Открыть Mini App."""
|
||||||
webapp_url = os.getenv("WEBAPP_URL", "")
|
webapp_url = os.getenv("WEBAPP_URL", "")
|
||||||
if not webapp_url:
|
if not webapp_url:
|
||||||
await message.answer("Mini App не настроен.")
|
await message.answer("Mini App не настроен.")
|
||||||
return
|
return
|
||||||
kb = InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))]
|
if message.chat.type == "private":
|
||||||
])
|
kb = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
await message.answer("⬇️ Жми кнопку чтобы открыть приложение", reply_markup=kb)
|
[InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))]
|
||||||
|
])
|
||||||
|
await message.answer("⬇️ Жми кнопку чтобы открыть приложение", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await message.answer("Братуха, встроенная кнопка Mini App пока работает только в личке (ограничение Telegram). Перейди в личные сообщения с ботом или используй прямую ссылку: https://t.me/FenyaBotTest_bot/app (если она настроена).")
|
||||||
|
|
||||||
async def handle_schedule_cmd(message: Message):
|
async def handle_schedule_cmd(message: Message):
|
||||||
now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
|
now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,13 @@ from games.casino import play_casino, get_user_length, spin_slots, MULTIPLIERS,
|
||||||
from games.betting import (
|
from games.betting import (
|
||||||
fetch_matches,
|
fetch_matches,
|
||||||
place_bet,
|
place_bet,
|
||||||
|
get_match_by_index,
|
||||||
|
resolve_match_outcome,
|
||||||
get_user_bets as _get_user_bets_raw,
|
get_user_bets as _get_user_bets_raw,
|
||||||
get_user_bet_history as _get_user_bet_history_raw,
|
get_user_bet_history as _get_user_bet_history_raw,
|
||||||
)
|
)
|
||||||
from webapp.auth import validate_init_data
|
from webapp.auth import validate_init_data
|
||||||
|
from games.uwu import fetch_uwu_post, fetch_furtok_feed
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -223,8 +226,19 @@ async def create_bet(
|
||||||
):
|
):
|
||||||
"""Разместить ставку на матч."""
|
"""Разместить ставку на матч."""
|
||||||
user_id = user["user_id"]
|
user_id = user["user_id"]
|
||||||
display_name = user["username"] or user["first_name"]
|
|
||||||
result = await place_bet(user_id, display_name, req.league, req.match_index, req.outcome, req.amount)
|
# 1. Найти матч по индексу
|
||||||
|
match = await get_match_by_index(req.league, req.match_index)
|
||||||
|
if not match:
|
||||||
|
raise HTTPException(status_code=404, detail="Матч не найден")
|
||||||
|
|
||||||
|
# 2. Преобразовать код исхода (1/X/2) в название команды
|
||||||
|
team = resolve_match_outcome(match, req.outcome)
|
||||||
|
if not team:
|
||||||
|
raise HTTPException(status_code=400, detail="Неверный исход ставки")
|
||||||
|
|
||||||
|
# 3. Разместить ставку (синхронная функция)
|
||||||
|
result = await asyncio.to_thread(place_bet, user_id, match, req.league, team, req.amount)
|
||||||
return {"message": result}
|
return {"message": result}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -352,6 +366,40 @@ async def get_schedule(day: int | None = None) -> list[SchedulePair]:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# --- UwU Арты ---
|
||||||
|
|
||||||
|
@app.get("/api/uwu")
|
||||||
|
async def get_uwu_api(
|
||||||
|
tags: str = "rating:safe score:>500 -animated",
|
||||||
|
user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Отправить случайный uwu арт по тегам."""
|
||||||
|
try:
|
||||||
|
tags += " order:random score:>300"
|
||||||
|
post = await fetch_uwu_post(tags)
|
||||||
|
return {"response": post}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("UwU API failed")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# --- FurTok Лента ---
|
||||||
|
|
||||||
|
@app.get("/api/furtok")
|
||||||
|
async def get_furtok_feed_api(
|
||||||
|
safe: bool = True,
|
||||||
|
page: int = 1,
|
||||||
|
user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Получить ленту анимированных постов для FurTok."""
|
||||||
|
try:
|
||||||
|
feed = await fetch_furtok_feed(safe=safe, page=page)
|
||||||
|
return {"feed": feed}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("FurTok API failed")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
# --- ИИ Чат ---
|
# --- ИИ Чат ---
|
||||||
|
|
||||||
@app.post("/api/talk")
|
@app.post("/api/talk")
|
||||||
|
|
|
||||||
|
|
@ -871,3 +871,137 @@ body::before {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── FurTok Feed ── */
|
||||||
|
|
||||||
|
.furtok-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
height: calc(100vh - var(--nav-height));
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-feed {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-feed::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
.furtok-card {
|
||||||
|
height: 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-card video,
|
||||||
|
.furtok-card img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(transparent, rgba(0,0,0,0.7));
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: linear-gradient(rgba(0,0,0,0.5), transparent);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-toggle input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 11px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-toggle input[type="checkbox"]::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-toggle input[type="checkbox"]:checked {
|
||||||
|
background: var(--green-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-toggle input[type="checkbox"]:checked::after {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.furtok-loader {
|
||||||
|
height: 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@
|
||||||
<span class="nav-icon">📅</span>
|
<span class="nav-icon">📅</span>
|
||||||
<span class="nav-label">Пары</span>
|
<span class="nav-label">Пары</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="nav-btn" data-page="furtok">
|
||||||
|
<span class="nav-icon">🐺</span>
|
||||||
|
<span class="nav-label">FurTok</span>
|
||||||
|
</button>
|
||||||
<button class="nav-btn" data-page="chat">
|
<button class="nav-btn" data-page="chat">
|
||||||
<span class="nav-icon">💬</span>
|
<span class="nav-icon">💬</span>
|
||||||
<span class="nav-label">Чат</span>
|
<span class="nav-label">Чат</span>
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,7 @@ export const API = {
|
||||||
playPenis: () => api('/api/penis', { method: 'POST' }),
|
playPenis: () => api('/api/penis', { method: 'POST' }),
|
||||||
getTop: () => api('/api/top'),
|
getTop: () => api('/api/top'),
|
||||||
getSchedule: (day) => api(`/api/schedule${day != null ? `?day=${day}` : ''}`),
|
getSchedule: (day) => api(`/api/schedule${day != null ? `?day=${day}` : ''}`),
|
||||||
|
getUwu: (tags) => api(`/api/uwu?tags=${encodeURIComponent(tags || 'rating:safe score:>500 -animated')}`),
|
||||||
|
getFurtokFeed: (safe = true, page = 1) => api(`/api/furtok?safe=${safe}&page=${page}`),
|
||||||
talk: (text) => api('/api/talk', { method: 'POST', body: { text } }),
|
talk: (text) => api('/api/talk', { method: 'POST', body: { text } }),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ const navBtns = document.querySelectorAll('.nav-btn');
|
||||||
function navigate(page, data = null) {
|
function navigate(page, data = null) {
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
haptic();
|
haptic();
|
||||||
|
// Убираем оверлеи при смене страницы
|
||||||
|
document.querySelector('.bet-slip')?.remove();
|
||||||
|
document.querySelector('.result-overlay')?.remove();
|
||||||
|
document.querySelector('.furtok-wrapper')?.remove();
|
||||||
navBtns.forEach(b => b.classList.toggle('active', b.dataset.page === page));
|
navBtns.forEach(b => b.classList.toggle('active', b.dataset.page === page));
|
||||||
container.style.animation = 'none';
|
container.style.animation = 'none';
|
||||||
container.offsetHeight;
|
container.offsetHeight;
|
||||||
|
|
@ -45,6 +49,7 @@ function navigate(page, data = null) {
|
||||||
case 'betting': renderBetting(data); break;
|
case 'betting': renderBetting(data); break;
|
||||||
case 'casino': renderCasino(); break;
|
case 'casino': renderCasino(); break;
|
||||||
case 'schedule': renderSchedule(); break;
|
case 'schedule': renderSchedule(); break;
|
||||||
|
case 'furtok': renderFurtok(); break;
|
||||||
case 'chat': renderChat(); break;
|
case 'chat': renderChat(); break;
|
||||||
default: renderHome();
|
default: renderHome();
|
||||||
}
|
}
|
||||||
|
|
@ -491,6 +496,146 @@ async function renderSchedule() {
|
||||||
loadDay(todayIdx);
|
loadDay(todayIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
// PAGE: FURTOK
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
|
||||||
|
let furtokSafe = true;
|
||||||
|
let furtokPage = 1;
|
||||||
|
let furtokLoading = false;
|
||||||
|
let furtokWrapper = null;
|
||||||
|
|
||||||
|
function renderFurtok() {
|
||||||
|
// Очищаем основной контейнер и создаём полноэкранную обёртку
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Удаляем старый wrapper если есть
|
||||||
|
document.querySelector('.furtok-wrapper')?.remove();
|
||||||
|
|
||||||
|
furtokPage = 1;
|
||||||
|
furtokLoading = false;
|
||||||
|
|
||||||
|
furtokWrapper = document.createElement('div');
|
||||||
|
furtokWrapper.className = 'furtok-wrapper';
|
||||||
|
furtokWrapper.innerHTML = `
|
||||||
|
<div class="furtok-header">
|
||||||
|
<div class="furtok-title">🐺 FurTok</div>
|
||||||
|
<label class="furtok-toggle">
|
||||||
|
<span>Safe</span>
|
||||||
|
<input type="checkbox" id="furtok-safe" ${furtokSafe ? 'checked' : ''}>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="furtok-feed" id="furtok-feed"></div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(furtokWrapper);
|
||||||
|
|
||||||
|
const feed = document.getElementById('furtok-feed');
|
||||||
|
const safeToggle = document.getElementById('furtok-safe');
|
||||||
|
|
||||||
|
safeToggle.addEventListener('change', () => {
|
||||||
|
furtokSafe = safeToggle.checked;
|
||||||
|
furtokPage = 1;
|
||||||
|
feed.innerHTML = '';
|
||||||
|
loadFurtokPage(feed);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Infinite scroll
|
||||||
|
feed.addEventListener('scroll', () => {
|
||||||
|
if (furtokLoading) return;
|
||||||
|
if (feed.scrollTop + feed.clientHeight >= feed.scrollHeight - 200) {
|
||||||
|
furtokPage++;
|
||||||
|
loadFurtokPage(feed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Первая загрузка
|
||||||
|
loadFurtokPage(feed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFurtokPage(feedEl) {
|
||||||
|
if (furtokLoading) return;
|
||||||
|
furtokLoading = true;
|
||||||
|
|
||||||
|
// Лоадер
|
||||||
|
const loader = document.createElement('div');
|
||||||
|
loader.className = 'furtok-loader';
|
||||||
|
loader.textContent = '⏳ Загрузка...';
|
||||||
|
feedEl.appendChild(loader);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await API.getFurtokFeed(furtokSafe, furtokPage);
|
||||||
|
loader.remove();
|
||||||
|
|
||||||
|
if (!res.feed || res.feed.length === 0) {
|
||||||
|
if (furtokPage === 1) {
|
||||||
|
feedEl.innerHTML = `<div class="furtok-loader">Ничего не найдено</div>`;
|
||||||
|
}
|
||||||
|
furtokLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.feed.forEach(post => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'furtok-card';
|
||||||
|
|
||||||
|
let mediaHtml = '';
|
||||||
|
if (post.ext === 'gif') {
|
||||||
|
mediaHtml = `<img src="${post.url}" loading="lazy">`;
|
||||||
|
} else {
|
||||||
|
mediaHtml = `<video src="${post.url}" loop playsinline preload="metadata" muted></video>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
${mediaHtml}
|
||||||
|
<div class="furtok-overlay">
|
||||||
|
<div class="furtok-stats">
|
||||||
|
<span>⭐ ${post.score}</span>
|
||||||
|
<span>❤️ ${post.fav_count}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Тап по видео = unmute + play/pause
|
||||||
|
const video = card.querySelector('video');
|
||||||
|
if (video) {
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
video.muted = false;
|
||||||
|
if (video.paused) video.play();
|
||||||
|
else video.pause();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
feedEl.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
// IntersectionObserver для автоплейа
|
||||||
|
setupAutoplay(feedEl);
|
||||||
|
haptic('success');
|
||||||
|
} catch (e) {
|
||||||
|
loader.remove();
|
||||||
|
haptic('error');
|
||||||
|
if (furtokPage === 1) {
|
||||||
|
feedEl.innerHTML = `<div class="furtok-loader">❌ ${e.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
furtokLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAutoplay(feedEl) {
|
||||||
|
const videos = feedEl.querySelectorAll('.furtok-card video');
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.play().catch(() => {});
|
||||||
|
} else {
|
||||||
|
entry.target.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { root: feedEl, threshold: 0.6 });
|
||||||
|
|
||||||
|
videos.forEach(v => observer.observe(v));
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════
|
// ══════════════════════════════════════════
|
||||||
// PAGE: CHAT
|
// PAGE: CHAT
|
||||||
// ══════════════════════════════════════════
|
// ══════════════════════════════════════════
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue