Представлен открытый проект vphone-cli для загрузки виртуального iPhone с помощью фреймворка Virtualization.framework от Apple, используя инфраструктуру виртуальных машин PCC Research.

Представлен открытый проект vphone-cli для загрузки виртуального iPhone с помощью фреймворка Virtualization.framework от Apple, используя инфраструктуру виртуальных машин PCC Research.

Хороший код: как понять, что его будет удобно менять
Понятные имена, небольшие методы и аккуратное форматирование делают код проще для чтения. Но даже аккуратный файл может оказаться дорогим в работе, если небольшое изменение требует восстановить много контекста и затронуть несколько частей системы.

Вместе с Ринатом, iOS-разработчиком в Naumen, разбираемся, почему хороший код проверяется следующей задачей, как проявляется сложность изменений и на что стоит смотреть при оценке кода.
Код проверяется следующей задачей
Пока работа над задачей еще свежая, почти любой код кажется понятным. Автор помнит, почему вызовы стоят именно в таком порядке, какой случай обсуждали на ревью и что здесь собирались переделать позже. Коллеги тоже держат часть контекста в голове.
Так что даже не самое удачное решение какое‑то время не доставляет особых проблем.
Через полгода ситуация меняется: исходная задача давно закрыта, участники обсуждения заняты другими частями проекта, а другому разработчику нужно внести небольшое изменение.
Именно в этот момент становится понятно, насколько код вообще рассчитан на изменения.
Простая задача может оказаться дорогой
Одна из задач у нас на планировании звучала безобидно:
После ошибки авторизации запрос повторять не нужно, после временной сетевой ошибки — нужно, причем с увеличивающейся задержкой.
Само условие укладывается в несколько строк. Но сначала приходится выяснить, где на самом деле живет это правило: в сетевом клиенте, в сервисе авторизации, в middleware.
А еще нужно понять, не запускает ли часть повторов таймер внутри другого объекта и не зависит ли соседний сценарий от текущего порядка вызовов. В итоге сам код меняется быстро, но день уходит на восстановление картины вокруг него.
Смотреть нужно на изменение, а не на файл
У кода есть свойства, которые легко заметить сразу: понятные имена, небольшие методы, аккуратное форматирование и простая структура. Все это полезно, но само по себе еще не говорит, насколько удобно систему менять.
Можно открыть класс и довольно быстро разобраться в каждом его методе. А потом выяснить, что для добавления одного состояния нужно исправить еще четыре модуля, обновить несколько почти одинаковых преобразований данных и соблюсти порядок вызовов, который нигде явно не зафиксирован.
Файл может выглядеть вполне нормально, а изменение при этом оказывается дорогим.
Мне в этом контексте близко описание сложности изменений у Джона Оустерхаута. Он выделяет три характерных проявления.
Маленькая правка расползается по системе
Добавили поле в модель и приходится менять сетевой слой, хранилище, аналитику, несколько экранов и тестовые данные.
Иногда это естественная цена изменения контракта, а иногда — признак того, что одно знание размазано по проекту.
Для локальной работы нужно слишком много контекста
Чтобы поправить один обработчик, нужно знать устройство навигации, жизненный цикл экрана, особенности кэша и два исторических обхода старых ошибок.
Есть зависимости, о которых разработчик даже не знает
Они обнаруживаются уже после изменения. Например, перестановка двух вызовов отключает сетевую проверку: первый метод использует закэшированное состояние и завершает сценарий раньше времени.
Эти признаки полезнее многих разговоров о «чистом коде»: о длине метода можно спорить, а вот с последствиями изменения — сложнее.
Обычно я смотрю на три вещи
Сколько мест потребуется затронуть?
Сколько информации нужно восстановить перед работой?
Как быстро мы узнаем, что ошиблись?
Чем меньше ответ зависит от памяти конкретного человека, тем спокойнее живется проекту.
Wildberries выпустила собственный мессенджер WB Chat
У Wildberries появился собственный мессенджер WB Chat. Приложение уже доступно пользователям на Android и iOS, а авторизация проходит через WB ID.
Интерфейс построен по знакомой схеме: диалоги разделены на чаты, группы и каналы, причём создать собственную группу или канал можно непосредственно из приложения. Есть отдельный раздел профиля с аватаром, именем, статусом и возможностью выбрать юзернейм, а для организации переписок предусмотрены папки.
Набор функций тоже постепенно расширяется. Сейчас WB Chat позволяет:
отправлять сообщения, фото, видео и файлы;
пересылать сообщения и отвечать на них;
использовать реакции, эмодзи и стикеры;
создавать публичные и приватные группы и каналы;
закреплять важные сообщения;
искать людей, чаты и сообщения;
совершать аудиозвонки;
расшифровывать голосовые сообщения в текст.
Последняя функция особенно интересна для повседневного использования: рядом с кнопкой воспроизведения голосового сообщения появляется возможность получить его текстовую расшифровку. То есть длинное голосовое необязательно прослушивать целиком.
При этом проект пока активно развивается. Например, в последних версиях разработчики отдельно сообщают об исправлениях синхронизации, работе контактов, медиафайлов, звонков и повышении стабильности приложения.
В Google Play приложение опубликовано компанией WB FZE, зарегистрированной в Hamriyah Free Zone в эмирате Шарджа, ОАЭ. На момент проверки там указано 1 тыс.+ скачиваний.
Есть и отдельный момент, на который стоит обратить внимание перед регистрацией: в информации Google Play разработчик указывает, что приложение может собирать фотографии и видео, файлы и документы, а также передавать некоторые категории данных третьим сторонам. При этом передача данных заявлена как шифруемая.
Скачать WB Chat для Android можно через Google Play, для iPhone и iPad — через App Store. Также заявлена веб-версия WB Chat.
Пока это выглядит скорее как новый игрок на рынке мессенджеров, чем полностью сформировавшаяся альтернатива привычным сервисам. Но наличие чатов, групп, каналов, звонков, поиска, папок и расшифровки голосовых показывает, что Wildberries постепенно собирает полноценную коммуникационную платформу.
✔ Код — журнал о технологиях https://t.me/kodjournal подпишитесь на наш Telegram-канал! 😎
ИИ-ревью кода как сомнительное удовольствие
Этот пост написан как реакция на сегодняшнюю хабровскую публикацию - Проблема «принципал — агент» в эпоху ИИ‑агентов
В ней рассматривается "интересная" такая схема - отдавать результаты человеческого ревью кода ИИ-агенту.
Меня это в определенной мере удивило, потому что, судя по тому, что сейчас пишут в сети, то и код пишет агент, и ревью тоже агент делает. Чаще всего другой.
Например, код пишет Claude Code, а ревью делает Codex (И это еще хорошо, если они по своим возможностям в написании кода примерно равны, а то ведь агенты-ревьюверы могут быть и гораздо слабее агентов-кодеров).
Но вот остается вопрос: как решаются случаи, когда они расходятся во мнениях? Кому доверять больше? Устраивать дискуссии? И кто принимает окончательное решение?
Или, реальный случай: Claude Code в "холодной сессии" написал ревью своего же кода из порядка 20 пунктов. Тот же код и Codex пишет ревью на 8 пунктов.
Что дальше? - Разбираться самому человеку или снова устроить дискуссию между агентами?
И сколько токенов они сожгут в этой дискуссии? И сколько времени это займет? И где гарантия от того, что если они по отдельности галюционируют, то и вместе они не начнут делать то же самое, а то и провоцировать друг друга на эти самые галюцинации?
Т.е., получается, что выигрыш от такого "автоматизированного" под ИИ ревью становится сомнительным удовольствием.
Около €170 тыс. заработал разработчик, создавший приложение, которое позволяет светить вспышкой смартфона в лицо пользователя — за подписку в нём люди платят €70 в год. Автор уверяет, что такие вспышки помогают расслабиться, уснуть и войти в состояние, похожее на транс. За прошлый месяц приложение скачали 70 тысяч раз. Весь продукт состоит из фонарика и таймера.
[пожалуйста, не злоупотребляйте эмодзи]
😻 Привет Халчане! Новое обновление HalChat for Android 1.0.3 (Pin, read and react)
Что нового?
🥷 Теперь можно закреплять сообщения
🤗 Поиск по чату
😳 Реакции на сообщения
🤓 Возможность выделять и копировать текст
🤖 Синхронизация действий в реальном времени
🫢 Возможность вернуться в самый низ, когда зашли далеко
Что исправлено?
🤠 Проведена оптимизация от улучшения распределения задач до ускорение процессов синхронизации
👻 Исправлены баги (опросы показывали зашифрованный текст, не всегда загружались люди, терялась синхронизация и данные и т.п.)
Добавлен выбранный вариант в опросе
Что дальше?
🙀 В следующем обновлении будут дополнительно добавлен локальный ИИ. Благодаря однобитным ИИ моделям, это новая технология уже доступна на HalChatWeb.
🤔 Также я дообучил собственную ИИ модель HalChat-RP с которой сняты больше ограничений и дообучена на RP общении, в том числе все 1к+ ИИ персонажей из генератора HalChatRP
И хотел бы попросить вас всем пройти опрос, проголосовало мало людей, а от этого выбора зависят новые звуковые эффекты мессенджера: https://halwarsing.questionpro.com/t/AddsYZ9TdN
😈 До новых встреч!
Google Play: https://play.google.com/store/apps/details?id=halwarsing.net.halchatandroid
RuStore: https://www.rustore.ru/catalog/app/halwarsing.net.halchatandroid
HalChat Web: https://halch.at/c/tZgWWT
GitHub: https://github.com/halwarsing/HalChat/tree/dev
Почему «я знаю слова, но не могу говорить» — это не всегда проблема словарного запаса
У языковых приложений есть измеримая ловушка: узнавание слова и самостоятельное производство слова выглядят как один навык, но требуют разных действий от памяти. В тесте можно выбрать правильный вариант из четырех. В разговоре нужно быстро достать слово, собрать фразу, произнести ее и продолжить после ошибки.
Из этого следуют несколько простых правил для практики:
Проверять нужно не только узнавание, но и свободное извлечение: показать ситуацию и попросить сказать фразу без списка вариантов.
Голосовые попытки должны быть короткими. Пять минут каждый день дают более честный сигнал, чем редкая длинная сессия.
Ошибку полезно разбирать после попытки. Если останавливать человека до того, как он договорил, тренируется избегание, а не речь.
Следующий шаг должен быть чуть сложнее предыдущего: знакомая фраза, небольшая вариация, затем новый контекст.
Я собираю KeelAI вокруг этой идеи: чтение, повторение слов и переход к спокойным голосовым ответам в Telegram. Сейчас особенно интересны менее популярные языки, где проблема не в отсутствии учебников, а в нехватке регулярной практики.
Текущая версия проекта доступна здесь: KeelAI.
Буду рад техническим замечаниям: какие метрики вы бы использовали, чтобы отличить «узнает в упражнении» от «может произнести в новом контексте»?
Представлен открытый проект Send it, with PairDrop (веб-версия проекта) — AirDrop для всех. Это универсальный способ передачи файлов на ПК и мобильных устройствах в браузере между Windows, Linux, Android, iOS, macOS и другими ОС:
работает без скачиваний драйверов и утилит и дополнительного ПО;
просто открываем сайт на обоих устройствах в браузере и начинаем передачу;
если используете одну сеть Wi‑Fi — устройства сразу увидят друг друга;
если используете разные сети — нужно один раз ввести шестизначный код, создать пару устройств, и дальше устройства будут находить друг друга автоматом;
передача файлов идем напрямую между устройствами;
можно передавать большие объёмы данных без ограничений.

Перед каждым релизом прохожу по одному и тому же списку. Не потому что умный, а потому что каждый пункт там появился после того как я облажался.
Три вещи которые горели чаще всего.
Разные версии Android. На эмуляторе всё красиво. На реальном устройстве со старой версией что-то обязательно едет. Держу под рукой старый телефон с Android 10, туда ставлю перед каждым релизом.
Разрешения. Забываешь добавить в манифест, на новых версиях система спрашивает пользователя, пользователь жмёт «запретить» и половина функций молча перестаёт работать. Без каких-либо ошибок в логах.
ProGuard и минификация. Локально всё работает. В release сборке падает что-то что ты вообще не трогал. Потому что минификатор убрал класс который использовался через рефлексию.
Список не длинный но каждый раз спасает от как минимум одного стыдного бага в продакшене.
Что у вас в чеклисте перед релизом чего нет у большинства?
Сравнение Claude Code Fable и Codex Open AI по ходу работы над одним и тем же проектом
Вчера, 1-го июля, программисты и активисты начали бурную трудовую неделю. А именно: вернулась модель Claude Fable 5 и она будет доступна в вольном режиме до (или по) 7 июля. Так что есть 7 дней, чтобы сделать буст своим проектам.
Я тоже не избежал этой участи и вот уже почти целый день делаю polishing своему текущему проекту мобильного приложения.
Что сказать про впечатления? - Ощущение вот того самого вайб кодинга, о котором говорил Карпаты. Говоришь модели что делать и она делает. Технических ошибок просто нет, от слова совсем. Есть ошибки архитектурные, но не существенные, исправляются одной-двумя итерациями.
И кстати, получилось сравнить с Codex'ом от Open AI, который решил попробовать на старте этого же проекта. Результат сравнения такой: Codex очень сильно подтянулся в работе с кодом, иногда даже кажется, что нет различий.
Но вот вокруг кода хуже: болтливые они оба, но у Codex больше какой-то разболтанности, разбрасывания в стороны. Особенно это видно на написании документации, пишет незначительные детали, теряет главное. И слабее держит инструкции.
Claude Code Fable в этом отношении гораздо чётче действует. Более жёстко держит инструкции, больше памяти, что характерно, помнит предыдущий и даже предыдущие чаты. Меньше разбрасывания на второстепенные детали, чётче фокус. Даже чек-лист у него выглядит проще, чётче и понятнее, чем у Codex.
Единственное, что может я так натаскал Claude. С другой стороны, не использую MCP, RAG, даже скилы и хуки. Зашил все в память, их там три: общая пользовательская, описание проекта и правила работы.
И напоследок обнаружил в Claude очень полезную функцию оценки загруженности контекстного окна.
Может она уже давно там была, о ней вроде писали, но что-то казалось, что это в CLI. А теперь оказывается её можно использовать и в декстопной версии. Думаю и другим пользователям это тоже пригодится.
Обычно смотришь, если чат начинает тормозить, значит пора. Или спросишь саму модель, но она обычно отвечает, что если на глаз, то загружена на 75%, но лучше начать новый чат. А теперь можно точно увидеть процент загруженности. Более того, можно даже увидеть чем именно загружено контекстное окно.
Для этого в чате Claude Code, в поле ввода достаточно ввести слэш команду - /context
Прикрепляю скриншот как это выглядит вживую

И ещё такое впечатление, что Claude Fable стал жечь меньше токенов за счёт какого-то более делового, но все ещё дружелюбного стиля общения.
Так что, удачи всем с проектами на этой бурной трудовой неделе!))
Сквозное шифрование, или как Telegram и Bitcord защищают переписку.
Хочу написать небольшой пост о сквозном шифровании, или, если использовать технический термин, E2EE (End-to-End Encryption).
Сегодня эта технология широко применяется во многих мессенджерах. Я тоже реализовал E2EE в мессенджере Bitcord . Однако далеко не все понимают, как именно работает этот механизм, поэтому попробую объяснить простыми словами.
Поскольку я являюсь разработчиком и основателем собственного мессенджера, реализовать эту схему для меня не составило особого труда. Главный секрет заключается в понимании принципов работы криптографических алгоритмов, таких как AES и RSA. Хотя современные реализации E2EE обычно используют не RSA, а алгоритмы на эллиптических кривых (например, X25519), я не стал прибегать к усложнениям.
Алгоритм AES я использовал для непосредственного шифрования текстового сообщения, которое один пользователь отправляет другому. Для шифрования применяется секретный ключ (или пароль). Основная проблема такого подхода заключается в том, что этот ключ необходимо каким-то образом передать получателю. Если злоумышленник перехватит ключ, он сможет расшифровать сообщение.
Чтобы этого не произошло, я использовал ещё один алгоритм - RSA. Для его работы требуется пара криптографических ключей: публичный и приватный. Так как RSA не предназначен для шифрования больших объёмов данных, я его использовал для безопасной передачи того самого секретного ключа, который используется алгоритмом AES.
В результате схема выглядит так.

Сначала текст сообщения шифруется алгоритмом AES с использованием случайного секретного ключа. Затем этот ключ шифруется алгоритмом RSA с использованием публичного ключа получателя. Когда получатель отправляет ответ, он выполняет ту же самую операцию, но уже с публичным ключом аппонента.
Важно понимать, что публичный ключ предназначен только для шифрования. Расшифровать данные с его помощью невозможно. Для расшифровки существует только соответствующий ему приватный ключ.
Когда получатель открывает сообщение, его устройство сначала с помощью приватного ключа RSA расшифровывает секретный ключ AES, а затем уже этим ключом расшифровывает само сообщение.
В моём приложении это работает следующим образом. Публичные ключи пользователей хранятся на сервере. Когда пользователь отправляет сообщение, приложение запрашивает у сервера публичный ключ получателя, шифрует сообщение на устройстве пользователя и отправляет на сервер уже зашифрованные данные. Сервер выступает лишь в роли посредника и пересылает этот зашифрованный пакет получателю. Сам сервер приватные ключи не хранит.
При этом приватные ключи никогда не покидают устройство пользователя. Они хранятся в защищённом хранилище смартфона, и получить к ним доступ не может ни сервер, ни разработчик приложения, ни кто-либо ещё. Поэтому расшифровать переписку может только владелец соответствующего приватного ключа.
В этом и заключается смысл сквозного шифрования: сервер передаёт сообщения, но не имеет возможности их прочитать.
Именно поэтому было довольно забавно наблюдать, как спецслужбы требовали у Павла Дурова "ключи шифрования", чтобы получить доступ к переписке пользователей Telegram. Никаких универсальных ключей у него нет и быть не может - приватные ключи находятся только на устройствах самих пользователей.
Именно поэтому требование "передать ключи" технически лишено смысла. Передавать попросту нечего.
П.С.
Я - сетевой долгожитель и начинал свой путь еще в эпоху Фидонета (FidoNet). Эта сеть была по-настоящему децентрализованной: никаких общих серверов и никакого DNS. Все строилось просто: компьютер, модем и терминальная программа для связи. Часто в роли узла (ноды) выступал сервер в банке, где знакомый сисадмин выделял адреса. При этом подключиться можно было к любому другому участнику, даже к частному лицу. Вот это и была настоящая децентрализация! Думаю, учитывая растущее давление регуляторов на современный интернет, мы скоро снова вернемся к проверенным идеям старого доброго Фидо.
Прямая Web3-монетизация без посредников (Peer-to-Peer) для артистов на радио.
Буквально вчера закончил написание сервисного бота для коммерческих нужд в мессенджере "Concord". Основной целью проекта является автоматизация процессов публикации авторского материала на интернет-радио. Бот был создан как помощник для авторов аудиоконтента: музыкантов, продюсеров, подкастеров и т. п.
Задача была непростой. Нужно было объединить возможности мессенджера с его токеномикой и реализовать передачу медиаконтента (картинок, аудиофайлов, текстовых данных) на удаленный сервер в формате JSON. Для этого я написал серверную страницу на PHP, в которой реализовал весь необходимый API.
При передаче медиаданных пользователь запускает нужного бота и отправляет ему в личные сообщения весь необходимый материал. Отправка изображения артиста и аудиофайла выполняется напрямую, без каких-либо команд. Бот сам идентифицирует тип данных, полученных от пользователя, и записывает их в локальные файлы. Вот как я реализовал проверку принадлежности данных к типу файлов:
function isJpeg(string $data): bool
{
return substr($data,0,2) === "\xFF\xD8";
}
function isMp3(string $data): bool
{
if (substr($data,0,3)==="ID3") {
return true;
}
return isset($data[1])
&&
ord($data[0])===0xFF
&&
(ord($data[1]) & 0xE0)===0xE0;
}После получения данных нужно сразу определить что именно пришло - команда или файл:
if (preg_match('/^\/(help|bio|title|tracks|done)\b/i', $data))
{
processCommand($db, $uuid, $data);
exit;
}
if (isBase64($data))
{
saveBinary($db, $uuid, $data);
exit;
}
reply("Unknown command, please use /help.", false);Описание профиля артиста и названия его треков передаются в текстовом виде используя специальный набор команд:
/help - Show this help
/bio text - Update artist biography
/tracks - Show info of all tracks
/title text - Update current track title
/pay amount - Pay for service
/done - Finalize current track
Send JPG image to update artist image
Send MP3 audio to update current track
Используя бота, артист может отправлять свои материалы в ротацию на радио для продвижения своего творчества. Протокол взаимодействия с ботом довольно простой:
артист отправляет фото профиля
артист отправляет описание профиля
артист загружает трек
артист отправляет описание трека
артист выполняет оплату сервиса
артист финализирует трек
Пока трек не финализирован, артист может изменять любые данные. Финализация трека возможна только после выполнения всех вышеуказанных действий. После этого любые отправленные артистом данные активируют запись уже нового трека, и процесс повторяется. При этом данные профиля артиста можно изменять без ограничений.
МОНЕТИЗАЦИЯ
Отправка материала артистом требует платы за сервис. Когда артист подтверждает оплату, бот списывает с его кошелька требуемую сумму и зачисляет её на кошелек владельца радиосервиса. Это значительно упрощает обмен активами между плательщиком и получателем.
Таким образом, технология Web3 с использованием блокчейна позволяет оперативно выполнять оплату за предоставленный сервис без лишних трат и банковских процедур. При этом все транзакции становятся видны в блокчейне уже через несколько минут.

Следующим этапом разработки я планирую реализовать возможность начисления роялти каждому автору музыкального материала. Для этого потребуется создать публичную таблицу рейтинга на сайте радио, где слушатели смогут голосовать за понравившиеся треки, продвигая артиста на верх списка. Исходя из количества прослушиваний конкретного трека, можно будет рассчитать сумму роялти и автоматически выплачивать её на личный кошелек автора в мессенджере.
Таким образом, объединение двух разных сущностей, а именно интернет-радио с приложением для обмена сообщениями, является неким ноу-хау для оказания помощи в развитии молодых дарований. Лично для меня как для разработчика это отличный вызов и прекрасная возможность "пошевелить мозгами".
Если у вас появятся предложения, буду рад подискуссировать.
Как перестать вручную поддерживать экран настроек
Новая настройка появилась в модели — значит, нужно добавить соответствующий UI‑компонент, настроить обработчики, связать все с системой хранения и не забыть ничего по пути.
Пока настроек немного, это не вызывает проблем. Но со временем поддержка такого экрана начинает занимать все больше времени.

Илья, iOS‑разработчик в Naumen, рассказывает, как пришел к подходу, при котором разработчику достаточно описать новое свойство, а интерфейс собирается автоматически.
Почему задача оказалась сложнее?
Все началось с настройки сжатия изображений перед отправкой на сервер. На первый взгляд задача выглядела вполне стандартной: подобрать параметры, проверить результат и убедиться, что все работает как нужно.
Но довольно быстро возник другой вопрос: как проверять изменения без постоянной пересборки приложения?
Для разработчика это не так критично, а вот для аналитиков на приемке и тестировщиков каждая новая проверка требовала участия разработчика. Тогда появилась идея вынести параметры в отдельный экран настроек.
Почему обычный экран настроек не решил проблему?
Сначала мы решили добавить переключатели, поля ввода и другие элементы интерфейса. Но появилась новая сложность: поддерживать такой экран вручную неудобно.
Чтобы добавить новую настройку, нужно было каждый раз:
добавлять свойство;
добавлять соответствующий UI‑компонент;
настраивать обработку;
связывать с хранилищем данных.
Я начал искать подход, при котором разработчику не нужно отдельно поддерживать интерфейс настроек. Хотелось, чтобы достаточно было просто описать новую настройку, а все остальное система делала сама.
В этот момент я вспомнил про Reflection. В Swift этот механизм ограничен и фактически работает как интроспекция, но даже этих возможностей оказалось достаточно для решения задачи.
Как сделать так, чтобы экран собирался автоматически?
В основе подхода лежит декларативный принцип: разработчик описывает свойства объекта настроек и добавляет к ним метаданные, например, название настройки или связи с другими параметрами.
Дальше система анализирует структуру объекта, определяет типы данных и автоматически подбирает нужные UI‑компоненты:
для булевых значений — переключатели;
для текста — поля ввода;
для чисел — поля с ограничением на числовой ввод.
Что изменилось после внедрения такого подхода?
Теперь для добавления новой настройки достаточно описать новое свойство и добавить необходимые метаданные. После этого настройка автоматически появляется в интерфейсе.
По моей оценке, трудозатраты на работу с настройками сократились примерно на 80–90%. Кроме того, уменьшилось количество дублирующего кода, а интерфейс стал более единообразным и предсказуемым для пользователей.
→ Подробнее своим опытом Илья поделился в статье.
Как я научил свою читалку различать примечания и комментарии в «Войне и мире»
Я не профессиональный программист. Пишу Android-читалку MRead для себя и для тех, кому тоже надоели комбайны. Код закрытый, всё работает локально, без серверов и трекинга. Раздаю через RuStore, 4PDA и GitHub.
В версии 1.4.0 я наконец победил баг, который меня лично бесил как читателя. Расскажу именно про него, потому что задача оказалась интереснее, чем выглядела.
Проблема
Открываю «Войну и мир». В одном предложении сразу два маркера сноски: примечание (перевод французского) и научный комментарий. В разных изданиях они нумеруются независимо, поэтому в тексте легко встретить два маркера с одинаковой цифрой. Старая логика по тапу вытаскивала из текста ближайшую цифру и искала абзац, начинающийся с этого числа, по всем главам, и показывала первое совпадение. Итог: тап по «6» открывал не ту сноску, а тап по числу вроде «1805» вообще пытался найти несуществующую сноску.
Почему наивный подход не работает
Цифра это не идентификатор. Она повторяется и в каждой главе, и между разделами «Примечания» и «Комментарии». А вот в исходнике всё однозначно: каждый маркер это ссылка с уникальным адресом.
EPUB: 1 FB2: [6] и {4}
То есть книга всегда знает правильный ответ. Беда была в том, что мой парсер при импорте выбрасывал href и оставлял только видимый текст маркера.
Что я поменял
При разборе HTML сохраняю не только текст абзаца, но и ссылки-сноски с точной позицией маркера. Позицию считаю надёжно: оборачиваю маркер невидимыми символами из Private Use Area до очистки текста, а после очистки нахожу их и вычисляю смещение. Так цифры в обычном тексте (годы, числа) не путаются с маркерами.
Позиции храню в абсолютных координатах главы. Это важно, потому что мой пагинатор режет абзацы на куски по строкам. Абсолютные смещения переживают нарезку без отдельной возни в пагинаторе.
По тапу беру не цифру, а ссылку под пальцем, и резолвлю сноску по точному id из нужного файла. Для FB2 пришлось ещё и сохранить id секций сносок при конвертации в HTML, потому что примечания и комментарии лежат в отдельных главах.
Старый поиск по цифре оставил как запасной вариант для книг без нормальной разметки, но убрал срабатывание на голые числа.
Результат: тап по примечанию открывает перевод, тап по комментарию открывает комментарий, а год «1805» больше никого не трогает.
Контекст для тех, кому интересно
Рендер текста у меня не WebView, а нативный движок на Canvas с собственной пагинацией (Jetpack Compose, Kotlin). Это даёт контроль над переносами, выравниванием по ширине и стабильной привязкой цитат и закладок, но за каждую такую фичу приходится платить ручной работой вроде этой истории со сносками.
Что ещё в 1.4.0, кратко
• Полнотекстовый поиск по PDF (для PDF с текстовым слоем)
• Озвучивание (TTS): голоса, скорость, таймер сна, автопереход, пауза с памятью места
• Передача слова во внешний словарь и контекст предложения в экспорт Anki
• Настраиваемые блоки настроек чтения, межабзацный интервал, Bold/Italic
• Полки, фоновый импорт из папок, оценки и заметки, Material You
Ссылки
• RuStore
• GitHub
• 4PDA
Если у вас была книга FB2, переоткройте её, чтобы заработали точные сноски (HTML генерится при импорте). EPUB подхватит сам.
Буду рад замечаниям по подходу. Если делали резолв сносок иначе, расскажите как, мне правда интересно.
Обратная сторона рабочих чатиков
Андрей Врацкий начал строить рынок корпоративных мессенджеров в 2015 году, когда WhatsApp и Telegram только начинали проникать в рабочие чаты, а безопасники в компаниях делали вид, что все ок и деловая переписка — это личная ответственность сотрудника.
На создание продукта ушло четыре года. Когда eXpress вышел в 2019-м, то выяснилось: все уже так привыкли к Telegram и WhatsApp, что менять ничего не хотят.
Прошло семь лет. Сегодня иностранные мессенджеры заблокированы, отечественный* удален из App Store на неопределенный срок. На этом фоне спрос на eXpress вырос в четыре раза — и это пока Telegram все еще работает. Что будет, когда перестанет?
Хотя по мнению Врацкого, хороший продукт будет востребован вне зависимости от обстоятельств и критерий его успеха — это конкурентоспособность на мировом рынке.
Подробнее — во втором выпуске подкаста «IT-фронтир».
Как я ускорил бэкапы в 20 раз и обошёл ловушку Jsoup: развитие самописной Android‑читалки MRead (v1.3.0)
Всем привет! Не так давно я рассказывал, как боль от перегруженных интерфейсов заставила меня открыть Android Studio и написать собственную читалку с кастомным движком рендеринга и точным выделением текста.
Статья получила теплый отклик и в комментариях набежало много отличных предложений. В этом посте я хочу поделиться техническими решениями, которые вошли в крупное обновление 1.3.0.
1. Бэкапы и боль от Storage Access Framework (SAF)
В приложении есть функция бэкапа: упаковка базы данных Room, настроек и распакованных HTML‑глав с картинками в один ZIP‑архив. Изначально я писал файлы напрямую в OutputStream, полученный через ContentResolver (SAF). Итог: библиотека на 500 МБ архивировалась около 5 минут. SAF проводит проверки безопасности для каждого записываемого чанка, что убивает I/O операции.
Решение: сборка архива переехала во внутренний кэш приложения. Туда пишем без ограничений SAF — буфером по 64 КБ и уровнем сжатия BEST_SPEED (картинки уже сжаты, гнать их через BEST_COMPRESSION бессмысленно). Когда ZIP готов целиком, он одним куском копируется в пользовательскую папку через SAF — вместо тысяч мелких защищённых записей получается одна
2. Material You: как получить правильные цвета обоев
При внедрении динамических тем (Android 12+) я столкнулся с тем, что стандартный вызов dynamicLightColorScheme().background на многих устройствах выдает просто унылый белый или бледно‑серый цвет, игнорируя сочные оттенки обоев.
Решение: Самые насыщенные цвета из системной палитры Monet хранятся в secondaryContainer и surface. Решение нашлось в самой палитре Monet: наиболее насыщенные цвета живут в secondaryContainer и surface, а не в background. Переориентировал маппинг цветов приложения на эти слоты и интерфейс действительно ожил. Теперь интерфейс действительно реагирует на смену обоев. Плюс привязал OnSharedPreferenceChangeListener, чтобы тема менялась мгновенно на всех экранах без перезапуска.
3. Странности парсинга FB2 и баги Jsoup
Иногда вместо обложки FB2 парсер ставил черно‑белую картинку из середины книги. FB2 хранит все изображения в тегах <binary> в конце файла в хаотичном порядке. Если тег <coverpage> отсутствует, старый алгоритм просто брал первую попавшуюся картинку из бинарной кучи.
Я переписал фоллбэк: теперь, если явной обложки нет, Jsoup ищет первый тег <image> прямо внутри <body> книги.
Попутно всплыло неочевидное поведение Jsoup: если атрибут отсутствует, attr() возвращает пустую строку, а не null — это задокументировано, но интуитивно ожидаешь null. Из‑за этого Элвис‑операторы (?:) молча проглатывали пустую строку вместо ухода в fallback. Написал строгую обертку takeIf { it.isNotEmpty() }, и теперь обложки извлекаются безошибочно.
4. Изолированный свайп яркости в Compose
Нужно было добавить регулировку яркости свайпом по левому краю экрана. Проблема: в режиме вертикального скролла (VerticalPager) свайпер страниц перехватывает вертикальные жесты на себя.
Решение: перехватывать жест на фазе Initial — до того, как пейджер успевает его обработать. Если касание началось в левых 15% ширины экрана, событие забирается себе и до пейджера не доходит.
Помимо этого в релизе 1.3.0:
Добавлен полноэкранный просмотрщик иллюстраций с pinch‑to‑zoom (на основе detectTransformGestures).
Написан собственный File Picker со сканированием вложенных папок и извлечением книг прямо из ZIP‑архивов на лету.
Добавлен поворот страниц для PDF с сохранением состояния в SharedPreferences.
Разделен UI верхнего меню: закладки теперь можно переименовывать, а тап по номеру страницы открывает быстрый переход.
Добавлен множественный выбор в библиотеке (массовое добавление на полки/удаление/скрытие).
Ссылки:
🤗 Привет всем!
😻 Обновление HalChat for Android: v1.0.2 (Vote or don't vote)
Что нового?
😌Теперь есть описание чатов.
🤖 Добавлено обновление данных чата.
😉 Добавлены метаданные файлов HD - мгновенное определение что за файл или какие размеры изображения.
😇 Доступны опросы/голосования в чатах.
Что исправлено?
🤓 Если получение или расшифровка пароля была прервана, то теперь он запросит заново, и пароль будет доступен в любом случае.
🫢 Теперь при получении пароля от чата, в списке чатов сообщения будут расшифроваться.
😎 Исправлена система файлов в чате, они будут отображаться всегда и в том числе изображения (исправлен баг).
До новых встреч!
Google Play: https://play.google.com/store/apps/details?id=halwarsing.net.halchatandroid
RuStore: https://www.rustore.ru/catalog/app/halwarsing.net.halchatandroid
HalChat Web: https://halch.at/c/tZgWWT
GitHub: https://github.com/halwarsing/HalChat/tree/dev