Compare commits

...
3 Commits
6 changed files with 154 additions and 37 deletions

No files matched your search

@@ -1,6 +1,6 @@
from pyrogram import Client from pyrogram import Client
from pyrogram.errors import FileIdInvalid, FileReferenceExpired, FileReferenceInvalid from pyrogram.errors import FileIdInvalid, FileReferenceExpired, FileReferenceInvalid
from pyrogram.types import Photo from pyrogram.types import ChatPhoto
from userbot.modules.avatars.repository import get_avatar_file, mark_avatar_downloaded from userbot.modules.avatars.repository import get_avatar_file, mark_avatar_downloaded
from userbot.modules.download import download_bytes from userbot.modules.download import download_bytes
@@ -15,12 +15,9 @@ async def _fresh_file_id(client: Client, owner_id: int, unique_id: str) -> str |
photo = chat.photo if chat else None photo = chat.photo if chat else None
if photo and photo.big_photo_unique_id == unique_id: if photo and photo.big_photo_unique_id == unique_id:
return photo.big_file_id return photo.big_file_id
photos = client.get_chat_photos(owner_id) async for item in client.get_chat_photos(owner_id):
if photos is None: if isinstance(item, ChatPhoto) and item.big_photo_unique_id == unique_id:
return None return item.big_file_id
async for item in photos:
if isinstance(item, Photo) and item.file_unique_id == unique_id:
return item.file_id
return None return None
+3
View File
@@ -22,6 +22,7 @@ from utils.read.models import (
StickerView, StickerView,
WebPageView, WebPageView,
) )
from utils.read.rich_text import rich_text
_MEDIA_KEYS = ( _MEDIA_KEYS = (
"photo", "photo",
@@ -382,6 +383,8 @@ def build_message_view(
row: asyncpg.Record, raw: dict[str, Any], media: list[MediaRef] row: asyncpg.Record, raw: dict[str, Any], media: list[MediaRef]
) -> MessageView: ) -> MessageView:
base = _base_fields(row) base = _base_fields(row)
if base["text"] is None:
base["text"] = rich_text(raw.get("rich_message"))
via_bot = raw.get("via_bot") via_bot = raw.get("via_bot")
sticker = _sticker(raw) sticker = _sticker(raw)
try: try:
+61
View File
@@ -0,0 +1,61 @@
from typing import Any
CELL_GAP = " | "
_TEXT_KEYS = ("summary", "text", "expression", "credit")
_INLINE_KEYS = ("text", "alternative_text", "expression")
def _button(button: dict[str, Any]) -> str:
label = _inline(button.get("text"))
url = button.get("url")
return f"{label} ({url})" if isinstance(url, str) else label
def _inline(value: object) -> str:
if isinstance(value, str):
return value
if isinstance(value, list):
return "".join(_inline(item) for item in value)
if not isinstance(value, dict):
return ""
button = value.get("button")
if isinstance(button, dict):
return _button(button)
return next((_inline(value[key]) for key in _INLINE_KEYS if key in value), "")
def _children(block: dict[str, Any], key: str) -> list[Any]:
value = block.get(key)
return value if isinstance(value, list) else []
def _row(row: object) -> str:
cells = row if isinstance(row, list) else []
return CELL_GAP.join(
_inline(cell.get("text")) for cell in cells if isinstance(cell, dict)
)
def _block(block: object) -> list[str]:
if not isinstance(block, dict):
return []
lines = [_inline(block[key]) for key in _TEXT_KEYS if key in block]
lines += [_row(row) for row in _children(block, "cells")]
lines += [
_button(button)
for button in _children(block, "buttons")
if isinstance(button, dict)
]
for key in ("items", "blocks"):
for child in _children(block, key):
lines += _block(child)
lines += _block(block.get("caption"))
lines = [line for line in lines if line]
label = block.get("label")
if isinstance(label, str) and lines:
lines[0] = f"{label} {lines[0]}"
return lines
def rich_text(rich: object) -> str | None:
return "\n".join(_block(rich)) or None
@@ -80,6 +80,10 @@
} }
} }
export function refresh() {
return load();
}
onMount(load); onMount(load);
</script> </script>
@@ -102,8 +106,13 @@
{#if confirming} {#if confirming}
<div class="text"> <div class="text">
<span> <span>
{filesLabel(files.files)}, {formatBytes(files.bytes)}, уйдут в корзину {filesLabel(files.files)}, {formatBytes(files.bytes)},
и перестанут скачиваться сами. {plural(
files.files,
"уйдёт в корзину и перестанет скачиваться сам",
"уйдут в корзину и перестанут скачиваться сами",
"уйдут в корзину и перестанут скачиваться сами"
)}.
</span> </span>
{#if files.gone > 0} {#if files.gone > 0}
<small class="warn"> <small class="warn">
@@ -175,6 +184,18 @@
</Button> </Button>
{/if} {/if}
</div> </div>
{:else if label && files && files.kept > 0}
<div class="files">
<div class="text">
<span>{label}</span>
<small>
{filesLabel(files.kept)}
есть и в других чатах, убрать
{plural(files.kept, "его", "их", "их")}
можно из списка ниже
</small>
</div>
</div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount, tick } from "svelte";
import { SvelteSet } from "svelte/reactivity"; import { SvelteSet } from "svelte/reactivity";
import { mediaUrl } from "#lib/api/client.js"; import { mediaUrl } from "#lib/api/client.js";
import { downloadMedia } from "#lib/api/download.js"; import { downloadMedia } from "#lib/api/download.js";
@@ -61,7 +61,7 @@
let summary = $state<StorageSummary | null>(null); let summary = $state<StorageSummary | null>(null);
let files = $state<StorageFile[]>([]); let files = $state<StorageFile[]>([]);
let heavyChats = $state<StorageChat[]>([]); let heavyChats = $state<StorageChat[]>([]);
let scopeRevision = $state(0); let scope = $state<ReturnType<typeof ScopeFiles>>();
let group = $state<StorageGroupName | null>(null); let group = $state<StorageGroupName | null>(null);
let loading = $state(false); let loading = $state(false);
let failed = $state(false); let failed = $state(false);
@@ -75,6 +75,7 @@
let viewerIndex = $state(0); let viewerIndex = $state(0);
const selected = new SvelteSet<number>(); const selected = new SvelteSet<number>();
let token = 0; let token = 0;
let scroller: HTMLElement | null = null;
let undoTimer: ReturnType<typeof setTimeout> | undefined; let undoTimer: ReturnType<typeof setTimeout> | undefined;
let undoShownAt = 0; let undoShownAt = 0;
@@ -130,10 +131,10 @@
} }
} }
async function loadPage(current: number): Promise<number> { async function loadPage(current: number, limit: number): Promise<number> {
if (listing === "chats") { if (listing === "chats") {
const batch = await getStorageChats(group, { const batch = await getStorageChats(group, {
limit: PAGE, limit,
offset: heavyChats.length, offset: heavyChats.length,
}); });
if (current === token) { if (current === token) {
@@ -144,7 +145,7 @@
} }
const batch = await getStorageFiles( const batch = await getStorageFiles(
group, group,
{ limit: PAGE, offset: files.length }, { limit, offset: files.length },
chatId chatId
); );
if (current === token) { if (current === token) {
@@ -154,7 +155,7 @@
return batch.length; return batch.length;
} }
async function loadMore() { async function loadMore(limit = PAGE) {
if (loading || done) { if (loading || done) {
return; return;
} }
@@ -162,9 +163,9 @@
loading = true; loading = true;
failed = false; failed = false;
try { try {
const loaded = await loadPage(current); const loaded = await loadPage(current, limit);
if (current === token) { if (current === token) {
done = loaded < PAGE; done = loaded < limit;
} }
} catch { } catch {
if (current === token) { if (current === token) {
@@ -177,15 +178,34 @@
} }
} }
function reload() { function reload(limit = PAGE) {
token += 1; token += 1;
files = []; files = [];
heavyChats = []; heavyChats = [];
done = false; done = false;
loading = false; loading = false;
selected.clear(); selected.clear();
loadSummary(); return Promise.all([loadSummary(), loadMore(limit)]);
loadMore(); }
async function resume() {
const from = chatId === null ? storageUi.takeResume() : null;
await reload(Math.ceil(Math.max(1, from?.count ?? 0) / PAGE) * PAGE);
if (from && scroller) {
await tick();
scroller.scrollTop = from.top;
}
}
function openChat(id: number) {
storageUi.openChat(id, {
count: heavyChats.length,
top: scroller?.scrollTop ?? 0,
});
}
function hold(node: HTMLElement) {
scroller = node.parentElement;
} }
function toggle(mediaId: number) { function toggle(mediaId: number) {
@@ -226,7 +246,7 @@
} }
files = files.filter((file) => !moved.has(file.media_id)); files = files.filter((file) => !moved.has(file.media_id));
selected.clear(); selected.clear();
scopeRevision += 1; scope?.refresh();
showUndo(outcome, trashed); showUndo(outcome, trashed);
if (outcome.blocked.length > 0) { if (outcome.blocked.length > 0) {
problem = "Файл занят в сторис или аватаре и остался на диске"; problem = "Файл занят в сторис или аватаре и остался на диске";
@@ -250,7 +270,7 @@
files = [...files, ...restored.files].sort( files = [...files, ...restored.files].sort(
(a, b) => b.file_size - a.file_size (a, b) => b.file_size - a.file_size
); );
scopeRevision += 1; scope?.refresh();
await loadSummary(); await loadSummary();
} catch { } catch {
problem = "Не удалось вернуть файлы"; problem = "Не удалось вернуть файлы";
@@ -301,16 +321,16 @@
} }
} }
onMount(reload); onMount(resume);
onDestroy(() => clearTimeout(undoTimer)); onDestroy(() => clearTimeout(undoTimer));
</script> </script>
<div class="storage"> <div class="storage" {@attach hold}>
{#if summary} {#if summary}
{#if summary.files > 0} {#if summary.files > 0 || chatId !== null}
<section class="summary"> <section class="summary">
<div class="hero"> <div class="hero">
<span class="total">{formatBytes(summary.bytes)}</span> <span class="total">{formatBytes(summary.bytes) || "0 B"}</span>
<span class="count"> <span class="count">
{filesLabel(summary.files)} {filesLabel(summary.files)}
{#if chatId !== null} {#if chatId !== null}
@@ -322,7 +342,7 @@
{/if} {/if}
</span> </span>
</div> </div>
<div class="stack"> <div class="stack" class:bare={groups.length === 0}>
{#each groups as item (item.group)} {#each groups as item (item.group)}
<span <span
title="{GROUP_LABELS[item.group]}: {formatBytes(item.bytes)}" title="{GROUP_LABELS[item.group]}: {formatBytes(item.bytes)}"
@@ -392,7 +412,6 @@
{/each} {/each}
</div> </div>
{:else} {:else}
{#key scopeRevision}
<ScopeFiles <ScopeFiles
label="Все файлы чата" label="Все файлы чата"
onchange={() => { onchange={() => {
@@ -400,8 +419,8 @@
reload(); reload();
}} }}
target={{ chatId }} target={{ chatId }}
bind:this={scope}
/> />
{/key}
{/if} {/if}
{/if} {/if}
@@ -423,15 +442,12 @@
<StorageChatRow <StorageChatRow
{chat} {chat}
hasAvatar={chats.byId(chat.chat_id)?.has_avatar ?? false} hasAvatar={chats.byId(chat.chat_id)?.has_avatar ?? false}
onopen={() => storageUi.openChat(chat.chat_id)} onopen={() => openChat(chat.chat_id)}
title={chatTitle(chat.chat_id)} title={chatTitle(chat.chat_id)}
weight={heaviestChat > 0 ? chat.bytes / heaviestChat : 0} weight={heaviestChat > 0 ? chat.bytes / heaviestChat : 0}
> >
{#snippet menu()} {#snippet menu()}
<ContextMenuItem <ContextMenuItem icon="data" onselect={() => openChat(chat.chat_id)}>
icon="data"
onselect={() => storageUi.openChat(chat.chat_id)}
>
Файлы чата Файлы чата
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem icon="reply" onselect={() => visit(chat)}> <ContextMenuItem icon="reply" onselect={() => visit(chat)}>
@@ -491,12 +507,14 @@
{:else if failed} {:else if failed}
<div class="state"> <div class="state">
<p>Не удалось загрузить хранилище.</p> <p>Не удалось загрузить хранилище.</p>
<Button onclick={reload} smaller variant="text">Повторить</Button> <Button onclick={() => reload()} smaller variant="text">
Повторить
</Button>
</div> </div>
{:else if shown === 0} {:else if shown === 0}
<div class="state"><p>{emptiness}</p></div> <div class="state"><p>{emptiness}</p></div>
{:else if !done} {:else if !done}
<button class="more" onclick={loadMore} type="button"> <button class="more" onclick={() => loadMore()} type="button">
Показать ещё Показать ещё
</button> </button>
{/if} {/if}
@@ -599,6 +617,11 @@
height: 0.625rem; height: 0.625rem;
margin-top: 0.875rem; margin-top: 0.875rem;
&.bare {
border-radius: 0.3125rem;
background-color: rgba(var(--color-text-secondary-rgb), 0.16);
}
span { span {
min-width: 0.1875rem; min-width: 0.1875rem;
border-radius: 2px; border-radius: 2px;
+13 -1
View File
@@ -2,9 +2,15 @@ import { ui } from "#lib/stores/ui.svelte.js";
export type StorageListing = "files" | "chats"; export type StorageListing = "files" | "chats";
interface Resume {
count: number;
top: number;
}
function createStorageUi() { function createStorageUi() {
let listing = $state<StorageListing>("files"); let listing = $state<StorageListing>("files");
let chatId = $state<number | null>(null); let chatId = $state<number | null>(null);
let resume: Resume | null = null;
return { return {
get chatId() { get chatId() {
@@ -16,10 +22,16 @@ function createStorageUi() {
set listing(next: StorageListing) { set listing(next: StorageListing) {
listing = next; listing = next;
}, },
openChat(id: number) { openChat(id: number, from: Resume) {
chatId = id; chatId = id;
resume = from;
ui.openPanel("storage-chat"); ui.openPanel("storage-chat");
}, },
takeResume(): Resume | null {
const taken = resume;
resume = null;
return taken;
},
}; };
} }