Compare commits
3
Commits
7f124059be
...
b8a38092eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8a38092eb | ||
|
|
2836dcb3a9 | ||
|
|
01cf01b629 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
from pyrogram import Client
|
||||
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.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
|
||||
if photo and photo.big_photo_unique_id == unique_id:
|
||||
return photo.big_file_id
|
||||
photos = client.get_chat_photos(owner_id)
|
||||
if photos is None:
|
||||
return None
|
||||
async for item in photos:
|
||||
if isinstance(item, Photo) and item.file_unique_id == unique_id:
|
||||
return item.file_id
|
||||
async for item in client.get_chat_photos(owner_id):
|
||||
if isinstance(item, ChatPhoto) and item.big_photo_unique_id == unique_id:
|
||||
return item.big_file_id
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from utils.read.models import (
|
||||
StickerView,
|
||||
WebPageView,
|
||||
)
|
||||
from utils.read.rich_text import rich_text
|
||||
|
||||
_MEDIA_KEYS = (
|
||||
"photo",
|
||||
@@ -382,6 +383,8 @@ def build_message_view(
|
||||
row: asyncpg.Record, raw: dict[str, Any], media: list[MediaRef]
|
||||
) -> MessageView:
|
||||
base = _base_fields(row)
|
||||
if base["text"] is None:
|
||||
base["text"] = rich_text(raw.get("rich_message"))
|
||||
via_bot = raw.get("via_bot")
|
||||
sticker = _sticker(raw)
|
||||
try:
|
||||
|
||||
@@ -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);
|
||||
</script>
|
||||
|
||||
@@ -102,8 +106,13 @@
|
||||
{#if confirming}
|
||||
<div class="text">
|
||||
<span>
|
||||
{filesLabel(files.files)}, {formatBytes(files.bytes)}, уйдут в корзину
|
||||
и перестанут скачиваться сами.
|
||||
{filesLabel(files.files)}, {formatBytes(files.bytes)},
|
||||
{plural(
|
||||
files.files,
|
||||
"уйдёт в корзину и перестанет скачиваться сам",
|
||||
"уйдут в корзину и перестанут скачиваться сами",
|
||||
"уйдут в корзину и перестанут скачиваться сами"
|
||||
)}.
|
||||
</span>
|
||||
{#if files.gone > 0}
|
||||
<small class="warn">
|
||||
@@ -175,6 +184,18 @@
|
||||
</Button>
|
||||
{/if}
|
||||
</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}
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import { mediaUrl } from "#lib/api/client.js";
|
||||
import { downloadMedia } from "#lib/api/download.js";
|
||||
@@ -61,7 +61,7 @@
|
||||
let summary = $state<StorageSummary | null>(null);
|
||||
let files = $state<StorageFile[]>([]);
|
||||
let heavyChats = $state<StorageChat[]>([]);
|
||||
let scopeRevision = $state(0);
|
||||
let scope = $state<ReturnType<typeof ScopeFiles>>();
|
||||
let group = $state<StorageGroupName | null>(null);
|
||||
let loading = $state(false);
|
||||
let failed = $state(false);
|
||||
@@ -75,6 +75,7 @@
|
||||
let viewerIndex = $state(0);
|
||||
const selected = new SvelteSet<number>();
|
||||
let token = 0;
|
||||
let scroller: HTMLElement | null = null;
|
||||
let undoTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
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") {
|
||||
const batch = await getStorageChats(group, {
|
||||
limit: PAGE,
|
||||
limit,
|
||||
offset: heavyChats.length,
|
||||
});
|
||||
if (current === token) {
|
||||
@@ -144,7 +145,7 @@
|
||||
}
|
||||
const batch = await getStorageFiles(
|
||||
group,
|
||||
{ limit: PAGE, offset: files.length },
|
||||
{ limit, offset: files.length },
|
||||
chatId
|
||||
);
|
||||
if (current === token) {
|
||||
@@ -154,7 +155,7 @@
|
||||
return batch.length;
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
async function loadMore(limit = PAGE) {
|
||||
if (loading || done) {
|
||||
return;
|
||||
}
|
||||
@@ -162,9 +163,9 @@
|
||||
loading = true;
|
||||
failed = false;
|
||||
try {
|
||||
const loaded = await loadPage(current);
|
||||
const loaded = await loadPage(current, limit);
|
||||
if (current === token) {
|
||||
done = loaded < PAGE;
|
||||
done = loaded < limit;
|
||||
}
|
||||
} catch {
|
||||
if (current === token) {
|
||||
@@ -177,15 +178,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
function reload(limit = PAGE) {
|
||||
token += 1;
|
||||
files = [];
|
||||
heavyChats = [];
|
||||
done = false;
|
||||
loading = false;
|
||||
selected.clear();
|
||||
loadSummary();
|
||||
loadMore();
|
||||
return Promise.all([loadSummary(), loadMore(limit)]);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -226,7 +246,7 @@
|
||||
}
|
||||
files = files.filter((file) => !moved.has(file.media_id));
|
||||
selected.clear();
|
||||
scopeRevision += 1;
|
||||
scope?.refresh();
|
||||
showUndo(outcome, trashed);
|
||||
if (outcome.blocked.length > 0) {
|
||||
problem = "Файл занят в сторис или аватаре и остался на диске";
|
||||
@@ -250,7 +270,7 @@
|
||||
files = [...files, ...restored.files].sort(
|
||||
(a, b) => b.file_size - a.file_size
|
||||
);
|
||||
scopeRevision += 1;
|
||||
scope?.refresh();
|
||||
await loadSummary();
|
||||
} catch {
|
||||
problem = "Не удалось вернуть файлы";
|
||||
@@ -301,16 +321,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(reload);
|
||||
onMount(resume);
|
||||
onDestroy(() => clearTimeout(undoTimer));
|
||||
</script>
|
||||
|
||||
<div class="storage">
|
||||
<div class="storage" {@attach hold}>
|
||||
{#if summary}
|
||||
{#if summary.files > 0}
|
||||
{#if summary.files > 0 || chatId !== null}
|
||||
<section class="summary">
|
||||
<div class="hero">
|
||||
<span class="total">{formatBytes(summary.bytes)}</span>
|
||||
<span class="total">{formatBytes(summary.bytes) || "0 B"}</span>
|
||||
<span class="count">
|
||||
{filesLabel(summary.files)}
|
||||
{#if chatId !== null}
|
||||
@@ -322,7 +342,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<div class="stack" class:bare={groups.length === 0}>
|
||||
{#each groups as item (item.group)}
|
||||
<span
|
||||
title="{GROUP_LABELS[item.group]}: {formatBytes(item.bytes)}"
|
||||
@@ -392,7 +412,6 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{#key scopeRevision}
|
||||
<ScopeFiles
|
||||
label="Все файлы чата"
|
||||
onchange={() => {
|
||||
@@ -400,8 +419,8 @@
|
||||
reload();
|
||||
}}
|
||||
target={{ chatId }}
|
||||
bind:this={scope}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -423,15 +442,12 @@
|
||||
<StorageChatRow
|
||||
{chat}
|
||||
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)}
|
||||
weight={heaviestChat > 0 ? chat.bytes / heaviestChat : 0}
|
||||
>
|
||||
{#snippet menu()}
|
||||
<ContextMenuItem
|
||||
icon="data"
|
||||
onselect={() => storageUi.openChat(chat.chat_id)}
|
||||
>
|
||||
<ContextMenuItem icon="data" onselect={() => openChat(chat.chat_id)}>
|
||||
Файлы чата
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="reply" onselect={() => visit(chat)}>
|
||||
@@ -491,12 +507,14 @@
|
||||
{:else if failed}
|
||||
<div class="state">
|
||||
<p>Не удалось загрузить хранилище.</p>
|
||||
<Button onclick={reload} smaller variant="text">Повторить</Button>
|
||||
<Button onclick={() => reload()} smaller variant="text">
|
||||
Повторить
|
||||
</Button>
|
||||
</div>
|
||||
{:else if shown === 0}
|
||||
<div class="state"><p>{emptiness}</p></div>
|
||||
{:else if !done}
|
||||
<button class="more" onclick={loadMore} type="button">
|
||||
<button class="more" onclick={() => loadMore()} type="button">
|
||||
Показать ещё
|
||||
</button>
|
||||
{/if}
|
||||
@@ -599,6 +617,11 @@
|
||||
height: 0.625rem;
|
||||
margin-top: 0.875rem;
|
||||
|
||||
&.bare {
|
||||
border-radius: 0.3125rem;
|
||||
background-color: rgba(var(--color-text-secondary-rgb), 0.16);
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0.1875rem;
|
||||
border-radius: 2px;
|
||||
|
||||
@@ -2,9 +2,15 @@ import { ui } from "#lib/stores/ui.svelte.js";
|
||||
|
||||
export type StorageListing = "files" | "chats";
|
||||
|
||||
interface Resume {
|
||||
count: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
function createStorageUi() {
|
||||
let listing = $state<StorageListing>("files");
|
||||
let chatId = $state<number | null>(null);
|
||||
let resume: Resume | null = null;
|
||||
|
||||
return {
|
||||
get chatId() {
|
||||
@@ -16,10 +22,16 @@ function createStorageUi() {
|
||||
set listing(next: StorageListing) {
|
||||
listing = next;
|
||||
},
|
||||
openChat(id: number) {
|
||||
openChat(id: number, from: Resume) {
|
||||
chatId = id;
|
||||
resume = from;
|
||||
ui.openPanel("storage-chat");
|
||||
},
|
||||
takeResume(): Resume | null {
|
||||
const taken = resume;
|
||||
resume = null;
|
||||
return taken;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user