Add the Collect page

The collector becomes usable. The page is a close copy of the parse page's
shape - same proxy gate, same batched flush to the observable collection, same
truncation cap that reports rather than truncates silently - because that shape
was built for streaming outcomes and the collector produces exactly those.

Two differences that are not cosmetic. The batch drops from 512 to 64: items
arrive at network speed, roughly one a second, and a batch of five hundred would
mean the list never visibly moved. And progress is explicitly indeterminate
until a source finishes listing, because a paginated listing genuinely does not
know its total until the last page - a bar pretending otherwise would be lying.

The input swaps shape with the source: an endpoint source wants one address, a
pasted-list source wants many lines. The proxy gate is unchanged in substance -
only network sources are gated, and the banner keeps its x:Name because the
headless tests find it that way.

Rendering the page caught a defect the tests could not: IsVisible sat on the
caption inside the header border rather than on the border, so hiding the text
left its padding and divider behind as an empty bar above the input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-13 21:39:21 +03:00
co-authored by Claude Opus 5
parent 6909884851
commit 70fb3a1df3
12 changed files with 1774 additions and 1 deletions
@@ -508,4 +508,172 @@
<data name="Settings.MinimumLiveHint" xml:space="preserve">
<value>При старте сначала проверяются те, что работали в прошлый раз, и проверка прекращается, как только набралось столько. Больше значение — дольше первый запуск.</value>
</data>
<data name="Page.Collect" xml:space="preserve">
<value>Сбор</value>
</data>
<data name="Collect.Source" xml:space="preserve">
<value>ИСТОЧНИК</value>
</data>
<data name="Collect.Actions" xml:space="preserve">
<value>ДЕЙСТВИЯ</value>
</data>
<data name="Collect.Run" xml:space="preserve">
<value>Собрать</value>
</data>
<data name="Collect.Cancel" xml:space="preserve">
<value>Остановить</value>
</data>
<data name="Collect.ClearTip" xml:space="preserve">
<value>Очистить результаты</value>
</data>
<data name="Collect.Input" xml:space="preserve">
<value>АДРЕСА</value>
</data>
<data name="Collect.InputPlaceholder" xml:space="preserve">
<value>По одному адресу в строке. Строки, начинающиеся с «#», игнорируются.</value>
</data>
<data name="Collect.Endpoint" xml:space="preserve">
<value>АДРЕС ЛИСТИНГА</value>
</data>
<data name="Collect.EndpointPlaceholder" xml:space="preserve">
<value>https://my-service.local/api/list</value>
</data>
<data name="Collect.ForceRefetch" xml:space="preserve">
<value>Скачать всё заново</value>
</data>
<data name="Collect.ForceRefetchHint" xml:space="preserve">
<value>Обычно уже обработанный адрес пропускается без запроса. Включите, чтобы игнорировать это.</value>
</data>
<data name="Collect.Items" xml:space="preserve">
<value>СОБРАНО</value>
</data>
<data name="Collect.Errors" xml:space="preserve">
<value>ОШИБКИ</value>
</data>
<data name="Collect.ProxyRequired" xml:space="preserve">
<value>Этому источнику нужна рабочая прокси, а живых нет. Проверьте список прокси или разрешите прямое подключение в настройках.</value>
</data>
<data name="Collect.GoToProxies" xml:space="preserve">
<value>Открыть прокси</value>
</data>
<data name="Collect.Item.Stored" xml:space="preserve">
<value>новое</value>
</data>
<data name="Collect.Item.Duplicate" xml:space="preserve">
<value>дубль</value>
</data>
<data name="Collect.Item.Skipped" xml:space="preserve">
<value>пропущено</value>
</data>
<data name="Collect.Status.Done" xml:space="preserve">
<value>Собрано {0} за {1} с.</value>
</data>
<data name="Collect.Status.Cancelled" xml:space="preserve">
<value>Остановлено после {0} за {1} с.</value>
</data>
<data name="Collect.Status.Duplicates" xml:space="preserve">
<value>{0} уже было.</value>
</data>
<data name="Collect.Status.Skipped" xml:space="preserve">
<value>{0} пропущено как уже обработанное.</value>
</data>
<data name="Collect.Status.Failed" xml:space="preserve">
<value>{0} не удалось.</value>
</data>
<data name="Collect.Status.Truncated" xml:space="preserve">
<value>Список обрывается на {0}; счётчики выше полные.</value>
</data>
<data name="Collect.Status.Crashed" xml:space="preserve">
<value>Сбор не удался: {0}</value>
</data>
<data name="Collect.Count.Images.One" xml:space="preserve">
<value>{0} изображение</value>
</data>
<data name="Collect.Count.Images.Few" xml:space="preserve">
<value>{0} изображения</value>
</data>
<data name="Collect.Count.Images.Many" xml:space="preserve">
<value>{0} изображений</value>
</data>
<data name="Collect.Count.Errors.One" xml:space="preserve">
<value>{0} ошибка</value>
</data>
<data name="Collect.Count.Errors.Few" xml:space="preserve">
<value>{0} ошибки</value>
</data>
<data name="Collect.Count.Errors.Many" xml:space="preserve">
<value>{0} ошибок</value>
</data>
<data name="Source.url-list.Name" xml:space="preserve">
<value>Список ссылок</value>
</data>
<data name="Source.url-list.Description" xml:space="preserve">
<value>Адреса, которые вы вставляете сами, по одному в строке.</value>
</data>
<data name="Source.own-service.Name" xml:space="preserve">
<value>Свой сервис</value>
</data>
<data name="Source.own-service.Description" xml:space="preserve">
<value>Читает листинг сервиса, который вы держите сами.</value>
</data>
<data name="Parse.Error.NotAnAddress" xml:space="preserve">
<value>«{0}» — не http- и не https-адрес.</value>
</data>
<data name="Parse.Error.NoEndpoint" xml:space="preserve">
<value>Не указан адрес листинга.</value>
</data>
<data name="Parse.Error.ListingFailed" xml:space="preserve">
<value>Не удалось прочитать листинг: {0}</value>
</data>
<data name="Parse.Error.ListingMalformed" xml:space="preserve">
<value>Листинг не удалось разобрать: {0}</value>
</data>
<data name="Parse.Error.SourceFailed" xml:space="preserve">
<value>Источник остановился: {0}</value>
</data>
<data name="Parse.Error.NoProxy" xml:space="preserve">
<value>Живых прокси нет, а прямое подключение выключено.</value>
</data>
<data name="Parse.Error.BadRedirect" xml:space="preserve">
<value>Перенаправление на адрес, который не является http.</value>
</data>
<data name="Parse.Error.RedirectLoop" xml:space="preserve">
<value>Перенаправления зациклены.</value>
</data>
<data name="Parse.Error.TooManyRedirects" xml:space="preserve">
<value>Слишком много перенаправлений.</value>
</data>
<data name="Parse.Error.NotMedia" xml:space="preserve">
<value>В ответе не изображение и не видео.</value>
</data>
<data name="Parse.Error.TooLarge" xml:space="preserve">
<value>Больше предельного размера.</value>
</data>
<data name="Parse.Error.TooSmall" xml:space="preserve">
<value>Меньше нижнего порога; вероятно, трекинг-пиксель.</value>
</data>
<data name="Parse.Error.Truncated" xml:space="preserve">
<value>Передача оборвалась.</value>
</data>
<data name="Parse.Error.Stalled" xml:space="preserve">
<value>Истекло время ожидания.</value>
</data>
<data name="Parse.Error.RateLimited" xml:space="preserve">
<value>Сайт попросил снизить темп.</value>
</data>
<data name="Parse.Error.Placeholder" xml:space="preserve">
<value>Известная заглушка мёртвой ссылки.</value>
</data>
<data name="Parse.Error.UnsupportedType" xml:space="preserve">
<value>Этот формат отключён.</value>
</data>
<data name="Parse.Error.HttpStatus" xml:space="preserve">
<value>Сайт ответил {0}.</value>
</data>
<data name="Parse.Error.RequestFailed" xml:space="preserve">
<value>Запрос не удался: {0}</value>
</data>
<data name="Parse.Error.Failed" xml:space="preserve">
<value>Не удалось собрать.</value>
</data>
</root>