Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec5979437d | ||
|
|
59f171a62d | ||
|
|
e3559da2a7 | ||
|
|
dcd6786ca5 | ||
|
|
32d58193f3 | ||
|
|
59d0e2c8ad | ||
|
|
850d39ad6f | ||
|
|
b8a38092eb | ||
|
|
2836dcb3a9 | ||
|
|
01cf01b629 |
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
_commit: v0.6.0
|
_commit: v0.7.1
|
||||||
_src_path: https://git.kotikot.com/templates/backend-python.git
|
_src_path: https://git.kotikot.com/templates/backend-python.git
|
||||||
author_email: h@kotikot.com
|
author_email: h@kotikot.com
|
||||||
author_name: h
|
author_name: h
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
_commit: v0.7.0
|
_commit: v0.8.0
|
||||||
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
||||||
install_deps: false
|
install_deps: false
|
||||||
layout: part
|
layout: part
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ _MODULES = (
|
|||||||
read_receipts,
|
read_receipts,
|
||||||
transcribed,
|
transcribed,
|
||||||
)
|
)
|
||||||
_REGISTRY: dict[type, RawHandler] = {
|
_REGISTRY: dict[type, list[RawHandler]] = {
|
||||||
update_type: module.handle for module in _MODULES for update_type in module.HANDLES
|
update_type: [module.handle for module in _MODULES if update_type in module.HANDLES]
|
||||||
|
for update_type in {kind for module in _MODULES for kind in module.HANDLES}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -33,8 +34,7 @@ _REGISTRY: dict[type, RawHandler] = {
|
|||||||
async def dispatch(
|
async def dispatch(
|
||||||
client: PyroClient, update: raw.base.Update, users: dict, chats: dict
|
client: PyroClient, update: raw.base.Update, users: dict, chats: dict
|
||||||
) -> None:
|
) -> None:
|
||||||
handler = _REGISTRY.get(type(update))
|
for handler in _REGISTRY.get(type(update), ()):
|
||||||
if handler is not None:
|
|
||||||
await handler(client, update, users, chats)
|
await handler(client, update, users, chats)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from pyrogram import Client
|
from pyrogram import Client
|
||||||
from pyrogram.errors import FileIdInvalid, FileReferenceExpired, FileReferenceInvalid
|
from pyrogram.errors import (
|
||||||
from pyrogram.types import Photo
|
ChannelInvalid,
|
||||||
|
ChannelPrivate,
|
||||||
|
FileIdInvalid,
|
||||||
|
FileReferenceExpired,
|
||||||
|
FileReferenceInvalid,
|
||||||
|
PeerIdInvalid,
|
||||||
|
)
|
||||||
|
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
|
||||||
@@ -8,19 +15,20 @@ from userbot.modules.jobs.context import JobContext
|
|||||||
from userbot.modules.jobs.registry import register
|
from userbot.modules.jobs.registry import register
|
||||||
|
|
||||||
STALE_FILE_ID = (FileIdInvalid, FileReferenceExpired, FileReferenceInvalid)
|
STALE_FILE_ID = (FileIdInvalid, FileReferenceExpired, FileReferenceInvalid)
|
||||||
|
UNKNOWN_OWNER = (PeerIdInvalid, ChannelInvalid, ChannelPrivate)
|
||||||
|
|
||||||
|
|
||||||
async def _fresh_file_id(client: Client, owner_id: int, unique_id: str) -> str | None:
|
async def _fresh_file_id(client: Client, owner_id: int, unique_id: str) -> str | None:
|
||||||
|
try:
|
||||||
chat = await client.get_chat(owner_id)
|
chat = await client.get_chat(owner_id)
|
||||||
|
except UNKNOWN_OWNER:
|
||||||
|
return None
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pyrogram.raw.core import TLObject
|
from pyrogram.raw.core import TLObject
|
||||||
from pyrogram.types import Object
|
from pyrogram.types import Contact, Object
|
||||||
|
|
||||||
EMPTY_JSON = "{}"
|
EMPTY_JSON = "{}"
|
||||||
|
PHONE_KEY = "phone_number"
|
||||||
|
|
||||||
|
|
||||||
|
def _default(obj: Any) -> Any: # noqa: ANN401
|
||||||
|
if isinstance(obj, TLObject):
|
||||||
|
return TLObject.default(obj)
|
||||||
|
data = Object.default(obj)
|
||||||
|
if isinstance(obj, Contact) and isinstance(data, dict) and PHONE_KEY in data:
|
||||||
|
data[PHONE_KEY] = obj.phone_number
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def raw_json(obj: Object | TLObject | None) -> str:
|
def raw_json(obj: Object | TLObject | None) -> str:
|
||||||
return EMPTY_JSON if obj is None else str(obj)
|
if obj is None:
|
||||||
|
return EMPTY_JSON
|
||||||
|
return json.dumps(obj, indent=4, default=_default, ensure_ascii=False)
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
Generated
+7
-6
@@ -411,7 +411,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.143.0"
|
version = "0.143.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-doc" },
|
{ name = "annotated-doc" },
|
||||||
@@ -421,9 +421,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/d7/6a8753ab6c1d432dc53703c3e1b92974a94531b7d047c32bbaae461ea844/fastapi-0.143.0.tar.gz", hash = "sha256:1acffe48206a80917cf7dac21992b5c44b25384e8902bf745c1fd9dabcf6c51f", size = 468391, upload-time = "2026-10-08T12:29:46.54Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/38/d5/b0aeb0b303650ac87b0411de4aaf3f901b06042dcc3fb00f30b97b7ff585/fastapi-0.143.3.tar.gz", hash = "sha256:b9ab567ee57afb4454004ce04c889f8f0160ac47946b99795887cd6e88484848", size = 469834, upload-time = "2026-10-16T14:14:13.31Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/f4/27e386913417ad32aae42bba48b0c0cce40e9ff2fba1a871ca2702c37324/fastapi-0.143.0-py3-none-any.whl", hash = "sha256:3e9395fd35276425b61b516a31fdd7c77fe2af83e41b4da22e30696fb1304c5d", size = 144665, upload-time = "2026-10-08T12:29:44.853Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/1a/03d246befcf307219bb2161c97d8c5d314f84125bf7dd245730a28589828/fastapi-0.143.3-py3-none-any.whl", hash = "sha256:75e8bd68e2f52f77d29b2879749c257d64de2d85282d25270caf2bfccce5efe5", size = 144849, upload-time = "2026-10-16T14:14:11.763Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1080,16 +1080,17 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-settings"
|
name = "pydantic-settings"
|
||||||
version = "2.15.0"
|
version = "2.16.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2e/3b/a5d2294799b53b448319978cfb5bd139d5a9d45e862af91661614f14c922/pydantic_settings-2.16.0.tar.gz", hash = "sha256:5b6c578049ede4db0e2ef3b4eaa4ad4069cfa9211f83fb38df899dfade50a614", size = 301031, upload-time = "2026-10-14T12:44:09.998Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/f4/b987bf8c51e5b19a95fa66d1ee596074141e085d9c2ddf97920803c7029b/pydantic_settings-2.16.0-py3-none-any.whl", hash = "sha256:7e73acf7f61936a15e5a3b6eedaea29f133357faf7272f2607ba479b049dd7f2", size = 80364, upload-time = "2026-10-14T12:44:08.233Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -80,6 +80,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function refresh() {
|
||||||
|
return load();
|
||||||
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -89,7 +93,7 @@
|
|||||||
<span>{filesLabel(moved.files)} в корзине</span>
|
<span>{filesLabel(moved.files)} в корзине</span>
|
||||||
<small>{formatBytes(moved.bytes)} освободится через 7 дней</small>
|
<small>{formatBytes(moved.bytes)} освободится через 7 дней</small>
|
||||||
{#if problem}
|
{#if problem}
|
||||||
<small class="warn">{problem}</small>
|
<small class="failed">{problem}</small>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Button disabled={moving} onclick={undo} smaller variant="text">
|
<Button disabled={moving} onclick={undo} smaller variant="text">
|
||||||
@@ -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">
|
||||||
@@ -112,7 +121,7 @@
|
|||||||
</small>
|
</small>
|
||||||
{/if}
|
{/if}
|
||||||
{#if problem}
|
{#if problem}
|
||||||
<small class="warn">{problem}</small>
|
<small class="failed">{problem}</small>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<Button
|
<Button
|
||||||
@@ -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">
|
||||||
@@ -214,6 +235,10 @@
|
|||||||
&.warn {
|
&.warn {
|
||||||
color: var(--color-storage-warning);
|
color: var(--color-storage-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.failed {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user