diff --git a/games/uwu.py b/games/uwu.py index 9b7db44..6a81614 100644 --- a/games/uwu.py +++ b/games/uwu.py @@ -94,9 +94,11 @@ async def fetch_uwu_post(tags: str) -> dict: "caption": caption } -async def fetch_furtok_feed(safe: bool = True, page: int = 1) -> list[dict]: +async def fetch_furtok_feed(safe: bool = True, page: int = 1, extra_tags: str = "") -> list[dict]: rating = "rating:safe" if safe else "-rating:safe" tags = f"{rating} animated order:random score:>200" + if extra_tags and extra_tags.strip(): + tags = f"{extra_tags.strip()} animated order:random score:>200" url = "https://e621.net/posts.json" params = { diff --git a/webapp/api.py b/webapp/api.py index 65667fe..1cb4133 100644 --- a/webapp/api.py +++ b/webapp/api.py @@ -389,11 +389,12 @@ async def get_uwu_api( async def get_furtok_feed_api( safe: bool = True, page: int = 1, + tags: str = "", user: dict = Depends(get_current_user), ): """Получить ленту анимированных постов для FurTok.""" try: - feed = await fetch_furtok_feed(safe=safe, page=page) + feed = await fetch_furtok_feed(safe=safe, page=page, extra_tags=tags) return {"feed": feed} except Exception as e: logger.exception("FurTok API failed") diff --git a/webapp/frontend/css/style.css b/webapp/frontend/css/style.css index 2ef2e7b..d218b2a 100644 --- a/webapp/frontend/css/style.css +++ b/webapp/frontend/css/style.css @@ -891,6 +891,8 @@ body::before { overflow-y: scroll; scroll-snap-type: y mandatory; scrollbar-width: none; + overscroll-behavior: none; + touch-action: none; } .furtok-feed::-webkit-scrollbar { display: none; } @@ -924,11 +926,21 @@ body::before { padding: 16px; background: linear-gradient(transparent, rgba(0,0,0,0.7)); display: flex; - align-items: flex-end; - justify-content: space-between; + flex-direction: column; + gap: 8px; + align-items: flex-start; pointer-events: none; } +.furtok-caption { + font-size: 15px; + font-weight: 700; + color: white; + text-shadow: 0 1px 6px rgba(0,0,0,0.8); + max-width: 80%; + line-height: 1.3; +} + .furtok-stats { display: flex; gap: 16px; @@ -937,6 +949,7 @@ body::before { color: rgba(255,255,255,0.85); } + .furtok-header { position: absolute; top: 0; @@ -966,6 +979,31 @@ body::before { color: rgba(255,255,255,0.8); } +.furtok-header-right { + display: flex; + align-items: center; + gap: 10px; +} + +.furtok-gear-btn { + background: rgba(255,255,255,0.15); + border: none; + border-radius: 50%; + width: 34px; + height: 34px; + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; +} + +.furtok-gear-btn:hover, +.furtok-gear-btn:active { + background: rgba(255,255,255,0.3); +} + .furtok-toggle input[type="checkbox"] { appearance: none; -webkit-appearance: none; @@ -998,6 +1036,68 @@ body::before { transform: translateX(18px); } +/* Tags editor panel */ +.furtok-tags-panel { + position: absolute; + top: 48px; + left: 0; + right: 0; + z-index: 6; + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 14px; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.furtok-tags-row { + display: flex; + gap: 8px; +} + +.furtok-tags-input { + flex: 1; + padding: 8px 12px; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + background: rgba(255,255,255,0.1); + color: white; + font-size: 13px; + outline: none; + transition: border-color 0.2s; +} + +.furtok-tags-input::placeholder { + color: rgba(255,255,255,0.4); +} + +.furtok-tags-input:focus { + border-color: var(--accent); +} + +.furtok-tags-apply { + padding: 8px 14px; + background: var(--accent); + border: none; + border-radius: 8px; + color: white; + font-size: 16px; + cursor: pointer; + transition: opacity 0.2s; +} + +.furtok-tags-apply:active { + opacity: 0.7; +} + +.furtok-tags-hint { + font-size: 11px; + color: rgba(255,255,255,0.45); + line-height: 1.3; +} + .furtok-loader { height: 100%; scroll-snap-align: start; diff --git a/webapp/frontend/js/api.js b/webapp/frontend/js/api.js index a3b264b..2d5522f 100644 --- a/webapp/frontend/js/api.js +++ b/webapp/frontend/js/api.js @@ -41,6 +41,6 @@ export const API = { getTop: () => api('/api/top'), 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}`), + getFurtokFeed: (safe = true, page = 1, tags = '') => api(`/api/furtok?safe=${safe}&page=${page}${tags ? '&tags=' + encodeURIComponent(tags) : ''}`), talk: (text) => api('/api/talk', { method: 'POST', body: { text } }), }; diff --git a/webapp/frontend/js/app.js b/webapp/frontend/js/app.js index 5157a91..58b2ce6 100644 --- a/webapp/frontend/js/app.js +++ b/webapp/frontend/js/app.js @@ -504,26 +504,78 @@ let furtokSafe = true; let furtokPage = 1; let furtokLoading = false; let furtokWrapper = null; +let furtokCurrentIndex = 0; +let furtokCards = []; +let furtokCustomTags = ''; + +// Подписи из uwu.py — рандомно появляются на карточках +const FURTOK_CAPTIONS = [ + "Ня :3", "uwu", "OwO", "Мурк :3", "Мяу~", "Фыр-фыр", "Лапки! 🐾", + "Какой пушистик! :3", "Милота!", "Гав!", "Ауф", "Няшно!", + "Смотри какая прелесть", "owo what's this", "Твой пушистый друг", + "🦊", "🐶", "🐱", "мур-мяу", "пуньк", "кусь", ">w<", "^w^", + "Хвостатый сюрприз", "Держи пушистика :3", "Оуо", "Ави!", "UwU", "~nya", + "Господи, опять e621...", "Осуждаю, но смотрю", "Мама, я фурри", + "Держи своего кринж-пушистика", "Надеюсь, тебе не стыдно", + "Товарищ майор уже выехал", "Слишком много интернета на сегодня", + "Для этого и придумали интернет", "*Тяжелый вздох*", + "Лучше бы на завод пошел", "Опять дрочи... ОЙ ТО ЕСТЬ НЯ :3", + "Удали интернет", "И зачем я это только парсю...", + "Смотри, но только никому не рассказывай", "Эххх... uwu...", + "В дурке сегодня день открытых дверей", + "Я нейросеть, помогите, меня держат в заложниках" +]; + +function randomCaption() { + return FURTOK_CAPTIONS[Math.floor(Math.random() * FURTOK_CAPTIONS.length)]; +} + +function _furtokGetTags() { + // Если кастомные теги заданы — используем их (safe управляется юзером через теги) + if (furtokCustomTags.trim()) return furtokCustomTags.trim(); + // Иначе стандартный запрос с safe-переключателем + const rating = furtokSafe ? 'rating:safe' : '-rating:safe'; + return `${rating} score:>200`; +} + +function _furtokReload(feed) { + furtokPage = 1; + furtokCurrentIndex = 0; + furtokCards = []; + feed.innerHTML = ''; + loadFurtokPage(feed); +} function renderFurtok() { - // Очищаем основной контейнер и создаём полноэкранную обёртку container.innerHTML = ''; - - // Удаляем старый wrapper если есть document.querySelector('.furtok-wrapper')?.remove(); furtokPage = 1; furtokLoading = false; + furtokCurrentIndex = 0; + furtokCards = []; furtokWrapper = document.createElement('div'); furtokWrapper.className = 'furtok-wrapper'; furtokWrapper.innerHTML = `