From cb01d7dff887b8420965f158aeeb64fc6971c3d0 Mon Sep 17 00:00:00 2001 From: h Date: Wed, 14 Oct 2026 04:20:38 +0200 Subject: [PATCH] feat(frontend): capture policy shows what each rule changes, rule card opens from folder and chat menus and can trash downloaded files, trash is paged --- frontend/src/lib/api/endpoints.ts | 44 +- frontend/src/lib/api/types.ts | 28 + frontend/src/lib/components/ChatHeader.svelte | 6 + .../src/lib/components/ChatListItem.svelte | 6 + frontend/src/lib/components/FolderTabs.svelte | 37 +- .../src/lib/components/RightColumn.svelte | 5 +- .../src/lib/components/disk/TrashPanel.svelte | 149 +++- .../policy/CaptureToggleList.svelte | 106 --- .../lib/components/policy/PolicyCard.svelte | 587 +++++++++++++++ .../lib/components/policy/PolicyDialog.svelte | 62 ++ .../lib/components/policy/PolicyEditor.svelte | 679 ++++++++++++------ frontend/src/lib/format/policy.ts | 148 ++++ frontend/src/lib/stores/policy.svelte.ts | 36 + frontend/src/lib/styles/dark-theme.scss | 4 + frontend/src/lib/styles/variables.scss | 5 + frontend/src/routes/app/+layout.svelte | 3 + 16 files changed, 1536 insertions(+), 369 deletions(-) delete mode 100644 frontend/src/lib/components/policy/CaptureToggleList.svelte create mode 100644 frontend/src/lib/components/policy/PolicyCard.svelte create mode 100644 frontend/src/lib/components/policy/PolicyDialog.svelte create mode 100644 frontend/src/lib/format/policy.ts create mode 100644 frontend/src/lib/stores/policy.svelte.ts diff --git a/frontend/src/lib/api/endpoints.ts b/frontend/src/lib/api/endpoints.ts index 68be46d..d57336a 100644 --- a/frontend/src/lib/api/endpoints.ts +++ b/frontend/src/lib/api/endpoints.ts @@ -26,13 +26,17 @@ import type { PolicyChatKind, PolicyCreate, PolicyRecord, + PolicyRule, + PolicyScope, PresenceHourly, PresenceSample, ReactionView, ResponseStats, + ScopeTarget, SearchHit, StorageFile, StorageGroupName, + StorageScope, StorageSummary, StoryView, TrashItem, @@ -148,6 +152,23 @@ export function deletePolicy(id: number): Promise { return request(`/policy/${id}`, { method: "DELETE" }); } +function scopeQuery(target: ScopeTarget): Record { + return "chatId" in target + ? { chat_id: target.chatId } + : { folder_id: target.folderId }; +} + +export function getPolicyOverview(): Promise { + return request("/policy/overview", { account: true }); +} + +export function getPolicyScope(target: ScopeTarget): Promise { + return request("/policy/scope", { + account: true, + query: scopeQuery(target), + }); +} + export function effectivePolicy(query: { chat_id: number; is_bot?: boolean; @@ -601,8 +622,25 @@ export function getStorageFiles( }); } -export function getTrash(): Promise { - return request("/storage/trash", { account: true }); +export function getTrash(page: Page = {}): Promise { + return request("/storage/trash", { + account: true, + query: { ...page }, + }); +} + +export function getStorageScope(target: ScopeTarget): Promise { + return request("/storage/scope", { + account: true, + query: scopeQuery(target), + }); +} + +export function trashScope(target: ScopeTarget): Promise { + return request("/storage/trash/scope", { + body: { account_id: accounts.selectedId, ...scopeQuery(target) }, + method: "POST", + }); } export function moveToTrash(mediaIds: number[]): Promise { @@ -612,7 +650,7 @@ export function moveToTrash(mediaIds: number[]): Promise { }); } -export function restoreFromTrash(keys: string[]): Promise { +export function restoreFromTrash(keys: string[] | null): Promise { return request("/storage/trash/restore", { body: { account_id: accounts.selectedId, keys }, method: "POST", diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 75f554a..7c42060 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -423,6 +423,24 @@ export interface PolicyRecord extends CaptureToggles { scope_type: PolicyScopeType; } +export interface PolicyParent { + scope_id: number | null; + scope_type: PolicyScopeType; + title: string | null; + toggles: CaptureToggles; +} + +export interface PolicyRule extends PolicyRecord { + parent: PolicyParent | null; + title: string | null; +} + +export interface PolicyScope { + parent: PolicyParent | null; + rule: PolicyRecord | null; + title: string | null; +} + export type PolicyChatKind = "dm" | "group" | "channel"; export interface PolicyCreate extends CaptureToggles { @@ -613,3 +631,13 @@ export interface TrashOutcome { files: number; keys: string[]; } + +export interface StorageScope { + bytes: number; + chats: number; + files: number; + gone: number; + kept: number; +} + +export type ScopeTarget = { chatId: number } | { folderId: number }; diff --git a/frontend/src/lib/components/ChatHeader.svelte b/frontend/src/lib/components/ChatHeader.svelte index 9bd6150..4b5cb58 100644 --- a/frontend/src/lib/components/ChatHeader.svelte +++ b/frontend/src/lib/components/ChatHeader.svelte @@ -15,6 +15,7 @@ import { accounts } from "#lib/stores/accounts.svelte.js"; import { chats } from "#lib/stores/chats.svelte.js"; import { discover } from "#lib/stores/discover.svelte.js"; + import { policyUi } from "#lib/stores/policy.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js"; import { ui } from "#lib/stores/ui.svelte.js"; import { goto } from "$app/navigation"; @@ -186,6 +187,11 @@ onselect={() => ui.openPanel("stories")} >Сторис + policyUi.edit({ chatId }, title)} + >Политика захвата {/snippet}
diff --git a/frontend/src/lib/components/ChatListItem.svelte b/frontend/src/lib/components/ChatListItem.svelte index c7d1ef5..1e624fa 100644 --- a/frontend/src/lib/components/ChatListItem.svelte +++ b/frontend/src/lib/components/ChatListItem.svelte @@ -7,6 +7,7 @@ import { formatListDate } from "#lib/format/datetime.js"; import { accounts } from "#lib/stores/accounts.svelte.js"; import { peers } from "#lib/stores/peers.svelte.js"; + import { policyUi } from "#lib/stores/policy.svelte.js"; import { type RightPanel, ui } from "#lib/stores/ui.svelte.js"; import { goto } from "$app/navigation"; @@ -108,6 +109,11 @@ openWith("stories")} >Сторис + policyUi.edit({ chatId: chat.chat_id }, title)} + >Политика захвата {/snippet} diff --git a/frontend/src/lib/components/FolderTabs.svelte b/frontend/src/lib/components/FolderTabs.svelte index da53da3..617d042 100644 --- a/frontend/src/lib/components/FolderTabs.svelte +++ b/frontend/src/lib/components/FolderTabs.svelte @@ -1,5 +1,8 @@ - -
- {#each rows as row (row.key)} - - {/each} -
- - diff --git a/frontend/src/lib/components/policy/PolicyCard.svelte b/frontend/src/lib/components/policy/PolicyCard.svelte new file mode 100644 index 0000000..a2bb6f6 --- /dev/null +++ b/frontend/src/lib/components/policy/PolicyCard.svelte @@ -0,0 +1,587 @@ + + +
+ {#if loading} +
+ {:else} + {#if hint} +

{hint}

+ {/if} + +
+ {#each PRESETS as item (item.label)} + + {/each} +
+ + {#if shown} +
+ {#each TOGGLE_KEYS as key (key)} + + {/each} +
+ {/if} + + {#if problem} +

{problem}

+ {/if} + + {#if moved && moved.files > 0} +
+
+ {filesLabel(moved.files)} в корзине + {formatBytes(moved.bytes)} освободится через 7 дней +
+ +
+ {:else if files && files.files > 0} +
+ {#if confirming} +
+ + {filesLabel(files.files)}, {formatBytes(files.bytes)}, уйдут в + корзину и перестанут скачиваться сами. + + {#if files.gone > 0} + + {files.gone} + из них потом не скачать заново + + {/if} +
+ + +
+
+ {:else} +
+ {formatBytes(files.bytes)} уже скачано + {filesLabel(files.files)} {where(files)} + {#if files.kept > 0} + + Ещё {filesLabel(files.kept)} есть и в других чатах, они + останутся + + {/if} +
+ + {/if} +
+ {/if} + + {#if rule && (isChat || isFolder)} + + {/if} + {/if} +
+ + diff --git a/frontend/src/lib/components/policy/PolicyDialog.svelte b/frontend/src/lib/components/policy/PolicyDialog.svelte new file mode 100644 index 0000000..8c5e468 --- /dev/null +++ b/frontend/src/lib/components/policy/PolicyDialog.svelte @@ -0,0 +1,62 @@ + + + + + + +
+
+ {policyUi.title} + + {isChat ? "Политика захвата чата" : "Политика захвата папки"} + +
+ + + +
+
+ {#key target} + policyUi.touch()} + {scopeId} + scopeType={isChat ? "chat" : "folder"} + /> + {/key} +
+
+
+
+ + diff --git a/frontend/src/lib/components/policy/PolicyEditor.svelte b/frontend/src/lib/components/policy/PolicyEditor.svelte index da7a2ff..d8830ce 100644 --- a/frontend/src/lib/components/policy/PolicyEditor.svelte +++ b/frontend/src/lib/components/policy/PolicyEditor.svelte @@ -1,241 +1,333 @@ -{#snippet card( - record: PolicyRecord, - title: string, - onremove?: () => void +{#snippet lead( + rule: PolicyRule )} -
-
- {title} - {#if record.account_id === null} - для всех аккаунтов - {/if} - {#if onremove && record.account_id !== null} - + {#if rule.scope_type === "chat" && rule.scope_id !== null} + 0 ? "peer" : "chat" }} + colorKey={rule.scope_id} + hasAvatar={chats.byId(rule.scope_id)?.has_avatar ?? false} + name={title(rule)} + size={2.25} + /> + {:else} + + + + {/if} +{/snippet} + +{#snippet row( + rule: PolicyRule, + plain: boolean +)} + {@const key = keyOf(rule.scope_type, rule.scope_id)} + {@const open = expanded === key} + {@const extra = !plain && redundant(rule)} +
+
+ + {#if extra} + {/if}
- toggle(record, key, value)} - toggles={record} - /> + {#if open} +
+ +
+ {/if} +
+{/snippet} + +{#snippet pending( + item: Draft +)} +
+
+
+ + + + + {item.title} + Новое правило + +
+ +
+
+ +
{/snippet} {#if loading}
+{:else if failed} +
+

Не удалось загрузить политику.

+ +
{:else} -
+
+ {#if problem} +

{problem}

+ {/if} +
-
По умолчанию
- {#each defaults as item (item.record.id)} - {@render card(item.record, item.label)} +

По умолчанию

+ {#each defaults as rule (rule.id)} + {@render row(rule, true)} {/each}
-
Оверрайды папок
- {#each folderPolicies as record (record.id)} - {@render card(record, folderTitle(record.scope_id), () => - removeOverride(record) - )} +
+

Папки

+ что меняют · побеждает первая подходящая +
+ {#if draft?.scopeType === "folder"} + {@render pending(draft)} + {/if} + {#each folderRules as rule (rule.id)} + {@render row(rule, false)} {/each} - {#if availableFolders.length > 0} + {#if freeFolders.length > 0} - {#if pickFolder} + {#if picking === "folder"}
- {#each availableFolders as folder (folder.folder_id)} + {#each freeFolders as folder (folder.folder_id)}
-
Оверрайды чатов
- {#each chatPolicies as record (record.id)} - {@render card(record, chatTitle(record.scope_id), () => - removeOverride(record) - )} +
+

Чаты

+ что меняют · важнее папок +
+ {#if draft?.scopeType === "chat"} + {@render pending(draft)} + {/if} + {#each chatRules as rule (rule.id)} + {@render row(rule, false)} {/each} - {#if pickChat} + {#if picking === "chat"}
picker.search(chatFilter)} placeholder="Поиск чата" type="text" bind:value={chatFilter} > - {#each availableChats as chat (chat.chat_id)} + {#each freeChats as chat (chat.chat_id)}
+ + shareUi.touch()}